@nubjs/loader 0.0.0 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1725 @@
1
+ // Shared preload machinery for BOTH tiers — CommonJS, zero top-level await.
2
+ //
3
+ // The fast tier (Node 22.15+, minus 23.0–23.4) loads this from a `--require` CJS
4
+ // preload (preload.cjs) so Node keeps its synchronous `Module.runMain` CJS entry path
5
+ // (top-level `executionAsyncId()===1`, sync exception origin, `require.main.id`
6
+ // `'.'`, `module.parent` `null`) — all of which the old `--import` ESM preload
7
+ // broke by forcing eager ESM-loader init that routed even a CJS entry through the
8
+ // async ESM module-job (R1). The compat tier (18.19–22.14 and 23.0–23.4) loads this from its
9
+ // async `--import` preload.mjs and reuses the same hook/require/watch/Temporal
10
+ // logic; only hook REGISTRATION differs (sync `module.registerHooks` on the fast
11
+ // tier vs async `module.register` loader worker on compat), which each entry owns.
12
+ //
13
+ // EVERYTHING here is synchronous and import-of-transform-core is a plain
14
+ // `require()` — transform-core.mjs has no top-level await and is require(esm)-able
15
+ // on the fast tier; the compat entry passes its already-imported core bindings in
16
+ // (it imported them as ESM), so this module never require()s the core there.
17
+
18
+ const compileBootstrap = process[Symbol.for("nub.compile.bootstrap")];
19
+ const getBuiltin = typeof compileBootstrap?.getBuiltin === "function"
20
+ ? compileBootstrap.getBuiltin
21
+ : require;
22
+ const module_ = getBuiltin("node:module");
23
+ const { readdirSync, existsSync } = getBuiltin("node:fs");
24
+ const { fileURLToPath, pathToFileURL } = getBuiltin("node:url");
25
+ const { join, dirname, extname: pathExtname } = getBuiltin("node:path");
26
+
27
+ // Hide nub's ARGV-only V8 flags from `process.execArgv`, FIRST — before any user
28
+ // code, and before anything here can hand the array out.
29
+ //
30
+ // Those flags are precisely the ones Node REFUSES in NODE_OPTIONS, which is why nub
31
+ // puts them on argv. But a lot of real tooling forwards `process.execArgv` into a
32
+ // Worker or into a child's NODE_OPTIONS, and Node then rejects nub's own flag with
33
+ // ERR_WORKER_INVALID_EXEC_ARGV and kills the build — that is exactly how a Next.js
34
+ // 16 + Turbopack build died on `--js-defer-import-eval`. V8 parses these flags at
35
+ // startup, so dropping them here keeps the feature ON while restoring the execArgv a
36
+ // plain-Node user would have seen. Only flags NUB injected are removed; a user's own
37
+ // `v8Flags` stay visible, because those are the user's choice to reason about.
38
+ try {
39
+ const injectedArgvFlags = process.env.__NUB_ARGV_ONLY_FLAGS;
40
+ if (injectedArgvFlags) {
41
+ // Delete rather than propagate: a descendant that nub augments gets its own
42
+ // signal, and one that nub does not never had the flags on argv anyway.
43
+ delete process.env.__NUB_ARGV_ONLY_FLAGS;
44
+ const injected = new Set(injectedArgvFlags.split(" ").filter(Boolean));
45
+ if (Array.isArray(process.execArgv)) {
46
+ process.execArgv = process.execArgv.filter((arg) => !injected.has(arg));
47
+ }
48
+ }
49
+ } catch {
50
+ // Never let execArgv hygiene break startup.
51
+ }
52
+
53
+ // Internal `__NUB_*` plumbing var carrying the running binary's version (set by
54
+ // the Rust spawn layer, coupled to preload injection). Read by installVersionMarker.
55
+ const VERSION_ENV = "__NUB_VERSION";
56
+
57
+ // Whether this Node natively supports import-text (`--experimental-import-text`,
58
+ // added Node 26.5.0 (#62300), backported to 24.19.0). Feature-DETECTED via the
59
+ // accepted-flag set rather than version-parsed — the flag is in
60
+ // `allowedNodeEnvironmentFlags` iff Node knows it. When true, the load hook steps
61
+ // aside and lets Node's own textStrategy own `type:"text"` imports (nub injects the
62
+ // flag in spawn.rs), per the additive contract; where the flag does not exist nub
63
+ // polyfills them via `loadTextImport`. Stepping aside is only safe while the
64
+ // feature-matrix `import-text` bands cover every release that KNOWS the flag: on a
65
+ // version nub steps aside on but does not inject for, the import falls through to
66
+ // Node's default loader and dies with ERR_UNKNOWN_FILE_EXTENSION (#688).
67
+ const NATIVE_IMPORT_TEXT = process.allowedNodeEnvironmentFlags.has("--experimental-import-text");
68
+
69
+ // ── data: URL unknown-format fidelity helpers ───────────────────────
70
+ // Mirror Node's internal/modules/esm/get_format.js so nub's sync registerHooks load
71
+ // hook surfaces ERR_UNKNOWN_MODULE_FORMAT for an unsupported `data:` MIME exactly as
72
+ // plain Node does (see the load hook for why the sync tier needs this pre-check).
73
+ // `mimeToFormat`: text/application javascript -> module, application/json -> json,
74
+ // application/wasm -> wasm, anything else -> null (unknown).
75
+ function dataUrlMimeToFormat(mime) {
76
+ if (mime == null) return null;
77
+ if (/^\s*(text|application)\/javascript\s*(;\s*charset=utf-?8\s*)?$/i.test(mime)) return "module";
78
+ if (mime === "application/json") return "json";
79
+ if (mime === "application/wasm") return "wasm";
80
+ return null;
81
+ }
82
+
83
+ // Returns true when `url` is a `data:` URL whose MIME maps to no module format —
84
+ // i.e. the case where Node would ultimately throw ERR_UNKNOWN_MODULE_FORMAT.
85
+ function unknownDataUrlFormat(url) {
86
+ // Strip the `data:` scheme; Node parses the pathname (everything after `data:`).
87
+ const m = /^([^/]+\/[^;,]+)(?:[^,]*?)(;base64)?,/.exec(url.slice(5));
88
+ const mime = m ? m[1] : null;
89
+ return dataUrlMimeToFormat(mime) === null;
90
+ }
91
+
92
+ // Yarn PnP API handle, fetched lazily via Node's `module.findPnpApi`. `.pnp.cjs`
93
+ // (injected by the Rust spawn layer via --require, ahead of nub's preload) sets
94
+ // `process.versions.pnp` and installs `findPnpApi`, which returns the pnpapi object
95
+ // governing a given path. Unlike a bare `require("pnpapi")` — which throws here,
96
+ // since this preload lives in nub's install dir, OUTSIDE the user's PnP tree —
97
+ // `findPnpApi` resolves by the queried path, so an out-of-tree issuer works. Being
98
+ // a plain query it never re-enters nub's resolve hooks, so there is no ordering
99
+ // constraint with `module.registerHooks` (the reason the previous abs-path require
100
+ // was load-bearing-fragile). nub resolves PnP specifiers through
101
+ // `pnpapi.resolveRequest` (its public, conditions-free resolver) in both the
102
+ // registerHooks resolve hook and the `_resolveFilename` override below. No env var
103
+ // (brand boundary); `null` when this is not a PnP run.
104
+ let __pnpApi;
105
+ function pnpApi() {
106
+ if (__pnpApi) return __pnpApi; // cache only a SUCCESSFUL lookup (see below)
107
+ if (!process.versions.pnp) return null;
108
+ // `findPnpApi` matches by the queried path. A single synthesized `cwd + sep` anchor
109
+ // can miss on Windows (drive-letter casing, 8.3 short paths, trailing separator),
110
+ // and a transient early miss must NOT be cached sticky — otherwise every later
111
+ // resolution falls through to PnP's `_resolveFilename`, which rejects the
112
+ // `conditions` option Node injects under a registered hook (the intermittent
113
+ // Windows `conditions` crash). So try several real in-tree anchors and cache only
114
+ // on success: `argv[1]` is the user's entry file (in-tree for `nub <file>`); cwd
115
+ // covers `nub run` / `nub exec`.
116
+ for (const anchor of [process.argv[1], cwdIssuer(), process.cwd()]) {
117
+ if (!anchor) continue;
118
+ try {
119
+ const api = module_.findPnpApi(anchor);
120
+ if (api) return (__pnpApi = api);
121
+ } catch {}
122
+ }
123
+ return null;
124
+ }
125
+
126
+ // Shared PnP ESM resolution (resolveRequest + format) + the directory-issuer
127
+ // helper, identical to the compat worker's — see runtime/pnp-util.cjs.
128
+ const { pnpResolveEsm, cwdIssuer } = require("./pnp-util.cjs");
129
+
130
+ // ── Watch-mode dependency reporting (main thread only) ──────────────
131
+ // Under `nub watch`, Node's FilesWatcher only watches files in the import graph;
132
+ // config files (tsconfig.json, package.json) and `.env*` are NOT in any graph, so
133
+ // an edit to them otherwise goes stale. Node accepts incremental
134
+ // `process.send({'watch:require': [...]})` over its WATCH_REPORT_DEPENDENCIES IPC
135
+ // at ANY point in the child's life (it adds each path to the watch set), so we
136
+ // report config paths AS the core loader discovers them. The reporters are
137
+ // injected into the core via setWatchHooks so getTsconfigForDir / getPackageType
138
+ // self-report. The flush is coalesced via setImmediate.
139
+ function installWatchReporting(core) {
140
+ const WATCH_REPORTING =
141
+ process.env.WATCH_REPORT_DEPENDENCIES === "1" && typeof process.send === "function";
142
+ const watchReported = new Set();
143
+ const watchPending = [];
144
+ let watchFlushScheduled = false;
145
+ function flushWatchDeps() {
146
+ watchFlushScheduled = false;
147
+ if (watchPending.length === 0) return;
148
+ const batch = watchPending.splice(0, watchPending.length);
149
+ try { process.send({ "watch:require": batch }); } catch {}
150
+ }
151
+ function reportWatchDep(path) {
152
+ if (!WATCH_REPORTING || !path || watchReported.has(path)) return;
153
+ watchReported.add(path);
154
+ watchPending.push(path);
155
+ if (!watchFlushScheduled) {
156
+ watchFlushScheduled = true;
157
+ // A scheduled immediate is drained before the loop would exit, so even a
158
+ // script that finishes synchronously flushes its deps. (Don't unref: an
159
+ // unref'd immediate is skipped on a synchronous exit, dropping the report.)
160
+ setImmediate(flushWatchDeps);
161
+ }
162
+ }
163
+ // Report a directory's `.env*` files (the natural watch targets). Scanned once
164
+ // per directory, lazily.
165
+ const watchEnvScannedDirs = new Set();
166
+ function reportEnvFilesIn(dir) {
167
+ if (!WATCH_REPORTING || watchEnvScannedDirs.has(dir)) return;
168
+ watchEnvScannedDirs.add(dir);
169
+ let entries;
170
+ try { entries = readdirSync(dir); } catch { return; }
171
+ for (const name of entries) {
172
+ if (name === ".env" || name.startsWith(".env.")) reportWatchDep(join(dir, name));
173
+ }
174
+ }
175
+ core.setWatchHooks({ reportDep: reportWatchDep, reportEnvDir: reportEnvFilesIn });
176
+ return WATCH_REPORTING;
177
+ }
178
+
179
+ // ── Resolve / load hooks (sync `module.registerHooks` shape) ────────
180
+ // Returns `{ resolve, load }` closing over `core` + the watch flag. The compat
181
+ // tier does NOT use these (its hooks run async in the loader worker via
182
+ // preload-async-hooks.mjs); only the fast tier's `module.registerHooks` does.
183
+
184
+ // True once USER code registers its own `module.registerHooks` (a ts-node/tsx-style
185
+ // transpiler). nub registers exactly one hook set from the preload (the FIRST call
186
+ // after the wrap below); every later call is the user's. This lets the load hook
187
+ // tell apart a bare `'typescript'` format that a USER resolve hook set (defer — the
188
+ // user's own load hook will transpile) from the bare `'typescript'` that Node's
189
+ // NATIVE CJS loader assigns to a `.ts` entry/require in a package with no explicit
190
+ // `type` (transpile — there is no user hook to do it, and Node's strip-only mode
191
+ // can't handle enums/namespaces). See makeHooks().load.
192
+ let __userHooksRegistered = false;
193
+ function installUserHookDetector() {
194
+ if (typeof module_.registerHooks !== "function") return;
195
+ const orig = module_.registerHooks;
196
+ if (orig.__nubWrapped) return;
197
+ let seen = 0;
198
+ const wrapped = function (...args) {
199
+ // Call #1 is nub's own preload registration; #2+ are user hooks.
200
+ if (seen >= 1) __userHooksRegistered = true;
201
+ seen += 1;
202
+ return orig.apply(this, args);
203
+ };
204
+ wrapped.__nubWrapped = true;
205
+ try { module_.registerHooks = wrapped; } catch {}
206
+ }
207
+
208
+ // True once a USER async ESM loader is active — registered via `module.register()`
209
+ // at runtime (the tsx / ts-node/esm pattern, incl. via `--import`). On the FAST tier
210
+ // nub itself NEVER calls `module.register()` (it uses sync `module.registerHooks`),
211
+ // so on that tier any `module.register()` call is the user's. We wrap it to observe
212
+ // runtime registrations; the static CLI-flag forms (`--experimental-loader`/`--loader`
213
+ // /`--import`) are read separately from `process.execArgv` (see userAsyncLoaderActive).
214
+ let __userAsyncLoaderRegistered = false;
215
+ function installUserAsyncLoaderDetector() {
216
+ if (typeof module_.register !== "function") return;
217
+ const orig = module_.register;
218
+ if (orig.__nubAsyncWrapped) return;
219
+ const wrapped = function (...args) {
220
+ // On the fast tier nub never calls module.register, so every call here is the
221
+ // user's. (registerLoaderWorker — nub's only register caller — is the compat /
222
+ // require(esm)-off path, which does not run the fast-tier sync load hook that
223
+ // performs the commonjs-sync relabel, so a false positive there is harmless.)
224
+ __userAsyncLoaderRegistered = true;
225
+ return orig.apply(this, args);
226
+ };
227
+ wrapped.__nubAsyncWrapped = true;
228
+ try { module_.register = wrapped; } catch {}
229
+ }
230
+
231
+ // Did the process start with a CLI flag that registers a user async ESM loader?
232
+ // `--experimental-loader` / `--loader` register a loader directly; `--import` runs a
233
+ // module that commonly calls `module.register()` (tsx, ts-node/esm). Read once from
234
+ // `process.execArgv` — these flags appear before any user code runs, so this is a
235
+ // reliable preload-time signal FOR THAT CHANNEL ONLY: `execArgv` carries the flags
236
+ // passed on the command line, and NONE of the NODE_OPTIONS ones (verified on Node
237
+ // 26.5.0 for `--import`/`--loader`/`--experimental-loader`/`--require`). Callers that
238
+ // must see a loader however it was delivered want foreignAsyncLoaderFlagPresent(),
239
+ // which scans both channels — reading only this one is what caused #669.
240
+ // Conservative on `--import`: a `--import` that does NOT register a loader is harmless
241
+ // to relabel, but presence of the flag declines the optimization rather than risk
242
+ // interop breakage (correctness over coverage).
243
+ let __cliAsyncLoaderCache;
244
+ function cliAsyncLoaderPresent() {
245
+ if (__cliAsyncLoaderCache !== undefined) return __cliAsyncLoaderCache;
246
+ let present = false;
247
+ try {
248
+ const argv = process.execArgv;
249
+ if (Array.isArray(argv)) {
250
+ for (const a of argv) {
251
+ if (typeof a !== "string") continue;
252
+ if (
253
+ a === "--loader" || a.startsWith("--loader=") ||
254
+ a === "--experimental-loader" || a.startsWith("--experimental-loader=") ||
255
+ a === "--import" || a.startsWith("--import=")
256
+ ) { present = true; break; }
257
+ }
258
+ }
259
+ } catch { /* execArgv unavailable — treat as no loader */ }
260
+ __cliAsyncLoaderCache = present;
261
+ return present;
262
+ }
263
+
264
+ // The guard for the `commonjs-sync` relabel: is a USER async ESM loader active on the
265
+ // import-of-CJS path? Relabel ONLY when nub is the sole loader (the common case —
266
+ // next build/dev) so we never route a user loader's inner require()s through its own
267
+ // ESM resolve hook (the interop break documented at the load hook below). Either a
268
+ // loader flag OR a runtime module.register() disqualifies the optimization.
269
+ //
270
+ // Goes through foreignAsyncLoaderFlagPresent (BOTH delivery channels), not the
271
+ // execArgv-only cliAsyncLoaderPresent: `--experimental-loader` registers its loader
272
+ // NATIVELY, so it never calls module.register() and never trips the runtime detector
273
+ // either. With the flag delivered via NODE_OPTIONS — which is how OpenTelemetry's own
274
+ // docs prescribe the ESM attach — both of the old channels missed it, the relabel ran
275
+ // against a user async loader, and Node rejected the `commonjs-sync`+null-source pair
276
+ // (#669). That helper also excludes nub's OWN preload chainer, which rides NODE_OPTIONS
277
+ // as `--import`; a raw scan here would read it as a user loader and silently decline
278
+ // the relabel for every chained project.
279
+ function userAsyncLoaderActive() {
280
+ return __userAsyncLoaderRegistered || foreignAsyncLoaderFlagPresent();
281
+ }
282
+
283
+ // The Node band where the async `module.register` loader's `resolveSync`/`loadSync`
284
+ // are unimplemented stubs that throw `ERR_METHOD_NOT_IMPLEMENTED`: 22.15.0 ..= 24.11.0
285
+ // (fixed in 24.11.1, nodejs/node#59666). Mirrors the Rust `node_hook_compose_broken`
286
+ // (spawn.rs) for every RELEASE version; a pre-release like `22.15.0-rc.1` reads as in-band
287
+ // here (the numeric parse ignores the `-rc` tag) where the Rust semver sorts it just below
288
+ // the 22.15.0 floor — a harmless over-selection (the async tier is always correct and the
289
+ // two signals are OR'd), never a crash. Outside this band a foreign async loader composes
290
+ // with nub's sync hooks natively, so the fast tier stays.
291
+ function nodeHookComposeBroken() {
292
+ const p = String(process.versions.node).split(".");
293
+ const maj = parseInt(p[0], 10) || 0;
294
+ const min = parseInt(p[1], 10) || 0;
295
+ const pat = parseInt(p[2], 10) || 0;
296
+ const geFloor = maj > 22 || (maj === 22 && min >= 15);
297
+ const leCeil = maj < 24 || (maj === 24 && (min < 11 || (min === 11 && pat === 0)));
298
+ return geFloor && leCeil;
299
+ }
300
+
301
+ // Does a foreign async ESM loader flag (`--import` / `--loader` / `--experimental-loader`)
302
+ // ride in THIS process's own startup flags, via EITHER channel? tsx/ts-node deliver their
303
+ // loader through one of two paths, and they land in different places:
304
+ // • re-exec argv → `process.execArgv` (tsx's bin re-execs `node --import <loader>`), and
305
+ // • `NODE_OPTIONS="--import tsx/esm"` → `process.env.NODE_OPTIONS` (NOT hoisted into
306
+ // execArgv — verified on Node 24.x), the common CI/shell-config delivery.
307
+ // Both must be scanned. nub's own fast-tier injection is `--require` (never `--import`/
308
+ // `--loader`) on this band, and its compat-tier `--import preload.mjs` lives below 22.15
309
+ // (outside the broken band this gates on), so any such flag here is FOREIGN.
310
+ // nub's OWN preload chainer is not a foreign loader. It rides `--import` like one, so
311
+ // a plain regex over NODE_OPTIONS matches it and nub forces ITSELF onto the async
312
+ // tier — which spawns a loader worker, and Node re-runs every `--require` preload in
313
+ // that worker's realm, so the CJS chain runs twice. Recognise and skip it.
314
+ const NUB_CHAIN_MARKER = /[\\/]\.nub[\\/]preload-chain\./;
315
+
316
+ // Memoized like cliAsyncLoaderPresent, and for the same reason: the flags are fixed
317
+ // before any user code runs, and the relabel guard now calls this per import-of-CJS,
318
+ // so a later mutation of process.env.NODE_OPTIONS must not make two modules in one
319
+ // process take different branches.
320
+ let __foreignAsyncLoaderCache;
321
+ function foreignAsyncLoaderFlagPresent() {
322
+ if (__foreignAsyncLoaderCache !== undefined) return __foreignAsyncLoaderCache;
323
+ __foreignAsyncLoaderCache = computeForeignAsyncLoaderFlagPresent();
324
+ return __foreignAsyncLoaderCache;
325
+ }
326
+
327
+ function computeForeignAsyncLoaderFlagPresent() {
328
+ if (cliAsyncLoaderPresent()) return true; // execArgv channel
329
+ const opts = process.env.NODE_OPTIONS;
330
+ if (typeof opts !== "string" || opts === "") return false;
331
+ const re = /(?:^|\s)--(?:experimental-)?(?:import|loader)(?:=|\s)("[^"]*"|\S*)/g;
332
+ for (const match of opts.matchAll(re)) {
333
+ const value = (match[1] || "").replace(/^"|"$/g, "");
334
+ if (!NUB_CHAIN_MARKER.test(value)) return true;
335
+ }
336
+ return false;
337
+ }
338
+
339
+ // Should nub auto-select its async loader-worker tier at PRELOAD time because a foreign
340
+ // async ESM loader (tsx/ts-node) rides in THIS process's own startup flags, on the
341
+ // broken-compose band? This is the INTRINSIC counterpart to the launcher's predictive argv
342
+ // scan (`__NUB_FORCE_ASYNC_TIER`, spawn.rs): because it reads the process's OWN flags, it
343
+ // fires regardless of how the process was launched — a nested `nub run`, a `child_process`
344
+ // spawn (Playwright's globalSetup), or a shell wrapper — all spawn shapes the launcher-side
345
+ // scan cannot see (nub#460). nub's `--require` preload runs before `--import` executes, so
346
+ // the flag is observable here before the foreign loader registers, letting nub pick the
347
+ // composable tier up front (a mid-flight sync→async swap is impossible — the loader-worker
348
+ // cannot be registered from inside another loader's registration on this band). Deliberately
349
+ // conservative: any such flag on the band takes the async tier, even the rare one that
350
+ // registers no loader — a small worker-startup cost on a shrinking Node band, never a crash.
351
+ function shouldAutoAsyncTierAtPreload() {
352
+ return nodeHookComposeBroken() && foreignAsyncLoaderFlagPresent();
353
+ }
354
+
355
+ // ── Internal `module.register()` without the DEP0205 leak ────────────
356
+ // `module.register()` is the loader-WORKER registration surface (async ESM hooks in
357
+ // a dedicated thread). nub uses it for the compat tier (18.19–22.14, where the sync
358
+ // `module.registerHooks` doesn't exist) and for the fast tier's
359
+ // `--no-experimental-require-module` fallback (where `require(esm)` is off, so the
360
+ // in-thread sync hooks can't load transform-core.mjs synchronously). On Node 26+,
361
+ // `module.register()` emits a one-shot `[DEP0205]` DeprecationWarning steering callers
362
+ // to `module.registerHooks()` — but nub CANNOT use `registerHooks` on these paths
363
+ // (no sync surface on compat; no sync core load when require(esm) is disabled), and
364
+ // the deprecation is for nub's OWN internal call, not anything the user wrote: the
365
+ // user has no action to take, so the warning is pure noise on their stderr. Suppress
366
+ // exactly that DEP0205 emission for the duration of nub's own register() call, then
367
+ // restore `process.emitWarning` untouched, so a user's later `module.register()` (or
368
+ // any other deprecation) still warns normally. Default-preserving: only nub's
369
+ // internal call is silenced, only for DEP0205, only on the versions that emit it.
370
+ function registerLoaderWorker(specifier, parentURL, options) {
371
+ const realEmitWarning = process.emitWarning;
372
+ let restored = false;
373
+ const restore = () => {
374
+ if (restored) return;
375
+ restored = true;
376
+ try { process.emitWarning = realEmitWarning; } catch {}
377
+ };
378
+ try {
379
+ process.emitWarning = function (warning, ...rest) {
380
+ // Node calls emitWarning(msg, 'DeprecationWarning', 'DEP0205', ...) for the
381
+ // module.register() deprecation. Swallow only that exact code; pass everything
382
+ // else (including any non-DEP0205 deprecation) straight through.
383
+ const code = typeof rest[0] === "object" && rest[0] !== null ? rest[0].code : rest[1];
384
+ if (code === "DEP0205") return;
385
+ return realEmitWarning.call(this, warning, ...rest);
386
+ };
387
+ return module_.register(specifier, parentURL, options);
388
+ } finally {
389
+ restore();
390
+ }
391
+ }
392
+
393
+ // Is this error Node's "an async `module.register` loader cannot service a SYNCHRONOUS
394
+ // resolve/load" stub? On Node 22.15–~24.11 the async-hooks proxy's `resolveSync`/
395
+ // `loadSync` are stubs that unconditionally `throw new ERR_METHOD_NOT_IMPLEMENTED(...)`.
396
+ // nub's fast-tier SYNC `module.registerHooks` hooks force EVERY resolve/load onto the
397
+ // synchronous chain; when a USER async loader (e.g. @tailwindcss/node's
398
+ // esm-cache.loader.mjs under Turbopack) is ALSO registered, the chain's default step
399
+ // reaches that stub and throws — killing the build. We must detect this WITHOUT the
400
+ // `userAsyncLoaderActive()` flag: the very FIRST throwing resolution is the loader
401
+ // module's own specifier, resolved DURING `module.register` before the detector flag is
402
+ // observable, so the flag is false exactly when recovery is needed. The error code +
403
+ // message is the reliable signal. (Node 24.12+/25.2+/26 implement these methods, so the
404
+ // stub never throws there and this never fires.)
405
+ function isAsyncLoaderSyncStub(err) {
406
+ if (!err || typeof err.message !== "string") return false;
407
+ // Two shapes across the affected Node band: (a) the method exists but is a stub that
408
+ // throws ERR_METHOD_NOT_IMPLEMENTED('resolveSync()'/'loadSync()') (e.g. 24.3); (b) the
409
+ // method is ENTIRELY ABSENT, so Node's `this[#customizations].resolveSync/loadSync(...)`
410
+ // throws a TypeError "... is not a function" (e.g. 22.16/24.11). Match both.
411
+ if (err.code === "ERR_METHOD_NOT_IMPLEMENTED" &&
412
+ (err.message.includes("resolveSync") || err.message.includes("loadSync"))) {
413
+ return true;
414
+ }
415
+ return err instanceof TypeError &&
416
+ (err.message.includes("resolveSync is not a function") ||
417
+ err.message.includes("loadSync is not a function"));
418
+ }
419
+
420
+ // Resolve a specifier to a URL the registerHooks resolve chain can return, as the
421
+ // recovery path when Node's default resolve step throws the async-loader stub (see
422
+ // isAsyncLoaderSyncStub). Returns `{ url, shortCircuit }`, or null if it cannot resolve
423
+ // (caller re-throws the original error so behavior is unchanged).
424
+ //
425
+ // Uses the parent's CommonJS resolver (`createRequire().resolve`) — the only fully-sync,
426
+ // conditions-capable resolver available in this `--require` CJS preload. It honors
427
+ // `node_modules`, package `exports`, relative + absolute paths, and bare/scoped
428
+ // specifiers, but under the REQUIRE condition; a DUAL package whose `exports` differ by
429
+ // `import`/`require` gets its `require` build where Node's default ESM resolve would
430
+ // return the `import` build (latent — the in-the-wild trigger resolves non-dual internal
431
+ // modules). Builtins MUST be mapped back to `node:`: `require.resolve("fs")` returns the
432
+ // bare name `"fs"`, which must not become a bogus `file://<cwd>/fs` URL.
433
+ function resolveViaParentRequire(specifier, parentURL) {
434
+ try {
435
+ // An ALREADY-RESOLVED specifier needs no resolution — return it verbatim. This is the
436
+ // common Turbopack/Tailwind trigger: Node resolves the loader module's OWN absolute
437
+ // `file://` URL (`@tailwindcss/node/dist/esm-cache.loader.mjs`) synchronously during
438
+ // `module.register`, with `parentURL` = `data:` (no useful base). `require.resolve`
439
+ // cannot take a `file://` URL string, so pass these through directly. Builtins and
440
+ // `data:` specifiers are likewise already-resolved.
441
+ if (specifier.startsWith("file:") || specifier.startsWith("data:")) {
442
+ return { url: specifier, shortCircuit: true };
443
+ }
444
+ if (specifier.startsWith("node:") || module_.isBuiltin(specifier)) {
445
+ return { url: specifier.startsWith("node:") ? specifier : `node:${specifier}`, shortCircuit: true };
446
+ }
447
+ const base = parentURL && String(parentURL).startsWith("file:")
448
+ ? String(parentURL)
449
+ : pathToFileURL(join(process.cwd(), "noop.js")).href;
450
+ const resolved = module_.createRequire(base).resolve(specifier);
451
+ if (module_.isBuiltin(resolved)) {
452
+ return { url: resolved.startsWith("node:") ? resolved : `node:${resolved}`, shortCircuit: true };
453
+ }
454
+ const url = resolved.startsWith("node:") || resolved.startsWith("data:")
455
+ ? resolved
456
+ : pathToFileURL(resolved).href;
457
+ return { url, shortCircuit: true };
458
+ } catch {
459
+ return null;
460
+ }
461
+ }
462
+
463
+ // Phantom-dependency remediation for nub's isolated layout. The isolated layout
464
+ // (the post-flip default for npm/yarn/bun incumbents, and the standing default
465
+ // for pnpm/fresh projects) exposes only DECLARED packages at the importer's
466
+ // node_modules, so an UNDECLARED transitive that resolved under a flat
467
+ // node_modules fails. When a BARE specifier hits (ERR_)MODULE_NOT_FOUND,
468
+ // `phantomDepHint` returns the one-line opt-out to append — but ONLY when the
469
+ // package is genuinely a phantom dep.
470
+ //
471
+ // Two conditions, both required, so the hint fires ONLY for a true phantom dep:
472
+ // (1) the bare package is in the project's graph — nub names every graph
473
+ // package `<name>@<version>` (scoped `/` → `+`) under `node_modules/.store`,
474
+ // so a matching entry means it's installed (GVS: symlinks into the global
475
+ // store; non-GVS: real dirs);
476
+ // (2) the bare package is NOT reachable at any `node_modules/<pkg>` up the
477
+ // tree — otherwise the package resolves and the MODULE_NOT_FOUND is a
478
+ // missing SUBPATH/file inside it (`react-dom/client-typo`), not a phantom.
479
+ // A genuine typo / absent dep fails (1); a subpath miss of a declared dep fails
480
+ // (2); the hoisted opt-out leaves no `<name>@*` entries (only internal state),
481
+ // and node-suite / plain-Node trees have no `.store` at all — none get the hint.
482
+ // NOT COVERED: the compat-tier ESM loader (Node 18.19–22.14 `import`) resolves
483
+ // in preload-async-hooks.mjs and can't reach this CJS helper, so that band
484
+ // surfaces Node's standard error without the hint; the fast tier (22.15+) and
485
+ // CommonJS `require()` on every tier are covered.
486
+ function phantomDepHint(specifier, fromPath) {
487
+ if (!specifier || !fromPath) return null;
488
+ const c = specifier[0];
489
+ if (c === "." || c === "/" || c === "#" || c === "\\") return null;
490
+ if (specifier.startsWith("node:") || module_.isBuiltin(specifier)) return null;
491
+ if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(specifier)) return null; // file:/data:/http:/C:\ …
492
+ const parts = specifier.split("/");
493
+ const pkg = c === "@" ? parts.slice(0, 2).join("/") : parts[0];
494
+ if (!pkg) return null;
495
+ let dir;
496
+ try {
497
+ dir = dirname(String(fromPath).startsWith("file:") ? fileURLToPath(fromPath) : fromPath);
498
+ } catch {
499
+ return null;
500
+ }
501
+ const prefix = pkg.replace(/\//g, "+") + "@";
502
+ let inStore = false;
503
+ for (let i = 0; i < 40 && dir; i++) {
504
+ const nm = join(dir, "node_modules");
505
+ // Reachable here ⇒ the bare package resolves, so this miss is a subpath/file
506
+ // inside it, not a phantom dep — suppress. existsSync follows the isolated
507
+ // layout's top-level symlink to the real package.
508
+ if (existsSync(join(nm, pkg))) return null;
509
+ if (!inStore) {
510
+ try {
511
+ inStore = readdirSync(join(nm, ".store")).some((e) => e.startsWith(prefix));
512
+ } catch {
513
+ /* no `.store` at this level */
514
+ }
515
+ }
516
+ const parent = dirname(dir);
517
+ if (parent === dir) break;
518
+ dir = parent;
519
+ }
520
+ // In the graph's virtual store yet never reachable up the tree → phantom dep.
521
+ return inStore
522
+ ? `\n\n"${pkg}" is installed but not reachable here — it's a phantom dependency (an ` +
523
+ `undeclared transitive). nub's isolated node_modules exposes only the packages ` +
524
+ `declared in package.json. Add "${pkg}" to your dependencies, or add ` +
525
+ "`node-linker=hoisted` to .npmrc to use a flat node_modules."
526
+ : null;
527
+ }
528
+
529
+ // Append `hint` to a thrown error's message AND its printed `.stack`. V8
530
+ // materializes `.stack` LAZILY from the current `.message`, so the original
531
+ // hint-free stack MUST be read before `.message` is mutated — reading it after
532
+ // would already contain the new message and the `.replace` would double the
533
+ // hint. The marker makes it idempotent if the same error passes two catch
534
+ // sites. Best-effort: a frozen/exotic error is left verbatim.
535
+ function annotateError(err, hint) {
536
+ try {
537
+ if (!err || typeof err !== "object" || err.__nubPhantomHinted) return;
538
+ Object.defineProperty(err, "__nubPhantomHinted", { value: true, configurable: true });
539
+ const oldStack = typeof err.stack === "string" ? err.stack : null;
540
+ const oldMsg = err.message;
541
+ err.message = oldMsg + hint;
542
+ if (oldStack !== null) {
543
+ err.stack = oldStack.includes(oldMsg) ? oldStack.replace(oldMsg, err.message) : oldStack + hint;
544
+ }
545
+ } catch {
546
+ /* read-only error: leave verbatim */
547
+ }
548
+ }
549
+
550
+ // Upstream Node bug: CJS `require()` of a SCHEME-ONLY builtin (`node:test`,
551
+ // `node:sqlite`, `node:sea`, `node:test/reporters`) throws
552
+ // ERR_INVALID_RETURN_PROPERTY_VALUE ("… but got null") whenever ANY sync resolve
553
+ // hook is registered. Measured per-release: broken on 22.15.0–22.17.1,
554
+ // 23.5.0–23.11.1 and 24.0.0–24.3.0; fixed in 22.18.0+, 24.4.0+ and 25+ by
555
+ // nodejs/node#58612 (bfc68c8ae8, for nodejs/node#58607). The 23.x
556
+ // line reached end-of-life without the backport, and below 22.15/23.5
557
+ // `module.registerHooks` does not exist at all, so the bug is unreachable there. A
558
+ // plain-Node pass-through hook reproduces it exactly — nub only makes it
559
+ // unconditional, by always hooking on the fast tier.
560
+ //
561
+ // Mechanism: with hooks present, `resolveForCJSWithHooks` leaves its fast path and
562
+ // recomputes the URL as `convertCJSFilenameToURL(<normalized id>)`. The old helper
563
+ // keyed on `BuiltinModule.normalizeRequirableId(id)`, which is FALSE for a bare
564
+ // scheme-only id — `require("test")` is not legal — so `test` matched neither the
565
+ // builtin branch nor `isAbsolute` and came back VERBATIM. That bare id then rides
566
+ // into the load chain, where `validateLoad` waives the string-source requirement
567
+ // only for a `node:`-prefixed url, so the default step's own correct
568
+ // `{ format: "builtin", source: null }` is rejected. Upstream's fix was to strip any
569
+ // `node:` prefix and test `canBeRequiredByUsers` instead. A REGULAR builtin was
570
+ // never affected: `normalizeRequirableId("fs")` is truthy, so it already round-
571
+ // tripped to `node:fs`.
572
+ //
573
+ // Re-prefixing reproduces the fixed helper's output at the one place nub can reach.
574
+ // It is a provable no-op on a fixed Node, where the url already starts with `node:`
575
+ // and the guard cannot fire, and `isBuiltin("node:" + url)` selects exactly the
576
+ // scheme-only set — a `file:`/`data:` url, a Windows path and a bare regular builtin
577
+ // all fail it.
578
+ //
579
+ // This hook is SHARED with ESM: a `registerHooks` resolve hook fires for `import`
580
+ // too, on every version (verified on 22.15.0, 24.3.0 and 26.7.0 — both
581
+ // `import("node:test")` and a relative `import` reach it). What keeps ESM safe is
582
+ // not unreachability but the colon guard: ESM resolution always yields a
583
+ // scheme-bearing URL (`node:test`, `file:///…`), so the rewrite short-circuits
584
+ // before it can apply. Only the CJS `require()` path ever produces a bare id.
585
+ function restoreSchemeOnlyBuiltinURL(result) {
586
+ try {
587
+ const url = result && result.url;
588
+ if (typeof url !== "string" || url === "" || url.includes(":")) return result;
589
+ if (!module_.isBuiltin(`node:${url}`)) return result;
590
+ return { ...result, url: `node:${url}` };
591
+ } catch {
592
+ return result;
593
+ }
594
+ }
595
+
596
+ function makeHooks(core, watchReporting) {
597
+ installUserHookDetector();
598
+ installUserAsyncLoaderDetector();
599
+
600
+ function resolve(specifier, context, nextResolve) {
601
+ const r = core.resolveSpec(specifier, context.parentURL);
602
+ if (r) return r;
603
+ // Yarn PnP (ESM): PnP doesn't patch the ESM loader, so `import` of a PnP dep must
604
+ // be resolved explicitly — through `pnpapi.resolveRequest`, passing Node's
605
+ // `context.conditions` (the import-side set) so a DUAL package resolves to its
606
+ // `import` build, not its `require` build. Returns a virtual `.zip` path Node
607
+ // reads via the zipfs patch. If the api is momentarily unavailable we fall through
608
+ // to `nextResolve`, which reaches nub's `_resolveFilename` override (delegating to
609
+ // PnP) — so a plain dep still resolves; only a dual package's condition is lost.
610
+ const pnp = pnpApi();
611
+ if (pnp && !module_.isBuiltin(specifier) && !specifier.startsWith("node:")) {
612
+ try {
613
+ const res = pnpResolveEsm(pnp, specifier, context);
614
+ if (res) return res;
615
+ } catch { /* fall through to Node's resolver */ }
616
+ }
617
+ try {
618
+ return restoreSchemeOnlyBuiltinURL(nextResolve(specifier, context));
619
+ } catch (err) {
620
+ if (isAsyncLoaderSyncStub(err)) {
621
+ const fallback = resolveViaParentRequire(specifier, context.parentURL);
622
+ if (fallback) return fallback;
623
+ }
624
+ if (err && err.code === "ERR_MODULE_NOT_FOUND") {
625
+ const hint = phantomDepHint(specifier, context.parentURL);
626
+ if (hint) annotateError(err, hint);
627
+ }
628
+ throw err;
629
+ }
630
+ }
631
+
632
+ // Recovery for the async-loader `loadSync` stub: load a `file:` module ourselves when
633
+ // Node's default load step throws ERR_METHOD_NOT_IMPLEMENTED (see isAsyncLoaderSyncStub).
634
+ // Reads source from disk and derives the format from extension + nearest package `type`
635
+ // (nub's own moduleFormatFor — the same source-of-truth as the transpile path). For a
636
+ // transpilable TS/JSX file outside node_modules, route through nub's transpiler; for a
637
+ // data-extension, through loadData; otherwise hand back raw source with the derived
638
+ // format. Returns null if it cannot (caller re-throws). A CJS result keeps source:null
639
+ // and hands off to the native CommonJS loader, matching the default-load contract.
640
+ function loadViaDisk(url, ext) {
641
+ try {
642
+ const path = fileURLToPath(url);
643
+ if (core.TRANSPILE_EXTS.has(ext) && !core.isDependency(url)) {
644
+ return core.loadTranspile(url, ext);
645
+ }
646
+ // Plain JS: transpile only when transformable; else fall through to the raw-
647
+ // source path below (which hands CJS back as `source:null` → Node's native
648
+ // CJS loader), byte-identical to a non-intercepted file.
649
+ if (core.PLAIN_JS_EXTS.has(ext) && !core.isDependency(url)) {
650
+ const r = core.maybeTranspilePlainJs(url, ext);
651
+ if (r) return r;
652
+ }
653
+ if (ext in core.dataExtsFor(url)) return core.loadData(url, ext);
654
+ const { readFileSync } = getBuiltin("node:fs");
655
+ const source = readFileSync(path);
656
+ const pkgType = core.getPackageType(dirname(path));
657
+ const format = core.moduleFormatFor(ext, pkgType, path, source.toString("utf8"));
658
+ if (format === "commonjs") return { format: "commonjs", source: null, shortCircuit: true };
659
+ return { format, source, shortCircuit: true };
660
+ } catch {
661
+ return null;
662
+ }
663
+ }
664
+
665
+ function load(url, context, nextLoad) {
666
+ const ext = core.extname(url);
667
+
668
+ // Watch mode: surface this file's nearest config files (tsconfig.json,
669
+ // package.json) + sibling `.env*` so edits to them restart the run. Done for
670
+ // every user file (not just transpiled ones) — getTsconfigForDir/
671
+ // getPackageType self-report via the injected watch hooks.
672
+ if (watchReporting && url.startsWith("file:") && !core.isNodeModules(url)) {
673
+ try {
674
+ const dir = dirname(fileURLToPath(url));
675
+ core.getTsconfigForDir(dir);
676
+ core.getPackageType(dir);
677
+ } catch {}
678
+ }
679
+
680
+ // Import Text (attribute-keyed): honor `with { type: "text" }` on ANY extension,
681
+ // ahead of extension dispatch so `import s from "./c.yaml" with {type:"text"}`
682
+ // returns raw text, not parsed YAML. Where Node knows the flag (NATIVE_IMPORT_TEXT
683
+ // — 24.19+ on the 24.x line, 26.5+ on 26.x) step aside and let Node's own
684
+ // textStrategy own it — nub injects --experimental-import-text there, so the
685
+ // additive "would plain Node + the flag do the same?" test holds and users get
686
+ // Node's exact semantics. Elsewhere Node has no text-import support, so nub
687
+ // polyfills via loadTextImport (placed after watch reporting so a text file gets
688
+ // the same watch treatment, and before the extension/data dispatch so the attribute
689
+ // wins over the `.txt`/`.yaml`/… data loaders and Node-native JSON; it shortCircuits
690
+ // so Node's own unknown-'text'-attribute validation never runs). The compat-tier
691
+ // hook in preload-async-hooks.mjs always polyfills — that tier tops out at Node
692
+ // 22.14, below every flag-bearing release, so native import-text is never reachable
693
+ // there.
694
+ // (Node 18.20+ parses the `with` syntax; the 18.19.x floor cannot.)
695
+ // The scheme gate applies only to the POLYFILL leg: this branch precedes
696
+ // extension dispatch, so `extname`'s gate does not cover it, and the polyfill
697
+ // reads the bytes off disk — only a `file:` URL has bytes there. The native
698
+ // leg needs no scheme restriction: it hands the URL straight back to the
699
+ // chain, and gating it would drop `data:text/plain` + `type: "text"` into
700
+ // the unknown-data-URL-format trap below instead of Node's own text answer.
701
+ // A non-`file:` URL on the polyfill tier falls through to `nextLoad` with
702
+ // every other unclaimed URL.
703
+ if (context?.importAttributes?.type === "text" && (NATIVE_IMPORT_TEXT || core.isFileUrl(url))) {
704
+ return NATIVE_IMPORT_TEXT ? nextLoad(url, context) : core.loadTextImport(url);
705
+ }
706
+
707
+ // A USER resolve hook (a ts-node/tsx-style transpiler registered AFTER nub's
708
+ // own preload hook) claimed this file with the bare 'typescript' format: defer
709
+ // to the user's own load chain. The discriminator is `__userHooksRegistered`,
710
+ // NOT the bare format alone — Node's NATIVE CJS loader ALSO emits the bare
711
+ // string 'typescript' for a `.ts` entry/require whose nearest package.json has
712
+ // no explicit `type` (cjs/loader.js getFormatOfExtensionlessFile, lines ~1986),
713
+ // and in that native case nub MUST transpile (Node's strip-only mode can't
714
+ // handle enums/namespaces). So we only step aside when a user hook is present:
715
+ // nub registers exactly one hook set from the preload, the user registers theirs
716
+ // later, and registering theirs OUTERMOST (LIFO) means their load hook wraps
717
+ // nub's — it sets format='typescript', calls nextLoad into nub, and (without this
718
+ // guard) nub would transpile with oxc — a type-stripper, not a module-format
719
+ // transformer — leaving `export {}` verbatim and, for a `type:commonjs` package,
720
+ // handing Node format='commonjs' + ESM source = invalid CJS. Stepping aside lets
721
+ // nub fall through to Node's native load, returning raw TS source back up to the
722
+ // user's outer hook, which does the real ESM->CJS conversion, matching Node.
723
+ // Native 'module-typescript'/'commonjs-typescript' formats still fall through to
724
+ // nub's transpile below, so normal augmentation is unchanged.
725
+ if (__userHooksRegistered && context && context.format === "typescript") {
726
+ return nextLoad(url, context);
727
+ }
728
+
729
+ // R12: never transpile `.ts`/`.tsx`/… inside node_modules. Node itself throws
730
+ // ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING for TS under node_modules; if
731
+ // nub transpiled it instead, that native error would never surface and nub
732
+ // would be MORE permissive than Node. Fall through to `nextLoad` so Node's own
733
+ // handling (and its error) applies. (The TS-parent extensionless resolution in
734
+ // the resolve hook is intended and stays — only this load-time transpile is
735
+ // gated.)
736
+ if (core.TRANSPILE_EXTS.has(ext) && !core.isDependency(url)) {
737
+ return core.loadTranspile(url, ext);
738
+ }
739
+ // Project-source plain JS (`.js`/`.mjs`/`.cjs`): transpile ONLY when it carries
740
+ // transformable syntax. A no-op plain-JS file returns null here and falls through
741
+ // to Node's native loader (the `nextLoad`/relabel path below) BYTE-FOR-BYTE — it
742
+ // is never intercepted, so native CJS/ESM behavior (the relabel, require.cache,
743
+ // the require-of-ESM-syntax-`.cjs` error) is preserved. node_modules excluded.
744
+ if (core.PLAIN_JS_EXTS.has(ext) && !core.isDependency(url)) {
745
+ const r = core.maybeTranspilePlainJs(url, ext);
746
+ if (r) return r;
747
+ }
748
+ // Data-format imports. dataExtsFor pins node_modules to nub's BUILT-IN loaders, so
749
+ // the project's `loader` config can't redefine how a dependency's imports load.
750
+ if (ext in core.dataExtsFor(url)) return core.loadData(url, ext);
751
+
752
+ // Fidelity: a `data:` URL whose MIME maps to no module format (e.g.
753
+ // `data:application/x-unknown,…`) must surface Node's ERR_UNKNOWN_MODULE_FORMAT.
754
+ // Node's default load returns `format: null` for this, which its ASYNC loader path
755
+ // later converts to ERR_UNKNOWN_MODULE_FORMAT in validateLoadResult. But nub's SYNC
756
+ // `module.registerHooks` load hook routes the default step's result through
757
+ // customization_hooks' validateLoadSloppy -> validateFormat, which accepts only a
758
+ // string or `undefined` and throws ERR_INVALID_RETURN_PROPERTY_VALUE on `null` —
759
+ // and it does so INSIDE the `nextLoad` call below (the validator wraps each step),
760
+ // so nub never gets the result back to normalize it, and nub's own load-hook frame
761
+ // leaks into the user-visible stack. Vanilla Node, having registered no hook on this
762
+ // path, never hits that validator and throws the correct ERR_UNKNOWN_MODULE_FORMAT.
763
+ // Return `format: undefined` (not the default step's `null`) WITHOUT calling the
764
+ // default load: undefined passes validateFormat, then Node's own
765
+ // #translate -> validateLoadResult sees format == null and throws the NATIVE
766
+ // ERR_UNKNOWN_MODULE_FORMAT — byte-identical to plain Node (the `[code]` name
767
+ // decoration, the exact message, and a stack with zero nub frames). Short-circuit
768
+ // so the chain stops here; the empty source is never read (the throw precedes any
769
+ // translation).
770
+ if (typeof url === "string" && url.startsWith("data:") &&
771
+ (!context || context.format == null) &&
772
+ unknownDataUrlFormat(url)) {
773
+ return { format: undefined, source: "", shortCircuit: true };
774
+ }
775
+
776
+ let r;
777
+ try {
778
+ r = nextLoad(url, context);
779
+ } catch (err) {
780
+ // Async-loader `loadSync` stub (the load-hook twin of the resolve recovery): with a
781
+ // USER async `module.register` loader present, nub's sync load hook forces the default
782
+ // step onto the synchronous chain, which calls the async-hooks `loadSync` stub →
783
+ // ERR_METHOD_NOT_IMPLEMENTED. Recover by loading the module ourselves: read source
784
+ // from disk and derive the format from the URL/extension (the same source-of-truth
785
+ // nub's own transpile path uses), so the synchronous module-job gets real source
786
+ // instead of crashing. node:/data:/non-file URLs and the no-format case fall through
787
+ // to a re-throw (we can't synthesize those here). Re-throw anything that isn't the stub.
788
+ if (isAsyncLoaderSyncStub(err) && typeof url === "string") {
789
+ // Builtins: hand back the `builtin` format with no source — Node loads them
790
+ // natively (a `node:`-scheme module the default load would have returned builtin for).
791
+ if (url.startsWith("node:") || module_.isBuiltin(url)) {
792
+ return { format: "builtin", source: null, shortCircuit: true };
793
+ }
794
+ if (url.startsWith("file:")) {
795
+ const recovered = loadViaDisk(url, ext);
796
+ if (recovered) return recovered;
797
+ }
798
+ }
799
+ throw err;
800
+ }
801
+
802
+ // #18 — relabel a `commonjs` result as `commonjs-sync` for `file:` URLs ON THE
803
+ // `import()`-OF-CJS PATH so the module (and its inner require()s) routes through
804
+ // Node's SOUND synchronous CJS translator (loadCJSModuleWithModuleLoad), which
805
+ // builds a real CJS `require` with `.cache`/`.extensions`. Without this, nub's
806
+ // sync load hook makes Node pick loadCJSModuleWithSpecialRequire — a hand-rolled
807
+ // `require` missing `.cache`/`.extensions` — so CJS that does `require.cache[...]`
808
+ // (next's bundled `conf`) crashes with "Cannot convert undefined or null to
809
+ // object". Version-banded in Node: broken 22.15–~25, fixed in 26 via the
810
+ // special-require repair (#60380); the relabel is a no-op on 26.
811
+ //
812
+ // Two narrowing guards make this surgical — relabel ONLY the path that actually
813
+ // hits the broken translator, so nothing else moves:
814
+ //
815
+ // (1) IMPORT PATH ONLY. The bad special-require is chosen only when CJS is
816
+ // reached via dynamic `import()`. On the broken band Node passes that load
817
+ // step `context.conditions` as an ARRAY containing "import"; a plain
818
+ // `require()` of the same file passes an empty-object `conditions` (no
819
+ // "import"). Gating on the "import" condition leaves `require()` loads
820
+ // untouched — without this, relabeling a `require()`-loaded `.cjs` that
821
+ // contains ESM syntax makes Node's sync translator accept it instead of
822
+ // throwing "Unexpected token 'export'" (a real regression: it swallows the
823
+ // syntax error vanilla Node raises). On Node 26 the require path also carries
824
+ // an array but without "import", and the relabel is a no-op there regardless.
825
+ //
826
+ // (2) NO USER LOADER/HOOK. Skip whenever a USER async ESM loader OR a USER sync
827
+ // `module.registerHooks` hook is active. Relabeling makes Node treat the
828
+ // module as ESM-translatable, re-routing inner resolution through the user's
829
+ // hook chain — which changes resolve-hook call shape/count (the
830
+ // module-hooks resolve-import-cjs contract) and breaks async-loader interop
831
+ // (the source-backfill EXCEPTION block below documents the same hazard). When
832
+ // nub is the SOLE loader (the next-build/dev common case) the path is nub's
833
+ // own, so the relabel is safe; otherwise leave the native-CJS handoff intact.
834
+ //
835
+ // (3) A REAL SOURCE. Node's validateSourcePermissive exempts a null source for
836
+ // 'commonjs' ONLY, so relabeling a null-source result to 'commonjs-sync'
837
+ // makes Node throw ERR_INVALID_RETURN_PROPERTY_VALUE out of nub's own hook.
838
+ // The default load step yields a Buffer here whenever nub is the sole loader,
839
+ // and null only on the async-loader defaultLoad quirk — i.e. exactly when (2)
840
+ // should already have declined. Keeping this as an independent precondition
841
+ // means a future gap in (2)'s detection degrades to "no optimization" rather
842
+ // than to a crash, which is how #669 reached users.
843
+ if (
844
+ r && r.format === "commonjs" && r.source != null &&
845
+ typeof url === "string" && url.startsWith("file:") &&
846
+ Array.isArray(context && context.conditions) &&
847
+ context.conditions.includes("import") &&
848
+ !__userHooksRegistered && !userAsyncLoaderActive()
849
+ ) {
850
+ return { ...r, format: "commonjs-sync" };
851
+ }
852
+
853
+ // nub's sync `module.registerHooks` load hook forces the synchronous
854
+ // module-job (ModuleJobSync.syncLink -> loadAndTranslateForImportInRequiredESM),
855
+ // which cannot async-fetch source. When a user `--experimental-loader` resolve
856
+ // hook sets `format` without a `source` (a pattern vanilla Node tolerates on its
857
+ // async load path by fetching the source itself), the default load returns
858
+ // source:null and Node's assertBufferSource throws ERR_INVALID_RETURN_PROPERTY_VALUE.
859
+ // Backfill the source from disk so the sync path matches Node — without touching
860
+ // nub's own resolve/transpile hooks.
861
+ //
862
+ // EXCEPTION — format 'commonjs' (and 'builtin') MUST keep source:null. For those
863
+ // formats Node's ESM loader deliberately returns no source and hands the module
864
+ // off to the NATIVE CommonJS loader (Module._load), where `require()` uses CJS
865
+ // resolution. A CJS `.js` ENTRY, when a user `--experimental-loader` is active, is
866
+ // routed through the ESM loader for format detection but still loads as CJS this
867
+ // way. If we backfilled its source, the ESM loader would instead translate it via
868
+ // its CommonJS-to-ESM wrapper, routing every inner `require()` through the ESM
869
+ // resolve hook — so `require('assert')` would hand the bare 'assert' specifier to
870
+ // the user's resolve hook and crash with ERR_INVALID_RETURN_PROPERTY_VALUE (the
871
+ // shadow-realm/custom-loaders corpus failure). Only ESM-shaped formats ('module',
872
+ // 'json', 'wasm', …) genuinely need the source on the sync path.
873
+ // ('commonjs-sync' is excluded for the same reason as 'commonjs' — it's the
874
+ // sync CJS handoff the #18 relabel above produces; backfilling its source would
875
+ // re-route inner require()s through the ESM wrapper. The relabel block early-
876
+ // returns, so this is defense-in-depth against a future refactor.)
877
+ if (
878
+ r && r.source == null && r.format &&
879
+ r.format !== "commonjs" && r.format !== "commonjs-sync" && r.format !== "builtin" &&
880
+ typeof url === "string" && url.startsWith("file:")
881
+ ) {
882
+ try {
883
+ const { readFileSync } = getBuiltin("node:fs");
884
+ return { ...r, source: readFileSync(fileURLToPath(url)) };
885
+ } catch { /* fall through with the original result */ }
886
+ }
887
+ return r;
888
+ }
889
+
890
+ return { resolve, load };
891
+ }
892
+
893
+ // Give a Node internal we are about to wrap the name its own frame used to print.
894
+ // V8 derives a CallSite's method name by finding the function as a property of its
895
+ // receiver, so once `Module._resolveFilename` points at our wrapper, delegating to
896
+ // the saved original through `.call()` prints `Module.<anonymous>` instead of
897
+ // `Module._resolveFilename`, and `CallSite.getFunctionName()` goes null (these
898
+ // internals carry no own name). The null is what breaks the REPL: node:repl cuts a
899
+ // trace at the LAST null-named frame — normally its own `REPL1:1` eval frame — and
900
+ // the frames nub adds to a CJS resolve push that one past the default
901
+ // `Error.stackTraceLimit` of 10, leaving the delegated original as the only
902
+ // null-named frame, so `require("./missing")` prints with no trace at all. Naming
903
+ // the original restores Node's exact frame text. The REPL's own frame is still past
904
+ // the capture limit, so no null-named frame remains and node:repl's `findLastIndex`
905
+ // returns -1 — `slice(0, -1)` then drops just the outermost frame instead of the
906
+ // whole trace.
907
+ function nameInternalFrame(fn, name) {
908
+ try {
909
+ Object.defineProperty(fn, "name", { value: name, configurable: true });
910
+ } catch { /* frozen/exotic: leave verbatim */ }
911
+ return fn;
912
+ }
913
+
914
+ // ── CommonJS require() augmentation (BOTH tiers) ────────────────────
915
+ // `module.registerHooks`' CJS-`require()` coverage is INCOMPLETE before ~Node 24:
916
+ // on Node 22.15 a `require()` from a `.cts` parent (which Node loads via the ESM
917
+ // translator's special-require) hits native Module._resolveFilename with no
918
+ // tsconfig/extensionless handling — a `require('@alias')` or `require('./x')` of a
919
+ // `.ts` target throws MODULE_NOT_FOUND, while the same code works on Node 26 (where
920
+ // registerHooks does cover it) and on the fast `import` path. On the compat tier
921
+ // (18.19–22.14) the only hook surface is `module.register`, which intercepts the
922
+ // ESM loader ONLY — so `require()` is entirely unaugmented there. Both gaps have
923
+ // the same closure: install this main-thread CJS shim, reusing the core's canonical
924
+ // resolveCjsPath / loadTranspile (no drift). It tries nub's resolution first and
925
+ // FALLS THROUGH to native on a miss, so it is a safe no-op on the versions where
926
+ // registerHooks already covers require (Node 24+/26). Mechanism stays within the
927
+ // augmenter rules: exactly what `--require`-installing the ts-node / tsx CJS shim
928
+ // has always done.
929
+ //
930
+ // This error is surfaced ONLY on Node versions without native require(esm)
931
+ // (< 20.19 / 22.0–22.11), where require() of an ES module genuinely cannot work.
932
+ // On every require(esm)-capable Node, Node loads the ES module itself and this is
933
+ // never reached. The message is user-facing: no internal mechanism names.
934
+ function requireEsmError(filename) {
935
+ const err = new Error(
936
+ `Cannot require() this file — it is an ES module.\n` +
937
+ ` ${filename}\n` +
938
+ `It uses \`import\`/\`export\`, so it loads as an ES module, and this version of ` +
939
+ `Node can't require() an ES module. Load it with \`import(...)\` instead, rename ` +
940
+ `it to .cts for a CommonJS module, or upgrade Node.`,
941
+ );
942
+ err.code = "ERR_REQUIRE_ESM";
943
+ return err;
944
+ }
945
+
946
+ // `withClassicTranspile` — also install the `require.extensions` (classic CommonJS
947
+ // loader) transpile hook. Needed ONLY on Node WITHOUT native require(esm)
948
+ // (< 20.19 / 22.0–22.11): there, `module.register`'s ESM-loader hooks can't reach a
949
+ // `require()`, AND an ES module simply can't be require()d, so we transpile CJS
950
+ // content classically and surface a clean error for ESM content. On require(esm)-
951
+ // capable Node we DON'T install it — registering `require.extensions['.ts']` would
952
+ // shadow Node's own native require(esm) of ES-module `.ts` files (breaking
953
+ // `require("./esm.ts")`), and the resolve shim below plus the tier's load hook
954
+ // already cover resolution + transpile.
955
+ function installCjsRequireHooks(core, withClassicTranspile) {
956
+ const origResolveFilename = nameInternalFrame(module_._resolveFilename, "_resolveFilename");
957
+
958
+ // The classic transpile handlers registered below put `.ts`/`.cts`/`.mts`/`.tsx`/
959
+ // `.jsx` into `Module._extensions`, and Node's `_findPath` runs `tryExtensions` over
960
+ // EVERY key of that object during LOAD_AS_FILE — before it ever tries
961
+ // LOAD_AS_DIRECTORY. Registering them therefore reorders Node's own resolution
962
+ // INSIDE dependencies: `require("pkg/sub")` picks a dep's unshipped `sub.ts` over the
963
+ // `sub/index.js` Node loads, and a dep's internal `require("./util")` picks `util.ts`
964
+ // where Node reports it missing. Both hand back a silently different module.
965
+ //
966
+ // Deps are NEVER transpiled — the same invariant the `.js`/`.cjs` handler below
967
+ // states — and the fast tier already resolves both shapes Node's way, so this keeps
968
+ // the two tiers in agreement. The check is on Node's ANSWER rather than a guess about
969
+ // the request, so nothing is re-resolved unless our own keys actually changed the
970
+ // outcome; the retry then reports whatever Node itself would, error included.
971
+ //
972
+ // Every extension nub adds to `Module._extensions` that Node does not have of its
973
+ // own. `.cjs` belongs here with the TypeScript family: Node registers only `.js`,
974
+ // `.json` and `.node`, so nub's `.cjs` handler likewise widens LOAD_AS_FILE and
975
+ // lets `require("dep/x/sub")` find a dependency's `sub.cjs` that plain Node reports
976
+ // missing. An explicit `require("dep/x/sub.cjs")` is unaffected either way — an
977
+ // exact path is found by stat, without consulting the extension list at all.
978
+ const NUB_ADDED_EXTS = [".ts", ".cts", ".mts", ".tsx", ".jsx", ".cjs"];
979
+ const withoutNubAddedExtensions = (fn) => {
980
+ const saved = [];
981
+ for (const ext of NUB_ADDED_EXTS) {
982
+ if (Object.hasOwn(module_._extensions, ext)) {
983
+ saved.push([ext, module_._extensions[ext]]);
984
+ delete module_._extensions[ext];
985
+ }
986
+ }
987
+ try {
988
+ return fn();
989
+ } finally {
990
+ for (const [ext, handler] of saved) module_._extensions[ext] = handler;
991
+ }
992
+ };
993
+ const isDepAddedExtHit = (filename) =>
994
+ typeof filename === "string" &&
995
+ NUB_ADDED_EXTS.includes(pathExtname(filename)) &&
996
+ core.isDependency(pathToFileURL(filename).href);
997
+
998
+ module_._resolveFilename = function (request, parent, isMain, options) {
999
+ let resolved = null;
1000
+ try {
1001
+ const parentPath = parent && typeof parent.filename === "string" ? parent.filename : null;
1002
+ resolved = core.resolveCjsPath(request, parentPath);
1003
+ } catch { /* fall through to Node */ }
1004
+ if (resolved) {
1005
+ // This pre-check stays at RESOLVE time even though `_resolveFilename` also
1006
+ // answers `require.resolve()`, for which a path lookup is not a load. Below the
1007
+ // Node #60380 fix, `require()` of a transpiled ES module dies inside the
1008
+ // loader-worker translator with an opaque `cjsCache.get(...)` TypeError, and
1009
+ // that happens before any load hook of ours can run — so a load-time refusal
1010
+ // never gets its turn and Node's own crash is what the user sees.
1011
+ //
1012
+ // The cost is that `require.resolve()` of an ESM-syntax TS target throws here
1013
+ // instead of returning the path. Telling the two callers apart is not reliable
1014
+ // across the band this is live on: Node passes 3 arguments from `Module._load`
1015
+ // and 4 from `require.resolve` up to 22.14, but 22.15 and 22.16 pass 4 for both
1016
+ // while still lacking native TS — and the translator crash is present on
1017
+ // exactly those versions. A clean error beats an opaque crash, so this stays.
1018
+ if (withClassicTranspile && core.requireTargetIsEsm(resolved, pathExtname(resolved))) {
1019
+ throw requireEsmError(resolved);
1020
+ }
1021
+ return resolved;
1022
+ }
1023
+ // Yarn PnP (CJS): `.pnp.cjs` already patched THIS function (origResolveFilename)
1024
+ // to resolve from PnP's manifest, including zip-stored deps — so we just delegate
1025
+ // to it. The one snag is that a registered customization hook makes Node thread a
1026
+ // `conditions` option that PnP rejects ("aren't supported by PnP yet
1027
+ // (conditions)"), so strip it first. The require/default condition PnP then
1028
+ // applies is exactly right for `require()`. This replaces the former
1029
+ // `pnpapi.resolveRequest` reimplementation: simpler, and with no `findPnpApi` in
1030
+ // the hot path there is no lookup-miss to leak a `conditions` crash on Windows.
1031
+ //
1032
+ // GATED ON PnP (`process.versions.pnp`). Off PnP the strip is NOT a harmless
1033
+ // no-op: a user who passes custom `conditions` to require-side resolution via
1034
+ // `module.registerHooks` (Node's module-hooks custom-conditions tests) relies on
1035
+ // Node's own `_resolveFilename` honoring them, and unconditionally deleting the
1036
+ // key silently dropped their conditions — breaking module-hooks/test-module-hooks-
1037
+ // custom-conditions{,-cjs}. PnP is the only resolver that rejects `conditions`, so
1038
+ // only strip when PnP is actually active; everywhere else conditions pass through.
1039
+ if (process.versions.pnp && options && "conditions" in options) {
1040
+ options = { ...options };
1041
+ delete options.conditions;
1042
+ }
1043
+ try {
1044
+ const byNode = origResolveFilename.call(this, request, parent, isMain, options);
1045
+ if (withClassicTranspile && isDepAddedExtHit(byNode)) {
1046
+ // `_findPath` memoizes into `Module._pathCache` and returns that entry before
1047
+ // it ever consults the extension list, so the retry would be handed the same
1048
+ // TS hit and the removal below would look like a no-op. Drop the memo first.
1049
+ // Purging by VALUE rather than rebuilding the cache key keeps this off Node's
1050
+ // internal key format, which differs across the supported range.
1051
+ for (const key of Object.keys(module_._pathCache)) {
1052
+ if (module_._pathCache[key] === byNode) delete module_._pathCache[key];
1053
+ }
1054
+ return withoutNubAddedExtensions(() =>
1055
+ origResolveFilename.call(this, request, parent, isMain, options),
1056
+ );
1057
+ }
1058
+ return byNode;
1059
+ } catch (e) {
1060
+ // Under PnP, an in-tree issuer requiring a dep NOT in its manifest makes PnP
1061
+ // throw. That is nub's OWN transpile helpers (e.g. `@oxc-project/runtime`),
1062
+ // injected into transpiled user code and resolved via NODE_PATH globalPaths
1063
+ // (A30). Fall back to Node's native path resolver, which PnP does NOT patch.
1064
+ // Gated to PnP so off-PnP a genuine miss surfaces Node's own error unchanged.
1065
+ if (process.versions.pnp && e && e.code === "MODULE_NOT_FOUND") {
1066
+ const lookupPaths = module_._resolveLookupPaths(request, parent) || [];
1067
+ const found = module_._findPath(request, lookupPaths, isMain);
1068
+ if (found) return found;
1069
+ }
1070
+ if (e && e.code === "MODULE_NOT_FOUND") {
1071
+ const fromPath = parent && typeof parent.filename === "string" ? parent.filename : null;
1072
+ const hint = phantomDepHint(request, fromPath);
1073
+ if (hint) annotateError(e, hint);
1074
+ }
1075
+ throw e;
1076
+ }
1077
+ };
1078
+
1079
+ if (!withClassicTranspile) return;
1080
+
1081
+ // require.extensions: transpile via the SAME loadTranspile the load hook uses —
1082
+ // target:'es2022' lowering (`using`), tsconfig, source maps, the Stage-3
1083
+ // decorator guard, and module-format detection are all identical to the fast
1084
+ // tier. The path is already a real TS file (Module._resolveFilename ran first).
1085
+ // A module-format source can't be _compile'd as CJS — same clean error as above.
1086
+ //
1087
+ // A dependency's TypeScript is never transpiled — the same invariant the `.js`/
1088
+ // `.cjs` handler below states as its case (1). The resolution-side retry keeps
1089
+ // most dep TS from ever reaching here, but an explicit path (`require(
1090
+ // "./node_modules/pkg/inner.ts")`) names the file directly and skips resolution
1091
+ // entirely, so the bail has to live at the load step too. Delegating to the native
1092
+ // `.js` handler rather than throwing is what Node itself does: an extension it has
1093
+ // no handler for falls back to `.js` via `findLongestRegisteredExtension`, so this
1094
+ // reproduces plain Node's own error instead of inventing one nub would have to own.
1095
+ const nativeJs = module_._extensions[".js"];
1096
+ const transpileExtension = (mod, filename) => {
1097
+ if (core.isDependency(pathToFileURL(filename).href)) {
1098
+ return nativeJs.call(module_._extensions, mod, filename);
1099
+ }
1100
+ const { source, format } = core.loadTranspile(pathToFileURL(filename).href, pathExtname(filename));
1101
+ if (format === "module") throw requireEsmError(filename);
1102
+ mod._compile(source, filename);
1103
+ };
1104
+ // Serve a data document the way the ESM load hook's `dataExtsFor` branch does.
1105
+ // Without any handler Node has none for `.yaml`/`.toml`/`.json5`/`.jsonc`/
1106
+ // `.txt`, so `findLongestRegisteredExtension` falls back to `.js` and compiles
1107
+ // the document AS JavaScript — `a: 1` is a valid labeled statement, so
1108
+ // `require("./x.yaml")` silently produced `{}` instead of the parsed document.
1109
+ const dataExtension = (mod, filename, url, ext) => {
1110
+ // Round-tripped through JSON because the ESM path emits
1111
+ // `export default ${JSON.stringify(parsed)}` — a TOML date reaches an
1112
+ // `import` as a string, so `require()` must not hand back something richer.
1113
+ // `{__esModule, default}` is the shape require(esm) already yields on the
1114
+ // fast tier, so the same file destructures identically on both.
1115
+ const value = core.dataValue(url, ext);
1116
+ mod.exports = {
1117
+ __esModule: true,
1118
+ // `== null` mirrors `loadData`'s own guard rather than testing undefined
1119
+ // strictly: it emits `export default undefined` for null AND undefined,
1120
+ // so a document parsing to null — an empty `.yaml`, a `.json5`/`.jsonc`
1121
+ // whose whole content is `null` — would otherwise default to `null` here
1122
+ // and `undefined` through `import`. Same file, two answers, which is the
1123
+ // divergence sharing `dataValue` exists to prevent.
1124
+ default: value == null ? undefined : JSON.parse(JSON.stringify(value)),
1125
+ };
1126
+ };
1127
+
1128
+ // ONE handler, dispatching PER URL rather than per extension — because the
1129
+ // right answer genuinely differs between two files sharing an extension. A
1130
+ // project that redirects `.yaml` to `ts` still has dependencies whose own
1131
+ // `.yaml` must parse as data, since `dataExtsFor` pins node_modules to the
1132
+ // built-in loaders so a project's `loader` cannot redefine how a dependency
1133
+ // reads its files. Two extension-keyed loops cannot express that: whichever
1134
+ // ran second simply won, which is how `{".yaml":"ts"}` came to compile
1135
+ // TypeScript as raw JavaScript here while the ESM path transpiled it.
1136
+ //
1137
+ // Data first, then transpile: `dataExtsFor` has already resolved the config
1138
+ // for this URL, so an extension it claims is data for this file, and anything
1139
+ // left that the config-aware transpile set claims is code.
1140
+ const nubExtension = (mod, filename) => {
1141
+ const url = pathToFileURL(filename).href;
1142
+ const ext = pathExtname(filename);
1143
+ if (ext in core.dataExtsFor(url)) return dataExtension(mod, filename, url, ext);
1144
+ if (core.TRANSPILE_EXTS.has(ext)) return transpileExtension(mod, filename);
1145
+ return nativeJs.call(module_._extensions, mod, filename);
1146
+ };
1147
+
1148
+ // Registered for every extension either path may claim, so the dispatcher is
1149
+ // reached at all; `nubExtension` then decides. Node's own `.js`/`.json`/`.node`
1150
+ // are never replaced — a project `loader` may redefine how nub reads a file,
1151
+ // not how Node reads `package.json`.
1152
+ //
1153
+ // ENUMERABILITY IS THE LOAD-BEARING PART. Node builds EXTENSIONLESS resolution
1154
+ // from `ObjectKeys(Module._extensions)` (cjs/loader.js `tryExtensions`), so a
1155
+ // plain assignment also enrolls the extension there. The TS family keeps the
1156
+ // enumerable registration it has always had, so extensionless `require("./m")`
1157
+ // still finds `m.ts`. Everything else is NON-ENUMERABLE: making `.yaml`
1158
+ // enumerable would let `require("./config")` resolve `config.yaml` on this tier
1159
+ // while the fast tier — which registers nothing and serves these through
1160
+ // require(esm) — still throws MODULE_NOT_FOUND, trading one tier divergence for
1161
+ // another. Dispatch reads the property directly
1162
+ // (`findLongestRegisteredExtension`, `Module.prototype.load`), so hiding it
1163
+ // from `ObjectKeys` serves an explicit `require("./x.yaml")` while leaving
1164
+ // resolution identical across tiers.
1165
+ const CODE_EXTS = new Set([".ts", ".cts", ".mts", ".tsx", ".jsx"]);
1166
+ for (const ext of new Set([...core.TRANSPILE_EXTS, ...core.allDataExts()])) {
1167
+ if (ext === ".js" || ext === ".json" || ext === ".node") continue;
1168
+ Object.defineProperty(module_._extensions, ext, {
1169
+ value: nubExtension,
1170
+ enumerable: CODE_EXTS.has(ext),
1171
+ configurable: true,
1172
+ writable: true,
1173
+ });
1174
+ }
1175
+
1176
+ // Project-source plain JS (`.js`/`.cjs`) routes through the SAME pipeline so
1177
+ // `using`/`v`-flag-RegExp/decorators lower uniformly on the classic require tier —
1178
+ // but ONLY when the file carries transformable syntax. THREE cases, each preserving
1179
+ // Node's native behavior where it must:
1180
+ // (1) node_modules dep → native handler (deps are NEVER transpiled). Node has no
1181
+ // own `_extensions['.cjs']` (only `.js`/`.json`/`.node`), and compiles `.cjs`
1182
+ // through the same CJS path as `.js`, so the `.cjs` bail falls back to the
1183
+ // `.js` handler (`|| nativeJs`) — without it a node_modules `.cjs` require
1184
+ // would call `undefined` and crash.
1185
+ // (2) project file with transformable syntax → transpile (lower it).
1186
+ // (3) project file with NOTHING to lower → the ORIGINAL native handler, raw bytes
1187
+ // compiled exactly as Node would. We never serve our own source for a no-op
1188
+ // file, so require.cache / the require-of-ESM-syntax-`.cjs` SyntaxError / every
1189
+ // native CJS behavior is byte-identical.
1190
+ // `.mjs` is ESM-only; Node registers no require.extensions handler for it, so a
1191
+ // `require()` of `.mjs` throws ERR_REQUIRE_ESM as before — we don't override it.
1192
+ // (`nativeJs` is captured above, before the TS handlers are registered.)
1193
+ for (const ext of [".js", ".cjs"]) {
1194
+ const origExtension = module_._extensions[ext] || nativeJs;
1195
+ module_._extensions[ext] = (mod, filename) => {
1196
+ // (0) The project pointed this extension at a data loader (`{".js":"text"}`),
1197
+ // which the ESM path honors. These two extensions are skipped by the
1198
+ // registration loop above because THIS wrapper owns them and runs after it,
1199
+ // so the check has to live here or the setting is silently dropped on this
1200
+ // tier alone. `dataExtsFor` is URL-keyed and pins node_modules to the
1201
+ // built-ins, so a dependency's own `.js` can never be captured this way.
1202
+ const url = pathToFileURL(filename).href;
1203
+ const fileExt = pathExtname(filename);
1204
+ if (fileExt in core.dataExtsFor(url)) {
1205
+ return dataExtension(mod, filename, url, fileExt);
1206
+ }
1207
+ if (core.isDependency(url)) {
1208
+ return origExtension.call(module_._extensions, mod, filename); // (1)
1209
+ }
1210
+ const r = core.maybeTranspilePlainJs(pathToFileURL(filename).href, pathExtname(filename));
1211
+ if (r) {
1212
+ if (r.format === "module") throw requireEsmError(filename); // (2)
1213
+ mod._compile(r.source, filename);
1214
+ return;
1215
+ }
1216
+ return origExtension.call(module_._extensions, mod, filename); // (3)
1217
+ };
1218
+ }
1219
+ }
1220
+
1221
+ // ── Clobbered-polyfill preloading + Temporal lazy global ────────────
1222
+ // Packages in the core's CLOBBER_MAP can't be imported after hooks register
1223
+ // because the resolve hook returns a synthetic module instead of the real package.
1224
+ // Load them here via CJS require (not yet hooked) and return them so the polyfill
1225
+ // installer can stash them. Temporal is the exception (A37): the polyfill is ~18ms
1226
+ // to load and most scripts never touch it, so we only RESOLVE its path now (cheap)
1227
+ // and defer the load to a lazy global getter. Requiring it later by absolute path
1228
+ // bypasses the CLOBBER_MAP resolve-hook entry, which keys on the specifier.
1229
+ function preloadPolyfillPackages(reqFromRuntime) {
1230
+ const preloaded = {};
1231
+ // Feature-detect before requiring (A39): URLPattern is native on Node 24+, so
1232
+ // skip loading the polyfill there. On 22.x it's absent → load it.
1233
+ if (typeof globalThis.URLPattern === "undefined") {
1234
+ try { preloaded.urlpattern = reqFromRuntime("urlpattern-polyfill"); } catch {}
1235
+ }
1236
+ // Float16Array: native on Node 24+, absent on the 22.x floor.
1237
+ if (typeof globalThis.Float16Array === "undefined") {
1238
+ try { preloaded.float16 = reqFromRuntime("@petamoriken/float16"); } catch {}
1239
+ }
1240
+ return preloaded;
1241
+ }
1242
+
1243
+ // Install the lazy `globalThis.Temporal` getter. The polyfill is loaded — and even
1244
+ // RESOLVED — only on first access. CRITICAL ordering note (regexp one-off): the
1245
+ // `require.resolve("@js-temporal/polyfill")` is deferred INTO the getter, NOT run at
1246
+ // preload top level. An unconditional resolve at startup mutates the legacy
1247
+ // `RegExp.$_` static (the resolved node_modules path matches an internal regex), so
1248
+ // a program inspecting `RegExp.$_` on its first line would otherwise see a leaked
1249
+ // path (test-startup-empty-regexp-statics). Deferring the resolve keeps `RegExp.$_`
1250
+ // empty at user-code start; the cost is paid only by a program that touches Temporal.
1251
+ const defineTemporal = (value) =>
1252
+ Object.defineProperty(globalThis, "Temporal", {
1253
+ value,
1254
+ configurable: true,
1255
+ writable: true,
1256
+ enumerable: false,
1257
+ });
1258
+
1259
+ function installTemporalValue(polyfill) {
1260
+ // @js-temporal/polyfill exports `toTemporalInstant` as a function but does
1261
+ // NOT auto-install it on Date.prototype (you assign it yourself). Install it
1262
+ // here so that on the floor (no native Temporal) `date.toTemporalInstant()`
1263
+ // AND the package clobber's re-export of `Date.prototype.toTemporalInstant`
1264
+ // both work — matching native Node. Guarded so we never replace a native
1265
+ // implementation on a runtime that ships Temporal.
1266
+ if (
1267
+ typeof Date.prototype.toTemporalInstant !== "function" &&
1268
+ typeof polyfill.toTemporalInstant === "function"
1269
+ ) {
1270
+ Object.defineProperty(Date.prototype, "toTemporalInstant", {
1271
+ value: polyfill.toTemporalInstant,
1272
+ configurable: true,
1273
+ writable: true,
1274
+ enumerable: false,
1275
+ });
1276
+ }
1277
+ const T = polyfill.Temporal;
1278
+ defineTemporal(T);
1279
+ return T;
1280
+ }
1281
+
1282
+ function installTemporalGlobal(polyfill) {
1283
+ if (typeof globalThis.Temporal !== "undefined") return globalThis.Temporal;
1284
+ return installTemporalValue(polyfill);
1285
+ }
1286
+
1287
+ function installTemporalLazyGlobal(reqFromRuntime) {
1288
+ if (typeof globalThis.Temporal !== "undefined") return;
1289
+ Object.defineProperty(globalThis, "Temporal", {
1290
+ configurable: true,
1291
+ enumerable: false,
1292
+ get() {
1293
+ let temporalPath;
1294
+ try { temporalPath = reqFromRuntime.resolve("@js-temporal/polyfill"); } catch {}
1295
+ if (!temporalPath) return undefined;
1296
+ const polyfill = reqFromRuntime(temporalPath);
1297
+ return installTemporalValue(polyfill);
1298
+ },
1299
+ set: defineTemporal,
1300
+ });
1301
+ }
1302
+
1303
+ // ── Compile-cache handling (R8) ─────────────────────────────────────
1304
+ // nub injects its preload chain via `--require`, which Node loads at bootstrap.
1305
+ // If the user set NODE_COMPILE_CACHE, Node would enable the V8 code cache BEFORE
1306
+ // this preload runs and cache every module the chain pulls in (preload.cjs,
1307
+ // transform-core.mjs, this file, polyfills.cjs, …) into the USER's dir — so a
1308
+ // program reading `fs.readdirSync(NODE_COMPILE_CACHE)` would see ~9 nub entries,
1309
+ // not its own 1 (program-observable; R8). spawn.rs prevents that by STRIPPING
1310
+ // NODE_COMPILE_CACHE from the child env (bootstrap caches nothing) and stashing
1311
+ // the original value in a sentinel file keyed on nub's PID — which is THIS child's
1312
+ // `process.ppid` (nub is our direct parent). The dir travels via a sentinel file,
1313
+ // never a NUB_* env var (brand boundary).
1314
+ //
1315
+ // Two preload steps consume it:
1316
+ // 1. restoreCompileCacheEnv() runs EARLY, before transform-core.mjs is required,
1317
+ // to put the original value BACK into process.env.NODE_COMPILE_CACHE. That
1318
+ // matters because (a) transform-core reads `NODE_COMPILE_CACHE === "0"` as
1319
+ // nub's transpile-cache disable signal, and (b) user code may read the env.
1320
+ // Restoring it in JS does NOT re-trigger Node's V8 compile cache (Node
1321
+ // configures that once at bootstrap from the now-stripped env), so the
1322
+ // preload chain stays uncached. It also DELETES the sentinel (consume-once,
1323
+ // so a recycled PID can't read stale state and the file never leaks).
1324
+ // 2. reenableUserCompileCache() runs LAST, after all nub modules are loaded
1325
+ // uncached and right before user code, and calls
1326
+ // `module.enableCompileCache(dir)` for a real dir so the user's OWN modules
1327
+ // cache as they always did. A value of "0" is nub's disable sentinel (Node
1328
+ // treats "0" as a literal dir named 0, but nub honors it as "no caching"),
1329
+ // so we skip enabling there.
1330
+ // Best-effort throughout: a missing/unreadable sentinel or an enableCompileCache
1331
+ // failure just means no user compile cache — strictly safer than the old pollution.
1332
+ // `os.tmpdir()` without requiring `node:os`. Requiring os at preload pulls
1333
+ // `Internal Binding os` + `NativeModule os` into process.moduleLoadList on EVERY
1334
+ // startup (test-bootstrap-modules observes this) even though almost no run touches
1335
+ // the compile-cache sentinel. This replica mirrors Node's libuv/os.tmpdir() env
1336
+ // resolution (POSIX: TMPDIR→TMP→TEMP→/tmp; Win32: TEMP→TMP→SystemRoot/windir+\temp),
1337
+ // trailing-separator-stripped, which is also what Rust's env::temp_dir() (the side
1338
+ // that WRITES the sentinel in spawn.rs) resolves to — so both ends agree.
1339
+ function tmpdirNoOs() {
1340
+ const env = process.env;
1341
+ if (process.platform === "win32") {
1342
+ let dir = env.TEMP || env.TMP || ((env.SystemRoot || env.windir || "") + "\\temp");
1343
+ if (dir.length > 1 && dir.endsWith("\\") && !dir.endsWith(":\\")) dir = dir.slice(0, -1);
1344
+ return dir;
1345
+ }
1346
+ let dir = env.TMPDIR || env.TMP || env.TEMP || "/tmp";
1347
+ if (dir.length > 1 && dir.endsWith("/")) dir = dir.slice(0, -1);
1348
+ return dir;
1349
+ }
1350
+
1351
+ function compileCacheSentinelPath() {
1352
+ return join(tmpdirNoOs(), `nub-ccache-${process.ppid}`);
1353
+ }
1354
+
1355
+ function restoreCompileCacheEnv() {
1356
+ try {
1357
+ const { readFileSync, rmSync } = getBuiltin("node:fs");
1358
+ const value = readFileSync(compileCacheSentinelPath(), "utf8");
1359
+ try { rmSync(compileCacheSentinelPath()); } catch {}
1360
+ if (value) process.env.NODE_COMPILE_CACHE = value;
1361
+ } catch { /* no sentinel: env was never set, or already consumed */ }
1362
+ // Propagate the R8 strip to node grandchildren the user spawns directly (plain
1363
+ // node inheriting nub's --require preload + a live NODE_COMPILE_CACHE → it would
1364
+ // cache nub's preload chain into the user's dir). The wrap MUST preserve each
1365
+ // function's own symbols (esp. [util.promisify.custom]) — dropping them broke
1366
+ // util.promisify(child_process.*) + abort/sync-io behavior. See wrapSpawnLike.
1367
+ try { armChildProcessCompileCacheWrap(); } catch {}
1368
+ }
1369
+
1370
+ // Arm the child_process compile-cache wrap WITHOUT eagerly requiring child_process.
1371
+ //
1372
+ // Eagerly `require("node:child_process")` at preload time pulls ~40 builtins into
1373
+ // process.moduleLoadList on EVERY startup — net, dgram, the entire streams tree,
1374
+ // spawn_sync/tty_wrap/pipe_wrap/tcp_wrap, os, vm, etc. (test-bootstrap-modules
1375
+ // observes the exact list; child_process is the dominant extra-builtin source).
1376
+ // A program that never spawns a child shouldn't pay that cost — and Node's own
1377
+ // startup never loads child_process.
1378
+ //
1379
+ // So we intercept `Module._load` and apply the wrap to the child_process module the
1380
+ // FIRST time USER code requires it (`require('child_process')` /
1381
+ // `require('node:child_process')`), patching the returned singleton before handing
1382
+ // it back. After patching once we restore the original `_load`, so steady-state
1383
+ // require() has zero added overhead. If the user never requires child_process, the
1384
+ // module is never loaded and the builtins stay out of the load list — matching Node.
1385
+ let __cpWrapArmed = false;
1386
+ // Compiled artifacts spawn children whose NODE_COMPILE_CACHE needs the same R8
1387
+ // treatment as an ordinary Nub run. Their fork IDENTITY policy (real executable,
1388
+ // bootstrap-first execArgv, private env channel) is NOT here: it has to be armed
1389
+ // before the entry chunk's static builtin imports evaluate, so compile-bootstrap.cjs
1390
+ // owns it. See the note there before adding fork behavior to this file.
1391
+ let __compiledArtifact = false;
1392
+
1393
+ function installCompiledChildProcess() {
1394
+ __compiledArtifact = true;
1395
+ // ESM `import { spawn } from "node:child_process"` bypasses Module._load, so a
1396
+ // compiled artifact must eagerly load + patch the builtin and synchronize its
1397
+ // named ESM exports. This startup cost is compiled-only; normal Nub preloads
1398
+ // continue to defer child_process until CommonJS user code requires it.
1399
+ try {
1400
+ wrapChildProcessCompileCache(getBuiltin("node:child_process"));
1401
+ module_.syncBuiltinESMExports();
1402
+ } catch {}
1403
+ }
1404
+
1405
+ function armChildProcessCompileCacheWrap() {
1406
+ if (__cpWrapArmed || __cpWrapped) return;
1407
+ __cpWrapArmed = true;
1408
+ if (typeof module_._load !== "function") return;
1409
+ const origLoad = nameInternalFrame(module_._load, "_load");
1410
+ module_._load = function (request, parent, isMain) {
1411
+ const exports = origLoad.call(this, request, parent, isMain);
1412
+ if (request === "child_process" || request === "node:child_process") {
1413
+ module_._load = origLoad; // restore: one-shot, no steady-state overhead
1414
+ try { wrapChildProcessCompileCache(exports); } catch {}
1415
+ }
1416
+ return exports;
1417
+ };
1418
+ }
1419
+
1420
+ // Monkey-patch child_process so node-targeted children the USER spawns with an
1421
+ // explicit live NODE_COMPILE_CACHE get the SAME R8 treatment spawn.rs gives nub's
1422
+ // own children: strip NODE_COMPILE_CACHE from the child env (so Node's bootstrap
1423
+ // caches nothing of nub's inherited preload chain) and stash the original dir in a
1424
+ // PID-keyed sentinel file the grandchild's restoreCompileCacheEnv() reads back via
1425
+ // `process.ppid` to re-enable caching for the USER's own modules post-bootstrap.
1426
+ // Brand rule: the dir travels via a sentinel file, never a NUB_* env var.
1427
+ // `cp` is the already-loaded child_process exports object, passed in by the lazy
1428
+ // `_load` interceptor so we never require it ourselves (which would defeat the
1429
+ // deferral).
1430
+ let __cpWrapped = false;
1431
+ function wrapChildProcessCompileCache(cp) {
1432
+ if (__cpWrapped || !cp) return;
1433
+ __cpWrapped = true;
1434
+ const { writeFileSync } = getBuiltin("node:fs");
1435
+ const { basename } = getBuiltin("node:path");
1436
+
1437
+ const isNodeTarget = (command) => {
1438
+ if (typeof command !== "string" || command.length === 0) return false;
1439
+ if (command === process.execPath) return true;
1440
+ const base = basename(command).toLowerCase();
1441
+ return base === "node" || base === "node.exe";
1442
+ };
1443
+
1444
+ // The fresh-invocation protocol attributes each rewritten environment value to
1445
+ // Nub. This preload is another Nub-owned writer: when it strips the live
1446
+ // compile-cache value for R8, move an existing current-protocol marker to the
1447
+ // exact absent state, so a later compat boundary restores the captured user
1448
+ // value instead of mistaking this removal for a user mutation. Legacy parents
1449
+ // carry neither marker and keep their legacy fallback.
1450
+ const CCACHE_MARKER = "__NUB_AUGMENTED_NODE_COMPILE_CACHE";
1451
+ const CCACHE_PRESENT_MARKER = `${CCACHE_MARKER}_PRESENT`;
1452
+ const markCompileCacheAbsent = (env) => {
1453
+ if (!Object.hasOwn(env, CCACHE_MARKER) && !Object.hasOwn(env, CCACHE_PRESENT_MARKER)) return;
1454
+ env[CCACHE_MARKER] = "";
1455
+ env[CCACHE_PRESENT_MARKER] = "0";
1456
+ };
1457
+
1458
+ // Returns a possibly-rewritten options object with NODE_COMPILE_CACHE stripped
1459
+ // from the child's env, after writing the sentinel keyed on THIS process's pid
1460
+ // (= the grandchild's process.ppid). Two source cases, both stripped:
1461
+ // • EXPLICIT env (options.env carries NODE_COMPILE_CACHE) — strip from it.
1462
+ // • INHERITED env (no options.env, child inherits this process's env) — when
1463
+ // OUR process.env carries a live NODE_COMPILE_CACHE, materialize an explicit
1464
+ // env from process.env with it removed. This case matters now that the
1465
+ // DEFAULT (nub-owned) cache also travels via the sentinel and gets restored
1466
+ // into process.env: a node child the user spawns with NO explicit env would
1467
+ // otherwise inherit it and enable the cache AT BOOTSTRAP — before any preload
1468
+ // gate — collapsing that child's V8 coverage if it runs under
1469
+ // --experimental-test-coverage (the test-runner coverage-width fixtures, which
1470
+ // are spawned with inherited env). Stripping here makes every node-target
1471
+ // child boot cache-off; its own preload re-enables the cache post-bootstrap
1472
+ // via reenableUserCompileCache UNLESS it's collecting coverage.
1473
+ const stripFromOptions = (options) => {
1474
+ const inheritedDir = process.env.NODE_COMPILE_CACHE;
1475
+ const opts = options && typeof options === "object" ? options : {};
1476
+ const env = opts.env;
1477
+ if (env && typeof env === "object") {
1478
+ const dir = env.NODE_COMPILE_CACHE;
1479
+ if (!dir || dir === "0") return options;
1480
+ try {
1481
+ writeFileSync(join(tmpdirNoOs(), `nub-ccache-${process.pid}`), String(dir));
1482
+ } catch { return options; }
1483
+ const newEnv = { ...env };
1484
+ delete newEnv.NODE_COMPILE_CACHE;
1485
+ markCompileCacheAbsent(newEnv);
1486
+ return { ...opts, env: newEnv };
1487
+ }
1488
+ // Inherited env path: only act when this process actually carries a live cache
1489
+ // dir (otherwise there is nothing for the child to inherit and we leave the
1490
+ // spawn's env untouched — `undefined` keeps Node's default inheritance).
1491
+ if (!inheritedDir || inheritedDir === "0") return options;
1492
+ try {
1493
+ writeFileSync(join(tmpdirNoOs(), `nub-ccache-${process.pid}`), String(inheritedDir));
1494
+ } catch { return options; }
1495
+ const newEnv = { ...process.env };
1496
+ delete newEnv.NODE_COMPILE_CACHE;
1497
+ markCompileCacheAbsent(newEnv);
1498
+ return { ...opts, env: newEnv };
1499
+ };
1500
+
1501
+ // For (command, args?, options?) signatures the options object is the last arg
1502
+ // that is a non-array object; args is an optional array in between. Rewrites the
1503
+ // call in place and dispatches to the original.
1504
+ // Copy `orig`'s OWN symbols onto `wrapped` — crucially [util.promisify.custom],
1505
+ // which Node sets on execFile/exec so `util.promisify(execFile)` returns a
1506
+ // {stdout,stderr} promise. A bare wrapper without it silently changes promisify's
1507
+ // result shape (broke test-child-process-promisified / -abortController /
1508
+ // util-promisify-custom-names / sync-io-option / test-output-abort).
1509
+ const preserveSymbols = (wrapped, orig) => {
1510
+ for (const s of Object.getOwnPropertySymbols(orig)) {
1511
+ try { wrapped[s] = orig[s]; } catch { /* read-only symbol: skip */ }
1512
+ }
1513
+ return wrapped;
1514
+ };
1515
+ const wrapSpawnLike = (orig) => preserveSymbols(function (command, ...rest) {
1516
+ if (isNodeTarget(command)) {
1517
+ let optIdx = -1;
1518
+ for (let i = rest.length - 1; i >= 0; i--) {
1519
+ const a = rest[i];
1520
+ if (a && typeof a === "object" && !Array.isArray(a)) { optIdx = i; break; }
1521
+ if (typeof a === "function") continue; // execFile callback
1522
+ if (Array.isArray(a)) break; // args array — no options object present
1523
+ }
1524
+ if (optIdx >= 0) rest[optIdx] = stripFromOptions(rest[optIdx]);
1525
+ }
1526
+ return orig.call(this, command, ...rest);
1527
+ }, orig);
1528
+
1529
+ cp.spawn = wrapSpawnLike(cp.spawn);
1530
+ cp.spawnSync = wrapSpawnLike(cp.spawnSync);
1531
+ cp.execFile = wrapSpawnLike(cp.execFile);
1532
+ cp.execFileSync = wrapSpawnLike(cp.execFileSync);
1533
+
1534
+ // fork() always launches a node target, so the compile-cache rewrite applies to
1535
+ // every (modulePath, args?, options?) shape — but only where the caller passed an
1536
+ // options object, matching what the spawn-like wrappers do. Restricted to compiled
1537
+ // artifacts because an ordinary Nub run reaches fork through the same
1538
+ // `restoreCompileCacheEnv` path its children already use.
1539
+ const origFork = cp.fork;
1540
+ cp.fork = function (modulePath, ...rest) {
1541
+ let optIdx = -1;
1542
+ if (rest.length === 1) {
1543
+ const [arg] = rest;
1544
+ if (arg !== null && typeof arg === "object" && !Array.isArray(arg)) optIdx = 0;
1545
+ } else if (rest.length === 2) {
1546
+ const [args, options] = rest;
1547
+ if ((Array.isArray(args) || args == null) &&
1548
+ options !== null && typeof options === "object" && !Array.isArray(options)) {
1549
+ optIdx = 1;
1550
+ }
1551
+ }
1552
+ if (__compiledArtifact && optIdx >= 0) {
1553
+ rest[optIdx] = stripFromOptions(rest[optIdx]);
1554
+ }
1555
+ return origFork.call(this, modulePath, ...rest);
1556
+ };
1557
+ }
1558
+
1559
+ // True when V8 code coverage is active for THIS process — `--experimental-test-
1560
+ // coverage` / a bare `--test-coverage*` flag in our own argv or execArgv, or a
1561
+ // non-empty NODE_V8_COVERAGE env. A WARM compile cache makes V8 coverage imprecise
1562
+ // (cached bytecode collapses/omits per-branch ranges, so a fixture's coverage
1563
+ // JSON loses `functions[].ranges[1]` and the line/branch percentages drift from
1564
+ // plain node). nub must therefore NOT (re)enable its compile cache for any process
1565
+ // that is collecting coverage. This mirrors spawn.rs's coverage gate, but catches
1566
+ // the case spawn.rs cannot see: a grandchild the USER's test code spawns directly
1567
+ // (e.g. `spawnSync(execPath, [fixture], { env: { NODE_V8_COVERAGE } })`), which
1568
+ // inherits nub's preload via NODE_OPTIONS but never goes through nub's Rust spawn
1569
+ // path — so the gate has to live here too. (Observed against parallel/test-v8-
1570
+ // coverage, test-runner-coverage-thresholds, and the test-runner coverage-width
1571
+ // snapshot tests, all of which warm-cache then collect coverage in a child.)
1572
+ function coverageActiveInProcess() {
1573
+ if (process.env.NODE_V8_COVERAGE) return true;
1574
+ const hasCovFlag = (a) =>
1575
+ typeof a === "string" &&
1576
+ (a === "--experimental-test-coverage" || a.startsWith("--test-coverage"));
1577
+ return (process.execArgv || []).some(hasCovFlag) || (process.argv || []).some(hasCovFlag);
1578
+ }
1579
+
1580
+ function reenableUserCompileCache() {
1581
+ // Coverage active: leave the compile cache OFF so V8 collects precise per-branch
1582
+ // ranges (a warm cache collapses them — see coverageActiveInProcess). Two things
1583
+ // matter, because Node's test runner spawns a SEPARATE isolated child to run the
1584
+ // covered fixture and that child enables its cache at BOOTSTRAP from an inherited
1585
+ // NODE_COMPILE_CACHE — too early for any preload gate to catch:
1586
+ // (a) don't enableCompileCache in THIS process, and
1587
+ // (b) set NODE_DISABLE_COMPILE_CACHE=1 in our env so EVERY descendant (incl. the
1588
+ // test runner's isolated coverage child) boots with the cache off. Node
1589
+ // honors NODE_DISABLE_COMPILE_CACHE at bootstrap and it travels via the env,
1590
+ // reaching children nub never spawns itself. We do NOT clear NODE_COMPILE_CACHE
1591
+ // (user code may read it); the disable var takes precedence at bootstrap.
1592
+ // This is the JS half of the compile-cache/coverage fix; spawn.rs is the Rust half
1593
+ // (it never sets the DEFAULT cache when it can see coverage in nub's own
1594
+ // argv/NODE_OPTIONS/NODE_V8_COVERAGE).
1595
+ //
1596
+ // INTENTIONAL COVERAGE JUDGMENT CALL (b): NODE_DISABLE_COMPILE_CACHE=1 is set in
1597
+ // process.env, so it propagates SUBTREE-WIDE for the rest of this coverage session —
1598
+ // it is inherited by EVERY descendant, including non-coverage grandchildren the
1599
+ // user's test code spawns mid-run (e.g. a build step a covered test shells out to).
1600
+ // Those grandchildren therefore ALSO lose compile caching for the duration, even
1601
+ // though they aren't themselves collecting coverage. We accept this: the disable var
1602
+ // is the only mechanism that reaches the test runner's isolated coverage child
1603
+ // (which boots its cache before any preload gate can fire), and scoping it more
1604
+ // tightly than "the whole coverage subtree" isn't possible via an inherited env var.
1605
+ // The cost is bounded (no caching during one coverage session) and self-healing (a
1606
+ // grandchild spawned outside a coverage run is unaffected); surprising only to a
1607
+ // user who expects an unrelated grandchild to keep caching while a parent collects
1608
+ // coverage. Documented here and at the spawn.rs coverage branch (judgment call (a)).
1609
+ if (coverageActiveInProcess()) {
1610
+ const dir = process.env.NODE_COMPILE_CACHE;
1611
+ if (!dir || dir === "0") {
1612
+ // No explicit cache: keep coverage precise by disabling nub's default
1613
+ // subtree-wide (the only mechanism that reaches the test runner's isolated
1614
+ // coverage child, which boots its cache before any preload gate can fire).
1615
+ process.env.NODE_DISABLE_COMPILE_CACHE = "1";
1616
+ return;
1617
+ }
1618
+ // Explicit NODE_COMPILE_CACHE present: the user's choice wins over coverage
1619
+ // precision (the maintainer, 2026-06-11) — same tradeoff they'd have on plain node.
1620
+ // Narrow accepted caveat: a nub-DEFAULT dir restored from the sentinel is
1621
+ // indistinguishable from a user dir here; that combination is reachable only
1622
+ // when coverage is invisible to nub's own spawn (e.g. a c8-style grandchild
1623
+ // setting NODE_V8_COVERAGE itself). Simple + documented beats provenance
1624
+ // plumbing through the sentinel.
1625
+ try { module_.enableCompileCache(dir); } catch {}
1626
+ return;
1627
+ }
1628
+ const dir = process.env.NODE_COMPILE_CACHE;
1629
+ // "0" is nub's disable signal (see transform-core); anything else is the user's
1630
+ // real cache dir, which we re-point Node's compile cache at for THEIR modules.
1631
+ if (!dir || dir === "0") return;
1632
+ try { module_.enableCompileCache(dir); } catch {}
1633
+ }
1634
+
1635
+ // Publish `process.versions.nub` = the running binary's version — the universal
1636
+ // `process.versions.<runtime>` self-identification convention (cf. `.bun`,
1637
+ // `.electron`). A read-only DETECTION marker, never an importable/callable API.
1638
+ // The value comes from the Rust spawn layer's `__NUB_VERSION` (env!("CARGO_PKG_VERSION"),
1639
+ // coupled to preload injection), so it always matches the running binary; under
1640
+ // `--node`/`NODE_COMPAT` no preload runs and the var is unset, so the marker is
1641
+ // absent (plain-Node fingerprint). Shape mirrors Node's own `process.versions`
1642
+ // entries: enumerable + configurable + non-writable. Called once per main-thread
1643
+ // preload entry (fast + compat), so a single mechanism covers both tiers.
1644
+ function installVersionMarker() {
1645
+ const v = process.env[VERSION_ENV];
1646
+ if (!v) return;
1647
+ try {
1648
+ Object.defineProperty(process.versions, "nub", {
1649
+ value: v,
1650
+ writable: false,
1651
+ enumerable: true,
1652
+ configurable: true,
1653
+ });
1654
+ } catch {}
1655
+ }
1656
+
1657
+ // ── User preloads (`nub.jsonc` `preload`) ───────────────────────────
1658
+ // nub loads the user's preload entries HERE rather than emitting one NODE_OPTIONS
1659
+ // token per entry. Two reasons, and the first is a correctness bug in the wild:
1660
+ //
1661
+ // 1. Consumers that re-parse NODE_OPTIONS destroy repeated same-name flags. Next.js
1662
+ // parses it into a `Record` keyed by option name and reformats it for every
1663
+ // forked worker, so `--require=a --require=b` becomes `--require=b`, silently
1664
+ // dropping whichever came first — which is nub's OWN preload. Filed upstream as
1665
+ // vercel/next.js#96582. Emitting at most one `--require` and one `--import`
1666
+ // survives that round-trip intact.
1667
+ // 2. Loading them here means nub stops GUESSING each entry's module format from its
1668
+ // file extension; Node's own resolver decides at load time.
1669
+ //
1670
+ // Resolution base is the CWD, not nub's runtime dir. That is what Node does for a
1671
+ // `--require`/`--import` specifier (measured: a bare specifier resolves through the
1672
+ // node_modules walk-up from the CWD, and fails outside the project), whereas nub's
1673
+ // runtime dir would resolve a bare entry against nub's OWN dependencies. Relative
1674
+ // entries already arrive absolute from the Rust side; bare ones do not.
1675
+ // The chainer path, present only when the spawn path decided nub's OWN preload
1676
+ // should load it (no second NODE_OPTIONS token exists in that case). Absent when
1677
+ // the chainer got its own `--import` — loading it in both places would run the
1678
+ // user's entries twice.
1679
+ function userPreloadChain() {
1680
+ try {
1681
+ const chain = JSON.parse(process.env.__NUB_RUNTIME_CONFIG || "{}").preloadChain;
1682
+ return typeof chain === "string" && chain.length > 0 ? chain : null;
1683
+ } catch {
1684
+ return null;
1685
+ }
1686
+ }
1687
+
1688
+ // Fast tier, `.cjs`-only entries: load the chainer SYNCHRONOUSLY, so `--require`'s
1689
+ // synchronous-entry semantics (R1) survive. The chainer's own `require()` calls
1690
+ // resolve from ITS directory, which is inside the user's project.
1691
+ function requireUserPreloadChain() {
1692
+ const chain = userPreloadChain();
1693
+ if (chain) module.require(chain);
1694
+ }
1695
+
1696
+ // Compat tier: nub's own preload is an `--import`, so it can await. The chainer is
1697
+ // ESM here, and awaiting it lets a user entry with top-level await settle before the
1698
+ // program starts — which `require()` could not do (ERR_REQUIRE_ASYNC_MODULE).
1699
+ async function importUserPreloadChain() {
1700
+ const chain = userPreloadChain();
1701
+ if (!chain) return;
1702
+ const { pathToFileURL } = require("node:url");
1703
+ await import(pathToFileURL(chain).href);
1704
+ }
1705
+
1706
+ module.exports = {
1707
+ installVersionMarker,
1708
+ installWatchReporting,
1709
+ registerLoaderWorker,
1710
+ makeHooks,
1711
+ shouldAutoAsyncTierAtPreload,
1712
+ // Consumed by the standalone loader entry (loader-entry.mjs), whose foreign-
1713
+ // loader scan must be value-aware (its own delivery IS an `--import`) and so
1714
+ // cannot reuse shouldAutoAsyncTierAtPreload directly.
1715
+ nodeHookComposeBroken,
1716
+ installCjsRequireHooks,
1717
+ preloadPolyfillPackages,
1718
+ installTemporalGlobal,
1719
+ installTemporalLazyGlobal,
1720
+ restoreCompileCacheEnv,
1721
+ installCompiledChildProcess,
1722
+ reenableUserCompileCache,
1723
+ requireUserPreloadChain,
1724
+ importUserPreloadChain,
1725
+ };