// require() of an ES module (sketches/require-esm.md): the browser's module linker // only runs asynchronously, so a required graph is evaluated here, from the kernel's // function form of each module (src/kernel/function-form.ts). One registry per // worker, keyed by real path. The whole graph links and compiles before any of it // runs, so top-level await anywhere refuses before a side effect, as in Node. // Modules the native loader evaluates register too (`natives`), and whichever // loader reaches a module first owns its one instance. import { ESM_DEFAULT_LOCAL, type EsmSyncModule, type ResolvedModule } from "../shared/protocol"; import type { RequireContext } from "./modules"; import { loadCjsByPath } from "./modules"; import { loadBuiltin } from "./builtins"; import { codedError, loaderError } from "./errors"; type Getters = Map unknown>; type Body = (meta: unknown, def: (g: Record unknown>) => void, bind: (assign: (g: (i: number) => unknown) => void) => void, importFrom: unknown) => () => void; type Dep = | { kind: "esm"; rec: EsmRecord } | { kind: "other"; getters: Getters; load: () => void; ns?: object }; interface EsmRecord { path: string; status: "linked" | "evaluating" | "evaluated" | "errored"; /** Whoever required it first: Node's cycle error names it. */ from: string; mod: EsmSyncModule; deps: Dep[]; make: Body; /** Its exports, each a getter: a local reads `locals` once the body has defined it. */ getters?: Getters; locals: Getters; refresh?: () => void; ns?: object; wrapped?: object; error?: unknown; } /** A module the native loader is evaluating or has evaluated, with its real namespace. */ interface NativeRecord { status: "evaluating" | "evaluated" | "errored"; ns: Record; error?: unknown; /** Whether its graph has a top-level await, computed on the first require(). */ async?: boolean; wrapped?: object; } const records = new Map(); const natives = new Map(); /** import() calls still in flight; `path` once resolved. */ const importing = new Set<{ from: string; specifier: string; path?: string }>(); /** Records evaluated by the outermost require() still running. */ let evaluatedThisRun: EsmRecord[] = []; const TLA = new Set([ "await is only valid in async functions and the top level bodies of modules", "Unexpected reserved word", ]); interface Hooks { meta(path: string): Record; importFrom(path: string): unknown } const hooks = (): Hooks => (globalThis as unknown as Record)[Symbol.for("nib.runtime")]!; /** A module namespace lookalike: null prototype, sorted live getters, not extensible. * A Proxy, so descriptors read as Node's (data, writable, non-configurable) and a * strict-mode write throws, as with a real namespace. */ function makeNamespace(getters: Getters): object { const target = Object.create(null) as Record; for (const n of [...getters.keys()].sort()) Object.defineProperty(target, n, { writable: true, enumerable: true }); Object.defineProperty(target, Symbol.toStringTag, { value: "Module" }); Object.preventExtensions(target); const own = (k: string | symbol): k is string => typeof k === "string" && getters.has(k); return new Proxy(target, { get: (t, k) => (own(k) ? getters.get(k)!() : t[k]), getOwnPropertyDescriptor: (t, k) => (own(k) ? { value: getters.get(k)!(), writable: true, enumerable: true, configurable: false } : Reflect.getOwnPropertyDescriptor(t, k)), set: () => false, defineProperty: () => false, deleteProperty: (t, k) => !own(k) && Reflect.deleteProperty(t, k), }); } /** The module's body as a function, or null when it has a top-level await: the body * is a plain function's now, so V8 refuses the await as it compiles. */ function compile(mod: EsmSyncModule): Body | null { try { return (0, eval)(new TextDecoder().decode(mod.code)); } catch (e) { if (e instanceof SyntaxError && TLA.has(e.message)) return null; throw e; } } const asyncModule = (from: string, root: string): Error => codedError("ERR_REQUIRE_ASYNC_MODULE", "require() cannot be used on an ESM graph with top-level await. Use import() instead. To see where the top-level await comes from, use --experimental-print-required-tla.\n" + ` From ${from} \n Requiring ${root} `); /** A natively loaded module's namespace, if require() may have it: a module still * evaluating is in a cycle with the require, and one that threw rethrows. */ function settled(n: NativeRecord, path: string, from: string): Record { if (n.status === "errored") throw n.error; if (n.status === "evaluating") throw codedError("ERR_REQUIRE_CYCLE_MODULE", `Cannot require() ES Module ${path} in a cycle. (from ${from})`); return n.ns; } /** Node's error for a require() of a module an import() has begun loading. */ function checkRace(ctx: RequireContext, path: string, from: string): void { for (const p of importing) { if (p.path === undefined) { const r = ctx.callSync("resolve", { specifier: p.specifier, parent: ctx.dirnameOf(p.from), kind: "import" }); p.path = r.kind === "file" ? r.path : ""; } if (p.path !== path) continue; throw codedError("ERR_REQUIRE_ESM_RACE_CONDITION", `Cannot require() ES Module ${path} because it is not yet fully loaded.\n` + "This may be caused by a race condition if the module is simultaneously dynamically import()-ed via Promise.all().\n" + `Try await-ing the import() sequentially in a loop instead.\n (From ${from} in non-loader-hook thread)`); } } function link(ctx: RequireContext, path: string, from: string, root: string): EsmRecord { const existing = records.get(path); if (existing) return existing; checkRace(ctx, path, from); const mod = ctx.callSync("loadEsmSync", { path }); const make = compile(mod); if (!make) throw asyncModule(from, root); const rec: EsmRecord = { path, status: "linked", from, mod, deps: [], make, locals: new Map() }; records.set(path, rec); try { for (const { specifier, resolved, names = [] } of mod.requests) { if (resolved.kind === "missing") throw loaderError(resolved.code, specifier, path); if (resolved.kind === "file" && resolved.format === "esm") { const n = natives.get(resolved.path); if (n) { const ns = settled(n, resolved.path, from); rec.deps.push({ kind: "other", getters: new Map(Object.keys(ns).map((k) => [k, () => ns[k]])), load() {}, ns }); } else rec.deps.push({ kind: "esm", rec: link(ctx, resolved.path, from, root) }); continue; } let value: unknown; const getters: Getters = new Map([["default", () => value]]); if (resolved.kind === "file" && resolved.format === "cjs") getters.set("module.exports", () => value); for (const n of names) if (n !== "default") getters.set(n, () => (value as Record | null)?.[n]); rec.deps.push({ kind: "other", getters, load: resolved.kind === "builtin" ? () => { value = loadBuiltin(resolved.id, ctx); } : () => { const cached = ctx.cache.get(resolved.path); if (cached && !cached.loaded) { throw codedError("ERR_REQUIRE_CYCLE_MODULE", `Cannot import CommonJS Module ${specifier} in a cycle. (from ${path})`); } value = loadCjsByPath(ctx, resolved.path, path); }, }); } } catch (e) { records.delete(path); throw e; } return rec; } /** A record's exports: its locals, then re-exports, then `export *` names that are * neither `default` nor already taken. */ function gettersOf(rec: EsmRecord): Getters { if (rec.getters) return rec.getters; const g: Getters = (rec.getters = new Map()); for (const e of rec.mod.exports) { if (e.kind === "local") { g.set(e.name, () => { const local = rec.locals.get(e.name); if (!local) throw new ReferenceError(`Cannot access '${e.name}' before initialization`); return local(); }); } else if (e.kind === "reexport") { const dep = rec.deps[e.request]!; g.set(e.name, e.imported === null ? () => namespaceOf(dep) : () => depGetters(dep).get(e.imported!)?.()); } } for (const e of rec.mod.exports) { if (e.kind !== "star") continue; for (const [name, get] of depGetters(rec.deps[e.request]!)) if (name !== "default" && !g.has(name)) g.set(name, get); } return g; } const depGetters = (dep: Dep): Getters => (dep.kind === "esm" ? gettersOf(dep.rec) : dep.getters); function namespaceOf(dep: Dep): object { if (dep.kind === "esm") return (dep.rec.ns ??= makeNamespace(gettersOf(dep.rec))); return (dep.ns ??= makeNamespace(dep.getters)); } function evaluate(rec: EsmRecord): void { if (rec.status === "errored") throw rec.error; if (rec.status !== "linked") return; rec.status = "evaluating"; try { for (const d of rec.deps) { if (d.kind === "esm") evaluate(d.rec); else d.load(); } const value = (i: number): unknown => { const b = rec.mod.bindings[i]!; const dep = rec.deps[b.request]!; return b.imported === null ? namespaceOf(dep) : depGetters(dep).get(b.imported)?.(); }; const meta = hooks().meta(rec.path); meta.main = false; const body = rec.make(meta, (getters) => { for (const k of Object.keys(getters)) rec.locals.set(k, getters[k]!); }, (assign) => { rec.refresh = () => assign((i) => { try { return value(i); } catch { return undefined; // still in its TDZ (a cycle): the refresh after the graph fills it } }); }, hooks().importFrom(rec.path)); rec.refresh?.(); evaluatedThisRun.push(rec); body(); const dflt = rec.locals.get("default")?.(); if (typeof dflt === "function" && dflt.name === ESM_DEFAULT_LOCAL) Object.defineProperty(dflt, "name", { value: "default" }); rec.status = "evaluated"; } catch (e) { rec.status = "errored"; rec.error = e; throw e; } } /** Whether `path` is an ES module that require() is evaluating right now. */ export function esmEvaluating(path: string): boolean { return records.get(path)?.status === "evaluating"; } /** Node's ERR_REQUIRE_CYCLE_MODULE for a require() back into an ES module that is * still evaluating; "from" is whoever required it first. */ export function esmCycle(path: string): Error { return codedError("ERR_REQUIRE_CYCLE_MODULE", `Cannot require() ES Module ${path} in a cycle. (from ${records.get(path)!.from})`); } /** Whether the graph under `path` has a top-level await anywhere. */ function asyncGraph(ctx: RequireContext, path: string, seen: Set): boolean { if (seen.has(path) || records.get(path)?.status === "evaluated") return false; seen.add(path); const mod = ctx.callSync("loadEsmSync", { path }); return !compile(mod) || mod.requests.some(({ resolved: r }) => r.kind === "file" && r.format === "esm" && asyncGraph(ctx, r.path, seen)); } /** Node's return shape for require(): the `"module.exports"` export when there is * one, else the namespace, wrapped with `__esModule: true` when it has a default * export and no `__esModule` of its own (a distinct object, as in Node). */ function requireShape(g: Getters, ns: object, holder: { wrapped?: object }): unknown { if (g.has("module.exports")) return g.get("module.exports")!(); if (!g.has("default") || g.has("__esModule")) return ns; return (holder.wrapped ??= makeNamespace(new Map([...g, ["__esModule", () => true]]))); } /** What require() of the ES module at `path` returns, per Node 24. */ export function requireEsm(ctx: RequireContext, path: string, parent: string | undefined): unknown { const from = parent ?? ""; const n = natives.get(path); if (n) { const ns = settled(n, path, from); if ((n.async ??= asyncGraph(ctx, path, new Set()))) throw asyncModule(from, path); return requireShape(new Map(Object.keys(ns).map((k) => [k, () => ns[k]])), ns, n); } const rec = link(ctx, path, from, path); const outer = evaluatedThisRun; evaluatedThisRun = []; try { evaluate(rec); } finally { // Bindings into a cycle were read before their exporter ran: fill them now. for (const r of evaluatedThisRun) r.refresh?.(); evaluatedThisRun = outer; } const g = gettersOf(rec); return requireShape(g, (rec.ns ??= makeNamespace(g)), rec); } /** The native loader started evaluating `path` (the kernel's ESM prologue); `ns` is * its own namespace, from an import of its own URL. A module require() already * evaluated keeps that instance. */ export function esmBegin(path: string, ns: Record): void { if (!records.has(path)) natives.set(path, { status: "evaluating", ns }); } export function esmEnd(path: string): void { const n = natives.get(path); if (n) n.status = "evaluated"; } /** A guest import(): the instance require() made when it got there first, else the * native loader's. */ export async function importEsm(from: string, specifier: string, resolve: () => Promise, load: (r: ResolvedModule) => Promise): Promise { const p: { from: string; specifier: string; path?: string } = { from, specifier }; importing.add(p); try { const resolved = await resolve(); if (resolved.kind !== "file" || resolved.format !== "esm") return await load(resolved); p.path = resolved.path; const rec = records.get(resolved.path); if (rec) { if (rec.status === "errored") throw rec.error; return (rec.ns ??= makeNamespace(gettersOf(rec))); } try { return await load(resolved); } catch (e) { // Its body threw before its epilogue could mark it evaluated. const n = natives.get(resolved.path); if (n?.status === "evaluating") Object.assign(n, { status: "errored", error: e }); throw e; } } finally { importing.delete(p); } }