import { build } from "esbuild"; import { copyFileSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs"; import { execFileSync } from "node:child_process"; import { createRequire } from "node:module"; import { dirname, join } from "node:path"; // Step 1 bundles + the step-6 Service Worker. The kernel and runtime run in workers; // the host runs on the page; the SW serves the ESM virtual origin (/~/ URLs). // // dist/runtime/ is what the package ships, as one unit: its files find each other by // import.meta.url, so they must stay side by side. dist/node/ holds the Node-side modules // the package also ships (./snapshot, ./server, ./vite). dist/demo/ holds the harness // bundle, which is never packed. (slopbox's own bundles live in examples/slopbox's build.) // // @swc/wasm-typescript embeds its 2.8MB wasm as base64 in its CJS entry. The kernel // build cuts that out: the binary is written to dist/runtime/swc.wasm, which strip.ts streams // and compiles (no 3.7MB string to parse, no decode copy, a third fewer bytes on the // wire and better compression), and the entry takes the compiled module from // src/kernel/swc-module.ts. The loader's `util` import aliases to a browser shim. // // The WebSocket shim is a classic script (it must run before a guest page's own // scripts, module or not) that the SW serves as /~/ws-shim.js. It is bundled into // dist/runtime/sw.js as a string rather than shipped as a file: the SW is its only server, so // guest documents load it without a network fetch and without knowing where it is served from. const RUNTIME = "dist/runtime"; const NODE = "dist/node"; const DEMO = "dist/demo"; const TYPES = "dist/types"; const SWC_ENTRY = createRequire(import.meta.url).resolve("@swc/wasm-typescript"); const SWC_EMBED = /const \{ Buffer \} = require\('node:buffer'\);\s*const bytes = Buffer\.from\('([A-Za-z0-9+/=]+)', 'base64'\);\s*const wasmModule = new WebAssembly\.Module\(bytes\);/; const swcWasm = { name: "swc-wasm", setup(b) { b.onLoad({ filter: /[\\/]@swc[\\/]wasm-typescript[\\/]wasm\.js$/ }, (args) => { const src = readFileSync(args.path, "utf8"); const m = SWC_EMBED.exec(src); if (!m) throw new Error(`${SWC_ENTRY}: embedded wasm not found; the package's loader changed shape`); mkdirSync(RUNTIME, { recursive: true }); writeFileSync(`${RUNTIME}/swc.wasm`, Buffer.from(m[1], "base64")); const take = `const wasmModule = require(${JSON.stringify(new URL("./src/kernel/swc-module.ts", import.meta.url).pathname)}).takeSwcModule();`; return { contents: src.slice(0, m.index) + take + src.slice(m.index + m[0].length), loader: "js" }; }); }, }; // Every bundle is minified: each process and thread worker parses runtime.js, and the // page parses host.js before boot. Comments go with minification, so the bundle that // carries code derived from Node.js states its license in a banner. const common = { bundle: true, target: "es2022", minify: true }; // The compat target; the per-file "Derived from Node.js vX" headers record what was ported. const PINNED_NODE = readFileSync(new URL("../../.node-version", import.meta.url), "utf8").trim(); const NODE_NOTICE = `/*! Portions derived from Node.js v${PINNED_NODE}. MIT License; Copyright Node.js contributors, and Joyent, Inc. and other Node contributors. */`; // process.versions.nib, inlined as __NIB_VERSION__. const NIB_VERSION = JSON.parse(readFileSync(new URL("./package.json", import.meta.url), "utf8")).version; // A clean slate, so a file the build no longer emits can't linger and get packed. rmSync(RUNTIME, { recursive: true, force: true }); rmSync(NODE, { recursive: true, force: true }); rmSync(DEMO, { recursive: true, force: true }); rmSync(TYPES, { recursive: true, force: true }); const shim = await build({ ...common, entryPoints: ["src/shim/ws-shim.ts"], format: "iife", write: false }); // The guest npm command is guest code the kernel seeds into the VFS at boot, so it rides // dist/runtime/kernel.js as a string, as the WebSocket shim rides dist/runtime/sw.js. Not // minified: a guest reading /usr/local/lib/node_modules/npm/bin/npm-cli.js sees readable // code. const npmCli = await build({ bundle: true, target: "es2022", entryPoints: ["src/npm-cli/npm-cli.ts"], format: "cjs", platform: "node", banner: { js: "#!/usr/bin/env node" }, write: false, }); await Promise.all([ build({ entryPoints: ["src/host/index.ts"], ...common, format: "esm", outfile: `${RUNTIME}/host.js` }), build({ ...common, entryPoints: ["src/kernel/main.ts"], format: "esm", outfile: `${RUNTIME}/kernel.js`, alias: { util: "./src/kernel/swc-shims.ts" }, define: { __NPM_CLI__: JSON.stringify(npmCli.outputFiles[0].text) }, plugins: [swcWasm], }), // keepNames: guests observe builtin class and function names (util.inspect prints // `` and `extends EventEmitter`), so bundling must not rename them. build({ ...common, entryPoints: ["src/runtime/main.ts"], format: "esm", outfile: `${RUNTIME}/runtime.js`, keepNames: true, banner: { js: NODE_NOTICE }, define: { __NIB_VERSION__: JSON.stringify(NIB_VERSION) }, }), build({ ...common, entryPoints: ["src/sw/sw.ts"], // A classic script a site's own worker can importScripts(): an IIFE keeps the // bundle's top-level names out of that worker's global scope. format: "iife", outfile: `${RUNTIME}/sw.js`, define: { __WS_SHIM__: JSON.stringify(shim.outputFiles[0].text) }, }), // Node-side modules, unminified: they run in the consumer's build or server, where // readable stack traces beat bytes. ...["snapshot", "server"].map((name) => build({ entryPoints: [`src/node/${name}.ts`], bundle: true, platform: "node", format: "esm", target: "node20", outfile: `${NODE}/${name}.js` })), // The Vite plugin. It takes only types from vite (the consumer's copy, a peer), and it // locates dist/runtime from its own import.meta.url, two levels up like its source. build({ entryPoints: ["src/vite/index.ts"], bundle: true, platform: "node", format: "esm", target: "node20", outfile: `${NODE}/vite.js`, external: ["vite"] }), // Editor library for the harness demo (microlighter). It lazy-loads grammars // via a wildcard dynamic import — esbuild expands it at bundle time, so no // runtime fetches and dist/demo stays flat. build({ ...common, entryPoints: ["demo/vendor.ts"], format: "esm", outfile: `${DEMO}/vendor.js` }), ]); mkdirSync(DEMO, { recursive: true }); // Declarations for the public entries (src/host/index.ts, src/node/{snapshot,server}.ts, // src/vite/index.ts, whose types import vite's) // and the modules they reference. tsc's program also covers every module an entry // imports only for its own internal implementation (shared/protocol.ts and what IT // pulls in), so it emits a .d.ts for those too, even though nothing in them is part of // the public API. Prune the output down to what the entries' import graphs reach. execFileSync(process.execPath, [createRequire(import.meta.url).resolve("typescript/bin/tsc"), "-p", "tsconfig.build.json"], { stdio: "inherit" }); pruneUnreachableTypes([`${TYPES}/host/index.d.ts`, `${TYPES}/node/snapshot.d.ts`, `${TYPES}/node/server.d.ts`, `${TYPES}/vite/index.d.ts`]); console.log(`built ${RUNTIME}/{host,kernel,runtime,sw}.js + swc.wasm, ${NODE}/{snapshot,server,vite}.js, ${TYPES}/, ${DEMO}/vendor.js`); function pruneUnreachableTypes(entries) { const keep = new Set(); const queue = [...entries]; while (queue.length) { const file = queue.pop(); if (keep.has(file)) continue; keep.add(file); const src = readFileSync(file, "utf8"); for (const m of src.matchAll(/\bfrom\s*["']([^"']+)["']/g)) { if (!m[1].startsWith(".")) continue; // a bare specifier never resolves inside dist/types queue.push(join(dirname(file), m[1].replace(/\.js$/, ".d.ts"))); } } const allDts = []; const walk = (dir) => { for (const name of readdirSync(dir)) { const p = join(dir, name); if (statSync(p).isDirectory()) walk(p); else if (p.endsWith(".d.ts")) allDts.push(p); } }; walk(TYPES); for (const file of allDts) if (!keep.has(file)) unlinkSync(file); }