Something went wrong. Try again.
Monorepo for the nibling.run platform
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561// Runtime worker entry: installs the syscall client, event loop, and process object,// then evaluates the entry script. Runs inside a (nested) worker spawned by the kernel.// The kernel bumps the mailbox heartbeat; the sync watchdog here only reads it.import type { FsResult, KernelToRuntime, KernelToRuntimeInit, ProcessRuntime, Res, ResolvedModule, RuntimeToKernel, SyscallSpec } from "../shared/protocol";import { awaitSyncReply, takeSignals, writeSyncRequest, SAB_BYTES } from "../shared/mailbox";import { builtinUrl, cjsUrl, vfsUrl, type HostToken } from "../shared/vurls";import { toNodeError, loaderError, requireNotFound, webEntryNotEsm } from "./errors";import { compileCjs, makeRequire, loadCjsByPath, newModule, type RequireContext, type ModuleCacheEntry } from "./modules";import { hasResolveHooks, resolveThroughHooks } from "./module";import { esmBegin, esmEnd, importEsm } from "./esm-sync";import { capture, runInFrame } from "./async_hooks";import { loadBuiltin } from "./builtins";import { deliverWatchEvents } from "./fs";import { installLoop, type Loop } from "./loop";import { createHttp, type HttpRuntime } from "./http";import { makeProcess, signalReceiver, uncaughtCaptures, type GuestProcess } from "./process";import { StdinStream } from "./stdin";import { StdioWriter } from "./stdout";import { ReadStream, WriteStream, installTty } from "./tty";import { join } from "./resolver";import { useNextTick } from "./ticks";import { createTimers, type Timers } from "./timers";import { installNodeGlobals, installWebGlobals } from "./globals";import { holdNativePromises } from "./native-holds";import { installConsole, makeConsole } from "./console";import { installPorts, type MessagePort as NodePort } from "./ports";import { makeWorkerThreads, type WorkerThreads } from "./worker_threads";import { makeChildProcess, type ChildProcesses } from "./child_process";
let kernelPort: MessagePort;let sab: SharedArrayBuffer;let loop!: Loop;let http!: HttpRuntime;let proc!: GuestProcess;let timers!: Timers;let threads!: WorkerThreads;let children!: ChildProcesses;/** A thread's channel to its parent: its stdio and its uncaught error go there. */let parentPort: NodePort | null = null;let takePendingSignals: () => void = () => {};let cwdValue = "/";/** The host page that owns this process; module URLs name it. Set by init. */let host!: HostToken;let nextId = 1;const pending = new Map<number, (res: Res) => void>();
// ---- typed syscall client ----// call() resolves a promise for runtime-internal awaits; submit() is the guest-facing// async form, whose result runs as a poll-phase loop callback, as libuv delivers it.
function handleReply(res: Res): void { const settle = pending.get(res.id); if (!settle) return; pending.delete(res.id); settle(res); loop.release(); // the in-flight syscall no longer holds the loop}
function send(op: keyof SyscallSpec, args: unknown, settle: (res: Res) => void): void { const id = nextId++; loop.hold(); pending.set(id, settle); kernelPort.postMessage({ t: "req", id, op, args });}
function call<K extends keyof SyscallSpec>(op: K, args: SyscallSpec[K]["args"]): Promise<SyscallSpec[K]["ret"]> { return new Promise((resolve, reject) => { send(op, args, (res) => (res.ok ? resolve(res.val as SyscallSpec[K]["ret"]) : reject(toNodeError(res.err!)))); });}
function submit<K extends keyof SyscallSpec>( op: K, args: SyscallSpec[K]["args"], done: (result: FsResult<SyscallSpec[K]["ret"]>) => void,): void { // Captured here, synchronously in the guest's call (fs.stat(), fs.watch(), ...): // the reply arrives over kernelPort.onmessage, a native callback with no frame of // its own, so without this the fs callback would always land in the root frame. const frame = capture(); send(op, args, (res) => { const result: FsResult<SyscallSpec[K]["ret"]> = res.ok ? { ok: true, val: res.val as SyscallSpec[K]["ret"] } : { ok: false, err: res.err! }; loop.enqueueCompletion(() => runInFrame(frame, done, undefined, [result])); });}
function trySync<K extends keyof SyscallSpec>(op: K, args: SyscallSpec[K]["args"]): FsResult<SyscallSpec[K]["ret"]> { writeSyncRequest(sab, op as string, args); // Doorbell: the kernel reads the real request from the SAB on receiving this message. kernelPort.postMessage({ t: "req", id: 0, op, args: null, sync: true }); return awaitSyncReply<SyscallSpec[K]["ret"]>(sab);}
function callSync<K extends keyof SyscallSpec>(op: K, args: SyscallSpec[K]["args"]): SyscallSpec[K]["ret"] { const reply = trySync(op, args); if (!reply.ok) throw toNodeError(reply.err); return reply.val;}
// ---- stdio capture (guest console → kernel → host streams) ----
const enc = new TextEncoder();function emitStdout(bytes: Uint8Array): void { if (parentPort) return parentPort.send({ k: "stdio", fd: 1, bytes }); kernelPort.postMessage({ t: "stdio", stream: "stdout", bytes } satisfies RuntimeToKernel);}function emitStderr(bytes: Uint8Array): void { if (parentPort) return parentPort.send({ k: "stdio", fd: 2, bytes }); kernelPort.postMessage({ t: "stdio", stream: "stderr", bytes } satisfies RuntimeToKernel);}
// ---- module context ----
const cache = new Map<string, ModuleCacheEntry>();const ctx: RequireContext = { call: call as RequireContext["call"], callSync: callSync as RequireContext["callSync"], trySync: trySync as RequireContext["trySync"], submit: submit as RequireContext["submit"], cache, dirnameOf: (path) => { const i = path.lastIndexOf("/"); return i <= 0 ? "/" : path.slice(0, i); }, get cwd() { return cwdValue; }, resolvePath(p: string): string { return p.startsWith("/") ? p : join(cwdValue, p); }, get http() { return http.module; }, get timers() { return timers; }, get loop() { return loop; }, get process() { return proc; }, get workerThreads() { return threads; }, get childProcess() { return children; },};
self.onmessage = (ev: MessageEvent<KernelToRuntime>): void => { const msg = ev.data; if (msg.t !== "init") return; // From here on the kernel speaks only over msg.port; the worker scope is the guest's. self.onmessage = null; kernelPort = msg.port; sab = new SharedArrayBuffer(SAB_BYTES); kernelPort.postMessage({ t: "attach", mailbox: sab } satisfies RuntimeToKernel); cwdValue = msg.cwd; host = msg.host;
loop = installLoop({ beforeExit: () => proc.emit("beforeExit", proc.exitCode ?? 0), exit: () => proc.exit(), onUncaught: (e) => uncaught(e), beforePoll: () => takePendingSignals(), }); useNextTick(loop.nextTick); holdNativePromises(loop); // Errors that escape the loop (microtasks, async functions) take the same path: // an uncaughtException listener, else Node's fatal report and exit 1. Rejections // follow Node's default --unhandled-rejections=throw. self.addEventListener("error", (ev) => { ev.preventDefault(); uncaught(ev.error ?? ev.message); }); self.addEventListener("unhandledrejection", (ev) => { ev.preventDefault(); unhandledRejection(ev.reason, ev.promise); }); http = createHttp({ call, loop, respond: (id, res) => kernelPort.postMessage({ t: "httpResponse", id, res } satisfies RuntimeToKernel), closed: (id) => kernelPort.postMessage({ t: "connectionClosed", id } satisfies RuntimeToKernel), }); installPorts(loop); threads = makeWorkerThreads({ loop, init: msg, host, callSync, post: (m, transfer) => kernelPort.postMessage(m, transfer), process: () => proc, resolvePath: (p) => ctx.resolvePath(p), }); parentPort = threads.parentPort; children = makeChildProcess({ loop, callSync, post: (m) => kernelPort.postMessage(m), process: () => proc, resolvePath: (p) => ctx.resolvePath(p), }); kernelPort.onmessage = (rev: MessageEvent<Res | KernelToRuntime>) => { const m = rev.data; switch (m.t) { case "res": handleReply(m); break; case "httpRequest": http.dispatch(m); break; case "connect": http.connect(m); break; case "signal": loop.interrupt(); break; case "stdin": stdin.receive(m.bytes); break; case "threadExited": threads.threadExited(m); break; case "childStdio": children.childStdio(m.pid, m.stream, m.bytes); break; case "childExited": children.childExited(m.pid, m.status); break; case "fsEvents": deliverWatchEvents(m.wid, m.events); break; } };
const emitFd = (fd: number, bytes: Uint8Array): void => (fd === 2 ? emitStderr(bytes) : emitStdout(bytes)); installTty({ loop, terminal: msg.tty !== null, callSync, write: emitFd, env: () => proc.env, emitWarning: (message, type) => proc.emitWarning(message, type), }); const stdin = msg.tty ? new ReadStream(0) : new StdinStream(loop); // A thread's stdin is empty unless asked for (worker stdin: true is not modelled). if (msg.thread && stdin instanceof StdinStream) stdin.receive(null); const [stdout, stderr] = msg.tty ? [new WriteStream(1), new WriteStream(2)] : [new StdioWriter(1, emitStdout, loop.nextTick), new StdioWriter(2, emitStderr, loop.nextTick)]; proc = makeProcess({ msg, isThread: msg.thread !== null, loop, stdin, stdout, stderr, callSync, cwd: { get: () => cwdValue, set: (dir) => (cwdValue = dir) }, emitStderr, reportFatal: (e) => emitStderr(enc.encode(stackOf(e))), exitWorker, getBuiltinModule: (id) => loadBuiltin(id, ctx), }); takePendingSignals = signalReceiver(proc, loop, () => takeSignals(sab)); // Node's getStdout/getStderr: a terminal's streams track its size through SIGWINCH. if (stdout instanceof WriteStream && stderr instanceof WriteStream) { proc.on("SIGWINCH", function refreshStdoutOnSigWinch() { stdout._refreshSize(); }); proc.on("SIGWINCH", function refreshStderrOnSigWinch() { stderr._refreshSize(); }); } timers = createTimers(loop, (message, type) => proc.emitWarning(message, type)); // Node's lib/timers.js: util.promisify(setTimeout) and util.promisify(setImmediate) are // the timers/promises forms, resolved lazily so they are the same functions. for (const name of ["setTimeout", "setImmediate"] as const) { Object.defineProperty(timers[name], Symbol.for("nodejs.util.promisify.custom"), { enumerable: true, get: () => (loadBuiltin("timers/promises", ctx) as Record<string, unknown>)[name], }); } const setup = processRuntimes[msg.runtime]; setup.installGlobals(timers, proc);
// ---- guest ESM hooks (installed before the entry runs) ---- // Facade modules served from the SW virtual origin reach back into the runtime // through this one well-known symbol. builtin()/cjs() share require()'s loader // and cache, so an ESM graph and a CJS require() of the same module agree on a // single evaluation per worker. const typelessWarned = new Set<string>(); (globalThis as Record<symbol, unknown>)[Symbol.for("nib.runtime")] = { builtin: (id: string) => loadBuiltin(id, ctx), cjs: (path: string) => loadCjsByPath(ctx, path), meta: (path: string) => ({ url: `file://${path}`, filename: path, dirname: ctx.dirnameOf(path), resolve: (specifier: string): string => hasResolveHooks() ? resolveThroughHooks(specifier, `file://${path}`, fileUrlFor) : moduleUrlFor( ctx.callSync("resolve", { specifier, parent: ctx.dirnameOf(path), kind: "import" }), specifier, path, ), }), // The guest npm command's way into the kernel's installer (src/npm-cli). Async, and // holding the loop while the install runs, as any pending syscall does. npm: (request: SyscallSpec["npm"]["args"]) => call("npm", request), npmLookup: (request: SyscallSpec["npmLookup"]["args"]) => call("npmLookup", request), // A module whose format syntax detection chose opens with this call (kernel // rewrite). Node warns once per package.json, so the process remembers which. typeless: (pjson: string, path: string): void => { if (typelessWarned.has(pjson)) return; typelessWarned.add(pjson); proc.emitWarning( `Module type of file://${path} is not specified and it doesn't parse as CommonJS.\n` + "Reparsing as ES module because module syntax was detected. This incurs a performance overhead.\n" + `To eliminate this warning, add "type": "module" to ${pjson}.`, { code: "MODULE_TYPELESS_PACKAGE_JSON" }, ); }, // Every guest import() lands here (kernel rewrite). The hold keeps the process // alive while the browser fetches and links the target, as Node does. importFrom: (from: string) => async (specifier: unknown): Promise<unknown> => { loop.hold(); try { let spec = String(specifier); // Resolve hooks (registerHooks) turn the specifier into a URL first. if (hasResolveHooks()) spec = resolveThroughHooks(spec, `file://${from}`, fileUrlFor); // A path or file: URL with a ?query or #hash names the file without them, and a module // of its own per query, as Node's loader does: Vite imports `astro.config.mjs?t=<now>` // to load a config fresh. const [file, suffix] = splitQuery(spec); if (suffix) { const resolved = await call("resolve", { specifier: file, parent: ctx.dirnameOf(from), kind: "import" }); const url = moduleUrlFor(resolved, spec, from); return await import(resolved.kind === "file" && resolved.format === "esm" ? `${url}&q=${encodeURIComponent(suffix)}` : url); } return await importEsm( from, spec, () => call("resolve", { specifier: spec, parent: ctx.dirnameOf(from), kind: "import" }), (resolved) => import(moduleUrlFor(resolved, spec, from)), ); } finally { loop.release(); } }, // Every served ES module opens and closes with these (kernel rewrite), so a // require() of a natively imported module gets that same instance. esmBegin, esmEnd, };
// Guest console → stdio: Node's own Console, bound to process.stdout/stderr, so a // patched write() sees console output and %-formatting/colours/grouping/tables all // behave like Node's (not the browser's native console). installConsole({ emitWarning: (msg) => proc.emitWarning(msg) }); console = makeConsole(proc.stdout, proc.stderr) as unknown as Console;
// Node emits 'online' when the thread starts running its entry. parentPort?.send({ k: "online" }); void runEntry(msg, setup.runEntry);};
/** What differs between process runtimes (VISION D2). Everything else (the loop, stdio, * console capture, node: builtins, the exit path) is the same for both. */interface ProcessRuntimeSetup { installGlobals(timers: Timers, proc: GuestProcess): void; /** Load and evaluate the entry, then mark the main script done. */ runEntry(msg: KernelToRuntimeInit): Promise<void>;}
const processRuntimes: Record<ProcessRuntime, ProcessRuntimeSetup> = { node: { installGlobals: installNodeGlobals, runEntry: runNodeEntry }, web: { installGlobals: installWebGlobals, runEntry: runWebEntry },};
/** The kernel's own resolution of `specifier` from `parentURL`, as the URL Node's resolver * returns: file:// for a file, node: for a builtin. The end of a resolve hook chain. */function fileUrlFor(specifier: string, parentURL: string | undefined): string { const [file, suffix] = splitQuery(specifier); const parent = parentURL?.startsWith("file:") ? decodeURIComponent(new URL(parentURL).pathname) : "/"; const resolved = callSync("resolve", { specifier: file, parent: parent.slice(0, parent.lastIndexOf("/")) || "/", kind: "import" }); if (resolved.kind === "builtin") return `node:${resolved.id}`; if (resolved.kind === "missing") throw loaderError(resolved.code, specifier, parent); return `file://${resolved.path}${suffix}`;}
/** A path-like specifier split at its `?query` or `#hash`; a bare or builtin one is left whole. */function splitQuery(spec: string): [file: string, suffix: string] { if (!/^(?:file:|\.{0,2}\/)/.test(spec)) return [spec, ""]; const at = spec.search(/[?#]/); return at < 0 ? [spec, ""] : [spec.slice(0, at), spec.slice(at)];}
/** Map a resolved module record to its virtual-origin URL — the same mapping the * kernel's transformer applies when rewriting static specifiers (shared/vurls.ts). */function moduleUrlFor(resolved: ResolvedModule, specifier: string, from: string): string { if (resolved.kind === "builtin") return builtinUrl(host, resolved.id); if (resolved.kind === "missing") throw loaderError(resolved.code, specifier, from); if (resolved.format === "cjs") return cjsUrl(host, resolved.path); // esm (and json, wrapped) ride the vfs URL; the version query keeps the browser's // module map coherent across re-seeds. const st = callSync("stat", { path: resolved.path, follow: true }); return vfsUrl(host, resolved.path, st);}
async function runEntry(msg: KernelToRuntimeInit, run: ProcessRuntimeSetup["runEntry"]): Promise<void> { try { await run(msg); } catch (e) { // A throw from the entry is an uncaught exception like any other: listeners it // registered before throwing keep the process running. if (uncaught(e)) loop.setMainDone(); }}
async function runWebEntry(msg: KernelToRuntimeInit): Promise<void> { if (msg.thread?.eval != null) throw webEntryNotEsm("the eval source", "a CommonJS script"); const resolved = await call("resolve", { specifier: msg.entry, parent: msg.cwd, kind: "import" }); if (resolved.kind === "file" && resolved.format !== "esm") { throw webEntryNotEsm(resolved.path, resolved.format === "cjs" ? "CommonJS" : "JSON"); } await import(moduleUrlFor(resolved, msg.entry, msg.cwd)); loop.setMainDone();}
async function runNodeEntry(msg: KernelToRuntimeInit): Promise<void> { if (msg.thread?.eval != null) { // An eval worker runs its source as a CJS script whose require() starts at cwd. const fn = compileCjs(msg.thread.eval, msg.entry, ["exports", "require", "module", "__filename", "__dirname", "process"]); const moduleObj = { exports: {}, id: msg.entry, filename: msg.entry, loaded: false, children: [] as unknown[] }; loop.runMain(() => fn.call(moduleObj.exports, moduleObj.exports, makeRequire(ctx, `${msg.cwd}/${msg.entry}`), moduleObj, msg.entry, ".", proc)); loop.setMainDone(); return; } const resolved = await call("resolve", { specifier: msg.entry, parent: msg.cwd, kind: "require" }); // Node resolves the entry through its CJS loader, whatever the entry's format. if (resolved.kind === "missing" && resolved.code === "ERR_MODULE_NOT_FOUND") throw requireNotFound(msg.entry, []); if (resolved.kind === "missing") throw loaderError(resolved.code, msg.entry, msg.cwd); if (resolved.kind !== "file") throw requireNotFound(msg.entry, []); const { source, format } = await call("loadModule", { path: resolved.path }); if (format === "esm") { // Guest ESM executes through the browser's native module linker: the SW // virtual origin serves the kernel-transformed source, so cycles, live // bindings and TLA come from the engine (DESIGN §Module system). const st = callSync("stat", { path: resolved.path, follow: true }); await import(vfsUrl(host, resolved.path, st)); loop.setMainDone(); return; } if (format !== "cjs") throw loaderError("ERR_REQUIRE_ESM", msg.entry, msg.cwd);
const mod = newModule(ctx, resolved.path); ctx.cache.set(resolved.path, mod); const text = new TextDecoder().decode(source); const fn = compileCjs(text, resolved.path, ["exports", "require", "module", "__filename", "__dirname", "process"]); loop.runMain(() => fn.call(mod.exports, mod.exports, makeRequire(ctx, resolved.path), mod, resolved.path, ctx.dirnameOf(resolved.path), proc)); mod.loaded = true; loop.setMainDone();}
/** Route an uncaught error. True when an uncaughtException listener took it; * otherwise the process dies and this does not return. */function uncaught(e: unknown, origin: "uncaughtException" | "unhandledRejection" = "uncaughtException"): boolean { const capture = proc && uncaughtCaptures.get(proc); if (capture) { try { capture(e); } catch (inner) { die(stackOf(inner), 7); } return true; } if (!proc || proc.listenerCount("uncaughtException") === 0) { if (parentPort) dieInThread(e); die(stackOf(e)); } try { proc.emit("uncaughtException", e, origin); } catch (inner) { die(stackOf(inner), 7); // Node's exit code for a throwing uncaughtException handler } return true;}
function unhandledRejection(reason: unknown, promise: Promise<unknown>): void { if (proc.listenerCount("unhandledRejection") > 0) { try { proc.emit("unhandledRejection", reason, promise); } catch (e) { uncaught(e); } return; } // No unhandledRejection listener: Node raises the rejection as an uncaught exception. if (proc.listenerCount("uncaughtException") > 0 || uncaughtCaptures.has(proc)) { uncaught(reason instanceof Error ? reason : unhandledRejectionError(reason), "unhandledRejection"); return; } if (parentPort) dieInThread(reason instanceof Error ? reason : unhandledRejectionError(reason)); die(reason instanceof Error ? stackOf(reason) : unhandledRejectionReport(reason));}
let dying = false;
/** A thread's uncaught error goes to its parent's 'error' event, cloned as Node clones * it, instead of stderr; then the thread exits 1. */function dieInThread(e: unknown): never { if (!dying) { dying = true; try { parentPort!.send({ k: "error", v: e }); } catch { parentPort!.send({ k: "error", v: String(e) }); } } if (proc) proc.exit(1); return exitWorker(1);}
/** Node's fatal path: the report on stderr, then the exit listeners, then exit. */function die(report: string, code = 1): never { if (!dying) { dying = true; emitStderr(enc.encode(report)); } if (proc) proc.exit(code); return exitWorker(code);}
/** `exit` never replies (DESIGN invariant): the kernel terminates this worker. Blocking * until then keeps process.exit() from returning into guest code, as in Node. */function exitWorker(code: number): never { kernelPort.postMessage({ t: "req", id: nextId++, op: "exit", args: { code } }); self.close(); for (;;) Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0);}
function stackOf(e: unknown): string { return `${(e as Error)?.stack ?? String(e)}\n`;}
function unhandledRejectionError(reason: unknown): Error & { code: string } { const shown = typeof reason === "string" ? JSON.stringify(reason) : String(reason); const e = new Error("This error originated either by throwing inside of an async function without a catch " + "block, or by rejecting a promise which was not handled with .catch(). " + `The promise rejected with the reason ${shown}.`); e.name = "UnhandledPromiseRejection"; return Object.assign(e, { code: "ERR_UNHANDLED_REJECTION" });}
function unhandledRejectionReport(reason: unknown): string { const e = unhandledRejectionError(reason); return `[${e.name}: ${e.message}] {\n code: '${e.code}'\n}\n`;}
export {};