/** * Bundle plugins for the sandbox runtime. * * A stand-in for `yaakcli build --target sandbox`, which does not exist yet. * The difference from the Node target is small and entirely in the resolver: * nothing may resolve to a Node built-in, because the sandbox has none — see * `packages/plugin-sandbox/README.md` for the full contract. Bundling here * rather than in the CLI keeps the CLI out of this slice; what the CLI would * need is written down at the bottom of this file. * * Output is a generated TypeScript module holding each bundle as a string, * which is how the browser host ships them today. That is the part most * obviously temporary: see the note at the bottom. */ import { build } from "esbuild"; import { mkdirSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; const root = join(dirname(fileURLToPath(import.meta.url)), ".."); /** * The plugins the browser tier ships. * * Three, not the whole corpus: this slice is about the runtime existing and * being proven, and each of these proves a different path through it — a * template function, an importer, an authentication method. */ const PLUGINS = ["template-function-timestamp", "importer-curl", "auth-bearer"]; /** Refuse Node built-ins loudly at build time rather than at first call. */ const noNodeBuiltins = { name: "no-node-builtins", setup(build) { build.onResolve({ filter: /^(node:|fs$|path$|crypto$|buffer$|process$|os$|util$|stream$)/ }, (args) => ({ errors: [ { text: `\`${args.path}\` is not available in the sandbox runtime. ` + `Replace it with a pure-JavaScript equivalent.`, }, ], })); }, }; export async function bundlePlugin(name, { dir = join(root, "plugins", name) } = {}) { const result = await build({ entryPoints: [join(dir, "src", "index.ts")], bundle: true, write: false, // CommonJS because that is what the shell evaluates: a `new Function` with // `module`, `exports` and a `require` that only throws. format: "cjs", platform: "browser", target: "es2022", minify: false, legalComments: "none", plugins: [noNodeBuiltins], }); return result.outputFiles[0].text; } async function main() { const bundles = []; for (const name of PLUGINS) { const source = await bundlePlugin(name); bundles.push({ name, source }); console.log(`${name}: ${(source.length / 1024).toFixed(1)} KB`); } const outFile = join(root, "packages", "platform", "src", "web", "sandboxPlugins.generated.ts"); mkdirSync(dirname(outFile), { recursive: true }); writeFileSync( outFile, [ "// Generated by scripts/bundle-sandbox-plugins.mjs. Do not edit.", "//", "// The plugins the browser tier loads into its sandbox, bundled for that", "// target and inlined as source text.", "", "export interface SandboxPluginBundle {", " name: string;", " source: string;", "}", "", "export const SANDBOX_PLUGINS: SandboxPluginBundle[] = [", ...bundles.map((b) => ` { name: ${JSON.stringify(b.name)}, source: ${JSON.stringify(b.source)} },`), "];", "", ].join("\n"), ); console.log(`Wrote ${outFile}`); } if (import.meta.url === `file://${process.argv[1]}`) await main(); /* * What `yaakcli build --target sandbox` would need, beyond this: * * 1. `Platform::Browser` in the rolldown options (crates-cli/yaak-cli/src/ * commands/plugin.rs `bundler_options`), plus a resolver that fails on a * Node built-in instead of shimming it — a silent shim turns a missing * capability into a runtime error inside someone else's plugin. * 2. A `runtime` field in the plugin manifest, so a plugin declares which * target it is for and the registry can refuse to install a `node` plugin * on a host that has no Node. * 3. Both targets emitted for the same source where they both work, since a * desktop with a sandbox and a desktop with Node are the same install. * 4. Distribution as files, not as inlined strings. Inlining is what this * script does because three small bundles cost less than an asset pipeline; * the corpus does not, and a plugin the user installs at runtime cannot be * inlined at build time at all. */