1
0
silverbullet/plugos/compile.ts
2024-01-14 13:38:39 +01:00

187 lines
4.9 KiB
TypeScript

import { denoPlugins, esbuild, path, YAML } from "./deps.ts";
import { bundleAssets } from "./asset_bundle/builder.ts";
import { Manifest } from "./types.ts";
import { version } from "../version.ts";
const workerRuntimeUrl = new URL("./worker_runtime.ts", import.meta.url);
// const workerRuntimeUrl =
// `https://deno.land/x/silverbullet@${version}/plugos/worker_runtime.ts`;
export type CompileOptions = {
debug?: boolean;
runtimeUrl?: string;
importMap?: string;
// Reload plug import cache
reload?: 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 manifest = YAML.parse(
await Deno.readTextFile(manifestPath),
) 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();
const jsFile = `
import { setupMessageListener } from "${
options.runtimeUrl || workerRuntimeUrl
}";
// 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 "file://${
// Replacaing \ 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);
`;
// console.log("Code:", jsFile);
const inFile = await Deno.makeTempFile({ suffix: ".js" });
const outFile = `${destPath}/${manifest.name}.plug.js`;
await Deno.writeTextFile(inFile, jsFile);
const result = await esbuild.build({
entryPoints: [path.basename(inFile)],
bundle: true,
format: "esm",
globalName: "mod",
platform: "browser",
sourcemap: options.debug ? "linked" : false,
minify: !options.debug,
outfile: outFile,
metafile: options.info,
treeShaking: true,
plugins: [
// {
// name: "json",
// setup: (build) =>
// build.onLoad({ filter: /\.json$/ }, () => ({ loader: "json" })),
// },
...denoPlugins({
// TODO do this differently
importMapURL: options.importMap ||
new URL("../import_map.json", import.meta.url).toString(),
loader: "native",
}),
],
absWorkingDir: path.resolve(path.dirname(inFile)),
});
if (options.info) {
const text = await esbuild.analyzeMetafile(result.metafile!);
console.log("Bundle info for", manifestPath, text);
}
let jsCode = await Deno.readTextFile(outFile);
jsCode = patchDenoLibJS(jsCode);
await Deno.writeTextFile(outFile, jsCode);
console.log(`Plug ${manifest.name} written to ${outFile}.`);
return outFile;
}
export async function compileManifests(
manifestFiles: string[],
dist: string,
watch: boolean,
options: CompileOptions = {},
) {
let building = false;
dist = path.resolve(dist);
async function buildAll() {
if (building) {
return;
}
console.log("Building", manifestFiles);
building = true;
Deno.mkdirSync(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) {
console.error(`Error building ${manifestPath}:`, e);
}
}));
console.log(`Done building plugs in ${Date.now() - startTime}ms`);
building = false;
}
await buildAll();
if (watch) {
console.log("Watching for changes...");
const watcher = Deno.watchFs(manifestFiles.map((p) => path.dirname(p)));
for await (const event of watcher) {
if (event.paths.length > 0) {
if (
event.paths[0].endsWith(".json") || event.paths[0].startsWith(dist)
) {
continue;
}
}
console.log("Change detected, rebuilding...");
await buildAll();
}
}
}
export function patchDenoLibJS(code: string): string {
// The Deno std lib has one occurence of a regex that Webkit JS doesn't (yet parse), we'll strip it because it's likely never invoked anyway, YOLO
return code.replaceAll("/(?<=\\n)/", "/()/");
}