@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 +0 -0
- package/package.json +1 -1
- package/runtime/addons/nub-native.node +0 -0
- package/runtime/polyfills.cjs +178 -0
- package/runtime/preload-common.cjs +548 -0
- package/runtime/preload.cjs +273 -0
- package/runtime/version.mjs +1 -1
package/bin/nub.exe
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
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
|
+
}
|
package/runtime/version.mjs
CHANGED
|
@@ -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.
|
|
12
|
+
export const NUB_VERSION = "0.0.14";
|