UNPKG

@embeddable.com/sdk-react

Version:

Embeddable SDK React plugin responsible for React components bundling.

1,332 lines (1,303 loc) 68 kB
import * as path$1 from 'node:path'; import { resolve, join } from 'node:path'; import * as fs$2 from 'fs/promises'; import * as path from 'path'; import * as vite from 'vite'; import ora from 'ora'; import viteReactPlugin from '@vitejs/plugin-react'; import * as fs from 'node:fs/promises'; import { readdir, lstat, readFile, rm } from 'node:fs/promises'; import * as url from 'node:url'; import { GlobalRegistrator } from '@happy-dom/global-registrator'; import 'node:child_process'; import * as crypto from 'node:crypto'; import { z } from 'zod'; import { parse } from '@babel/parser'; import traverse from '@babel/traverse'; import * as fs$1 from 'fs'; import { existsSync } from 'node:fs'; import dts from 'vite-plugin-dts'; var createContext = (pluginRoot, coreCtx) => { coreCtx["sdk-react"] = { rootDir: pluginRoot, templatesDir: resolve(pluginRoot, "templates"), outputOptions: { fileName: "embeddable-prepared", buildName: "embeddable-prepared-build", componentsEntryPointFilename: "embeddable-entry-point.jsx", }, }; }; GlobalRegistrator.register({ url: "http://localhost:3000", width: 1920, height: 1080, }); const loadComponentMeta = async (moduleId) => { const module = await import(url.pathToFileURL(moduleId).href); return module.meta; }; /** * TODO: for some reason code from @embeddable.com/extract-components-config * is being cached. Thus we use this code duplication in place. Please investigate and fix. */ var extractComponentsConfigPlugin = ({ globalKey, outputDir, fileName, componentFileRegex, isDev = false, searchEntry = "", useBundleHash = true, ctx, externalComponents = [], }) => { let configs = []; return { name: "extract-components-config", moduleParsed: async (moduleInfo) => { var _a; if (componentFileRegex.test(moduleInfo.id) && ((_a = moduleInfo.code) === null || _a === void 0 ? void 0 : _a.includes(searchEntry))) { try { const meta = await loadComponentMeta(moduleInfo.id); const configJSON = buildConfigJson(meta); configs.push(configJSON); } catch (error) { console.log("Error parsing component meta: ", moduleInfo.id); console.error(error); } } }, buildEnd: async () => { for (const [_, externalComponent] of externalComponents) { const meta = await loadComponentMeta(externalComponent); configs.push(buildConfigJson(meta)); } configs.sort((a, b) => a.localeCompare(b)); const contentString = configs.filter(Boolean).join(",\n"); let newFileName = fileName; const bundleHash = ctx.client.bundleHash; if (!isDev && bundleHash) { const fileHashExtension = `-${bundleHash}.js`; newFileName = `${fileName.replace(".js", fileHashExtension)}`; } let template; if (useBundleHash && bundleHash) { template = ` globalThis.__EMBEDDABLE__ = globalThis.__EMBEDDABLE__ || {}; globalThis.__EMBEDDABLE__["${bundleHash}"] = globalThis.__EMBEDDABLE__["${bundleHash}"] || {}; globalThis.__EMBEDDABLE__["${bundleHash}"].${globalKey} = globalThis.__EMBEDDABLE__["${bundleHash}"].${globalKey} || [ __PLACEHOLDER__ ]; globalThis.__EMBEDDABLE_BUNDLE_HASH__ = "${bundleHash}"; `; } else { template = ` globalThis.__EMBEDDABLE__ = globalThis.__EMBEDDABLE__ || {}; globalThis.__EMBEDDABLE__.${globalKey} = globalThis.__EMBEDDABLE__.${globalKey} || [ __PLACEHOLDER__ ]; `; } await fs.writeFile(`${outputDir}/${newFileName}`, template.replace("__PLACEHOLDER__", contentString)); configs = []; }, }; }; const buildConfigJson = (meta) => { return JSON.stringify(meta, (_key, value) => typeof value === "object" && value !== null && "__embeddableType" in value ? value.toString() : value); }; var findFiles = async (initialSrcDir, regex) => { const filesList = []; async function findFilesRec(srcDir) { const allFiles = await readdir(srcDir); for (const file of allFiles) { const filePath = join(srcDir, file); const status = await lstat(filePath); if (status.isDirectory()) { await findFilesRec(filePath); } const fileName = file.match(regex); if (fileName) { filesList.push([fileName[1], filePath]); } } } await findFilesRec(initialSrcDir); return filesList; }; var util; (function (util) { util.assertEqual = (val) => val; function assertIs(_arg) { } util.assertIs = assertIs; function assertNever(_x) { throw new Error(); } util.assertNever = assertNever; util.arrayToEnum = (items) => { const obj = {}; for (const item of items) { obj[item] = item; } return obj; }; util.getValidEnumValues = (obj) => { const validKeys = util.objectKeys(obj).filter((k) => typeof obj[obj[k]] !== "number"); const filtered = {}; for (const k of validKeys) { filtered[k] = obj[k]; } return util.objectValues(filtered); }; util.objectValues = (obj) => { return util.objectKeys(obj).map(function (e) { return obj[e]; }); }; util.objectKeys = typeof Object.keys === "function" // eslint-disable-line ban/ban ? (obj) => Object.keys(obj) // eslint-disable-line ban/ban : (object) => { const keys = []; for (const key in object) { if (Object.prototype.hasOwnProperty.call(object, key)) { keys.push(key); } } return keys; }; util.find = (arr, checker) => { for (const item of arr) { if (checker(item)) return item; } return undefined; }; util.isInteger = typeof Number.isInteger === "function" ? (val) => Number.isInteger(val) // eslint-disable-line ban/ban : (val) => typeof val === "number" && isFinite(val) && Math.floor(val) === val; function joinValues(array, separator = " | ") { return array .map((val) => (typeof val === "string" ? `'${val}'` : val)) .join(separator); } util.joinValues = joinValues; util.jsonStringifyReplacer = (_, value) => { if (typeof value === "bigint") { return value.toString(); } return value; }; })(util || (util = {})); var objectUtil; (function (objectUtil) { objectUtil.mergeShapes = (first, second) => { return { ...first, ...second, // second overwrites first }; }; })(objectUtil || (objectUtil = {})); util.arrayToEnum([ "string", "nan", "number", "integer", "float", "boolean", "date", "bigint", "symbol", "function", "undefined", "null", "array", "object", "unknown", "promise", "void", "never", "map", "set", ]); const ZodIssueCode = util.arrayToEnum([ "invalid_type", "invalid_literal", "custom", "invalid_union", "invalid_union_discriminator", "invalid_enum_value", "unrecognized_keys", "invalid_arguments", "invalid_return_type", "invalid_date", "invalid_string", "too_small", "too_big", "invalid_intersection_types", "not_multiple_of", "not_finite", ]); class ZodError extends Error { get errors() { return this.issues; } constructor(issues) { super(); this.issues = []; this.addIssue = (sub) => { this.issues = [...this.issues, sub]; }; this.addIssues = (subs = []) => { this.issues = [...this.issues, ...subs]; }; const actualProto = new.target.prototype; if (Object.setPrototypeOf) { // eslint-disable-next-line ban/ban Object.setPrototypeOf(this, actualProto); } else { this.__proto__ = actualProto; } this.name = "ZodError"; this.issues = issues; } format(_mapper) { const mapper = _mapper || function (issue) { return issue.message; }; const fieldErrors = { _errors: [] }; const processError = (error) => { for (const issue of error.issues) { if (issue.code === "invalid_union") { issue.unionErrors.map(processError); } else if (issue.code === "invalid_return_type") { processError(issue.returnTypeError); } else if (issue.code === "invalid_arguments") { processError(issue.argumentsError); } else if (issue.path.length === 0) { fieldErrors._errors.push(mapper(issue)); } else { let curr = fieldErrors; let i = 0; while (i < issue.path.length) { const el = issue.path[i]; const terminal = i === issue.path.length - 1; if (!terminal) { curr[el] = curr[el] || { _errors: [] }; // if (typeof el === "string") { // curr[el] = curr[el] || { _errors: [] }; // } else if (typeof el === "number") { // const errorArray: any = []; // errorArray._errors = []; // curr[el] = curr[el] || errorArray; // } } else { curr[el] = curr[el] || { _errors: [] }; curr[el]._errors.push(mapper(issue)); } curr = curr[el]; i++; } } } }; processError(this); return fieldErrors; } static assert(value) { if (!(value instanceof ZodError)) { throw new Error(`Not a ZodError: ${value}`); } } toString() { return this.message; } get message() { return JSON.stringify(this.issues, util.jsonStringifyReplacer, 2); } get isEmpty() { return this.issues.length === 0; } flatten(mapper = (issue) => issue.message) { const fieldErrors = {}; const formErrors = []; for (const sub of this.issues) { if (sub.path.length > 0) { fieldErrors[sub.path[0]] = fieldErrors[sub.path[0]] || []; fieldErrors[sub.path[0]].push(mapper(sub)); } else { formErrors.push(mapper(sub)); } } return { formErrors, fieldErrors }; } get formErrors() { return this.flatten(); } } ZodError.create = (issues) => { const error = new ZodError(issues); return error; }; typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { var e = new Error(message); return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }; var errorUtil; (function (errorUtil) { errorUtil.errToObj = (message) => typeof message === "string" ? { message } : message || {}; errorUtil.toString = (message) => typeof message === "string" ? message : message === null || message === void 0 ? void 0 : message.message; })(errorUtil || (errorUtil = {})); var ZodFirstPartyTypeKind; (function (ZodFirstPartyTypeKind) { ZodFirstPartyTypeKind["ZodString"] = "ZodString"; ZodFirstPartyTypeKind["ZodNumber"] = "ZodNumber"; ZodFirstPartyTypeKind["ZodNaN"] = "ZodNaN"; ZodFirstPartyTypeKind["ZodBigInt"] = "ZodBigInt"; ZodFirstPartyTypeKind["ZodBoolean"] = "ZodBoolean"; ZodFirstPartyTypeKind["ZodDate"] = "ZodDate"; ZodFirstPartyTypeKind["ZodSymbol"] = "ZodSymbol"; ZodFirstPartyTypeKind["ZodUndefined"] = "ZodUndefined"; ZodFirstPartyTypeKind["ZodNull"] = "ZodNull"; ZodFirstPartyTypeKind["ZodAny"] = "ZodAny"; ZodFirstPartyTypeKind["ZodUnknown"] = "ZodUnknown"; ZodFirstPartyTypeKind["ZodNever"] = "ZodNever"; ZodFirstPartyTypeKind["ZodVoid"] = "ZodVoid"; ZodFirstPartyTypeKind["ZodArray"] = "ZodArray"; ZodFirstPartyTypeKind["ZodObject"] = "ZodObject"; ZodFirstPartyTypeKind["ZodUnion"] = "ZodUnion"; ZodFirstPartyTypeKind["ZodDiscriminatedUnion"] = "ZodDiscriminatedUnion"; ZodFirstPartyTypeKind["ZodIntersection"] = "ZodIntersection"; ZodFirstPartyTypeKind["ZodTuple"] = "ZodTuple"; ZodFirstPartyTypeKind["ZodRecord"] = "ZodRecord"; ZodFirstPartyTypeKind["ZodMap"] = "ZodMap"; ZodFirstPartyTypeKind["ZodSet"] = "ZodSet"; ZodFirstPartyTypeKind["ZodFunction"] = "ZodFunction"; ZodFirstPartyTypeKind["ZodLazy"] = "ZodLazy"; ZodFirstPartyTypeKind["ZodLiteral"] = "ZodLiteral"; ZodFirstPartyTypeKind["ZodEnum"] = "ZodEnum"; ZodFirstPartyTypeKind["ZodEffects"] = "ZodEffects"; ZodFirstPartyTypeKind["ZodNativeEnum"] = "ZodNativeEnum"; ZodFirstPartyTypeKind["ZodOptional"] = "ZodOptional"; ZodFirstPartyTypeKind["ZodNullable"] = "ZodNullable"; ZodFirstPartyTypeKind["ZodDefault"] = "ZodDefault"; ZodFirstPartyTypeKind["ZodCatch"] = "ZodCatch"; ZodFirstPartyTypeKind["ZodPromise"] = "ZodPromise"; ZodFirstPartyTypeKind["ZodBranded"] = "ZodBranded"; ZodFirstPartyTypeKind["ZodPipeline"] = "ZodPipeline"; ZodFirstPartyTypeKind["ZodReadonly"] = "ZodReadonly"; })(ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {})); const errorFormatter = (issues) => { const errors = []; for (let issue of issues) { if (issue.code === ZodIssueCode.invalid_union && issue.unionErrors.length) { const error = issue.unionErrors[issue.unionErrors.length - 1]; issue = error.issues[error.issues.length - 1]; } const path = formatErrorPath(issue.path); const message = issue.message; errors.push(`"${path}": ${message}`); } return errors; }; const formatErrorPath = (path) => { let formatted = ""; for (const pathElement of path) { if (formatted.length === 0) { formatted = `${pathElement}`; } else { formatted += typeof pathElement === "number" ? `[${pathElement}]` : `.${pathElement}`; } } return formatted; }; /** * Get the hash of the content string. It returns the first 5 characters of the hash * Example: getContentHash("Hello World") * @param contentString The content string to hash * @returns */ const getContentHash = (contentString) => { return crypto .createHash("md5") .update(contentString) .digest("hex") .substring(0, 5); }; const loadJson = async (filePath) => { const data = await readFile(filePath, "utf-8"); return JSON.parse(data); }; const getComponentLibraryConfig = (componentLibrary) => { let libraryName = componentLibrary; let include; const exclude = []; if (typeof componentLibrary === "object" && componentLibrary !== null) { libraryName = componentLibrary.name; if (componentLibrary.include) { include = [...componentLibrary.include]; } if (componentLibrary.exclude) { exclude.push(...componentLibrary.exclude); } } return { libraryName, include, exclude }; }; const EXTERNAL_LIBRARY_META_FILE_NAME = "embeddable-components.json"; const getComponentLibraryMeta = async (ctx, componentLibrary) => { const { libraryName, include, exclude } = getComponentLibraryConfig(componentLibrary); const libraryMeta = (await loadJson(getLibraryPath(ctx, libraryName, EXTERNAL_LIBRARY_META_FILE_NAME))); const filterItems = (items) => { let result = items; if (include) { result = result.filter((item) => include.includes(item)); } return result.filter((item) => !exclude.includes(item)); }; return { ...libraryMeta, components: filterItems(libraryMeta.components), editors: filterItems(libraryMeta.editors), }; }; const getLibraryPath = (ctx, libraryName, fileName) => path.resolve(ctx.client.rootDir, "node_modules", libraryName, "dist", fileName); const DEFAULT_LOCALE = "en-US"; const createBuiltInType = (name, typeConfig = {}) => ({ __embeddableType: "built-in", toString: () => name, typeConfig: { label: name, optionLabel: () => name, ...typeConfig, }, }); const STRING = "string"; const NUMBER = "number"; const BOOLEAN = "boolean"; const TIME = "time"; const TIME_RANGE = "timeRange"; const GRANULARITY = "granularity"; const DATASET = "dataset"; const MEASURE = "measure"; const DIMENSION = "dimension"; const DIMENSION_OR_MEASURE = "dimensionOrMeasure"; const DEFAULT_NATIVE_TYPES = [ STRING, NUMBER, BOOLEAN, TIME, TIME_RANGE, GRANULARITY, ]; const ALL_NATIVE_TYPES = [ ...DEFAULT_NATIVE_TYPES, DATASET, MEASURE, DIMENSION, DIMENSION_OR_MEASURE, ]; const MEASURE_TYPE_STRING = "string"; const MEASURE_TYPE_TIME = "time"; const MEASURE_TYPE_BOOLEAN = "boolean"; const MEASURE_TYPE_NUMBER = "number"; const MEASURE_TYPE_COUNT = "count"; const MEASURE_TYPE_COUNT_DISTINCT = "count_distinct"; const MEASURE_TYPE_COUNT_DISTINCT_APPROX = "count_distinct_approx"; const MEASURE_TYPE_SUM = "sum"; const MEASURE_TYPE_AVG = "avg"; const MEASURE_TYPE_MIN = "min"; const MEASURE_TYPE_MAX = "max"; const MEASURE_TYPES = [ MEASURE_TYPE_STRING, MEASURE_TYPE_TIME, MEASURE_TYPE_BOOLEAN, MEASURE_TYPE_NUMBER, MEASURE_TYPE_COUNT, MEASURE_TYPE_COUNT_DISTINCT, MEASURE_TYPE_COUNT_DISTINCT_APPROX, MEASURE_TYPE_SUM, MEASURE_TYPE_AVG, MEASURE_TYPE_MIN, MEASURE_TYPE_MAX, ]; const DIMENSION_TYPE_STRING = "string"; const DIMENSION_TYPE_NUMBER = "number"; const DIMENSION_TYPE_BOOLEAN = "boolean"; const DIMENSION_TYPE_GEO = "geo"; const DIMENSION_TYPE_TIME = "time"; const DIMENSION_TYPES = [ DIMENSION_TYPE_STRING, DIMENSION_TYPE_NUMBER, DIMENSION_TYPE_BOOLEAN, DIMENSION_TYPE_GEO, DIMENSION_TYPE_TIME, ]; [ ...DIMENSION_TYPES, ...MEASURE_TYPES, ]; createBuiltInType("string", { transform: (value) => value, optionLabel: (value) => Array.isArray(value) ? `[${value.map((v) => `"${v}"`).join(",")}]` : `"${value}"`, }); createBuiltInType("number", { transform: (value) => Array.isArray(value) ? value : value ? Number(value) : value, optionLabel: (value) => Array.isArray(value) ? `[${value.join(",")}]` : (value?.toLocaleString(DEFAULT_LOCALE) ?? ""), }); createBuiltInType("boolean", { transform: (value) => value === "true" || value === true, optionLabel: (value) => (value ? "true" : "false"), }); createBuiltInType("time", { transform: (value) => { const date = value?.date ? new Date(value.date) : undefined; const isValid = date && date.toString() !== "Invalid Date"; return { date: isValid ? date : undefined, relativeTimeString: value?.relativeTimeString, }; }, optionLabel: (value) => { if (!value) return ""; if (value?.date) { return (value.date?.toLocaleDateString(DEFAULT_LOCALE) ?? value.date.toLocaleString()); } return value.relativeTimeString; }, }); createBuiltInType("timeRange", { transform: (value) => { // Return undefined instead of a null populated object if (!value) return undefined; const [from, to] = [value?.from, value?.to]; const fromDate = new Date(from); const toDate = new Date(to); return { from: fromDate.toString() !== "Invalid Date" ? fromDate : undefined, to: toDate.toString() !== "Invalid Date" ? toDate : undefined, relativeTimeString: value?.relativeTimeString, }; }, optionLabel: (value) => { if (!value) return ""; if (value?.from && value?.to) { return `${value.from?.toLocaleDateString(DEFAULT_LOCALE) ?? value.from?.toLocaleString()},${value.to?.toLocaleDateString(DEFAULT_LOCALE) ?? value.to?.toLocaleString()}`; } return value?.relativeTimeString; }, }); createBuiltInType("granularity", { transform: (value) => value, optionLabel: (value) => value, }); createBuiltInType("dataset"); createBuiltInType("measure"); createBuiltInType("dimension"); createBuiltInType("dimensionOrMeasure"); const embeddableTypeSchema = z .object({ typeConfig: z.object({ label: z.string(), toString: z.function(), }), }) .or(z.enum(ALL_NATIVE_TYPES)); const editorMetaSchema = z .object({ name: z.string(), label: z.string(), type: embeddableTypeSchema, config: z.object({}).optional(), }) .strict(); const editorMetaValidator = (metaInfo) => { const result = editorMetaSchema.safeParse(metaInfo.meta); if (result.success) return []; return errorFormatter(result.error.issues); }; const embeddableInputTypeSchema = z .object({ typeConfig: z.object({ label: z.string(), toString: z.function(), }), }) .or(z.enum(DEFAULT_NATIVE_TYPES)); const embeddableInputSupportedTypesSchema = z .union([z.enum(DIMENSION_TYPES), z.enum(MEASURE_TYPES)]) .array() .optional(); const uniqueNameRefinement = (items, refinementContext, message) => { const names = items.map((item) => item.name); if (new Set(names).size !== names.length) { const duplicateNames = names.filter((name, index) => names.indexOf(name) !== index); refinementContext.addIssue({ code: z.ZodIssueCode.custom, message: `${message} Duplicate names: ${duplicateNames.join(", ")}`, }); } }; const componentMetaSchema = z .object({ name: z.string(), label: z.string(), category: z.string().optional(), classNames: z.string().array().min(1).optional(), defaultWidth: z.number().optional(), defaultHeight: z.number().optional(), inputs: z .object({ name: z.string(), label: z.string(), description: z.string().optional(), defaultValue: z.any().optional(), config: z.record(z.string(), z.any()).optional(), type: embeddableTypeSchema, array: z.boolean().optional(), category: z.string().optional(), required: z.boolean().optional(), inputs: z .object({ name: z.string(), label: z.string(), description: z.string().optional(), defaultValue: z.any().optional(), config: z.record(z.string(), z.any()).optional(), type: embeddableInputTypeSchema, required: z.boolean().optional(), supportedTypes: embeddableInputSupportedTypesSchema, }) .superRefine((subInput, ctx) => { if (subInput.name === "granularity") { if (!subInput.supportedTypes || JSON.stringify(subInput.supportedTypes) !== JSON.stringify(["time"])) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: `A 'granularity' sub-input must have 'supportedTypes' of ['time'].`, path: ["supportedTypes"], }); } if (subInput.type !== "granularity") { ctx.addIssue({ code: z.ZodIssueCode.custom, message: `A 'granularity' sub-input must have a 'type' of 'granularity'.`, path: ["type"], }); } const allowedKeys = [ "name", "type", "label", "defaultValue", "supportedTypes", ]; const extraKeys = Object.keys(subInput).filter((key) => !allowedKeys.includes(key)); if (extraKeys.length > 0) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: `The 'granularity' sub-input has forbidden properties: ${extraKeys.join(", ")}. Only 'label' and 'defaultValue' are allowed.`, path: [extraKeys[0]], }); } } }) .array() .superRefine((inputs, ctx) => uniqueNameRefinement(inputs, ctx, "SubInput names must be unique.")) .optional(), }) .strict() .array() .superRefine((inputs, ctx) => uniqueNameRefinement(inputs, ctx, "Input names must be unique.")) .superRefine((inputs, ctx) => { for (const input of inputs) { if (input.type === "dimension" && input.inputs) { const granularityInput = input.inputs.find((sub) => sub.name === "granularity"); if (granularityInput) { if (!granularityInput.supportedTypes || JSON.stringify(granularityInput.supportedTypes) !== JSON.stringify(["time"])) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: `Input '${input.name}': a 'granularity' sub-input must have 'supportedTypes' of ['time'].`, path: [input.name, "inputs", "granularity", "supportedTypes"], }); } if (granularityInput.type !== "granularity") { ctx.addIssue({ code: z.ZodIssueCode.custom, message: `Input '${input.name}': the 'granularity' sub-input must have a 'type' of 'granularity'.`, path: [input.name, "inputs", "granularity", "type"], }); } const allowedKeys = [ "name", "type", "label", "defaultValue", "supportedTypes", ]; const extraKeys = Object.keys(granularityInput).filter((key) => !allowedKeys.includes(key)); if (extraKeys.length > 0) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: `Input '${input.name}': the 'granularity' sub-input has forbidden overrides: ${extraKeys.join(", ")}. Only 'label' and 'defaultValue' are allowed.`, path: [input.name, "inputs", "granularity"], }); } } } } }) .optional(), events: z .object({ name: z.string(), label: z.string(), properties: z .object({ name: z.string(), type: embeddableTypeSchema, array: z.boolean().optional(), label: z.string().optional(), }) .array() .optional(), }) .strict() .array() .optional(), variables: z .object({ name: z.string(), type: embeddableTypeSchema, array: z.boolean().optional(), defaultValue: z.any().optional(), inputs: z.array(z.string()).optional(), events: z .array(z.object({ name: z.string(), property: z.string(), })) .optional(), }) .strict() .array() .optional(), }) .strict() .superRefine(({ defaultWidth, defaultHeight }, refinementContext) => { const widthAndHeight = [defaultHeight, defaultWidth].filter(Boolean); if (widthAndHeight.length === 1) { refinementContext.addIssue({ code: z.ZodIssueCode.custom, message: "both defaultWidth and defaultHeight must be set", path: ["defaultWidth | defaultHeight"], }); } }); // @ts-ignore const babelTraverse = traverse.default; const parseAndTraverse = (code, visitor) => { const ast = parse(code || "", { sourceType: "module", }); babelTraverse(ast, visitor); }; const componentMetaValidator = (metaInfo) => { const result = componentMetaSchema.safeParse(metaInfo.meta); if (!result.success) return errorFormatter(result.error.issues); let moduleNameErrors = validateModuleName(metaInfo); const variableErrors = validateVariables(metaInfo.meta); const eventErrors = validateComponentEvents(metaInfo); const loadDataErrors = validateComponentProps(metaInfo); return [ ...moduleNameErrors, ...variableErrors, ...eventErrors, ...loadDataErrors, ]; }; const validateComponentProps = (metaInfo) => { const errors = []; parseAndTraverse(metaInfo.moduleInfo.code, { ExportDefaultDeclaration: (path) => { var _a; const componentConfig = path.node.declaration .arguments[2]; const propsNode = (_a = componentConfig.properties) === null || _a === void 0 ? void 0 : _a.find((x) => { var _a; return ((_a = x.key) === null || _a === void 0 ? void 0 : _a.name) === "props"; }); // There is no props defined if (!propsNode) return; // if propsNode is a function defined elsewhere if (propsNode.value.type === "Identifier") { const functionName = propsNode.value.name; path .findParent((path) => path.isProgram()) .traverse({ VariableDeclarator(path) { if (path.node.id.name === functionName && (path.node.init.type === "FunctionExpression" || path.node.init.type === "ArrowFunctionExpression")) { path.node.init.body.body; } }, }); } else { // assume that propsNode is anonymous function propsNode.value.body.body; } }, }); return errors; }; const validateComponentEvents = (metaInfo) => { var _a, _b; const definedEvents = (_b = (_a = metaInfo.meta.events) === null || _a === void 0 ? void 0 : _a.map((e) => e.name)) !== null && _b !== void 0 ? _b : []; let implementedEvents = []; const errors = []; parseAndTraverse(metaInfo.moduleInfo.code, { ExportDefaultDeclaration: (path) => { var _a, _b, _c, _d; const componentConfig = path.node.declaration .arguments[2]; const eventsNode = (_a = componentConfig.properties) === null || _a === void 0 ? void 0 : _a.find((p) => { var _a; return ((_a = p.key) === null || _a === void 0 ? void 0 : _a.name) === "events"; }); implementedEvents = (_d = (_c = (_b = eventsNode === null || eventsNode === void 0 ? void 0 : eventsNode.value) === null || _b === void 0 ? void 0 : _b.properties) === null || _c === void 0 ? void 0 : _c.map((p) => p.key.name)) !== null && _d !== void 0 ? _d : []; }, }); const definedEventsSet = new Set(definedEvents); const implementedEventsSet = new Set(implementedEvents); const notImplementedEvents = definedEvents.filter((e) => !implementedEventsSet.has(e)); const notDefinedEvents = implementedEvents.filter((e) => !definedEventsSet.has(e)); for (const notDefinedEvent of notDefinedEvents) { errors.push(`event '${notDefinedEvent}' is not defined in the component's meta`); } for (const notImplementedEvent of notImplementedEvents) { errors.push(`event '${notImplementedEvent}' is defined but not implemented`); } return errors; }; const validateModuleName = (metaInfo) => { const basename = path.basename(metaInfo.moduleId); const fileName = basename.split(".")[0]; if (fileName !== metaInfo.meta.name) { return [ `meta.name: The name of the .emb.* file and [meta.name]'s value must match. In this case, [meta.name] must be ${fileName}`, ]; } return []; }; const validateVariables = (meta) => { const variables = meta.variables; if (!variables) return []; const errors = []; // all inputs should be defined errors.push(...validateVariableInputs(meta)); // all events and properties should be valid errors.push(...validateVariableEvents(meta)); return errors; }; const validateVariableInputs = (meta) => { const variables = meta.variables; const definedInputs = meta.inputs; if (!variables) return []; const errors = []; // all inputs should be defined variables.forEach((variableConfig, idx) => { const inputs = variableConfig.inputs; if (inputs) { inputs.forEach((input, inputIdx) => { const definedInput = definedInputs === null || definedInputs === void 0 ? void 0 : definedInputs.find((d) => d.name === input); if (!definedInput) { const path = formatErrorPath(["variables", idx, "inputs", inputIdx]); errors.push(`${path}: input "${input}" is not defined`); } }); } }); return errors; }; const validateVariableEvents = (meta) => { const variables = meta.variables; const definedEvents = meta.events; if (!variables) return []; const errors = []; variables.forEach((variableConfig, idx) => { const events = variableConfig.events; if (events) { events.forEach((event, eventIdx) => { const definedEvent = definedEvents === null || definedEvents === void 0 ? void 0 : definedEvents.find((d) => d.name === event.name); if (!definedEvent) { const path = formatErrorPath([ "variables", idx, "events", eventIdx, "name", ]); errors.push(`${path}: event "${event.name}" is not defined`); return; } const definedProperties = definedEvent.properties; const definedProperty = definedProperties === null || definedProperties === void 0 ? void 0 : definedProperties.find((p) => p.name === event.property); if (!definedProperty) { const path = formatErrorPath([ "variables", idx, "events", eventIdx, "property", ]); errors.push(`${path}: property "${event.property}" is not defined`); return; } const path = formatErrorPath(["variables", idx]); if (definedProperty.type !== variableConfig.type) { errors.push(`${path}: the type of the variable "${variableConfig.name}" and the type of the property "${event.property}" do not match`); } if (Boolean(definedProperty.array) !== Boolean(variableConfig.array)) { errors.push(`${path}: the array of the variable "${variableConfig.name}" and the array of the property "${event.property}" do not match`); } }); } }); return errors; }; const EDITOR = "editor"; const COMPONENT = "component"; const validators = { [EDITOR]: editorMetaValidator, [COMPONENT]: componentMetaValidator, }; const getModuleType = (moduleInfo) => { var _a, _b; if ((_a = moduleInfo.code) === null || _a === void 0 ? void 0 : _a.includes("defineComponent")) { return COMPONENT; } else if ((_b = moduleInfo.code) === null || _b === void 0 ? void 0 : _b.includes("defineEditor")) { return EDITOR; } else { return null; } }; var validateComponentMetaPlugin = (componentFileRegex) => { let metaConfigs = []; return { name: "validate-component-meta", moduleParsed: async (moduleInfo) => { const moduleType = componentFileRegex.test(moduleInfo.id) ? getModuleType(moduleInfo) : null; if (moduleType) { const meta = await loadComponentMeta(moduleInfo.id); metaConfigs.push({ moduleId: moduleInfo.id, meta, moduleType, moduleInfo, }); } }, buildEnd: function () { const errors = []; for (const metaConfig of metaConfigs) { const validator = validators[metaConfig.moduleType]; const validationResult = validator(metaConfig); if (validationResult && validationResult.length) { errors.push(`\nComponent meta is not valid ${metaConfig.moduleId}:\n${validationResult.join("\n")}`); } } metaConfigs = []; if (errors.length) { // @ts-ignore this.error(errors.join("\n")); } }, }; }; /** * Checks if a given package exists in dependencies or devDependencies of a package.json object. * @param packageJson The parsed package.json object. * @param packageName The name of the package to check. * @returns True if the package exists, false otherwise. */ function hasPackage(packageJson, packageName) { var _a, _b, _c; return !!((_b = (_a = packageJson.dependencies) === null || _a === void 0 ? void 0 : _a[packageName]) !== null && _b !== void 0 ? _b : (_c = packageJson.devDependencies) === null || _c === void 0 ? void 0 : _c[packageName]); } /** * Modifies entry point only if Emotion is installed in the client's package.json. * Wraps the app with CacheProvider and creates a custom cache to inject Emotion styles * into the Shadow DOM or desired container. */ const emotionReactEntrypointModifier = { getContentBegin() { return `<EmbeddableEmotionCacheProvider value={createCache({ key: 'css', container: rootEl.getRootNode() })}>`; }, getContentEnd() { return `</EmbeddableEmotionCacheProvider>`; }, getImports() { return ` import { CacheProvider as EmbeddableEmotionCacheProvider } from '@emotion/react'; import createCache from '@emotion/cache'; `; }, async needToModify(ctx) { const packageJsonFilePath = path.resolve(ctx.client.rootDir, "package.json"); try { const packageJson = JSON.parse(await fs.readFile(packageJsonFilePath, "utf-8")); return !!(hasPackage(packageJson, "@emotion/react") || hasPackage(packageJson, "react-select")); } catch (error) { console.error("emotionReactEntrypointModifier: Error reading package.json", error); return false; } }, }; /** * Modifies entry point only if there is 'styled-components' in the client's package.json. * Imports StyleSheetManager from styled-components and adds shadow root as "target". * That will force styled-components to add classes to the customers web-component * see https://styled-components.com/docs/api#stylesheetmanager */ const styledComponentsEntrypointModifier = { getContentBegin() { return "<StyleSheetManager target={rootEl.getRootNode()}>"; }, getContentEnd() { return "</StyleSheetManager>"; }, getImports() { return "import {StyleSheetManager} from 'styled-components'"; }, async needToModify(ctx) { const packageJsonFilePath = path.resolve(ctx.client.rootDir, "package.json"); try { const packageJson = JSON.parse(await fs.readFile(packageJsonFilePath, "utf-8")); return !!hasPackage(packageJson, "styled-components"); } catch (error) { console.error("styledComponentsEntrypointModifier: Error reading package.json", error); return false; } }, }; const entrypointModifiers = [ styledComponentsEntrypointModifier, emotionReactEntrypointModifier, ]; function findFilesWithWrongUsage(directory, pattern, mustEndWith = []) { const files = fs$1.readdirSync(directory); const result = []; files.forEach((file) => { const filePath = path.join(directory, file); if (fs$1.statSync(filePath).isDirectory()) { // Recursively check files in subdirectories result.push(...findFilesWithWrongUsage(filePath, pattern, mustEndWith)); } else { const fileContent = fs$1.readFileSync(filePath, "utf8"); if (fileContent.match(pattern)) { result.push(filePath); } } }); return result.filter((file) => !mustEndWith.some((ending) => file.endsWith(ending))); } function findTypeNames(directory) { const files = fs$1.readdirSync(directory); const typeNames = []; files.forEach((file) => { const filePath = path.join(directory, file); if (fs$1.statSync(filePath).isDirectory()) { // Recursively check files in subdirectories typeNames.push(...findTypeNames(filePath)); } else { const fileContent = fs$1.readFileSync(filePath, "utf8"); const regexDefineTypeFunctionTypeName = /defineType\(["'](.*?)["']\,/g; const matches = fileContent.match(regexDefineTypeFunctionTypeName) || []; const extractedContent = matches.map((match) => match.slice(12, -2))[0]; if (extractedContent) { typeNames.push({ file: filePath, typeName: extractedContent }); } } }); return typeNames; } const regexDefineComponentFunction = /defineComponent(<\w+>)?\(/; const regexDefineTypeFunction = /defineType\(/; const regexDefineOptionFunction = /defineOption\(/; const regexCubeFunction = /cube\(/; const regexCubes = /cubes:/; const usageValidator = async (directory, validateCube = true) => { let isValid = true; const progress = ora("React: function usage validating...").start(); // defineComponent function const filesWithWrongDefineComponentFunctionUsage = findFilesWithWrongUsage(directory, regexDefineComponentFunction, [".emb.js", ".emb.ts"]); if (filesWithWrongDefineComponentFunctionUsage.length) { isValid = false; progress.fail("React: defineComponent() usage is only allowed inside files with the extension .emb.(js|ts) files.\nFix the following files:\n" + filesWithWrongDefineComponentFunctionUsage.join("\n")); } // defineType function const filesWithWrongDefineTypeFunctionUsage = findFilesWithWrongUsage(directory, regexDefineTypeFunction, [".type.emb.js", ".type.emb.ts"]); if (filesWithWrongDefineTypeFunctionUsage.length) { isValid = false; progress.fail("React: defineType() usage is only allowed inside files with the extension .type.emb.(js|ts) files.\nFix the following files:\n" + filesWithWrongDefineTypeFunctionUsage.join("\n")); } // defineOption function const filesWithWrongDefineOptionFunctionUsage = findFilesWithWrongUsage(directory, regexDefineOptionFunction, [".type.emb.js", ".type.emb.ts"]); if (filesWithWrongDefineOptionFunctionUsage.length) { isValid = false; progress.fail("React: defineOption() usage is only allowed inside files with the extension .type.emb.(js|ts) files.\nFix the following files:\n" + filesWithWrongDefineOptionFunctionUsage.join("\n")); } if (validateCube) { // cube function const filesWithWrongCubeFunctionUsage = findFilesWithWrongUsage(directory, regexCubeFunction, [".cube.js", ".cube.ts"]); if (filesWithWrongCubeFunctionUsage.length) { isValid = false; progress.fail("React: cube() usage is only allowed inside files with the extension .cube.(ts|js) files.\nFix the following files:\n" + filesWithWrongCubeFunctionUsage.join("\n")); } // cube in yml or yaml files const filesWithWrongCubeUsage = findFilesWithWrongUsage(directory, regexCubes, [".cube.yml", ".cube.yaml"]); if (filesWithWrongCubeUsage.length) { isValid = false; progress.fail("React: cubes: usage is only allowed inside files with the extension .cube.(yml|yaml) files.\nFix the following files:\n" + filesWithWrongCubeUsage.join("\n")); } } const typeNames = findTypeNames(directory); // check for definedTypes using a native typeNames const duplicatedNativeTypeNames = typeNames.filter((item) => ALL_NATIVE_TYPES.includes(item.typeName)); if (duplicatedNativeTypeNames.length) { isValid = false; progress.fail("React: defineType() can't be used with the same typeName as the nativeTypes. Fix the following files:\n" + duplicatedNativeTypeNames .map((dtn) => `${dtn.typeName} (file: ${dtn.file})`) .join("\n")); } // check for duplicated typeNames const duplicatedTypeNames = typeNames.filter((item, index) => { return typeNames.some((otherItem, otherIndex) => { if (index !== otherIndex) { return item.typeName === otherItem.typeName; } return false; }); }); if (duplicatedTypeNames.length) { isValid = false; progress.fail("React: defineType() must be used with a unique typeName. Fix the following files:\n" + duplicatedTypeNames .map((dtn) => `${dtn.typeName} (file: ${dtn.file})`) .join("\n")); } isValid ? progress.succeed("React: function usage validated") : process.exit(1); }; const validateComponentDuplicates = (filesList, additionalComponents, ora) => { const progress = ora("React: validating component names...").start(); const errors = []; // Ensure filesList components are unique const filesSet = new Set(); for (const [componentName] of filesList) { if (filesSet.has(componentName)) { errors.push(`Error: Component '${componentName}' must be unique.`); } else { filesSet.add(componentName); } } // Ensure no filesList component exists in additionalComponents values for (const [componentName] of filesList) { for (const [library, components] of Object.entries(additionalComponents)) { if (components.includes(componentName)) { errors.push(`Error: Component '${componentName}' must be unique but is already present in library '${library}'.`); } } } // Ensure additionalComponents values are unique across different keys const componentToLibraries = {}; for (const [library, components] of Object.entries(additionalComponents)) { for (const component of components) { if (!componentToLibraries[component]) { componentToLibraries[component] = []; } componentToLibraries[component].push(library); } } for (const [component, libraries] of Object.entries(componentToLibraries)) { if (libraries.length > 1) { errors.push(`Error: Component '${component}' is declared in multiple libraries: ${libraries.join(", ")}.`); } } if (errors.length > 0) { progress.fail("Validation errors found:\n"); console.error(errors.join("\n")); } else { progress.succeed("Validation completed successfully. No issues found."); } return errors; }; const EMB_FILE_REGEX = /^(.*)(?<!\.type|\.options)\.emb\.[jt]s$/; var generate = async (ctx) => { var _a; await usageValidator(ctx.client.srcDir); const filesList = await findFiles(ctx.client.srcDir, EMB_FILE_REGEX); const additionalComponents = await getAdditionalComponents$1(ctx); const errors = validateComponentDuplicates(filesList, additionalComponents, ora); if (errors.length) { process.exit(1); } await prepareEntrypoint(ctx, filesList, additionalComponents); let result; if ((_a = ctx.dev) === null || _a === void 0 ? void 0 : _a.watch) { result = await runViteWatch(ctx); } else { const progress = ora("React: building components...").start(); result = await runViteBuild(ctx); progress.succeed("Building React components completed"); } return result; }; function getViteConfig(ctx, watch, plugins) { var _a; return { logLevel: (!!watch ? "info" : "error"), resolve: { extensions: [".ts", ".tsx", ".js", ".jsx", ".json"], ...(_a = ctx.client.vi