204 lines
5.4 KiB
TypeScript
204 lines
5.4 KiB
TypeScript
import * as path from "node:path";
|
|
import { readFile, writeFile, mkdtemp, rm, mkdir } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import * as YAML from "js-yaml";
|
|
|
|
import * as esbuild from "esbuild";
|
|
import { bundleAssets } from "../asset_bundle/builder.ts";
|
|
import type { Manifest } from "./types.ts";
|
|
|
|
// When bundled by build_plug_compile.ts, this is replaced with the pre-bundled
|
|
// worker runtime JS. When running from source, it's undefined and esbuild
|
|
// resolves the .ts file directly (it handles TypeScript natively).
|
|
declare const __EMBEDDED_WORKER_RUNTIME_JS__: string | undefined;
|
|
|
|
const workerRuntimePlugin: esbuild.Plugin = {
|
|
name: "worker-runtime",
|
|
setup(build) {
|
|
if (typeof __EMBEDDED_WORKER_RUNTIME_JS__ !== "undefined") {
|
|
// Bundled CLI: serve pre-bundled JS from the embedded constant
|
|
build.onResolve({ filter: /^worker-runtime$/ }, () => ({
|
|
path: "worker-runtime",
|
|
namespace: "worker-runtime",
|
|
}));
|
|
build.onLoad({ filter: /.*/, namespace: "worker-runtime" }, () => ({
|
|
contents: __EMBEDDED_WORKER_RUNTIME_JS__,
|
|
loader: "js",
|
|
}));
|
|
} else {
|
|
// From source: point directly at the .ts file, esbuild handles it
|
|
build.onResolve({ filter: /^worker-runtime$/ }, () => ({
|
|
path: path.join(import.meta.dirname, "worker_runtime.ts"),
|
|
}));
|
|
}
|
|
},
|
|
};
|
|
|
|
export type CompileOptions = {
|
|
debug?: boolean;
|
|
// Print info on bundle size
|
|
info?: boolean;
|
|
};
|
|
|
|
export async function compileManifest(
|
|
manifestPath: string,
|
|
destPath: string,
|
|
options: CompileOptions = {},
|
|
): Promise<string> {
|
|
const rootPath = path.dirname(manifestPath);
|
|
const manifestContent = await readFile(manifestPath, "utf-8");
|
|
const manifest = YAML.load(manifestContent) as Manifest<any>;
|
|
|
|
if (!manifest.name) {
|
|
throw new Error(`Missing 'name' in ${manifestPath}`);
|
|
}
|
|
|
|
// Assets
|
|
const assetsBundle = await bundleAssets(
|
|
path.resolve(rootPath),
|
|
(manifest.assets as string[]) || [],
|
|
);
|
|
manifest.assets = assetsBundle.toJSON();
|
|
|
|
// Normalize the edge case of a plug with no functions
|
|
if (!manifest.functions) {
|
|
manifest.functions = {};
|
|
}
|
|
|
|
const jsFile = `
|
|
import { setupMessageListener } from "worker-runtime";
|
|
|
|
// Imports
|
|
${Object.entries(manifest.functions)
|
|
.map(([funcName, def]) => {
|
|
if (!def.path) {
|
|
return "";
|
|
}
|
|
let [filePath, jsFunctionName] = def.path.split(":");
|
|
// Resolve path
|
|
filePath = path.join(rootPath, filePath);
|
|
|
|
return `import {${jsFunctionName} as ${funcName}} from "${
|
|
// Replacing \ with / for Windows
|
|
path.resolve(filePath).replaceAll("\\", "\\\\")
|
|
}";\n`;
|
|
})
|
|
.join("")}
|
|
|
|
// Function mapping
|
|
const functionMapping = {
|
|
${Object.entries(manifest.functions)
|
|
.map(([funcName, def]) => {
|
|
if (!def.path) {
|
|
return "";
|
|
}
|
|
return ` ${funcName}: ${funcName},\n`;
|
|
})
|
|
.join("")}
|
|
};
|
|
|
|
// Manifest
|
|
const manifest = ${JSON.stringify(manifest, null, 2)};
|
|
|
|
export const plug = {manifest, functionMapping};
|
|
|
|
setupMessageListener(functionMapping, manifest, self.postMessage);
|
|
`;
|
|
|
|
// console.log("Code:", jsFile);
|
|
|
|
const tempDir = await mkdtemp(path.join(tmpdir(), "plug-compile-"));
|
|
const inFile = path.join(tempDir, "input.js");
|
|
const outFile = `${destPath}/${manifest.name}.plug.js`;
|
|
await writeFile(inFile, jsFile, "utf-8");
|
|
|
|
const result = await esbuild.build({
|
|
entryPoints: [inFile],
|
|
bundle: true,
|
|
format: "esm",
|
|
globalName: "mod",
|
|
platform: "browser",
|
|
sourcemap: "linked",
|
|
minify: !options.debug,
|
|
outfile: outFile,
|
|
metafile: options.info,
|
|
treeShaking: true,
|
|
plugins: [workerRuntimePlugin],
|
|
});
|
|
|
|
if (options.info) {
|
|
const text = await esbuild.analyzeMetafile(result.metafile!);
|
|
console.log("Bundle info for", manifestPath, text);
|
|
}
|
|
|
|
let jsCode = await readFile(outFile, "utf-8");
|
|
jsCode = patchBundledJS(jsCode);
|
|
await writeFile(outFile, jsCode, "utf-8");
|
|
|
|
// Clean up temp directory
|
|
await rm(tempDir, { recursive: true, force: true });
|
|
|
|
console.log(`Plug ${manifest.name} written to ${outFile}.`);
|
|
return outFile;
|
|
}
|
|
|
|
export async function compileManifests(
|
|
manifestFiles: string[],
|
|
dist: string,
|
|
options: CompileOptions = {},
|
|
) {
|
|
let building = false;
|
|
dist = path.resolve(dist);
|
|
|
|
async function buildAll() {
|
|
if (building) {
|
|
return;
|
|
}
|
|
console.log("Building", manifestFiles);
|
|
building = true;
|
|
await mkdir(dist, { recursive: true });
|
|
const startTime = Date.now();
|
|
// Build all plugs in parallel
|
|
await Promise.all(
|
|
manifestFiles.map(async (plugManifestPath) => {
|
|
const manifestPath = plugManifestPath as string;
|
|
try {
|
|
await compileManifest(manifestPath, dist, options);
|
|
} catch (e: any) {
|
|
console.error(`Error building ${manifestPath}:`, e.message);
|
|
throw e;
|
|
}
|
|
}),
|
|
);
|
|
console.log(`Done building plugs in ${Date.now() - startTime}ms`);
|
|
building = false;
|
|
}
|
|
|
|
await buildAll();
|
|
}
|
|
|
|
export function patchBundledJS(code: string): string {
|
|
// One bundled dependency has a lookbehind regex that WebKit can't parse; replace it with a no-op
|
|
return code.replaceAll("/(?<=\\n)/", "/()/");
|
|
}
|
|
|
|
export async function plugCompileCommand(
|
|
{
|
|
dist,
|
|
debug,
|
|
info,
|
|
}: {
|
|
dist: string;
|
|
debug: boolean;
|
|
info: boolean;
|
|
},
|
|
...manifestPaths: string[]
|
|
) {
|
|
await compileManifests(manifestPaths, dist, {
|
|
debug: debug,
|
|
info: info,
|
|
});
|
|
await esbuild.stop();
|
|
process.exit(0);
|
|
}
|