@embeddable.com/sdk-core
Version:
Core Embeddable SDK module responsible for web-components bundling and publishing.
230 lines (191 loc) • 6.58 kB
text/typescript
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 });
}