UNPKG

@embeddable.com/sdk-core

Version:

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

230 lines (191 loc) 6.58 kB
import * as fs from "node:fs/promises"; import * as path from "node:path"; import { createNodeLogger, createNodeSys } from "@stencil/core/sys/node"; import { createCompiler, loadConfig } from "@stencil/core/compiler"; import { PluginName, ResolvedEmbeddableConfig } from "./defineConfig"; import { findFiles, getComponentLibraryConfig, } from "@embeddable.com/sdk-utils"; import * as sorcery from "sorcery"; const STYLE_IMPORTS_TOKEN = "{{STYLES_IMPORT}}"; const RENDER_IMPORT_TOKEN = "{{RENDER_IMPORT}}"; // stencil doesn't support dynamic component tag name, so we need to replace it manually const COMPONENT_TAG_TOKEN = "replace-this-with-component-name"; export default async ( ctx: ResolvedEmbeddableConfig, pluginName: PluginName, ) => { await injectCSS(ctx, pluginName); await injectBundleRender(ctx, pluginName); await runStencil(ctx); await generateSourceMap(ctx, pluginName); }; async function injectCSS( ctx: ResolvedEmbeddableConfig, pluginName: PluginName, ) { const CUSTOMER_BUILD = path.resolve( ctx.client.buildDir, ctx[pluginName].outputOptions.buildName, ); const allFiles = await fs.readdir(CUSTOMER_BUILD); const imports = allFiles .filter((fileName) => fileName.endsWith(".css")) .map( (fileName) => `@import '../../${ctx[pluginName].outputOptions.buildName}/${fileName}';`, ); const componentLibraries = ctx.client.componentLibraries; for (const componentLibrary of componentLibraries) { const { libraryName } = getComponentLibraryConfig(componentLibrary); const allLibFiles = await fs.readdir( path.resolve(ctx.client.rootDir, "node_modules", libraryName, "dist"), ); allLibFiles .filter((fileName) => fileName.endsWith(".css")) .forEach((fileName) => imports.push(`@import '~${libraryName}/dist/${fileName}';`), ); } const cssFilesImportsStr = imports.join("\n"); const content = await fs.readFile( path.resolve(ctx.core.templatesDir, "style.css.template"), "utf8", ); await fs.writeFile( path.resolve(ctx.client.componentDir, "style.css"), content.replace(STYLE_IMPORTS_TOKEN, cssFilesImportsStr), ); } async function injectBundleRender( ctx: ResolvedEmbeddableConfig, pluginName: PluginName, ) { const importStr = `import render from '../../${ctx[pluginName].outputOptions.buildName}/${ctx[pluginName].outputOptions.fileName}';`; let content = await fs.readFile( path.resolve(ctx.core.templatesDir, "component.tsx.template"), "utf8", ); if (!!ctx.dev?.watch) { content = content.replace(COMPONENT_TAG_TOKEN, "embeddable-component"); } await fs.writeFile( path.resolve(ctx.client.componentDir, "component.tsx"), content.replace(RENDER_IMPORT_TOKEN, importStr), ); } async function addComponentTagName(filePath: string, bundleHash: string) { // find entry file with a name *.entry.js const entryFiles = await findFiles(path.dirname(filePath), /.*\.entry\.js/); if (!entryFiles.length) { return; } const entryFileName = entryFiles[0]; const [entryFileContent, fileContent] = await Promise.all([ fs.readFile(entryFileName[1], "utf8"), fs.readFile(filePath, "utf8"), ]); const newFileContent = fileContent.replace( COMPONENT_TAG_TOKEN, `embeddable-component-${bundleHash}`, ); const newEntryFileContent = entryFileContent.replace( COMPONENT_TAG_TOKEN.replaceAll("-", "_"), `embeddable_component_${bundleHash}`, ); await Promise.all([ fs.writeFile(filePath, newFileContent), fs.writeFile(entryFileName[1], newEntryFileContent), ]); } async function runStencil(ctx: ResolvedEmbeddableConfig): Promise<void> { const logger = ctx.dev?.logger || createNodeLogger(); const sys = ctx.dev?.sys || createNodeSys({ process }); const devMode = !!ctx.dev?.watch; const isWindows = process.platform === "win32"; const validated = await loadConfig({ initTsConfig: true, logger, sys, config: { devMode, maxConcurrentWorkers: isWindows ? 0 : 8, // workers break on windows rootDir: ctx.client.webComponentRoot, configPath: path.resolve( ctx.client.webComponentRoot, "stencil.config.ts", ), tsconfig: path.resolve(ctx.client.webComponentRoot, "tsconfig.json"), namespace: "embeddable-wrapper", srcDir: ctx.client.componentDir, sourceMap: true, // always generate source maps in both dev and prod minifyJs: !devMode, minifyCss: !devMode, outputTargets: [ { type: "dist", buildDir: path.resolve(ctx.client.buildDir, "dist"), }, ], }, }); const compiler = await createCompiler(validated.config); const buildResults = await compiler.build(); if (!devMode) { if (buildResults.hasError) { console.error("Stencil build error:", buildResults.diagnostics); throw new Error("Stencil build error"); } else { await handleStencilBuildOutput(ctx); } await compiler.destroy(); } process.chdir(ctx.client.rootDir); } async function handleStencilBuildOutput(ctx: ResolvedEmbeddableConfig) { const entryFilePath = path.resolve( ctx.client.stencilBuild, "embeddable-wrapper.esm.js", ); let fileName = "embeddable-wrapper.esm.js"; if (!ctx.dev?.watch && ctx.client.bundleHash) { fileName = `embeddable-wrapper.esm-${ctx.client.bundleHash}.js`; await addComponentTagName(entryFilePath, ctx.client.bundleHash); } await fs.rename( entryFilePath, path.resolve(ctx.client.stencilBuild, fileName), ); } async function generateSourceMap( ctx: ResolvedEmbeddableConfig, pluginName: PluginName, ) { const componentBuildDir = path.resolve( ctx.client.buildDir, ctx[pluginName].outputOptions.buildName, ); const stencilBuild = path.resolve(ctx.client.stencilBuild); const tmpComponentDir = path.resolve( stencilBuild, ctx[pluginName].outputOptions.buildName, ); await fs.cp(componentBuildDir, tmpComponentDir, { recursive: true }); const stencilFiles = await fs.readdir(stencilBuild); const jsFiles = stencilFiles.filter((file) => file.toLowerCase().endsWith(".js"), ); await Promise.all( jsFiles.map(async (jsFile) => { try { const chain = await sorcery.load(path.resolve(stencilBuild, jsFile)); // overwrite the existing file await chain.write(); } catch (e) { // do nothing if a map file can not be generated } }), ); await fs.rm(tmpComponentDir, { recursive: true }); }