@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.
- package/LICENSE +21 -0
- package/README.md +48 -1
- package/cache-evict.mjs +180 -0
- package/floor-builtin.mjs +57 -0
- package/loader-addon-env.mjs +24 -0
- package/loader-entry.mjs +197 -0
- package/loader-esm.mjs +6 -0
- package/loader-platform.cjs +109 -0
- package/loader-register.cjs +33 -0
- package/loader-register.mjs +5 -0
- package/package.json +57 -3
- package/pnp-util.cjs +62 -0
- package/preload-async-hooks.mjs +133 -0
- package/preload-common.cjs +1725 -0
- package/transform-core.mjs +1028 -0
|
@@ -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
|
+
};
|