UNPKG

@croct/plug

Version:

A fully-featured devkit for building natively personalized applications.

257 lines (256 loc) 9.05 kB
import { SdkFacade } from "@croct/sdk/facade/sdkFacade"; import { formatCause } from "@croct/sdk/error"; import { describe } from "@croct/sdk/validation"; import { Token } from "@croct/sdk/token"; import { VERSION } from "@croct/sdk"; import { loadSlotContent } from "@croct/content"; import { CDN_URL } from "./constants.js"; import { factory as playgroundPluginFactory } from "./playground.js"; import { factory as previewPluginFactory } from "./preview.js"; const PLUGIN_NAMESPACE = "Plugin"; function detectAppId() { const script = window.document.querySelector(`script[src^='${CDN_URL}']`); if (!(script instanceof HTMLScriptElement)) { return null; } return new URL(script.src).searchParams.get("appId"); } class GlobalPlug { constructor() { this.pluginFactories = { playground: playgroundPluginFactory, preview: previewPluginFactory }; this.plugins = {}; this.ready = new Promise((resolve) => { this.initialize = resolve; }); } extend(name, plugin) { if (this.pluginFactories[name] !== void 0) { throw new Error(`Another plugin is already registered with name "${name}".`); } this.pluginFactories[name] = plugin; } plug(configuration = {}) { if (this.instance !== void 0) { const logger2 = this.instance.getLogger(); logger2.info("Croct is already plugged in."); return; } const detectedAppId = detectAppId(); const configuredAppId = configuration.appId ?? null; if (detectedAppId !== null && configuredAppId !== null && detectedAppId !== configuredAppId) { throw new Error( 'The specified app ID and the auto-detected app ID are conflicting. There is no need to specify an app ID when using an application-specific tag. Please try again omitting the "appId" option. For help, see https://croct.help/sdk/javascript/conflicting-app-id' ); } const appId = detectedAppId ?? configuredAppId; if (appId === null) { throw new Error( 'The app ID must be specified when it cannot be auto-detected. Please try again specifying the "appId" option.For help, see https://croct.help/sdk/javascript/missing-app-id' ); } const { plugins, test, ...sdkConfiguration } = configuration; if (sdkConfiguration.defaultPreferredLocale === "") { delete sdkConfiguration.defaultPreferredLocale; } const sdk = SdkFacade.init({ ...sdkConfiguration, appId, test: test ?? (typeof process === "object" && (process.env?.CROCT_TEST_MODE !== void 0 ? process.env.CROCT_TEST_MODE === "true" : process.env?.NODE_ENV === "test")) }); this.instance = sdk; const logger = this.instance.getLogger(); if (detectedAppId === configuredAppId) { logger.warn( 'It is strongly recommended omitting the "appId" option when using the application-specific tag as it is detected automatically.' ); } const pending = []; const defaultEnabledPlugins = Object.fromEntries( Object.keys(this.pluginFactories).map((name) => [name, true]) ); for (const [name, options] of Object.entries({ ...defaultEnabledPlugins, ...plugins })) { logger.debug(`Initializing plugin "${name}"...`); const factory = this.pluginFactories[name]; if (factory === void 0) { logger.error(`Plugin "${name}" is not registered.`); continue; } if (typeof options !== "boolean" && (options === null || typeof options !== "object")) { logger.error( `Invalid options for plugin "${name}", expected either boolean or object but got ${describe(options)}` ); continue; } if (options === false) { logger.warn(`Plugin "${name}" is declared but not enabled`); continue; } const args = { options: options === true ? {} : options, sdk: { version: VERSION, appId, tracker: sdk.tracker, evaluator: sdk.evaluator, user: sdk.user, session: sdk.session, tab: sdk.context.getTab(), userTokenStore: { getToken: sdk.getToken.bind(sdk), setToken: sdk.setToken.bind(sdk) }, previewTokenStore: sdk.previewTokenStore, cidAssigner: sdk.cidAssigner, eventManager: sdk.eventManager, getLogger: (...namespace) => sdk.getLogger(PLUGIN_NAMESPACE, name, ...namespace), getTabStorage: (...namespace) => sdk.getTabStorage(PLUGIN_NAMESPACE, name, ...namespace), getBrowserStorage: (...namespace) => sdk.getBrowserStorage(PLUGIN_NAMESPACE, name, ...namespace) } }; let plugin; try { plugin = factory(args); } catch (error) { logger.error(`Failed to initialize plugin "${name}": ${formatCause(error)}`); continue; } logger.debug(`Plugin "${name}" initialized`); if (typeof plugin !== "object") { continue; } this.plugins[name] = plugin; const promise = plugin.enable(); if (!(promise instanceof Promise)) { logger.debug(`Plugin "${name}" enabled`); continue; } pending.push( promise.then(() => logger.debug(`Plugin "${name}" enabled`)).catch((error) => logger.error(`Failed to enable plugin "${name}": ${formatCause(error)}`)) ); } Promise.all(pending).then(() => { this.initialize(); logger.debug("Initialization complete"); }); } get initialized() { return this.instance !== void 0; } get plugged() { return this.ready.then(() => this); } get flushed() { return this.tracker.flushed.then(() => this); } get sdk() { if (this.instance === void 0) { throw new Error("Croct is not plugged in. For help, see https://croct.help/sdk/javascript/not-plugged-in"); } return this.instance; } get tracker() { return this.sdk.tracker; } get evaluator() { return this.sdk.evaluator; } get user() { return this.sdk.user; } get session() { return this.sdk.session; } isAnonymous() { return this.sdk.context.isAnonymous(); } getUserId() { return this.sdk.context.getUser(); } identify(userId) { if (typeof userId !== "string") { throw new Error( "The user ID must be a string. For help, see https://croct.help/sdk/javascript/invalid-user-id" ); } this.sdk.identify(userId); } anonymize() { this.sdk.anonymize(); } setToken(token) { this.sdk.setToken(Token.parse(token)); } unsetToken() { this.sdk.unsetToken(); } track(type, payload) { return this.sdk.tracker.track(type, payload); } evaluate(query, options = {}) { return this.sdk.evaluator.evaluate(query, options).catch((error) => { const logger = this.sdk.getLogger(); const reference = query.length > 20 ? `${query.slice(0, 20)}...` : query; logger.error(`Failed to evaluate query "${reference}": ${formatCause(error)}`); throw error; }); } test(expression, options = {}) { return this.evaluate(expression, options).then((result) => result === true); } fetch(slotId, { fallback, preferredLocale = "", ...options } = {}) { const [id, version = "latest"] = slotId.split("@"); const logger = this.sdk.getLogger(); const normalizedLocale = preferredLocale === "" ? void 0 : preferredLocale; return this.sdk.contentFetcher.fetch(id, { ...options, ...normalizedLocale !== void 0 ? { preferredLocale: normalizedLocale } : {}, ...version !== "latest" ? { version } : {} }).catch(async (error) => { logger.error(`Failed to fetch content for slot "${id}@${version}": ${formatCause(error)}`); const resolvedFallback = fallback === void 0 ? await loadSlotContent(slotId, normalizedLocale) ?? void 0 : fallback; if (resolvedFallback === void 0) { throw error; } return { content: resolvedFallback }; }); } async unplug() { if (this.instance === void 0) { return; } const { instance, plugins } = this; const logger = this.sdk.getLogger(); const pending = []; for (const [pluginName, controller] of Object.entries(plugins)) { if (typeof controller.disable !== "function") { continue; } logger.debug(`Disabling plugin "${pluginName}"...`); const promise = controller.disable(); if (!(promise instanceof Promise)) { logger.debug(`Plugin "${pluginName}" disabled`); continue; } pending.push( promise.then(() => logger.debug(`Plugin "${pluginName}" disabled`)).catch((error) => logger.error(`Failed to disable "${pluginName}": ${formatCause(error)}`)) ); } delete this.instance; this.plugins = {}; this.ready = new Promise((resolve) => { this.initialize = resolve; }); await Promise.all(pending); try { await instance.close(); } finally { logger.info("\u{1F50C} Croct has been unplugged."); } } } export { GlobalPlug };