@nubjs/nub-win32-x64 0.0.13 → 0.0.14

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/bin/nub.exe CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nubjs/nub-win32-x64",
3
- "version": "0.0.13",
3
+ "version": "0.0.14",
4
4
  "description": "Nub binary for win32-x64",
5
5
  "license": "MIT",
6
6
  "repository": "https://github.com/nub-js/nub",
Binary file
@@ -0,0 +1,178 @@
1
+ // Polyfill preloads for Nub v0.1 — the shared implementation for BOTH tiers.
2
+ //
3
+ // This is a CommonJS module with ZERO top-level await so the fast tier
4
+ // (Node 22.15+, `--require` CJS preload) can `require()` it synchronously: a
5
+ // `require()`-loaded preload keeps Node's synchronous `Module.runMain` CJS entry
6
+ // path (top-level `executionAsyncId()===1`, sync exception origin), which the old
7
+ // `--import` ESM preload broke (R1). The compat tier (`--import` preload.mjs)
8
+ // reuses this same logic via the `installSyncPolyfills` export, then loads the two
9
+ // ESM side-effect modules (worker-polyfill, navigator-locks) with dynamic
10
+ // `import()` — on the < 22.15 floor `require()` of an ES module is unreliable.
11
+ //
12
+ // All polyfills feature-detect and bow out if the global is already present.
13
+ //
14
+ // Node 22.15+ (our floor) already has: navigator, navigator.locks,
15
+ // navigator.hardwareConcurrency, WebSocket. No polyfills needed.
16
+ //
17
+ // Node 24+ adds: URLPattern, RegExp.escape, Error.isError, Promise.try.
18
+ // We polyfill those on Node 22.x only.
19
+ //
20
+ // No Node version ships: Temporal, reportError, browser-shape Worker.
21
+ // These need polyfills on all supported versions. (Temporal is a lazy global
22
+ // installed by the preload entry, NOT here — see preload.cjs / preload.mjs.)
23
+
24
+ const { createRequire } = require("node:module");
25
+ const __require = createRequire(__filename);
26
+
27
+ // Install every globalThis/prototype polyfill that doesn't depend on loading the
28
+ // ESM side-effect modules (worker-polyfill, navigator-locks). Synchronous and
29
+ // idempotent — safe to call once per realm. `preloaded` carries the CJS-required
30
+ // polyfill packages the preload entry stashed (urlpattern, float16), since the
31
+ // resolve hook would otherwise clobber a later import of them.
32
+ function installSyncPolyfills(preloaded) {
33
+ preloaded = preloaded || {};
34
+
35
+ // ── reportError (WinterTC min-common-API, not in any Node) ──────────
36
+ // Defined NON-ENUMERABLE so it is invisible to `Object.keys(globalThis)` /
37
+ // for-in / structured-clone-of-keys — that invisibility-to-enumeration IS the
38
+ // additive contract: code written for vanilla Node must not observe nub's
39
+ // injected globals when it enumerates the global object. Node defines its own
40
+ // globals non-enumerably for the same reason. Kept writable+configurable so
41
+ // user code can still override or delete it, matching Node's global descriptors.
42
+ if (typeof globalThis.reportError !== "function") {
43
+ Object.defineProperty(globalThis, "reportError", {
44
+ value: (err) => {
45
+ queueMicrotask(() => {
46
+ throw err;
47
+ });
48
+ },
49
+ enumerable: false,
50
+ writable: true,
51
+ configurable: true,
52
+ });
53
+ }
54
+
55
+ // ── URLPattern (native on Node 24+, missing on 22.x) ───────────────
56
+ if (typeof globalThis.URLPattern === "undefined") {
57
+ const mod = preloaded.urlpattern;
58
+ const URLPattern = mod?.URLPattern;
59
+ if (URLPattern) globalThis.URLPattern = URLPattern;
60
+ }
61
+
62
+ // Temporal (in no Node version) is installed as a LAZY global by the preload
63
+ // entry after this runs — see preload.cjs / preload.mjs (A37). Touching
64
+ // globalThis.Temporal here would defeat that laziness, so we must not.
65
+
66
+ // ── Stage 4 polyfills (native on Node 24+, missing on 22.x) ────────
67
+
68
+ // RegExp.escape — spec-faithful port of the TC39 proposal (native on Node 24+),
69
+ // so the 22.x floor behaves byte-for-byte like native: a leading digit/letter is
70
+ // control-escaped, syntax chars are backslashed, control chars use \t\n\v\f\r, and
71
+ // the "other punctuators" + whitespace set is hex-escaped. Verified byte-identical
72
+ // to Node's native RegExp.escape across every ASCII char + leading/whitespace/
73
+ // astral cases (so a concatenated `escape(s)` is safe too, not just
74
+ // `new RegExp(escape(s))`). The earlier reduced-fidelity version only escaped the
75
+ // syntax chars.
76
+ if (typeof RegExp.escape !== "function") {
77
+ const SYNTAX = new Set(["^", "$", "\\", ".", "*", "+", "?", "(", ")", "[", "]", "{", "}", "|", "/"]);
78
+ const CONTROL = { "\t": "\\t", "\n": "\\n", "\v": "\\v", "\f": "\\f", "\r": "\\r" };
79
+ // ASCII "other punctuators" the spec escapes by code, plus SPACE.
80
+ const OTHER = new Set([..." ,-=<>#&!%:;@~'\"`"]);
81
+ const isWhiteSpace = (cp) =>
82
+ cp === 0x09 || cp === 0x0a || cp === 0x0b || cp === 0x0c || cp === 0x0d ||
83
+ cp === 0x20 || cp === 0xa0 || cp === 0x1680 || (cp >= 0x2000 && cp <= 0x200a) ||
84
+ cp === 0x2028 || cp === 0x2029 || cp === 0x202f || cp === 0x205f || cp === 0x3000 ||
85
+ cp === 0xfeff;
86
+ const hexEscape = (cp) => {
87
+ if (cp <= 0xff) return "\\x" + cp.toString(16).padStart(2, "0");
88
+ if (cp <= 0xffff) return "\\u" + cp.toString(16).padStart(4, "0");
89
+ const h = cp - 0x10000;
90
+ const hi = 0xd800 + (h >> 10);
91
+ const lo = 0xdc00 + (h & 0x3ff);
92
+ return "\\u" + hi.toString(16).padStart(4, "0") + "\\u" + lo.toString(16).padStart(4, "0");
93
+ };
94
+ const encode = (ch, cp) =>
95
+ SYNTAX.has(ch)
96
+ ? "\\" + ch
97
+ : CONTROL[ch] ?? ((OTHER.has(ch) || isWhiteSpace(cp)) ? hexEscape(cp) : ch);
98
+ RegExp.escape = (s) => {
99
+ if (typeof s !== "string") throw new TypeError("RegExp.escape argument must be a string");
100
+ const cps = [...s]; // iterate by code point (astral-safe)
101
+ let out = "";
102
+ for (let i = 0; i < cps.length; i++) {
103
+ const ch = cps[i];
104
+ const cp = ch.codePointAt(0);
105
+ // A leading decimal-digit/ASCII-letter is control-escaped so a preceding `\`
106
+ // in a concatenated pattern can't form an escape sequence.
107
+ if (i === 0 && ((cp >= 0x30 && cp <= 0x39) || (cp >= 0x41 && cp <= 0x5a) || (cp >= 0x61 && cp <= 0x7a))) {
108
+ out += "\\x" + cp.toString(16).padStart(2, "0");
109
+ } else {
110
+ out += encode(ch, cp);
111
+ }
112
+ }
113
+ return out;
114
+ };
115
+ }
116
+
117
+ // Error.isError (~95% fidelity — cross-realm internal-slot unreachable)
118
+ if (typeof Error.isError !== "function") {
119
+ Error.isError = (value) => {
120
+ if (value == null || typeof value !== "object") return false;
121
+ return value instanceof Error;
122
+ };
123
+ }
124
+
125
+ // Promise.try
126
+ if (typeof Promise.try !== "function") {
127
+ Promise.try = (fn, ...args) => {
128
+ return new Promise((resolve) => resolve(fn(...args)));
129
+ };
130
+ }
131
+
132
+ // Float16Array (TC39 Stage 4, native on Node 24+; absent on our 22.x floor).
133
+ // Installed from the spec-compliant @petamoriken/float16 polyfill (vendored,
134
+ // preloaded by the preload entry). It provides the full TypedArray method
135
+ // surface (map/filter/subarray/set/reduce/…) and correct round-to-nearest-even,
136
+ // including subnormals — unlike the prior hand-rolled Proxy shim, which had
137
+ // ~30 methods missing and truncating/denormal-flushing conversion.
138
+ //
139
+ // INHERENT userland limitation (not fixable by any JS polyfill): a polyfilled
140
+ // Float16Array isn't recognized by `ArrayBuffer.isView()` (it has no V8 internal
141
+ // [[TypedArrayName]] slot). Code needing that check should use the polyfill's
142
+ // `isFloat16Array`. See wiki/runtime/float16array-polyfill.md.
143
+ if (typeof globalThis.Float16Array === "undefined") {
144
+ const f16 = preloaded.float16;
145
+ if (f16?.Float16Array) {
146
+ globalThis.Float16Array = f16.Float16Array;
147
+
148
+ if (typeof DataView.prototype.getFloat16 !== "function") {
149
+ DataView.prototype.getFloat16 = function (offset, littleEndian) {
150
+ return f16.getFloat16(this, offset, littleEndian);
151
+ };
152
+ DataView.prototype.setFloat16 = function (offset, value, littleEndian) {
153
+ f16.setFloat16(this, offset, value, littleEndian);
154
+ };
155
+ }
156
+
157
+ if (typeof Math.f16round !== "function") {
158
+ Math.f16round = f16.f16round;
159
+ }
160
+ }
161
+ }
162
+ }
163
+
164
+ // Load the two ESM side-effect modules — Web Locks (navigator.locks) and the
165
+ // browser-shape Worker global — synchronously via `require()`. Valid on the fast
166
+ // tier ONLY (Node 22.15+), where require(esm) of these side-effecting ES modules
167
+ // works (verified). The compat tier must NOT call this; it loads them with
168
+ // dynamic `import()` from preload.mjs instead.
169
+ function installEsmPolyfillsSync() {
170
+ // ── navigator.locks (native on Node 24+, missing on 22.x) ──────────
171
+ if (typeof globalThis.navigator?.locks === "undefined") {
172
+ __require("./navigator-locks.mjs");
173
+ }
174
+ // ── Worker (browser-shape global, not in any Node) ──────────────────
175
+ __require("./worker-polyfill.mjs");
176
+ }
177
+
178
+ module.exports = { installSyncPolyfills, installEsmPolyfillsSync };
@@ -0,0 +1,548 @@
1
+ // Shared preload machinery for BOTH tiers — CommonJS, zero top-level await.
2
+ //
3
+ // The fast tier (Node 22.15+) loads this from a `--require` CJS preload
4
+ // (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) 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 module_ = require("node:module");
19
+ const { readdirSync } = require("node:fs");
20
+ const { fileURLToPath, pathToFileURL } = require("node:url");
21
+ const { join, dirname, extname: pathExtname } = require("node:path");
22
+
23
+ // ── Watch-mode dependency reporting (main thread only) ──────────────
24
+ // Under `nub watch`, Node's FilesWatcher only watches files in the import graph;
25
+ // config files (tsconfig.json, package.json) and `.env*` are NOT in any graph, so
26
+ // an edit to them otherwise goes stale. Node accepts incremental
27
+ // `process.send({'watch:require': [...]})` over its WATCH_REPORT_DEPENDENCIES IPC
28
+ // at ANY point in the child's life (it adds each path to the watch set), so we
29
+ // report config paths AS the core loader discovers them. The reporters are
30
+ // injected into the core via setWatchHooks so getTsconfigForDir / getPackageType
31
+ // self-report. The flush is coalesced via setImmediate.
32
+ function installWatchReporting(core) {
33
+ const WATCH_REPORTING =
34
+ process.env.WATCH_REPORT_DEPENDENCIES === "1" && typeof process.send === "function";
35
+ const watchReported = new Set();
36
+ const watchPending = [];
37
+ let watchFlushScheduled = false;
38
+ function flushWatchDeps() {
39
+ watchFlushScheduled = false;
40
+ if (watchPending.length === 0) return;
41
+ const batch = watchPending.splice(0, watchPending.length);
42
+ try { process.send({ "watch:require": batch }); } catch {}
43
+ }
44
+ function reportWatchDep(path) {
45
+ if (!WATCH_REPORTING || !path || watchReported.has(path)) return;
46
+ watchReported.add(path);
47
+ watchPending.push(path);
48
+ if (!watchFlushScheduled) {
49
+ watchFlushScheduled = true;
50
+ // A scheduled immediate is drained before the loop would exit, so even a
51
+ // script that finishes synchronously flushes its deps. (Don't unref: an
52
+ // unref'd immediate is skipped on a synchronous exit, dropping the report.)
53
+ setImmediate(flushWatchDeps);
54
+ }
55
+ }
56
+ // Report a directory's `.env*` files (the natural watch targets). Scanned once
57
+ // per directory, lazily.
58
+ const watchEnvScannedDirs = new Set();
59
+ function reportEnvFilesIn(dir) {
60
+ if (!WATCH_REPORTING || watchEnvScannedDirs.has(dir)) return;
61
+ watchEnvScannedDirs.add(dir);
62
+ let entries;
63
+ try { entries = readdirSync(dir); } catch { return; }
64
+ for (const name of entries) {
65
+ if (name === ".env" || name.startsWith(".env.")) reportWatchDep(join(dir, name));
66
+ }
67
+ }
68
+ core.setWatchHooks({ reportDep: reportWatchDep, reportEnvDir: reportEnvFilesIn });
69
+ return WATCH_REPORTING;
70
+ }
71
+
72
+ // ── Resolve / load hooks (sync `module.registerHooks` shape) ────────
73
+ // Returns `{ resolve, load }` closing over `core` + the watch flag. The compat
74
+ // tier does NOT use these (its hooks run async in the loader worker via
75
+ // preload-async-hooks.mjs); only the fast tier's `module.registerHooks` does.
76
+
77
+ // True once USER code registers its own `module.registerHooks` (a ts-node/tsx-style
78
+ // transpiler). nub registers exactly one hook set from the preload (the FIRST call
79
+ // after the wrap below); every later call is the user's. This lets the load hook
80
+ // tell apart a bare `'typescript'` format that a USER resolve hook set (defer — the
81
+ // user's own load hook will transpile) from the bare `'typescript'` that Node's
82
+ // NATIVE CJS loader assigns to a `.ts` entry/require in a package with no explicit
83
+ // `type` (transpile — there is no user hook to do it, and Node's strip-only mode
84
+ // can't handle enums/namespaces). See makeHooks().load.
85
+ let __userHooksRegistered = false;
86
+ function installUserHookDetector() {
87
+ if (typeof module_.registerHooks !== "function") return;
88
+ const orig = module_.registerHooks;
89
+ if (orig.__nubWrapped) return;
90
+ let seen = 0;
91
+ const wrapped = function (...args) {
92
+ // Call #1 is nub's own preload registration; #2+ are user hooks.
93
+ if (seen >= 1) __userHooksRegistered = true;
94
+ seen += 1;
95
+ return orig.apply(this, args);
96
+ };
97
+ wrapped.__nubWrapped = true;
98
+ try { module_.registerHooks = wrapped; } catch {}
99
+ }
100
+
101
+ function makeHooks(core, watchReporting) {
102
+ installUserHookDetector();
103
+
104
+ function resolve(specifier, context, nextResolve) {
105
+ const r = core.resolveSpec(specifier, context.parentURL);
106
+ return r ?? nextResolve(specifier, context);
107
+ }
108
+
109
+ function load(url, context, nextLoad) {
110
+ const ext = core.extname(url);
111
+
112
+ // Watch mode: surface this file's nearest config files (tsconfig.json,
113
+ // package.json) + sibling `.env*` so edits to them restart the run. Done for
114
+ // every user file (not just transpiled ones) — getTsconfigForDir/
115
+ // getPackageType self-report via the injected watch hooks.
116
+ if (watchReporting && url.startsWith("file:") && !core.isNodeModules(url)) {
117
+ try {
118
+ const dir = dirname(fileURLToPath(url));
119
+ core.getTsconfigForDir(dir);
120
+ core.getPackageType(dir);
121
+ } catch {}
122
+ }
123
+
124
+ // A USER resolve hook (a ts-node/tsx-style transpiler registered AFTER nub's
125
+ // own preload hook) claimed this file with the bare 'typescript' format: defer
126
+ // to the user's own load chain. The discriminator is `__userHooksRegistered`,
127
+ // NOT the bare format alone — Node's NATIVE CJS loader ALSO emits the bare
128
+ // string 'typescript' for a `.ts` entry/require whose nearest package.json has
129
+ // no explicit `type` (cjs/loader.js getFormatOfExtensionlessFile, lines ~1986),
130
+ // and in that native case nub MUST transpile (Node's strip-only mode can't
131
+ // handle enums/namespaces). So we only step aside when a user hook is present:
132
+ // nub registers exactly one hook set from the preload, the user registers theirs
133
+ // later, and registering theirs OUTERMOST (LIFO) means their load hook wraps
134
+ // nub's — it sets format='typescript', calls nextLoad into nub, and (without this
135
+ // guard) nub would transpile with oxc — a type-stripper, not a module-format
136
+ // transformer — leaving `export {}` verbatim and, for a `type:commonjs` package,
137
+ // handing Node format='commonjs' + ESM source = invalid CJS. Stepping aside lets
138
+ // nub fall through to Node's native load, returning raw TS source back up to the
139
+ // user's outer hook, which does the real ESM->CJS conversion, matching Node.
140
+ // Native 'module-typescript'/'commonjs-typescript' formats still fall through to
141
+ // nub's transpile below, so normal augmentation is unchanged.
142
+ if (__userHooksRegistered && context && context.format === "typescript") {
143
+ return nextLoad(url, context);
144
+ }
145
+
146
+ // R12: never transpile `.ts`/`.tsx`/… inside node_modules. Node itself throws
147
+ // ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING for TS under node_modules; if
148
+ // nub transpiled it instead, that native error would never surface and nub
149
+ // would be MORE permissive than Node. Fall through to `nextLoad` so Node's own
150
+ // handling (and its error) applies. (The TS-parent extensionless resolution in
151
+ // the resolve hook is intended and stays — only this load-time transpile is
152
+ // gated.)
153
+ if (core.TRANSPILE_EXTS.has(ext) && !core.isNodeModules(url)) {
154
+ return core.loadTranspile(url, ext);
155
+ }
156
+ if (ext in core.DATA_EXTS) return core.loadData(url, ext);
157
+
158
+ const r = nextLoad(url, context);
159
+ // nub's sync `module.registerHooks` load hook forces the synchronous
160
+ // module-job (ModuleJobSync.syncLink -> loadAndTranslateForImportInRequiredESM),
161
+ // which cannot async-fetch source. When a user `--experimental-loader` resolve
162
+ // hook sets `format` without a `source` (a pattern vanilla Node tolerates on its
163
+ // async load path by fetching the source itself), the default load returns
164
+ // source:null and Node's assertBufferSource throws ERR_INVALID_RETURN_PROPERTY_VALUE.
165
+ // Backfill the source from disk so the sync path matches Node — without touching
166
+ // nub's own resolve/transpile hooks.
167
+ //
168
+ // EXCEPTION — format 'commonjs' (and 'builtin') MUST keep source:null. For those
169
+ // formats Node's ESM loader deliberately returns no source and hands the module
170
+ // off to the NATIVE CommonJS loader (Module._load), where `require()` uses CJS
171
+ // resolution. A CJS `.js` ENTRY, when a user `--experimental-loader` is active, is
172
+ // routed through the ESM loader for format detection but still loads as CJS this
173
+ // way. If we backfilled its source, the ESM loader would instead translate it via
174
+ // its CommonJS-to-ESM wrapper, routing every inner `require()` through the ESM
175
+ // resolve hook — so `require('assert')` would hand the bare 'assert' specifier to
176
+ // the user's resolve hook and crash with ERR_INVALID_RETURN_PROPERTY_VALUE (the
177
+ // shadow-realm/custom-loaders corpus failure). Only ESM-shaped formats ('module',
178
+ // 'json', 'wasm', …) genuinely need the source on the sync path.
179
+ if (
180
+ r && r.source == null && r.format &&
181
+ r.format !== "commonjs" && r.format !== "builtin" &&
182
+ typeof url === "string" && url.startsWith("file:")
183
+ ) {
184
+ try {
185
+ const { readFileSync } = require("node:fs");
186
+ return { ...r, source: readFileSync(fileURLToPath(url)) };
187
+ } catch { /* fall through with the original result */ }
188
+ }
189
+ return r;
190
+ }
191
+
192
+ return { resolve, load };
193
+ }
194
+
195
+ // ── CommonJS require() augmentation (BOTH tiers) ────────────────────
196
+ // `module.registerHooks`' CJS-`require()` coverage is INCOMPLETE before ~Node 24:
197
+ // on Node 22.15 a `require()` from a `.cts` parent (which Node loads via the ESM
198
+ // translator's special-require) hits native Module._resolveFilename with no
199
+ // tsconfig/extensionless handling — a `require('@alias')` or `require('./x')` of a
200
+ // `.ts` target throws MODULE_NOT_FOUND, while the same code works on Node 26 (where
201
+ // registerHooks does cover it) and on the fast `import` path. On the compat tier
202
+ // (18.19–22.14) the only hook surface is `module.register`, which intercepts the
203
+ // ESM loader ONLY — so `require()` is entirely unaugmented there. Both gaps have
204
+ // the same closure: install this main-thread CJS shim, reusing the core's canonical
205
+ // resolveCjsPath / loadTranspile (no drift). It tries nub's resolution first and
206
+ // FALLS THROUGH to native on a miss, so it is a safe no-op on the versions where
207
+ // registerHooks already covers require (Node 24+/26). Mechanism stays within the
208
+ // augmenter rules: exactly what `--require`-installing the ts-node / tsx CJS shim
209
+ // has always done.
210
+ //
211
+ // This error is surfaced ONLY on Node versions without native require(esm)
212
+ // (< 20.19 / 22.0–22.11), where require() of an ES module genuinely cannot work.
213
+ // On every require(esm)-capable Node, Node loads the ES module itself and this is
214
+ // never reached. The message is user-facing: no internal mechanism names.
215
+ function requireEsmError(filename) {
216
+ const err = new Error(
217
+ `Cannot require() this file — it is an ES module.\n` +
218
+ ` ${filename}\n` +
219
+ `It uses \`import\`/\`export\`, so it loads as an ES module, and this version of ` +
220
+ `Node can't require() an ES module. Load it with \`import(...)\` instead, rename ` +
221
+ `it to .cts for a CommonJS module, or upgrade Node.`,
222
+ );
223
+ err.code = "ERR_REQUIRE_ESM";
224
+ return err;
225
+ }
226
+
227
+ // `withClassicTranspile` — also install the `require.extensions` (classic CommonJS
228
+ // loader) transpile hook. Needed ONLY on Node WITHOUT native require(esm)
229
+ // (< 20.19 / 22.0–22.11): there, `module.register`'s ESM-loader hooks can't reach a
230
+ // `require()`, AND an ES module simply can't be require()d, so we transpile CJS
231
+ // content classically and surface a clean error for ESM content. On require(esm)-
232
+ // capable Node we DON'T install it — registering `require.extensions['.ts']` would
233
+ // shadow Node's own native require(esm) of ES-module `.ts` files (breaking
234
+ // `require("./esm.ts")`), and the resolve shim below plus the tier's load hook
235
+ // already cover resolution + transpile.
236
+ function installCjsRequireHooks(core, withClassicTranspile) {
237
+ const origResolveFilename = module_._resolveFilename;
238
+ module_._resolveFilename = function (request, parent, isMain, options) {
239
+ let resolved = null;
240
+ try {
241
+ const parentPath = parent && typeof parent.filename === "string" ? parent.filename : null;
242
+ resolved = core.resolveCjsPath(request, parentPath);
243
+ } catch { /* fall through to Node */ }
244
+ if (resolved) {
245
+ if (withClassicTranspile && core.requireTargetIsEsm(resolved, pathExtname(resolved))) {
246
+ throw requireEsmError(resolved);
247
+ }
248
+ return resolved;
249
+ }
250
+ return origResolveFilename.call(this, request, parent, isMain, options);
251
+ };
252
+
253
+ if (!withClassicTranspile) return;
254
+
255
+ // require.extensions: transpile via the SAME loadTranspile the load hook uses —
256
+ // target:'es2022' lowering (`using`), tsconfig, source maps, the Stage-3
257
+ // decorator guard, and module-format detection are all identical to the fast
258
+ // tier. The path is already a real TS file (Module._resolveFilename ran first).
259
+ // A module-format source can't be _compile'd as CJS — same clean error as above.
260
+ const transpileExtension = (mod, filename) => {
261
+ const { source, format } = core.loadTranspile(pathToFileURL(filename).href, pathExtname(filename));
262
+ if (format === "module") throw requireEsmError(filename);
263
+ mod._compile(source, filename);
264
+ };
265
+ for (const ext of [".ts", ".cts", ".mts", ".tsx", ".jsx"]) {
266
+ module_._extensions[ext] = transpileExtension;
267
+ }
268
+ }
269
+
270
+ // ── Clobbered-polyfill preloading + Temporal lazy global ────────────
271
+ // Packages in the core's CLOBBER_MAP can't be imported after hooks register
272
+ // because the resolve hook returns a synthetic module instead of the real package.
273
+ // Load them here via CJS require (not yet hooked) and return them so the polyfill
274
+ // installer can stash them. Temporal is the exception (A37): the polyfill is ~18ms
275
+ // to load and most scripts never touch it, so we only RESOLVE its path now (cheap)
276
+ // and defer the load to a lazy global getter. Requiring it later by absolute path
277
+ // bypasses the CLOBBER_MAP resolve-hook entry, which keys on the specifier.
278
+ function preloadPolyfillPackages(reqFromRuntime) {
279
+ const preloaded = {};
280
+ // Feature-detect before requiring (A39): URLPattern is native on Node 24+, so
281
+ // skip loading the polyfill there. On 22.x it's absent → load it.
282
+ if (typeof globalThis.URLPattern === "undefined") {
283
+ try { preloaded.urlpattern = reqFromRuntime("urlpattern-polyfill"); } catch {}
284
+ }
285
+ // Float16Array: native on Node 24+, absent on the 22.x floor.
286
+ if (typeof globalThis.Float16Array === "undefined") {
287
+ try { preloaded.float16 = reqFromRuntime("@petamoriken/float16"); } catch {}
288
+ }
289
+ return preloaded;
290
+ }
291
+
292
+ // Install the lazy `globalThis.Temporal` getter. The polyfill is loaded — and even
293
+ // RESOLVED — only on first access. CRITICAL ordering note (regexp one-off): the
294
+ // `require.resolve("@js-temporal/polyfill")` is deferred INTO the getter, NOT run at
295
+ // preload top level. An unconditional resolve at startup mutates the legacy
296
+ // `RegExp.$_` static (the resolved node_modules path matches an internal regex), so
297
+ // a program inspecting `RegExp.$_` on its first line would otherwise see a leaked
298
+ // path (test-startup-empty-regexp-statics). Deferring the resolve keeps `RegExp.$_`
299
+ // empty at user-code start; the cost is paid only by a program that touches Temporal.
300
+ function installTemporalLazyGlobal(reqFromRuntime) {
301
+ if (typeof globalThis.Temporal !== "undefined") return;
302
+
303
+ const defineTemporal = (value) =>
304
+ Object.defineProperty(globalThis, "Temporal", {
305
+ value,
306
+ configurable: true,
307
+ writable: true,
308
+ enumerable: false,
309
+ });
310
+ Object.defineProperty(globalThis, "Temporal", {
311
+ configurable: true,
312
+ enumerable: false,
313
+ get() {
314
+ let temporalPath;
315
+ try { temporalPath = reqFromRuntime.resolve("@js-temporal/polyfill"); } catch {}
316
+ if (!temporalPath) return undefined;
317
+ const polyfill = reqFromRuntime(temporalPath);
318
+ // @js-temporal/polyfill exports `toTemporalInstant` as a function but does
319
+ // NOT auto-install it on Date.prototype (you assign it yourself). Install it
320
+ // here so that on the floor (no native Temporal) `date.toTemporalInstant()`
321
+ // AND the package clobber's re-export of `Date.prototype.toTemporalInstant`
322
+ // both work — matching native Node. Guarded so we never replace a native
323
+ // implementation on a runtime that ships Temporal.
324
+ if (
325
+ typeof Date.prototype.toTemporalInstant !== "function" &&
326
+ typeof polyfill.toTemporalInstant === "function"
327
+ ) {
328
+ Object.defineProperty(Date.prototype, "toTemporalInstant", {
329
+ value: polyfill.toTemporalInstant,
330
+ configurable: true,
331
+ writable: true,
332
+ enumerable: false,
333
+ });
334
+ }
335
+ const T = polyfill.Temporal;
336
+ defineTemporal(T);
337
+ return T;
338
+ },
339
+ set: defineTemporal,
340
+ });
341
+ }
342
+
343
+ // ── Compile-cache handling (R8) ─────────────────────────────────────
344
+ // nub injects its preload chain via `--require`, which Node loads at bootstrap.
345
+ // If the user set NODE_COMPILE_CACHE, Node would enable the V8 code cache BEFORE
346
+ // this preload runs and cache every module the chain pulls in (preload.cjs,
347
+ // transform-core.mjs, this file, polyfills.cjs, …) into the USER's dir — so a
348
+ // program reading `fs.readdirSync(NODE_COMPILE_CACHE)` would see ~9 nub entries,
349
+ // not its own 1 (program-observable; R8). spawn.rs prevents that by STRIPPING
350
+ // NODE_COMPILE_CACHE from the child env (bootstrap caches nothing) and stashing
351
+ // the original value in a sentinel file keyed on nub's PID — which is THIS child's
352
+ // `process.ppid` (nub is our direct parent). The dir travels via a sentinel file,
353
+ // never a NUB_* env var (brand boundary).
354
+ //
355
+ // Two preload steps consume it:
356
+ // 1. restoreCompileCacheEnv() runs EARLY, before transform-core.mjs is required,
357
+ // to put the original value BACK into process.env.NODE_COMPILE_CACHE. That
358
+ // matters because (a) transform-core reads `NODE_COMPILE_CACHE === "0"` as
359
+ // nub's transpile-cache disable signal, and (b) user code may read the env.
360
+ // Restoring it in JS does NOT re-trigger Node's V8 compile cache (Node
361
+ // configures that once at bootstrap from the now-stripped env), so the
362
+ // preload chain stays uncached. It also DELETES the sentinel (consume-once,
363
+ // so a recycled PID can't read stale state and the file never leaks).
364
+ // 2. reenableUserCompileCache() runs LAST, after all nub modules are loaded
365
+ // uncached and right before user code, and calls
366
+ // `module.enableCompileCache(dir)` for a real dir so the user's OWN modules
367
+ // cache as they always did. A value of "0" is nub's disable sentinel (Node
368
+ // treats "0" as a literal dir named 0, but nub honors it as "no caching"),
369
+ // so we skip enabling there.
370
+ // Best-effort throughout: a missing/unreadable sentinel or an enableCompileCache
371
+ // failure just means no user compile cache — strictly safer than the old pollution.
372
+ // `os.tmpdir()` without requiring `node:os`. Requiring os at preload pulls
373
+ // `Internal Binding os` + `NativeModule os` into process.moduleLoadList on EVERY
374
+ // startup (test-bootstrap-modules observes this) even though almost no run touches
375
+ // the compile-cache sentinel. This replica mirrors Node's libuv/os.tmpdir() env
376
+ // resolution (POSIX: TMPDIR→TMP→TEMP→/tmp; Win32: TEMP→TMP→SystemRoot/windir+\temp),
377
+ // trailing-separator-stripped, which is also what Rust's env::temp_dir() (the side
378
+ // that WRITES the sentinel in spawn.rs) resolves to — so both ends agree.
379
+ function tmpdirNoOs() {
380
+ const env = process.env;
381
+ if (process.platform === "win32") {
382
+ let dir = env.TEMP || env.TMP || ((env.SystemRoot || env.windir || "") + "\\temp");
383
+ if (dir.length > 1 && dir.endsWith("\\") && !dir.endsWith(":\\")) dir = dir.slice(0, -1);
384
+ return dir;
385
+ }
386
+ let dir = env.TMPDIR || env.TMP || env.TEMP || "/tmp";
387
+ if (dir.length > 1 && dir.endsWith("/")) dir = dir.slice(0, -1);
388
+ return dir;
389
+ }
390
+
391
+ function compileCacheSentinelPath() {
392
+ return join(tmpdirNoOs(), `nub-ccache-${process.ppid}`);
393
+ }
394
+
395
+ function restoreCompileCacheEnv() {
396
+ try {
397
+ const { readFileSync, rmSync } = require("node:fs");
398
+ const value = readFileSync(compileCacheSentinelPath(), "utf8");
399
+ try { rmSync(compileCacheSentinelPath()); } catch {}
400
+ if (value) process.env.NODE_COMPILE_CACHE = value;
401
+ } catch { /* no sentinel: env was never set, or already consumed */ }
402
+ // Propagate the R8 strip to node grandchildren the user spawns directly (plain
403
+ // node inheriting nub's --require preload + a live NODE_COMPILE_CACHE → it would
404
+ // cache nub's preload chain into the user's dir). The wrap MUST preserve each
405
+ // function's own symbols (esp. [util.promisify.custom]) — dropping them broke
406
+ // util.promisify(child_process.*) + abort/sync-io behavior. See wrapSpawnLike.
407
+ try { armChildProcessCompileCacheWrap(); } catch {}
408
+ }
409
+
410
+ // Arm the child_process compile-cache wrap WITHOUT eagerly requiring child_process.
411
+ //
412
+ // Eagerly `require("node:child_process")` at preload time pulls ~40 builtins into
413
+ // process.moduleLoadList on EVERY startup — net, dgram, the entire streams tree,
414
+ // spawn_sync/tty_wrap/pipe_wrap/tcp_wrap, os, vm, etc. (test-bootstrap-modules
415
+ // observes the exact list; child_process is the dominant extra-builtin source).
416
+ // A program that never spawns a child shouldn't pay that cost — and Node's own
417
+ // startup never loads child_process.
418
+ //
419
+ // So we intercept `Module._load` and apply the wrap to the child_process module the
420
+ // FIRST time USER code requires it (`require('child_process')` /
421
+ // `require('node:child_process')`), patching the returned singleton before handing
422
+ // it back. After patching once we restore the original `_load`, so steady-state
423
+ // require() has zero added overhead. If the user never requires child_process, the
424
+ // module is never loaded and the builtins stay out of the load list — matching Node.
425
+ let __cpWrapArmed = false;
426
+ function armChildProcessCompileCacheWrap() {
427
+ if (__cpWrapArmed || __cpWrapped) return;
428
+ __cpWrapArmed = true;
429
+ if (typeof module_._load !== "function") return;
430
+ const origLoad = module_._load;
431
+ module_._load = function (request, parent, isMain) {
432
+ const exports = origLoad.call(this, request, parent, isMain);
433
+ if (request === "child_process" || request === "node:child_process") {
434
+ module_._load = origLoad; // restore: one-shot, no steady-state overhead
435
+ try { wrapChildProcessCompileCache(exports); } catch {}
436
+ }
437
+ return exports;
438
+ };
439
+ }
440
+
441
+ // Monkey-patch child_process so node-targeted children the USER spawns with an
442
+ // explicit live NODE_COMPILE_CACHE get the SAME R8 treatment spawn.rs gives nub's
443
+ // own children: strip NODE_COMPILE_CACHE from the child env (so Node's bootstrap
444
+ // caches nothing of nub's inherited preload chain) and stash the original dir in a
445
+ // PID-keyed sentinel file the grandchild's restoreCompileCacheEnv() reads back via
446
+ // `process.ppid` to re-enable caching for the USER's own modules post-bootstrap.
447
+ // Brand rule: the dir travels via a sentinel file, never a NUB_* env var.
448
+ // `cp` is the already-loaded child_process exports object, passed in by the lazy
449
+ // `_load` interceptor so we never require it ourselves (which would defeat the
450
+ // deferral).
451
+ let __cpWrapped = false;
452
+ function wrapChildProcessCompileCache(cp) {
453
+ if (__cpWrapped || !cp) return;
454
+ __cpWrapped = true;
455
+ const { writeFileSync } = require("node:fs");
456
+ const { basename } = require("node:path");
457
+
458
+ const isNodeTarget = (command) => {
459
+ if (typeof command !== "string" || command.length === 0) return false;
460
+ if (command === process.execPath) return true;
461
+ const base = basename(command).toLowerCase();
462
+ return base === "node" || base === "node.exe";
463
+ };
464
+
465
+ // Returns a possibly-rewritten options object with NODE_COMPILE_CACHE stripped
466
+ // from its env, after writing the sentinel keyed on THIS process's pid (= the
467
+ // grandchild's process.ppid). No-op unless the child env explicitly carries a
468
+ // live (non-empty, != "0") NODE_COMPILE_CACHE — an inherited (undefined env)
469
+ // value was already stripped from this process by nub, so nothing to do there.
470
+ const stripFromOptions = (options) => {
471
+ if (!options || typeof options !== "object") return options;
472
+ const env = options.env;
473
+ if (!env || typeof env !== "object") return options;
474
+ const dir = env.NODE_COMPILE_CACHE;
475
+ if (!dir || dir === "0") return options;
476
+ try {
477
+ writeFileSync(join(tmpdirNoOs(), `nub-ccache-${process.pid}`), String(dir));
478
+ } catch { return options; }
479
+ const newEnv = { ...env };
480
+ delete newEnv.NODE_COMPILE_CACHE;
481
+ return { ...options, env: newEnv };
482
+ };
483
+
484
+ // For (command, args?, options?) signatures the options object is the last arg
485
+ // that is a non-array object; args is an optional array in between. Rewrites the
486
+ // call in place and dispatches to the original.
487
+ // Copy `orig`'s OWN symbols onto `wrapped` — crucially [util.promisify.custom],
488
+ // which Node sets on execFile/exec so `util.promisify(execFile)` returns a
489
+ // {stdout,stderr} promise. A bare wrapper without it silently changes promisify's
490
+ // result shape (broke test-child-process-promisified / -abortController /
491
+ // util-promisify-custom-names / sync-io-option / test-output-abort).
492
+ const preserveSymbols = (wrapped, orig) => {
493
+ for (const s of Object.getOwnPropertySymbols(orig)) {
494
+ try { wrapped[s] = orig[s]; } catch { /* read-only symbol: skip */ }
495
+ }
496
+ return wrapped;
497
+ };
498
+ const wrapSpawnLike = (orig) => preserveSymbols(function (command, ...rest) {
499
+ if (isNodeTarget(command)) {
500
+ let optIdx = -1;
501
+ for (let i = rest.length - 1; i >= 0; i--) {
502
+ const a = rest[i];
503
+ if (a && typeof a === "object" && !Array.isArray(a)) { optIdx = i; break; }
504
+ if (typeof a === "function") continue; // execFile callback
505
+ if (Array.isArray(a)) break; // args array — no options object present
506
+ }
507
+ if (optIdx >= 0) rest[optIdx] = stripFromOptions(rest[optIdx]);
508
+ }
509
+ return orig.call(this, command, ...rest);
510
+ }, orig);
511
+
512
+ cp.spawn = wrapSpawnLike(cp.spawn);
513
+ cp.spawnSync = wrapSpawnLike(cp.spawnSync);
514
+ cp.execFile = wrapSpawnLike(cp.execFile);
515
+ cp.execFileSync = wrapSpawnLike(cp.execFileSync);
516
+
517
+ // fork() always runs `process.execPath`, so it is always a node target. Its
518
+ // signature is (modulePath, args?, options?); reuse the same options rewrite.
519
+ const origFork = cp.fork;
520
+ cp.fork = function (modulePath, ...rest) {
521
+ let optIdx = -1;
522
+ for (let i = rest.length - 1; i >= 0; i--) {
523
+ const a = rest[i];
524
+ if (a && typeof a === "object" && !Array.isArray(a)) { optIdx = i; break; }
525
+ if (Array.isArray(a)) break;
526
+ }
527
+ if (optIdx >= 0) rest[optIdx] = stripFromOptions(rest[optIdx]);
528
+ return origFork.call(this, modulePath, ...rest);
529
+ };
530
+ }
531
+
532
+ function reenableUserCompileCache() {
533
+ const dir = process.env.NODE_COMPILE_CACHE;
534
+ // "0" is nub's disable signal (see transform-core); anything else is the user's
535
+ // real cache dir, which we re-point Node's compile cache at for THEIR modules.
536
+ if (!dir || dir === "0") return;
537
+ try { module_.enableCompileCache(dir); } catch {}
538
+ }
539
+
540
+ module.exports = {
541
+ installWatchReporting,
542
+ makeHooks,
543
+ installCjsRequireHooks,
544
+ preloadPolyfillPackages,
545
+ installTemporalLazyGlobal,
546
+ restoreCompileCacheEnv,
547
+ reenableUserCompileCache,
548
+ };
@@ -0,0 +1,273 @@
1
+ // Nub fast-tier preload — Node 22.15+, injected via `--require` (CommonJS).
2
+ //
3
+ // WHY CJS / `--require` (not the `.mjs` `--import` the compat tier uses): the mere
4
+ // presence of `--import` forces Node to eagerly initialize the ESM loader, which
5
+ // then routes EVEN A CJS ENTRY POINT through the async ESM module-job
6
+ // (`ModuleJob.run`) instead of the synchronous `Module.runMain` CJS path. That one
7
+ // change is the root cause of a whole regression cluster (R1): top-level
8
+ // `executionAsyncId()===0` (Node: 1), extra PROMISE async-hook events, a top-level
9
+ // sync `throw` surfacing as `unhandledRejection` instead of `uncaughtException`,
10
+ // `require.main.id` `'.'`→abspath, `module.parent` `null`→`undefined`, and a
11
+ // missing-entry `ERR_MODULE_NOT_FOUND` instead of `MODULE_NOT_FOUND`. Loading this
12
+ // preload via `--require` (CJS) keeps Node on the synchronous CJS entry path and
13
+ // restores all of them, while STILL supporting `module.registerHooks` + TS
14
+ // transpile (both work from a `--require` CJS module on Node 22.15+).
15
+ //
16
+ // HARD CONSTRAINT: this file and everything it pulls in synchronously must be
17
+ // TLA-free. `require(esm)` (which loads transform-core.mjs / the polyfill ESM
18
+ // modules) rejects any module with top-level await (ERR_REQUIRE_ASYNC_MODULE), so
19
+ // transform-core.mjs, polyfills.cjs, worker-polyfill.mjs and navigator-locks.mjs
20
+ // are all TLA-free by construction. The compat tier (< 22.15), where require(esm)
21
+ // is unreliable, keeps its async `--import` preload.mjs path UNCHANGED.
22
+ //
23
+ // ROBUSTNESS TO `--no-experimental-require-module` (the require-module cluster):
24
+ // a user may set `--no-experimental-require-module` (e.g. to assert the legacy
25
+ // require(esm)→ERR_REQUIRE_ESM contract for THEIR code). That flag globally
26
+ // disables require(esm) — including for THIS `--require` CJS preload's own
27
+ // `require("./transform-core.mjs")`, which would otherwise crash the process at
28
+ // startup (ERR_REQUIRE_ESM) before any user code runs. nub's preload must survive
29
+ // that. The fix: detect when sync require(esm) is unavailable and fall back to the
30
+ // compat tier's async loader-worker hooks (`module.register("./preload-async-
31
+ // hooks.mjs")`), which loads transform-core.mjs as a STATIC ESM import inside the
32
+ // worker — a path the flag does not gate. User code still gets Node's own
33
+ // ERR_REQUIRE_ESM for ITS require(esm), exactly as the flag promises; only nub's
34
+ // preload is made robust. (See the require-module corpus cluster:
35
+ // test-cjs-esm-warn, test-disable-require-module-with-detection,
36
+ // test-esm-type-field-errors-2, parallel/test-require-mjs.)
37
+
38
+ const { createRequire } = require("node:module");
39
+ const module_ = require("node:module");
40
+
41
+ const __require = createRequire(__filename);
42
+
43
+ // Load preload-common FIRST so we can restore NODE_COMPILE_CACHE (R8) BEFORE
44
+ // transform-core.mjs is required: spawn.rs stripped that env var to keep nub's
45
+ // preload chain out of the user's V8 compile cache, and transform-core reads
46
+ // `NODE_COMPILE_CACHE === "0"` as its transpile-cache disable signal — so the
47
+ // value must be back in process.env before transform-core's module body runs.
48
+ // Restoring it in JS does NOT re-enable Node's bootstrap compile cache (already
49
+ // configured from the stripped env), so the chain below stays uncached.
50
+ const common = __require("./preload-common.cjs");
51
+ common.restoreCompileCacheEnv();
52
+
53
+ // `--no-experimental-require-module` disables require(esm) globally, so the
54
+ // transform-core require below (and the worker/locks ESM side-effect modules)
55
+ // would throw ERR_REQUIRE_ESM and abort the process before user code. Detect that
56
+ // and load via the async-register fallback instead. We probe by attempting the
57
+ // require and catching ERR_REQUIRE_ESM — robust regardless of how the flag arrived
58
+ // (CLI, NODE_OPTIONS, or a config file), and a no-op cost on the common path where
59
+ // require(esm) works.
60
+ let core = null;
61
+ let requireEsmDisabled = false;
62
+ try {
63
+ // The transform core is the single source of truth for resolution + transpile,
64
+ // shared verbatim with the compat tier. It's an ES module with no top-level
65
+ // await, so `require(esm)` loads it synchronously here on Node 22.15+.
66
+ core = __require("./transform-core.mjs");
67
+ } catch (err) {
68
+ if (err && err.code === "ERR_REQUIRE_ESM") {
69
+ requireEsmDisabled = true;
70
+ } else {
71
+ throw err;
72
+ }
73
+ }
74
+
75
+ const { installSyncPolyfills } = __require("./polyfills.cjs");
76
+
77
+ if (!requireEsmDisabled) {
78
+ // ── Fast tier (sync require(esm) available) ───────────────────────
79
+
80
+ // ── Watch-mode dependency reporting + hooks ───────────────────────
81
+ const watchReporting = common.installWatchReporting(core);
82
+
83
+ // Best-effort bounded-cache eviction (main thread only; the core guards on it).
84
+ // DEFERRED to setImmediate: maybeSweepCache probes `worker_threads.isMainThread`
85
+ // and dynamic-imports cache-evict.mjs, which would otherwise pull worker_threads
86
+ // (and its streams/worker-io transitive set) into the BOOTSTRAP module-load list
87
+ // on every startup — a cold-start regression (test-bootstrap-modules snapshots
88
+ // process.moduleLoadList at user code's first line). Running it one turn later
89
+ // keeps those out of the bootstrap snapshot while preserving the once-a-day sweep.
90
+ // unref so a purely-synchronous program still exits promptly without waiting on it.
91
+ setImmediate(() => {
92
+ try { core.maybeSweepCache(); } catch {}
93
+ }).unref();
94
+
95
+ // ── Pre-load clobbered polyfill packages BEFORE hooks register ────
96
+ // Packages in the core's CLOBBER_MAP can't be imported after hooks register (the
97
+ // resolve hook returns a synthetic module instead of the real package), so
98
+ // require them now via the not-yet-hooked CJS require and stash them for the
99
+ // polyfill installer. Temporal is deferred entirely to a lazy global (below).
100
+ const __preloadedPolyfills = common.preloadPolyfillPackages(__require);
101
+
102
+ // ── Hook registration (fast tier: sync, in-thread) ────────────────
103
+ // Same realm as user code; covers `import` and (Node 24+) `require`.
104
+ // registerHooks' require RESOLUTION is incomplete on 22.15–24, so also install
105
+ // the main-thread CJS resolve shim. Install the classic require.extensions
106
+ // transpile shim only on 22.15–22.17 (no native `.ts`); on 22.18+/24+ skip it so
107
+ // Node's native require() of `.ts` — incl. ES modules — isn't shadowed (see
108
+ // installCjsRequireHooks).
109
+ const __hasNativeTs = !!process.features?.typescript;
110
+ const { resolve, load } = common.makeHooks(core, watchReporting);
111
+ module_.registerHooks({ resolve, load });
112
+ common.installCjsRequireHooks(core, !__hasNativeTs);
113
+
114
+ // ── Sync polyfills + lazy ESM-side-effect polyfills ───────────────
115
+ installSyncPolyfills(__preloadedPolyfills);
116
+ installLazyEsmPolyfills();
117
+
118
+ // ── Temporal: lazy global (A37) ───────────────────────────────────
119
+ common.installTemporalLazyGlobal(__require);
120
+
121
+ // ── Compile-cache: re-enable for the USER's modules (R8) ──────────
122
+ common.reenableUserCompileCache();
123
+ } else {
124
+ // ── Fallback tier (`--no-experimental-require-module`): async hooks ─
125
+ // The user disabled require(esm), so the in-thread sync `module.registerHooks`
126
+ // core can't be loaded here. Register the SAME hooks the compat tier uses, run
127
+ // in a dedicated loader worker via `module.register`; that worker imports
128
+ // transform-core.mjs as a static ESM import (not gated by the flag). The
129
+ // main-thread CJS require() transpile shim, which would need the core
130
+ // synchronously in-thread, is unavailable in this mode — an honest, additive
131
+ // degradation: the user opted out of require(esm), and nub's `.ts`-via-require()
132
+ // transpile rides on exactly that mechanism. `import`-side TS still transpiles
133
+ // through the registered loader-worker hooks. User require(esm) of THEIR own ES
134
+ // modules still gets Node's native ERR_REQUIRE_ESM, exactly as the flag promises.
135
+ const { pathToFileURL } = require("node:url");
136
+ module_.register("./preload-async-hooks.mjs", pathToFileURL(__filename).href);
137
+
138
+ // Sync, non-require(esm) polyfills still install (none of them require(esm)).
139
+ // Clobbered-polyfill packages are CJS requires, unaffected by the flag.
140
+ const __preloadedPolyfills = common.preloadPolyfillPackages(__require);
141
+ installSyncPolyfills(__preloadedPolyfills);
142
+ installLazyEsmPolyfills();
143
+
144
+ // Temporal lazy global needs only `__require` (it loads a CJS package), and the
145
+ // user's compile-cache re-enable is independent of require(esm).
146
+ common.installTemporalLazyGlobal(__require);
147
+ common.reenableUserCompileCache();
148
+ }
149
+
150
+ // ── Lazy ESM-side-effect polyfills (R7) ─────────────────────────────
151
+ // The two ESM side-effect polyfills — the browser-shape Worker global
152
+ // (worker-polyfill.mjs) and Web Locks (navigator-locks.mjs) — were previously
153
+ // installed EAGERLY at preload (polyfills.cjs:installEsmPolyfillsSync). That drags
154
+ // ~50 builtins into bootstrap on EVERY startup: worker-polyfill.mjs imports
155
+ // node:worker_threads, which pulls internal/streams/* (readable/writable/duplex/
156
+ // transform/pipeline/…), internal/worker, internal/worker/io,
157
+ // internal/worker/messaging, vm, net, child_process, os, etc.; navigator-locks.mjs
158
+ // pulls internal/locks + internal/navigator. None of that is needed by the common
159
+ // "run a plain file, never touch Worker or navigator.locks" case, and the eager
160
+ // load is a cold-start regression that contradicts the fast-runner premise
161
+ // (test-bootstrap-modules: moduleLoadList must match Node's bootstrap set).
162
+ //
163
+ // Replace the eager install with lazy globals:
164
+ // • `globalThis.Worker` — a non-enumerable getter that, on first access (the
165
+ // first `new Worker(...)`), deletes itself, requires worker-polyfill.mjs (which
166
+ // then defines the real `globalThis.Worker`), and returns it.
167
+ // • `navigator.locks` — a non-enumerable getter that loads navigator-locks.mjs on
168
+ // first access (only when not native — Node 24.5+ ships it).
169
+ // In a WORKER thread, the worker-side bootstrap inside worker-polyfill.mjs (self/
170
+ // postMessage/message wiring) MUST run at startup, so we load it eagerly there.
171
+ // That costs nothing for bootstrap accounting: a worker already loaded
172
+ // worker_threads to exist, and test-bootstrap-modules measures the main thread.
173
+ function installLazyEsmPolyfills() {
174
+ // Cheap main-thread detection that does NOT pull node:worker_threads into the
175
+ // main-thread bootstrap (requiring it eagerly is exactly the regression we're
176
+ // fixing): in a worker, worker_threads is already in the module-load list by the
177
+ // time this preload runs; on the main thread it is not.
178
+ const inWorkerThread = process.moduleLoadList.some(
179
+ (m) => m === "NativeModule worker_threads",
180
+ );
181
+
182
+ const loadEsmSideEffect = (specifier) => {
183
+ try {
184
+ __require(specifier);
185
+ } catch (err) {
186
+ if (err && err.code === "ERR_REQUIRE_ESM") {
187
+ // require(esm) disabled — load via dynamic import (not flag-gated). Async,
188
+ // but side-effect-only; the Worker/locks polyfills are needed lazily, and
189
+ // for a worker thread the worker-side wiring lands a tick later, which is
190
+ // still before any user message round-trip can complete.
191
+ import(specifier).catch(() => {});
192
+ } else {
193
+ throw err;
194
+ }
195
+ }
196
+ };
197
+
198
+ if (inWorkerThread) {
199
+ // Worker-side scope bootstrap must be present synchronously where possible.
200
+ loadEsmSideEffect("./worker-polyfill.mjs");
201
+ if (typeof globalThis.navigator?.locks === "undefined") {
202
+ loadEsmSideEffect("./navigator-locks.mjs");
203
+ }
204
+ return;
205
+ }
206
+
207
+ // Main thread: lazy Worker global. Defined NON-ENUMERABLE so it stays invisible
208
+ // to `Object.keys(globalThis)` / for-in — the additive contract — matching how
209
+ // worker-polyfill.mjs defines the real one.
210
+ if (typeof globalThis.Worker === "undefined") {
211
+ let installing = false;
212
+ Object.defineProperty(globalThis, "Worker", {
213
+ configurable: true,
214
+ enumerable: false,
215
+ get() {
216
+ if (installing) return undefined;
217
+ installing = true;
218
+ // Drop this lazy accessor so worker-polyfill.mjs's own
219
+ // `if (typeof globalThis.Worker === "undefined")` guard fires and defines
220
+ // the real Worker.
221
+ delete globalThis.Worker;
222
+ loadEsmSideEffect("./worker-polyfill.mjs");
223
+ return globalThis.Worker;
224
+ },
225
+ set(value) {
226
+ // A user assigning their own Worker wins — replace the lazy accessor.
227
+ Object.defineProperty(globalThis, "Worker", {
228
+ value,
229
+ configurable: true,
230
+ enumerable: false,
231
+ writable: true,
232
+ });
233
+ },
234
+ });
235
+ }
236
+
237
+ // Main thread: lazy navigator.locks (native on Node 24.5+, absent on the 22.x
238
+ // floor). VERSION-GATE so we never even READ `globalThis.navigator` where locks
239
+ // is native: on Node 24.5+ the native `navigator` global is a lazy getter that,
240
+ // on first access, eagerly realizes internal/navigator + internal/locks AND the
241
+ // whole stream/worker-io transitive set (~30 builtins) — touching it at preload
242
+ // would be exactly the cold-start regression test-bootstrap-modules guards
243
+ // against, for zero benefit (locks is already there). Below 24.5 navigator is
244
+ // present but lacks `locks`, and accessing it is cheap (one internal module), so
245
+ // installing the lazy polyfill there is fine.
246
+ const [navMaj, navMin] = process.versions.node.split(".").map((n) => parseInt(n, 10));
247
+ const locksNative = navMaj > 24 || (navMaj === 24 && navMin >= 5);
248
+ if (locksNative) return;
249
+
250
+ const nav = globalThis.navigator;
251
+ if (nav && typeof nav.locks === "undefined") {
252
+ let installing = false;
253
+ Object.defineProperty(nav, "locks", {
254
+ configurable: true,
255
+ enumerable: true,
256
+ get() {
257
+ if (installing) return undefined;
258
+ installing = true;
259
+ delete nav.locks;
260
+ loadEsmSideEffect("./navigator-locks.mjs");
261
+ return nav.locks;
262
+ },
263
+ set(value) {
264
+ Object.defineProperty(nav, "locks", {
265
+ value,
266
+ configurable: true,
267
+ enumerable: true,
268
+ writable: true,
269
+ });
270
+ },
271
+ });
272
+ }
273
+ }
@@ -9,4 +9,4 @@
9
9
  // previously lived as a literal inside preload.mjs, which `make version` patched,
10
10
  // while the worker carried a hand-maintained "…-compat" copy that `make version`
11
11
  // never touched — a latent staleness bug this module closes.)
12
- export const NUB_VERSION = "0.0.13";
12
+ export const NUB_VERSION = "0.0.14";