UNPKG

@embeddable.com/sdk-core

Version:

Core Embeddable SDK module responsible for web-components bundling and publishing.

254 lines (218 loc) 7.69 kB
import * as fsSync from "node:fs"; import * as fs from "node:fs/promises"; import * as path from "node:path"; import * as vite from "vite"; import { existsSync } from "node:fs"; import ora from "ora"; import { EXTERNAL_LIBRARY_GLOBAL_HOOKS_META_NAME, getComponentLibraryConfig, getContentHash, getGlobalHooksMeta, } from "@embeddable.com/sdk-utils"; import { ResolvedEmbeddableConfig } from "./defineConfig"; import { RollupWatcher, RollupWatcherEvent } from "rollup"; const TEMP_JS_HOOK_FILE = "embeddableThemeHook.js"; const LIFECYCLE_OUTPUT_NAME = "embeddable-lifecycle"; const THEME_PROVIDER_OUTPUT_NAME = "embeddable-theme"; export default async (ctx: ResolvedEmbeddableConfig) => { const watch = ctx.dev?.watch; const progress = watch ? undefined : ora("Building global hooks...").start(); try { await fs.mkdir(ctx.client.tmpDir, { recursive: true }); const { fileName: themeProvider, watcher: themeWatcher } = await buildThemeHook(ctx); const { lifecycleHooks, watcher: lifecycleWatcher } = await buildLifecycleHooks(ctx); await saveGlobalHooksMeta(ctx, themeProvider, lifecycleHooks); progress?.succeed("Global hooks build completed"); return { themeWatcher, lifecycleWatcher }; } catch (error) { progress?.fail("Global hooks build failed"); throw error; } }; /** * Build theme hooks for a given component library. */ async function buildThemeHook(ctx: ResolvedEmbeddableConfig) { const componentLibraries = ctx.client.componentLibraries; const repoThemeHookExists = existsSync(ctx.client.customizationFile); const imports = []; const functionNames = []; for (let i = 0; i < componentLibraries.length; i++) { const libraryConfig = componentLibraries[i]; const { libraryName } = getComponentLibraryConfig(libraryConfig); const libMeta = await getGlobalHooksMeta(ctx, libraryName); const themeProvider = libMeta.themeProvider; if (!themeProvider) continue; // Prepare imports: library theme + repo theme (if exists) const functionName = `libraryThemeProvider${i}`; const libraryThemeImport = `import ${functionName} from '${libraryName}/dist/${themeProvider}'`; functionNames.push(functionName); imports.push(libraryThemeImport); } if (!imports.length && !repoThemeHookExists) { return { fileName: undefined, watcher: undefined }; } const repoThemeImport = repoThemeHookExists ? `import localThemeProvider from '${ctx.client.customizationFile.replace(/\\/g, "/")}';` : "const localThemeProvider = () => {};"; // Generate a temporary file that imports both library and repo theme await generateTemporaryHookFile(ctx, imports, functionNames, repoThemeImport); // Build the temporary file with Vite const buildResults = await buildWithVite( ctx, getTempHookFilePath(ctx), THEME_PROVIDER_OUTPUT_NAME, ctx.dev?.watch, !ctx.dev?.watch, ); // Cleanup temporary file if (!ctx.dev?.watch) { await cleanupTemporaryHookFile(ctx); } return buildResults; } /** * Build theme hooks for a given component library. */ async function buildLifecycleHooks(ctx: ResolvedEmbeddableConfig) { const componentLibraries = ctx.client.componentLibraries; const builtLifecycleHooks: string[] = []; const repoLifecycleExist = existsSync(ctx.client.lifecycleHooksFile); let lifecycleWatcher: RollupWatcher | undefined = undefined; // If lifecycle exists, build it right away to get the hashed output if (repoLifecycleExist) { const { fileName: repoLifecycleFileName, watcher } = await buildWithVite( ctx, ctx.client.lifecycleHooksFile, LIFECYCLE_OUTPUT_NAME, ctx.dev?.watch, false, ); if (ctx.dev?.watch) { lifecycleWatcher = watcher; } builtLifecycleHooks.push(repoLifecycleFileName); } for (const libraryConfig of componentLibraries) { const { libraryName } = getComponentLibraryConfig(libraryConfig); const libMeta = await getGlobalHooksMeta(ctx, libraryName); const lifecycleHooks = libMeta.lifecycleHooks; for (const lifecycleHook of lifecycleHooks) { const libLifecycleHook = path.resolve( ctx.client.rootDir, "node_modules", libraryName, "dist", lifecycleHook, ); const { fileName: lifecycleHookFileName } = await buildWithVite( ctx, libLifecycleHook, LIFECYCLE_OUTPUT_NAME, ); builtLifecycleHooks.push(lifecycleHookFileName); } } return { lifecycleHooks: builtLifecycleHooks, watcher: lifecycleWatcher }; } /** * Write the final global hooks metadata to disk (themeHooksMeta, lifecycleHookMeta). */ async function saveGlobalHooksMeta( ctx: ResolvedEmbeddableConfig, themeProvider?: string, lifecycleHooks?: string[], ) { const metaFilePath = path.resolve( ctx.client.buildDir, EXTERNAL_LIBRARY_GLOBAL_HOOKS_META_NAME, ); const data = JSON.stringify({ themeProvider, lifecycleHooks }, null, 2); fsSync.writeFileSync(metaFilePath, data); } /** * Generate a temporary file which imports the library theme and repository theme, * replacing template placeholders. */ async function generateTemporaryHookFile( ctx: ResolvedEmbeddableConfig, libraryThemeImports: string[], functionNames: string[], repoThemeImport: string, ) { const templatePath = path.resolve( ctx.core.templatesDir, "embeddableThemeHook.js.template", ); const templateContent = await fs.readFile(templatePath, "utf8"); const newContent = templateContent .replace("{{LIBRARY_THEME_IMPORTS}}", libraryThemeImports.join("\n")) .replace("{{ARRAY_OF_LIBRARY_THEME_PROVIDERS}}", functionNames.join("\n")) .replace("{{LOCAL_THEME_IMPORT}}", repoThemeImport); // Write to temporary hook file await fs.writeFile(getTempHookFilePath(ctx), newContent, "utf8"); } /** * Build a file with Vite and return the hashed output file name (e.g., embeddable-theme-xxxx.js). */ async function buildWithVite( ctx: ResolvedEmbeddableConfig, entryFile: string, outputFile: string, watch = false, useHash = true, ) { const fileContent = await fs.readFile(entryFile, "utf8"); const fileHash = getContentHash(fileContent); // Bundle using Vite const fileName = useHash ? `${outputFile}-${fileHash}` : outputFile; const fileWatcher = await vite.build({ logLevel: watch ? "info" : "error", build: { emptyOutDir: false, lib: { entry: entryFile, formats: ["es"], fileName: fileName, }, outDir: ctx.client.buildDir, watch: watch ? {} : undefined, }, }); if (watch) { await waitForInitialBuild(fileWatcher as RollupWatcher); } const watcher: RollupWatcher | undefined = watch ? (fileWatcher as RollupWatcher) : undefined; return { fileName: `${fileName}.js`, watcher }; } /** * Remove the temporary hook file after building. */ async function cleanupTemporaryHookFile(ctx: ResolvedEmbeddableConfig) { await fs.rm(getTempHookFilePath(ctx), { force: true }); } /** * Get the path to the temporary hook file in the build directory. */ function getTempHookFilePath(ctx: ResolvedEmbeddableConfig): string { return path.resolve(ctx.client.tmpDir, TEMP_JS_HOOK_FILE); } function waitForInitialBuild(watcher: RollupWatcher): Promise<void> { return new Promise((resolve, reject) => { function onEvent(event: RollupWatcherEvent) { if (event.code === "END") { watcher.off("event", onEvent); resolve(); } else if (event.code === "ERROR") { watcher.off("event", onEvent); reject(event.error); } } watcher.on("event", onEvent); }); }