@croct/plug
Version:
A fully-featured devkit for building natively personalized applications.
257 lines (256 loc) • 9.05 kB
JavaScript
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
};