// node:module. In Node, `require("module")` is the Module class itself — every // require() knob (the cache, the resolver, the per-extension compile step, the // wrapper template) hangs off it as a static. modules.ts owns the real loader; // this file is a thin face on its hook points, so patching a static here really // changes require()'s behavior instead of shadowing it. import { builtinIdOf, cacheView, currentResolveFilename, extensions, loadCjsByPath, makeRequire, newModule, resolveFilename, setResolveFilenameHook, wrap, wrapper, type ExtensionHandler, type ModuleLike, type RequireContext, } from "./modules"; import { BUILTIN_IDS, SCHEME_ONLY_BUILTINS, isBuiltinId } from "../shared/builtin-ids"; import { markRefused, unsupported } from "./refuse"; import { makePath } from "./path"; /** A path, or a file: URL as a string or URL, as createRequire/findPackageJSON take. */ function filePath(filename: string | URL): string { const s = String(filename); return s.startsWith("file:") ? decodeURIComponent(new URL(s).pathname) : s; } type ModuleInstance = ModuleLike & { path: string; readonly parent: ModuleInstance | undefined; readonly isPreloading: boolean; require(specifier: string): unknown; load(filename: string): void; _compile(content: string, filename: string): unknown; }; /** Node's own Module._extensions handler shape: no ctx — a guest's hook only ever * sees (module, filename), the same as Node's own default handlers take. */ type PublicExtensionHandler = (mod: ModuleInstance, filename: string) => void; const compileCacheStatus = { FAILED: 0, ENABLED: 1, ALREADY_ENABLED: 2, DISABLED: 3 } as const; // Node's module.parent is a deprecated prototype *accessor*, not a per-instance data // property (DEP0144) — kept out-of-band so Module.prototype actually carries a // `parent` getter, matching what Node's own introspection sees. const parentOf = new WeakMap(); /** What a registerHooks() resolve hook returns: the URL a specifier names. */ export interface ResolveResult { url: string; format?: string | null; shortCircuit?: boolean; } export interface ResolveContext { conditions: string[]; importAttributes: Record; parentURL: string | undefined; } type NextResolve = (specifier: string, context?: Partial) => ResolveResult; type ResolveHook = (specifier: string, context: ResolveContext, nextResolve: NextResolve) => ResolveResult; /** The resolve hooks registerHooks() added, in registration order. */ const resolveHooks: ResolveHook[] = []; /** Whether any resolve hook is registered, so an unhooked import() keeps its fast path. */ export const hasResolveHooks = (): boolean => resolveHooks.length > 0; /** * Resolves `specifier` through the registered resolve hooks, the last registered first, as * Node chains them; the end of the chain is `base`, the kernel's own resolution. */ export function resolveThroughHooks(specifier: string, parentURL: string | undefined, base: (specifier: string, parentURL: string | undefined) => string): string { const chain = resolveHooks.reduce( (next, hook) => (spec, context) => hook(spec, { conditions: ["node", "import"], importAttributes: {}, parentURL, ...context }, next), (spec, context) => ({ url: base(spec, context?.parentURL ?? parentURL) }), ); return chain(specifier, { parentURL }).url; } export function makeModule(ctx: RequireContext) { const path = makePath(() => ctx.cwd, () => ctx.process.env); function Module(this: ModuleInstance, id = "", parent?: ModuleInstance) { // newModule's require/_compile close over its own `mod.filename`, frozen at // this call; the prototype methods below read `this.filename` live instead // (load() changes it after construction), so only the plain data fields copy. const { exports, loaded, children, paths } = newModule(ctx, id); Object.assign(this, { id, exports, loaded, children, paths }); this.path = path.dirname(id); parentOf.set(this, parent); this.filename = null as unknown as string; // Node: null until load() sets it } Object.defineProperty(Module.prototype, "parent", { get(this: ModuleInstance): ModuleInstance | undefined { return parentOf.get(this); }, configurable: true, }); Module.prototype.require = function (this: ModuleInstance, specifier: string): unknown { return makeRequire(ctx, this.filename ?? this.id)(specifier); }; Module.prototype.load = function (this: ModuleInstance, filename: string): void { this.filename = filename; this.paths = []; // computed lexically by nodeModulePaths; left empty for a direct load() const ext = (/\.[^./]+$/.exec(filename) ?? [".js"])[0] ?? ".js"; const handler: ExtensionHandler = extensions[ext] ?? extensions[".js"]!; handler(ctx, this, filename, undefined); this.loaded = true; }; Module.prototype._compile = function (this: ModuleInstance, content: string, filename: string): unknown { const indirectEval = eval; const fn = indirectEval(`${wrap(content)}\n//# sourceURL=${filename}`) as (...a: unknown[]) => unknown; return fn.call(this.exports, this.exports, makeRequire(ctx, filename), this, filename, ctx.dirnameOf(filename)); }; Object.defineProperty(Module.prototype, "isPreloading", { get(): boolean { return false; }, // nibling has no --require/NODE_OPTIONS preload phase configurable: true, }); Module.Module = Module; Module.builtinModules = BUILTIN_IDS.filter((id) => !SCHEME_ONLY_BUILTINS.has(id)); Module.isBuiltin = (name: string): boolean => { const n = String(name); const id = n.replace(/^node:/, ""); if (!isBuiltinId(id)) return false; return n.startsWith("node:") || !SCHEME_ONLY_BUILTINS.has(id); }; Module.createRequire = (filename: string | URL) => makeRequire(ctx, filePath(filename)); // The exact same object require.cache hands back (modules.ts's cacheView): Node's // require.cache IS Module._cache, not a lookalike, so a delete on either name // evicts what require() will reload next time. Module._cache = cacheView(ctx); // A view over modules.ts's own extensions table (not a copy): reading a handler // back out binds it to this worker's ctx, and assigning one — the ts-node/tsx // style require hook — installs it there, in Node's own (module, filename) shape, // so a patch really changes what require() compiles for that extension. Module._extensions = new Proxy({} as Record, { get: (_t, key) => { if (typeof key !== "string" || !Object.hasOwn(extensions, key)) return undefined; const internal = extensions[key]!; return (mod: ModuleInstance, filename: string) => internal(ctx, mod, filename, undefined); }, set: (_t, key, value) => { if (typeof key === "string") extensions[key] = (_c, mod, filename) => (value as PublicExtensionHandler)(mod as ModuleInstance, filename); return true; }, has: (_t, key) => typeof key === "string" && Object.hasOwn(extensions, key), deleteProperty: (_t, key) => { if (typeof key === "string") delete extensions[key]; return true; }, ownKeys: () => Object.keys(extensions), getOwnPropertyDescriptor: (_t, key) => (typeof key === "string" && Object.hasOwn(extensions, key) ? { enumerable: true, configurable: true, value: undefined } : undefined), }); // Module._resolveFilename is a real hook point: reassigning it installs the // override in modules.ts, so require() itself (not just a direct call here) // resolves through whatever a guest patched it to. Object.defineProperty(Module, "_resolveFilename", { get(): (request: string, parent?: ModuleInstance, isMain?: boolean) => string { const impl = currentResolveFilename(); // see its own comment: must snapshot, not re-dispatch return (request, parent, isMain = false) => impl(ctx, request, parent?.filename, isMain); }, set(fn: (request: string, parent?: ModuleInstance, isMain?: boolean) => string) { setResolveFilenameHook((c, request, parentFilename, isMain) => fn(request, parentFilename ? ({ filename: parentFilename } as ModuleInstance) : undefined, isMain)); }, configurable: true, }); Module._load = (request: string, parent?: ModuleInstance, isMain = false): unknown => { const resolved = resolveFilename(ctx, request, parent?.filename ?? undefined, isMain); const builtinId = builtinIdOf(resolved); if (builtinId !== null) return makeRequire(ctx, parent?.filename ?? ctx.cwd)(request); return loadCjsByPath(ctx, resolved, parent?.filename); }; Module.runMain = (main: string): unknown => Module._load(main, undefined, true); Module.wrap = wrap; Module.wrapper = wrapper; Module.globalPaths = [] as string[]; // NODE_PATH / the global node_modules dirs: none inside a worker Module.constants = { compileCacheStatus }; // register()/registerHooks() customize the ESM loader's resolve/load step. Guest // ESM runs through the browser's native linker — dynamic import() against the SW // virtual origin — which links static imports with no JS-reachable hook. What the // runtime does resolve itself, import() and import.meta.resolve, runs registerHooks()'s // resolve hooks (Vite's module runner registers one). Load hooks, and register()'s // off-thread hooks, would never run, so they refuse by name. const WHY_HOOKS = "ESM loads through the browser's native linker; there is no hook point to attach a resolve/load chain to"; const WHY_LOAD = "ESM loads through the browser's native linker; only resolve hooks run, and only for import() and import.meta.resolve"; Module.register = markRefused(() => { throw unsupported("module", "register()", WHY_HOOKS); }, WHY_HOOKS); Module.registerHooks = (hooks: { resolve?: ResolveHook; load?: unknown }): { deregister(): void } => { if (hooks?.load !== undefined) throw unsupported("module", "registerHooks() load hooks", WHY_LOAD); const hook = hooks?.resolve; if (typeof hook === "function") resolveHooks.push(hook); return { deregister() { const at = hook ? resolveHooks.indexOf(hook) : -1; if (at >= 0) resolveHooks.splice(at, 1); }, }; }; // syncBuiltinESMExports re-copies a builtin's mutated CJS exports onto its live // ESM bindings. Every builtin facade here exports `const = m.` (a // snapshot, not a live binding — see transform.ts), so there is nothing to sync; // a no-op is the honest answer, not a lie, since nothing it could do would do more. Module.syncBuiltinESMExports = (): void => {}; // No V8 on-disk code cache is reachable from a worker; FAILED is Node's own // vocabulary for "asked, didn't happen" rather than a thrown refusal, since // tooling calls this speculatively and checks the status, not a try/catch. Module.enableCompileCache = (): { status: number; message?: string } => ({ status: compileCacheStatus.FAILED, message: "no on-disk compile cache in this environment" }); Module.getCompileCacheDir = (): undefined => undefined; Module.flushCompileCache = (): void => {}; Module.getSourceMapsSupport = (): { enabled: boolean; nodeModules: boolean; generatedCode: boolean } => ({ ...sourceMapsSupport }); Module.setSourceMapsSupport = (enabled: boolean, options?: { nodeModules?: boolean; generatedCode?: boolean }): void => { sourceMapsSupport = { enabled: Boolean(enabled), nodeModules: Boolean(options?.nodeModules), generatedCode: Boolean(options?.generatedCode) }; }; // Stack traces are never rewritten through a source map here (strip-only TS // erasure keeps line numbers 1:1, so there is none to apply) — setSourceMapsSupport // records the toggle tools expect to read back, but it has no effect on a trace. let sourceMapsSupport = { enabled: false, nodeModules: false, generatedCode: false }; // No module here ever registers a source map (same reason): always undefined. Module.findSourceMap = (): undefined => undefined; Module.SourceMap = makeSourceMap(); Module.findPackageJSON = (specifier: string | URL, base?: string | URL | ModuleInstance): string | undefined => { const baseFilename = base === undefined ? undefined : base instanceof URL || typeof base === "string" ? filePath(base) : base.filename ?? base.id; const spec = String(specifier); // What Node's ESM resolution throws when the specifier names nothing, builtins included. const notFound = (what: string): Error => Object.assign( new Error(`Cannot find ${what} imported from ${baseFilename ?? ctx.cwd}`), { code: "ERR_MODULE_NOT_FOUND" }); let resolvedFile: string; if (spec.startsWith(".") || spec.startsWith("/") || spec.startsWith("file:")) { resolvedFile = spec.startsWith(".") ? path.resolve(path.dirname(baseFilename ?? path.join(ctx.cwd, "-")), spec) : filePath(spec); if (!ctx.trySync("stat", { path: resolvedFile, follow: true }).ok) throw notFound(`module '${resolvedFile}'`); } else { if (builtinIdOf(spec) !== null) throw notFound(`package '${spec}'`); try { resolvedFile = resolveFilename(ctx, spec, baseFilename); } catch { throw notFound(`package '${spec}'`); } } let dir = /\.[^./]+$/.test(resolvedFile) ? path.dirname(resolvedFile) : resolvedFile; for (;;) { const candidate = path.join(dir, "package.json"); if (ctx.trySync("stat", { path: candidate, follow: true }).ok) return candidate; const up = path.dirname(dir); if (up === dir) return undefined; dir = up; } }; let warnedStripExperimental = false; Module.stripTypeScriptTypes = (code: string, _options?: unknown): string => { if (!warnedStripExperimental) { warnedStripExperimental = true; ctx.process.emitWarning("stripTypeScriptTypes is an experimental feature and might change at any time", "ExperimentalWarning"); } const r = ctx.callSync("stripTypes", { code }); if (!r.ok) { const ctor = r.code === "ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX" || r.code === "ERR_INVALID_TYPESCRIPT_SYNTAX" ? SyntaxError : Error; throw Object.assign(new ctor(r.message), { code: r.code }); } return r.js; }; return Module as unknown as ((id?: string, parent?: ModuleInstance) => void) & { Module: typeof Module; builtinModules: string[]; isBuiltin(name: string): boolean; createRequire(filename: string | URL): (specifier: string) => unknown; _cache: Record; _extensions: Record; _resolveFilename(request: string, parent?: ModuleInstance, isMain?: boolean): string; _load(request: string, parent?: ModuleInstance, isMain?: boolean): unknown; runMain(main: string): unknown; wrap(script: string): string; wrapper: [string, string]; globalPaths: string[]; constants: { compileCacheStatus: typeof compileCacheStatus }; register(): never; registerHooks(hooks: { resolve?: ResolveHook; load?: unknown }): { deregister(): void }; syncBuiltinESMExports(): void; enableCompileCache(): { status: number; message?: string }; getCompileCacheDir(): undefined; flushCompileCache(): void; getSourceMapsSupport(): { enabled: boolean; nodeModules: boolean; generatedCode: boolean }; setSourceMapsSupport(enabled: boolean, options?: { nodeModules?: boolean; generatedCode?: boolean }): void; findSourceMap(): undefined; SourceMap: ReturnType; findPackageJSON(specifier: string | URL, base?: string | URL | ModuleInstance): string | undefined; stripTypeScriptTypes(code: string, options?: unknown): string; }; } /** A real (not stubbed) decoder for the one thing module.SourceMap needs to do: * turn a v3 sourcemap payload's `mappings` VLQ into line/column lookups. Nothing * here ever hands one of these to findSourceMap (it always returns undefined — * no module is tracked with a generated map), so this only serves a guest that * constructs `new Module.SourceMap(payload)` from its own bundler/debug tooling. */ interface SourceMapPayload { sources: string[]; sourcesContent?: string[]; names: string[]; mappings: string } /** One decoded VLQ segment: generated position, then (if the segment carries one) the * source index/line/col and name index, or -1 where the segment had none. */ type SourceMapEntry = [genLine: number, genCol: number, srcIdx: number, srcLine: number, srcCol: number, nameIdx: number]; function makeSourceMap() { class SourceMap { #payload: SourceMapPayload; #decoded: SourceMapEntry[] | null = null; constructor(payload: SourceMapPayload, _lineLengths?: unknown) { this.#payload = payload; } get payload(): SourceMapPayload { return this.#payload; } get lineLengths(): number[] | undefined { return undefined; // Node only fills this for a map it generated internally } #entries(): SourceMapEntry[] { if (this.#decoded) return this.#decoded; const rows = this.#payload.mappings.split(";"); let genLine = 0; let srcIdx = 0, srcLine = 0, srcCol = 0, nameIdx = 0; const out: SourceMapEntry[] = []; for (const row of rows) { let genCol = 0; if (row.length) { for (const seg of row.split(",")) { if (!seg) continue; const fields = decodeVlq(seg); genCol += fields[0] ?? 0; if (fields.length > 1) { srcIdx += fields[1] ?? 0; srcLine += fields[2] ?? 0; srcCol += fields[3] ?? 0; if (fields.length > 4) nameIdx += fields[4] ?? 0; out.push([genLine, genCol, srcIdx, srcLine, srcCol, fields.length > 4 ? nameIdx : -1]); } else { out.push([genLine, genCol, -1, -1, -1, -1]); } } } genLine++; } this.#decoded = out; return out; } findEntry(lineOffset: number, columnOffset: number): Record { const hit = this.#entries().find(([gl, gc]) => gl === lineOffset && gc <= columnOffset && this.#entries().filter(([l]) => l === gl).every(([, c]) => c <= columnOffset || c !== gc)); return this.#entryToObject(hit); } findOrigin(lineNumber: number, columnNumber: number): Record { // 1-indexed line, matching Error.prepareStackTrace call sites, vs findEntry's 0-indexed. const hit = this.#entries().filter(([gl]) => gl === lineNumber - 1).sort((a, b) => b[1] - a[1]).find(([, gc]) => gc <= columnNumber); if (!hit) return {}; return { name: hit[5] >= 0 ? this.#payload.names[hit[5]] : undefined, fileName: this.#payload.sources[hit[2]], lineNumber: hit[3] + 1, columnNumber: hit[4] + 1, }; } #entryToObject(hit: SourceMapEntry | undefined): Record { if (!hit) return {}; const [genLine, genCol, srcIdx, srcLine, srcCol, nameIdx] = hit; return { generatedLine: genLine, generatedColumn: genCol, originalSource: srcIdx >= 0 ? this.#payload.sources[srcIdx] : undefined, originalLine: srcLine, originalColumn: srcCol, name: nameIdx >= 0 ? this.#payload.names[nameIdx] : undefined, }; } } return SourceMap; } const VLQ_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; /** Base64-VLQ decode, the sourcemap spec's own encoding for one `mappings` segment. */ function decodeVlq(segment: string): number[] { const out: number[] = []; let value = 0, shift = 0; for (const ch of segment) { const digit = VLQ_CHARS.indexOf(ch); const cont = digit & 32; value += (digit & 31) << shift; if (cont) { shift += 5; continue; } const negate = value & 1; value >>>= 1; out.push(negate ? -value : value); value = 0; shift = 0; } return out; }