@nubjs/nub-darwin-x64 0.2.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/nub +0 -0
- package/bin/nubx +0 -0
- package/package.json +1 -1
- package/runtime/addons/nub-native.node +0 -0
- package/runtime/navigator-locks.mjs +188 -81
- package/runtime/navigator-shim.mjs +144 -0
- package/runtime/polyfills.cjs +68 -0
- package/runtime/preload-async-hooks.mjs +16 -3
- package/runtime/preload-common.cjs +49 -0
- package/runtime/preload.cjs +15 -0
- package/runtime/preload.mjs +8 -0
- package/runtime/transform-core.mjs +86 -14
- package/runtime/version.mjs +1 -1
package/bin/nub
CHANGED
|
Binary file
|
package/bin/nubx
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
Binary file
|
|
@@ -1,10 +1,21 @@
|
|
|
1
|
-
// Web Locks API polyfill for Node
|
|
2
|
-
// Single-process only — locks don't coordinate across
|
|
1
|
+
// Web Locks API polyfill for Node < 24.5 (native on Node 24.5+, #58666).
|
|
2
|
+
// Single-process only — locks don't coordinate across worker threads (a deliberate
|
|
3
|
+
// scope: the common in-process serialization use is covered; cross-thread coordination
|
|
4
|
+
// would need a SharedArrayBuffer waitlist and is out of scope until there's demand).
|
|
5
|
+
//
|
|
6
|
+
// Spec: https://w3c.github.io/web-locks/. Modeled to match Node's own
|
|
7
|
+
// internal/locks.js (the native impl on 24.5+) so behavior is identical across the
|
|
8
|
+
// version boundary — including `steal`, AbortSignal integration, and the option/name
|
|
9
|
+
// validation that the WPT web-locks suite exercises. Requires a `navigator` object to
|
|
10
|
+
// host `navigator.locks`; navigator-shim.mjs backfills that on Node < 21 and MUST run
|
|
11
|
+
// first.
|
|
3
12
|
|
|
4
13
|
if (typeof globalThis.navigator === "object" && typeof globalThis.navigator.locks === "undefined") {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const
|
|
14
|
+
const AbortSig = globalThis.AbortSignal;
|
|
15
|
+
|
|
16
|
+
const abortError = (msg) => new DOMException(msg || "The operation was aborted", "AbortError");
|
|
17
|
+
const stolenError = () => abortError("The lock was stolen by another request");
|
|
18
|
+
const notSupported = (msg) => new DOMException(msg, "NotSupportedError");
|
|
8
19
|
|
|
9
20
|
class Lock {
|
|
10
21
|
#name;
|
|
@@ -17,113 +28,209 @@ if (typeof globalThis.navigator === "object" && typeof globalThis.navigator.lock
|
|
|
17
28
|
get mode() { return this.#mode; }
|
|
18
29
|
}
|
|
19
30
|
|
|
31
|
+
// held: name → { mode, holders:Set<Holder> }. A shared grant has N holders sharing
|
|
32
|
+
// one record; an exclusive grant has exactly one. queue: name → Array<Waiter> (FIFO).
|
|
33
|
+
const held = new Map();
|
|
34
|
+
const queue = new Map();
|
|
35
|
+
|
|
20
36
|
function canAcquire(name, mode) {
|
|
21
|
-
const
|
|
22
|
-
if (!
|
|
23
|
-
|
|
24
|
-
return false;
|
|
37
|
+
const rec = held.get(name);
|
|
38
|
+
if (!rec) return true;
|
|
39
|
+
return mode === "shared" && rec.mode === "shared";
|
|
25
40
|
}
|
|
26
41
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
42
|
+
// A FRESH request may be granted immediately only when no waiters are already
|
|
43
|
+
// queued for the name — otherwise it must queue behind them. This is the
|
|
44
|
+
// reader/writer FAIRNESS rule: a new shared request must NOT barge ahead of a
|
|
45
|
+
// pending exclusive request and join the current shared holders, which would
|
|
46
|
+
// starve the writer (WPT mode-mixed "An exclusive lock between shared locks").
|
|
47
|
+
// drainQueue, which only ever grants from the FRONT of the queue, keeps using
|
|
48
|
+
// canAcquire directly.
|
|
49
|
+
function canGrantNow(name, mode) {
|
|
50
|
+
const q = queue.get(name);
|
|
51
|
+
if (q && q.length > 0) return false;
|
|
52
|
+
return canAcquire(name, mode);
|
|
34
53
|
}
|
|
35
54
|
|
|
36
|
-
function
|
|
37
|
-
const
|
|
38
|
-
if (
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
55
|
+
function addHolder(name, mode, holder) {
|
|
56
|
+
const rec = held.get(name);
|
|
57
|
+
if (rec) rec.holders.add(holder);
|
|
58
|
+
else held.set(name, { mode, holders: new Set([holder]) });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function removeHolder(name, holder) {
|
|
62
|
+
const rec = held.get(name);
|
|
63
|
+
if (!rec) return;
|
|
64
|
+
rec.holders.delete(holder);
|
|
65
|
+
if (rec.holders.size === 0) held.delete(name);
|
|
44
66
|
}
|
|
45
67
|
|
|
68
|
+
function enqueue(name, waiter) {
|
|
69
|
+
let q = queue.get(name);
|
|
70
|
+
if (!q) queue.set(name, (q = []));
|
|
71
|
+
q.push(waiter);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Grant as many head-of-queue waiters as the current held state allows: a head
|
|
75
|
+
// exclusive takes the lock alone; a run of head shared waiters is all granted.
|
|
46
76
|
function drainQueue(name) {
|
|
47
77
|
const q = queue.get(name);
|
|
48
78
|
if (!q || q.length === 0) return;
|
|
79
|
+
while (q.length > 0) {
|
|
80
|
+
const first = q[0];
|
|
81
|
+
if (!canAcquire(name, first.mode)) break;
|
|
82
|
+
q.shift();
|
|
83
|
+
first.grant(); // synchronously addHolders, so canAcquire reflects it next iter
|
|
84
|
+
if (first.mode === "exclusive") break;
|
|
85
|
+
}
|
|
86
|
+
if (q.length === 0) queue.delete(name);
|
|
87
|
+
}
|
|
49
88
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
89
|
+
// Run `cb(lock)` as a holder of (name, mode); return the promise that settles when
|
|
90
|
+
// the holder releases (cb's result/throw) — or rejects early if a steal BREAKS it.
|
|
91
|
+
// The callback is invoked one microtask later (matching Node), so a synchronously
|
|
92
|
+
// signaled abort is observed before the callback runs.
|
|
93
|
+
function runHolder(name, mode, cb) {
|
|
94
|
+
const lock = new Lock(name, mode);
|
|
95
|
+
let broken = false;
|
|
96
|
+
let rejectReleased;
|
|
97
|
+
const holder = {
|
|
98
|
+
mode,
|
|
99
|
+
break(reason) {
|
|
100
|
+
if (broken) return;
|
|
101
|
+
broken = true;
|
|
102
|
+
// Remove WITHOUT draining — the stealer takes the lock next; queued waiters
|
|
103
|
+
// stay pending until the steal's own holder releases.
|
|
104
|
+
removeHolder(name, holder);
|
|
105
|
+
rejectReleased(reason);
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
addHolder(name, mode, holder);
|
|
109
|
+
return new Promise((resolve, reject) => {
|
|
110
|
+
rejectReleased = reject;
|
|
111
|
+
Promise.resolve()
|
|
112
|
+
.then(() => cb(lock))
|
|
113
|
+
.then(
|
|
114
|
+
(value) => { if (!broken) { removeHolder(name, holder); resolve(value); drainQueue(name); } },
|
|
115
|
+
(err) => { if (!broken) { removeHolder(name, holder); reject(err); drainQueue(name); } },
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// The lock engine: returns the `released` promise. `cb` receives the granted Lock
|
|
121
|
+
// (or null on an ifAvailable miss). Mirrors the grant/steal/ifAvailable/queue shape
|
|
122
|
+
// of Node's internalBinding('locks').request.
|
|
123
|
+
function engineRequest(name, mode, steal, ifAvailable, cb) {
|
|
124
|
+
if (steal) {
|
|
125
|
+
const rec = held.get(name);
|
|
126
|
+
if (rec) for (const h of [...rec.holders]) h.break(stolenError());
|
|
127
|
+
return runHolder(name, "exclusive", cb);
|
|
128
|
+
}
|
|
129
|
+
if (canGrantNow(name, mode)) {
|
|
130
|
+
return runHolder(name, mode, cb);
|
|
65
131
|
}
|
|
132
|
+
if (ifAvailable) {
|
|
133
|
+
return Promise.resolve().then(() => cb(null));
|
|
134
|
+
}
|
|
135
|
+
return new Promise((resolve, reject) => {
|
|
136
|
+
const waiter = {
|
|
137
|
+
mode,
|
|
138
|
+
settled: false,
|
|
139
|
+
grant() {
|
|
140
|
+
if (waiter.settled) return;
|
|
141
|
+
waiter.settled = true;
|
|
142
|
+
runHolder(name, mode, cb).then(resolve, reject);
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
enqueue(name, waiter);
|
|
146
|
+
});
|
|
66
147
|
}
|
|
67
148
|
|
|
68
149
|
class LockManager {
|
|
69
|
-
async request(name, optionsOrCallback,
|
|
70
|
-
let options =
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
options =
|
|
150
|
+
async request(name, optionsOrCallback, maybeCallback) {
|
|
151
|
+
let options = optionsOrCallback;
|
|
152
|
+
let callback = maybeCallback;
|
|
153
|
+
if (callback === undefined) {
|
|
154
|
+
callback = options;
|
|
155
|
+
options = undefined;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// WebIDL DOMString coercion (throws TypeError on a Symbol, like the binding).
|
|
159
|
+
name = `${name}`;
|
|
160
|
+
if (typeof callback !== "function") {
|
|
161
|
+
throw new TypeError("Failed to execute 'request' on 'LockManager': parameter 2 is not a function.");
|
|
75
162
|
}
|
|
163
|
+
if (options === undefined || typeof options === "function") options = {};
|
|
76
164
|
|
|
77
|
-
const mode = options.mode
|
|
78
|
-
|
|
165
|
+
const mode = options.mode === undefined ? "exclusive" : options.mode;
|
|
166
|
+
if (mode !== "exclusive" && mode !== "shared") {
|
|
167
|
+
throw new TypeError(`Failed to execute 'request' on 'LockManager': '${mode}' is not a valid value for enumeration LockMode.`);
|
|
168
|
+
}
|
|
169
|
+
const ifAvailable = !!options.ifAvailable;
|
|
170
|
+
const steal = !!options.steal;
|
|
79
171
|
const signal = options.signal;
|
|
172
|
+
if (signal !== undefined && signal !== null && !(AbortSig && signal instanceof AbortSig)) {
|
|
173
|
+
throw new TypeError("Failed to execute 'request' on 'LockManager': member signal is not of type AbortSignal.");
|
|
174
|
+
}
|
|
80
175
|
|
|
81
|
-
|
|
82
|
-
|
|
176
|
+
// Already-aborted signal rejects with its reason BEFORE the option-combo checks
|
|
177
|
+
// (matching Node's signal.throwIfAborted() ordering).
|
|
178
|
+
if (signal && signal.aborted) {
|
|
179
|
+
throw signal.reason || abortError();
|
|
180
|
+
}
|
|
181
|
+
if (name[0] === "-") {
|
|
182
|
+
throw notSupported("Lock name may not start with hyphen '-'");
|
|
183
|
+
}
|
|
184
|
+
if (ifAvailable && steal) {
|
|
185
|
+
throw notSupported("ifAvailable and steal options cannot be used together");
|
|
186
|
+
}
|
|
187
|
+
if (mode !== "exclusive" && steal) {
|
|
188
|
+
throw notSupported("mode must be 'exclusive' when using the steal option");
|
|
189
|
+
}
|
|
190
|
+
if (signal && (steal || ifAvailable)) {
|
|
191
|
+
throw notSupported("signal cannot be used with the steal or ifAvailable options");
|
|
83
192
|
}
|
|
84
193
|
|
|
85
|
-
if
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
signal.
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
194
|
+
// Signal path: the callback is deferred and skipped if the signal aborts before
|
|
195
|
+
// it runs; the abort rejects the OUTER promise iff the callback hasn't entered
|
|
196
|
+
// (lockGranted false). Verbatim shape of Node's internal/locks.js so a queued
|
|
197
|
+
// abort releases the slot (callback skipped) and the next waiter is granted.
|
|
198
|
+
if (signal) {
|
|
199
|
+
return new Promise((resolve, reject) => {
|
|
200
|
+
let lockGranted = false;
|
|
201
|
+
const onAbort = () => {
|
|
202
|
+
if (!lockGranted) reject(signal.reason || abortError());
|
|
203
|
+
};
|
|
204
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
205
|
+
const wrapped = (lock) =>
|
|
206
|
+
Promise.resolve().then(() => {
|
|
207
|
+
if (signal.aborted) return undefined;
|
|
208
|
+
lockGranted = true;
|
|
209
|
+
return callback(lock);
|
|
210
|
+
});
|
|
211
|
+
const released = engineRequest(name, mode, false, false, wrapped);
|
|
212
|
+
released.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
|
|
100
213
|
});
|
|
101
214
|
}
|
|
102
215
|
|
|
103
|
-
|
|
104
|
-
const lock = new Lock(name, mode);
|
|
105
|
-
|
|
106
|
-
try {
|
|
107
|
-
return await callback(lock);
|
|
108
|
-
} finally {
|
|
109
|
-
release(name);
|
|
110
|
-
}
|
|
216
|
+
return engineRequest(name, mode, steal, ifAvailable, (lock) => callback(lock));
|
|
111
217
|
}
|
|
112
218
|
|
|
113
219
|
async query() {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
220
|
+
// clientId is per-realm and opaque: `node-<pid>-0` (single-process; this polyfill
|
|
221
|
+
// does not coordinate across worker threads, so the threadId slot is always 0 —
|
|
222
|
+
// and we avoid importing node:worker_threads to keep this module builtin-free for a
|
|
223
|
+
// cheap bootstrap). The cross-context distinct-id WPT cases are browser-specific.
|
|
224
|
+
const clientId = `node-${process.pid}-0`;
|
|
225
|
+
const heldOut = [];
|
|
226
|
+
for (const [name, rec] of held) {
|
|
227
|
+
for (let i = 0; i < rec.holders.size; i++) heldOut.push({ name, mode: rec.mode, clientId });
|
|
119
228
|
}
|
|
120
229
|
const pending = [];
|
|
121
230
|
for (const [name, q] of queue) {
|
|
122
|
-
for (const
|
|
123
|
-
pending.push({ name, mode: req.mode, clientId: "" });
|
|
124
|
-
}
|
|
231
|
+
for (const w of q) if (!w.settled) pending.push({ name, mode: w.mode, clientId });
|
|
125
232
|
}
|
|
126
|
-
return { held:
|
|
233
|
+
return { held: heldOut, pending };
|
|
127
234
|
}
|
|
128
235
|
}
|
|
129
236
|
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// `navigator` global backfill for Node < 21 (where the global is wholly absent).
|
|
2
|
+
//
|
|
3
|
+
// Node ships `globalThis.navigator` from 21.0.0. Below that it is `undefined`, so
|
|
4
|
+
// any web-platform API hosted ON navigator — chiefly `navigator.locks` (Web Locks,
|
|
5
|
+
// see navigator-locks.mjs) — has no object to attach to and silently does nothing.
|
|
6
|
+
// nub's floor is 18.19, so to make those APIs reach the floor we synthesize the
|
|
7
|
+
// `navigator` object Node 21+ would provide. Companion to navigator-locks.mjs:
|
|
8
|
+
// this MUST run BEFORE it so locks has a host on 18.19–20.x.
|
|
9
|
+
//
|
|
10
|
+
// Shape mirrors Node's internal/navigator.js: a `Navigator` instance with the
|
|
11
|
+
// enumerable prototype getters `hardwareConcurrency`, `language`, `languages`,
|
|
12
|
+
// `userAgent`, `platform` (locks is added separately by navigator-locks.mjs). The
|
|
13
|
+
// userAgent is `Node.js/<major>` — NEVER `Nub/…`: the user is running Node, nub is
|
|
14
|
+
// the augmenter, and a `Nub/` UA would be a brand-boundary leak.
|
|
15
|
+
//
|
|
16
|
+
// VERSION-GATE, not a global read: installNavigatorShim() returns early on Node >= 21
|
|
17
|
+
// from `process.versions.node` WITHOUT ever touching `globalThis.navigator`. That
|
|
18
|
+
// matters because on Node 24.5+ the native `navigator` is a lazy getter whose first
|
|
19
|
+
// access realizes ~30 internal/stream/worker-io builtins — a cold-start regression
|
|
20
|
+
// (test-bootstrap-modules). The shim must never trigger that; the version check makes
|
|
21
|
+
// the fast tier (>= 22.15, navigator always present) a free no-op.
|
|
22
|
+
//
|
|
23
|
+
// node: builtins (node:os) are fetched via `process.getBuiltinModule` when present,
|
|
24
|
+
// else via a createRequire THREADED IN through `setBootstrapCreateRequire` — the same
|
|
25
|
+
// brand-safe, off-the-user-loader-chain pattern worker-polyfill.mjs uses. The narrow
|
|
26
|
+
// floor (18.19.x, 20.11–20.15) lacks `process.getBuiltinModule`, and the shim only
|
|
27
|
+
// touches os lazily (the `hardwareConcurrency` getter), so the threaded require is
|
|
28
|
+
// needed exactly there. `os` is never loaded on Node >= 21 (the early return).
|
|
29
|
+
|
|
30
|
+
let _bootstrapCreateRequire = null;
|
|
31
|
+
export function setBootstrapCreateRequire(fn) {
|
|
32
|
+
_bootstrapCreateRequire = fn;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function __getBuiltin(id) {
|
|
36
|
+
if (typeof process.getBuiltinModule === "function") return process.getBuiltinModule(id);
|
|
37
|
+
if (_bootstrapCreateRequire) return _bootstrapCreateRequire(import.meta.url)(id);
|
|
38
|
+
// Last-resort: a bare specifier require off this module's own createRequire is
|
|
39
|
+
// unavailable in ESM without node:module, which is exactly what the threading
|
|
40
|
+
// avoids importing statically. If neither path is wired, surface a clear error.
|
|
41
|
+
throw new Error("navigator-shim: no builtin accessor for " + id);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function deriveLanguage() {
|
|
45
|
+
// Approximate Node's ICU default locale from the POSIX locale env, falling back
|
|
46
|
+
// to "en-US" (Node's own fallback). `en_US.UTF-8` → `en-US`.
|
|
47
|
+
const raw =
|
|
48
|
+
process.env.LC_ALL || process.env.LC_MESSAGES || process.env.LANG || "";
|
|
49
|
+
const base = raw.split(".")[0].split("@")[0].replace("_", "-");
|
|
50
|
+
return base && base !== "C" && base !== "POSIX" ? base : "en-US";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function navigatorPlatform(platform, arch) {
|
|
54
|
+
// Mirror node/lib/internal/navigator.js getNavigatorPlatform.
|
|
55
|
+
if (platform === "darwin") return "MacIntel";
|
|
56
|
+
if (platform === "win32") return "Win32";
|
|
57
|
+
if (platform === "linux") {
|
|
58
|
+
if (arch === "ia32") return "Linux i686";
|
|
59
|
+
if (arch === "x64") return "Linux x86_64";
|
|
60
|
+
return `Linux ${arch}`;
|
|
61
|
+
}
|
|
62
|
+
if (platform === "freebsd") return arch === "ia32" ? "FreeBSD i386" : arch === "x64" ? "FreeBSD amd64" : `FreeBSD ${arch}`;
|
|
63
|
+
if (platform === "openbsd") return arch === "ia32" ? "OpenBSD i386" : arch === "x64" ? "OpenBSD amd64" : `OpenBSD ${arch}`;
|
|
64
|
+
if (platform === "sunos") return arch === "ia32" ? "SunOS i86pc" : `SunOS ${arch}`;
|
|
65
|
+
if (platform === "aix") return "AIX";
|
|
66
|
+
return `${platform[0].toUpperCase()}${platform.slice(1)} ${arch}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Build the Navigator class lazily (only when actually backfilling) so loading this
|
|
70
|
+
// module on the fast tier costs nothing beyond the early-return check.
|
|
71
|
+
function makeNavigator() {
|
|
72
|
+
let _hw, _lang, _langs, _ua, _plat;
|
|
73
|
+
class Navigator {
|
|
74
|
+
get hardwareConcurrency() {
|
|
75
|
+
// os.availableParallelism honors cgroup CPU limits (Node 18.14+/19.4+; present
|
|
76
|
+
// on the 18.19 floor); fall back to cpus().length on the off chance it's absent.
|
|
77
|
+
if (_hw === undefined) {
|
|
78
|
+
const os = __getBuiltin("node:os");
|
|
79
|
+
_hw = typeof os.availableParallelism === "function" ? os.availableParallelism() : os.cpus().length;
|
|
80
|
+
}
|
|
81
|
+
return _hw;
|
|
82
|
+
}
|
|
83
|
+
get language() {
|
|
84
|
+
return (_lang ??= deriveLanguage());
|
|
85
|
+
}
|
|
86
|
+
get languages() {
|
|
87
|
+
return (_langs ??= Object.freeze([this.language]));
|
|
88
|
+
}
|
|
89
|
+
get userAgent() {
|
|
90
|
+
// `Node.js/<major>` — match Node exactly; never a nub-branded UA.
|
|
91
|
+
return (_ua ??= `Node.js/${process.versions.node.split(".")[0]}`);
|
|
92
|
+
}
|
|
93
|
+
get platform() {
|
|
94
|
+
return (_plat ??= navigatorPlatform(process.platform, process.arch));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// Match Node's instance shape: the property getters live on the prototype and are
|
|
98
|
+
// ENUMERABLE (Node sets them with kEnumerableProperty), so `for (const k in
|
|
99
|
+
// navigator)` walks them while `Object.keys(navigator)` (own enumerable) is empty.
|
|
100
|
+
// Class-body getters default to enumerable:false, so flip them in place.
|
|
101
|
+
for (const k of ["hardwareConcurrency", "language", "languages", "userAgent", "platform"]) {
|
|
102
|
+
Object.defineProperty(Navigator.prototype, k, { enumerable: true });
|
|
103
|
+
}
|
|
104
|
+
return { Navigator, instance: new Navigator() };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Install the navigator backfill if and only if the running Node lacks the global
|
|
108
|
+
// (i.e. Node < 21). Idempotent and side-effect-free on Node >= 21.
|
|
109
|
+
export function installNavigatorShim() {
|
|
110
|
+
const major = parseInt(process.versions.node.split(".")[0], 10);
|
|
111
|
+
// Node 21+ ships navigator natively — never read the (possibly lazy) global here.
|
|
112
|
+
if (major >= 21) return;
|
|
113
|
+
// Defensive idempotency for the < 21 path (navigator is genuinely undefined there,
|
|
114
|
+
// so this read can't trigger a lazy realization).
|
|
115
|
+
if (typeof globalThis.navigator !== "undefined") return;
|
|
116
|
+
|
|
117
|
+
const { Navigator, instance } = makeNavigator();
|
|
118
|
+
|
|
119
|
+
// The binding on globalThis is NON-ENUMERABLE (invisible to Object.keys(globalThis)
|
|
120
|
+
// / for-in) — nub's additive-global contract, matching how reportError and Worker
|
|
121
|
+
// are installed. Configurable + writable so user code may still override it.
|
|
122
|
+
Object.defineProperty(globalThis, "navigator", {
|
|
123
|
+
value: instance,
|
|
124
|
+
enumerable: false,
|
|
125
|
+
writable: true,
|
|
126
|
+
configurable: true,
|
|
127
|
+
});
|
|
128
|
+
// Node also exposes the `Navigator` constructor globally; mirror it (non-enumerable).
|
|
129
|
+
if (typeof globalThis.Navigator === "undefined") {
|
|
130
|
+
Object.defineProperty(globalThis, "Navigator", {
|
|
131
|
+
value: Navigator,
|
|
132
|
+
enumerable: false,
|
|
133
|
+
writable: true,
|
|
134
|
+
configurable: true,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Fast tier / modern compat (getBuiltinModule present, Node >= 20.16): install eagerly
|
|
140
|
+
// at module eval — a guaranteed no-op above Node 21 thanks to the version gate, and
|
|
141
|
+
// correct on a 20.16–20.x compat run where navigator is still absent. On the narrow
|
|
142
|
+
// floor below getBuiltinModule the compat entry calls setBootstrapCreateRequire(...)
|
|
143
|
+
// + installNavigatorShim() explicitly (mirroring worker-polyfill's wiring).
|
|
144
|
+
if (typeof process.getBuiltinModule === "function") installNavigatorShim();
|
package/runtime/polyfills.cjs
CHANGED
|
@@ -84,6 +84,74 @@ function installSyncPolyfills(preloaded) {
|
|
|
84
84
|
});
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
// ── File (global on Node 20+, missing on the 18.x compat floor) ─────
|
|
88
|
+
// Node exposes the WHATWG `File` as a global from Node 20; on 18.13–18.x it
|
|
89
|
+
// exists only as `node:buffer`'s `File` export. Backfill the global from there
|
|
90
|
+
// so worker/messaging code that constructs `new File(...)` works down to the
|
|
91
|
+
// floor (polyfill-all-the-way-down). Identity is preserved (same constructor as
|
|
92
|
+
// `node:buffer`), so `instanceof` and undici's webidl brand checks hold. Blob is
|
|
93
|
+
// already global on 18.x, but the same backfill guards it for completeness.
|
|
94
|
+
// Non-enumerable to match Node's own global descriptors (the additive contract:
|
|
95
|
+
// invisible to global enumeration).
|
|
96
|
+
//
|
|
97
|
+
// Node 18 emits a one-time `ExperimentalWarning: buffer.File …` on the FIRST
|
|
98
|
+
// `new File(...)` (the constructor, NOT the property read). Without nub the floor
|
|
99
|
+
// simply has no `File` global, so backfilling it would newly surface that warning
|
|
100
|
+
// when user code first constructs a File. To keep the floor backfill silent we
|
|
101
|
+
// force one throwaway construction INSIDE a suppression window: that consumes
|
|
102
|
+
// Node's once-per-feature guard (the warning is dropped here) so the user's later
|
|
103
|
+
// `new File(...)` is silent.
|
|
104
|
+
if (typeof globalThis.File === "undefined" || typeof globalThis.Blob === "undefined") {
|
|
105
|
+
const origEmitWarning = process.emitWarning;
|
|
106
|
+
process.emitWarning = function (warning, ...rest) {
|
|
107
|
+
const opt = rest[0];
|
|
108
|
+
const type = opt && typeof opt === "object" ? opt.type : opt;
|
|
109
|
+
const msg = typeof warning === "string" ? warning : (warning && warning.message) || "";
|
|
110
|
+
if (type === "ExperimentalWarning" && /buffer\.(File|Blob)/.test(msg)) return;
|
|
111
|
+
return origEmitWarning.call(this, warning, ...rest);
|
|
112
|
+
};
|
|
113
|
+
try {
|
|
114
|
+
const buffer = require("node:buffer");
|
|
115
|
+
const sampleArgs = { File: [[], ""], Blob: [[]] };
|
|
116
|
+
for (const name of ["File", "Blob"]) {
|
|
117
|
+
const Ctor = buffer[name];
|
|
118
|
+
if (typeof globalThis[name] === "undefined" && typeof Ctor === "function") {
|
|
119
|
+
Object.defineProperty(globalThis, name, {
|
|
120
|
+
value: Ctor,
|
|
121
|
+
enumerable: false,
|
|
122
|
+
writable: true,
|
|
123
|
+
configurable: true,
|
|
124
|
+
});
|
|
125
|
+
// Trip (and suppress) the experimental-feature warning now, so user code
|
|
126
|
+
// never sees it.
|
|
127
|
+
try { new Ctor(...sampleArgs[name]); } catch { /* construction shape varies; the warning fires regardless */ }
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
} finally {
|
|
131
|
+
process.emitWarning = origEmitWarning;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ── MessageEvent.ports → frozen array (WHATWG read-only requirement) ─
|
|
136
|
+
// The spec mandates `MessageEvent.ports` be a read-only (frozen) array; Node's
|
|
137
|
+
// native MessageEvent returns a mutable array. Wrap the configurable prototype
|
|
138
|
+
// getter so every read yields a frozen array, for both a native MessageChannel's
|
|
139
|
+
// delivery and nub's worker-side MessageEvents. Idempotent (the wrapper is marked
|
|
140
|
+
// so a re-run in the same realm doesn't double-wrap).
|
|
141
|
+
if (typeof globalThis.MessageEvent === "function") {
|
|
142
|
+
const proto = globalThis.MessageEvent.prototype;
|
|
143
|
+
const desc = Object.getOwnPropertyDescriptor(proto, "ports");
|
|
144
|
+
if (desc && typeof desc.get === "function" && desc.configurable && !desc.get.__nubFreezesPorts) {
|
|
145
|
+
const origGet = desc.get;
|
|
146
|
+
const get = function () {
|
|
147
|
+
const ports = origGet.call(this);
|
|
148
|
+
return Array.isArray(ports) ? Object.freeze(ports) : ports;
|
|
149
|
+
};
|
|
150
|
+
get.__nubFreezesPorts = true;
|
|
151
|
+
Object.defineProperty(proto, "ports", { ...desc, get });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
87
155
|
// ── URLPattern (native on Node 24+, missing on 22.x) ───────────────
|
|
88
156
|
if (typeof globalThis.URLPattern === "undefined") {
|
|
89
157
|
const mod = preloaded.urlpattern;
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
// loader worker, not a user realm, so it installs no browser globals.)
|
|
29
29
|
import "./floor-builtin.mjs";
|
|
30
30
|
import {
|
|
31
|
-
TRANSPILE_EXTS, DATA_EXTS,
|
|
32
|
-
extname, resolveSpec, loadTranspile, loadData,
|
|
31
|
+
TRANSPILE_EXTS, PLAIN_JS_EXTS, DATA_EXTS,
|
|
32
|
+
extname, resolveSpec, loadTranspile, maybeTranspilePlainJs, loadData, isNodeModules,
|
|
33
33
|
} from "./transform-core.mjs";
|
|
34
34
|
import { createRequire, isBuiltin } from "node:module";
|
|
35
35
|
import { existsSync } from "node:fs";
|
|
@@ -85,13 +85,26 @@ export async function resolve(specifier, context, nextResolve) {
|
|
|
85
85
|
// ── Load hook ───────────────────────────────────────────────────────
|
|
86
86
|
export async function load(url, context, nextLoad) {
|
|
87
87
|
const ext = extname(url);
|
|
88
|
-
|
|
88
|
+
// node_modules deps are NEVER transpiled (the byte-parity boundary). This guard is
|
|
89
|
+
// make-or-break now that TRANSPILE_EXTS includes `.js`/`.mjs`/`.cjs`: without it,
|
|
90
|
+
// the compat tier would route every dependency `.js` through oxc. (loadTranspile's
|
|
91
|
+
// own skip-gate handles the project-source no-op case; this keeps deps off the
|
|
92
|
+
// pipeline entirely.) Mirrors the fast-tier sync hook's `!isNodeModules` gate.
|
|
93
|
+
if (TRANSPILE_EXTS.has(ext) && !isNodeModules(url)) {
|
|
89
94
|
// Module-format + decorator detection inside loadTranspile is a synchronous
|
|
90
95
|
// native call (nub's addon), available on every supported Node — no parser
|
|
91
96
|
// warm-up needed (the old `await ensureParser()` for the ESM-only oxc-parser
|
|
92
97
|
// is gone with the package).
|
|
93
98
|
return loadTranspile(url, ext);
|
|
94
99
|
}
|
|
100
|
+
// Project-source plain JS: transpile ONLY when it carries transformable syntax. A
|
|
101
|
+
// no-op plain-JS file returns null and falls through to `nextLoad` — Node's own
|
|
102
|
+
// loader handles it byte-identically, preserving every native CJS/ESM behavior.
|
|
103
|
+
// node_modules excluded (the byte-parity boundary).
|
|
104
|
+
if (PLAIN_JS_EXTS.has(ext) && !isNodeModules(url)) {
|
|
105
|
+
const r = maybeTranspilePlainJs(url, ext);
|
|
106
|
+
if (r) return r;
|
|
107
|
+
}
|
|
95
108
|
if (ext in DATA_EXTS) return loadData(url, ext);
|
|
96
109
|
return nextLoad(url, context);
|
|
97
110
|
}
|
|
@@ -373,6 +373,13 @@ function makeHooks(core, watchReporting) {
|
|
|
373
373
|
if (core.TRANSPILE_EXTS.has(ext) && !core.isNodeModules(url)) {
|
|
374
374
|
return core.loadTranspile(url, ext);
|
|
375
375
|
}
|
|
376
|
+
// Plain JS: transpile only when transformable; else fall through to the raw-
|
|
377
|
+
// source path below (which hands CJS back as `source:null` → Node's native
|
|
378
|
+
// CJS loader), byte-identical to a non-intercepted file.
|
|
379
|
+
if (core.PLAIN_JS_EXTS.has(ext) && !core.isNodeModules(url)) {
|
|
380
|
+
const r = core.maybeTranspilePlainJs(url, ext);
|
|
381
|
+
if (r) return r;
|
|
382
|
+
}
|
|
376
383
|
if (ext in core.DATA_EXTS) return core.loadData(url, ext);
|
|
377
384
|
const { readFileSync } = require("node:fs");
|
|
378
385
|
const source = readFileSync(path);
|
|
@@ -432,6 +439,15 @@ function makeHooks(core, watchReporting) {
|
|
|
432
439
|
if (core.TRANSPILE_EXTS.has(ext) && !core.isNodeModules(url)) {
|
|
433
440
|
return core.loadTranspile(url, ext);
|
|
434
441
|
}
|
|
442
|
+
// Project-source plain JS (`.js`/`.mjs`/`.cjs`): transpile ONLY when it carries
|
|
443
|
+
// transformable syntax. A no-op plain-JS file returns null here and falls through
|
|
444
|
+
// to Node's native loader (the `nextLoad`/relabel path below) BYTE-FOR-BYTE — it
|
|
445
|
+
// is never intercepted, so native CJS/ESM behavior (the relabel, require.cache,
|
|
446
|
+
// the require-of-ESM-syntax-`.cjs` error) is preserved. node_modules excluded.
|
|
447
|
+
if (core.PLAIN_JS_EXTS.has(ext) && !core.isNodeModules(url)) {
|
|
448
|
+
const r = core.maybeTranspilePlainJs(url, ext);
|
|
449
|
+
if (r) return r;
|
|
450
|
+
}
|
|
435
451
|
if (ext in core.DATA_EXTS) return core.loadData(url, ext);
|
|
436
452
|
|
|
437
453
|
// Fidelity: a `data:` URL whose MIME maps to no module format (e.g.
|
|
@@ -673,6 +689,39 @@ function installCjsRequireHooks(core, withClassicTranspile) {
|
|
|
673
689
|
for (const ext of [".ts", ".cts", ".mts", ".tsx", ".jsx"]) {
|
|
674
690
|
module_._extensions[ext] = transpileExtension;
|
|
675
691
|
}
|
|
692
|
+
|
|
693
|
+
// Project-source plain JS (`.js`/`.cjs`) routes through the SAME pipeline so
|
|
694
|
+
// `using`/`v`-flag-RegExp/decorators lower uniformly on the classic require tier —
|
|
695
|
+
// but ONLY when the file carries transformable syntax. THREE cases, each preserving
|
|
696
|
+
// Node's native behavior where it must:
|
|
697
|
+
// (1) node_modules dep → native handler (deps are NEVER transpiled). Node has no
|
|
698
|
+
// own `_extensions['.cjs']` (only `.js`/`.json`/`.node`), and compiles `.cjs`
|
|
699
|
+
// through the same CJS path as `.js`, so the `.cjs` bail falls back to the
|
|
700
|
+
// `.js` handler (`|| nativeJs`) — without it a node_modules `.cjs` require
|
|
701
|
+
// would call `undefined` and crash.
|
|
702
|
+
// (2) project file with transformable syntax → transpile (lower it).
|
|
703
|
+
// (3) project file with NOTHING to lower → the ORIGINAL native handler, raw bytes
|
|
704
|
+
// compiled exactly as Node would. We never serve our own source for a no-op
|
|
705
|
+
// file, so require.cache / the require-of-ESM-syntax-`.cjs` SyntaxError / every
|
|
706
|
+
// native CJS behavior is byte-identical.
|
|
707
|
+
// `.mjs` is ESM-only; Node registers no require.extensions handler for it, so a
|
|
708
|
+
// `require()` of `.mjs` throws ERR_REQUIRE_ESM as before — we don't override it.
|
|
709
|
+
const nativeJs = module_._extensions[".js"];
|
|
710
|
+
for (const ext of [".js", ".cjs"]) {
|
|
711
|
+
const origExtension = module_._extensions[ext] || nativeJs;
|
|
712
|
+
module_._extensions[ext] = (mod, filename) => {
|
|
713
|
+
if (core.isNodeModules(pathToFileURL(filename).href)) {
|
|
714
|
+
return origExtension.call(module_._extensions, mod, filename); // (1)
|
|
715
|
+
}
|
|
716
|
+
const r = core.maybeTranspilePlainJs(pathToFileURL(filename).href, pathExtname(filename));
|
|
717
|
+
if (r) {
|
|
718
|
+
if (r.format === "module") throw requireEsmError(filename); // (2)
|
|
719
|
+
mod._compile(r.source, filename);
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
return origExtension.call(module_._extensions, mod, filename); // (3)
|
|
723
|
+
};
|
|
724
|
+
}
|
|
676
725
|
}
|
|
677
726
|
|
|
678
727
|
// ── Clobbered-polyfill preloading + Temporal lazy global ────────────
|
package/runtime/preload.cjs
CHANGED
|
@@ -209,6 +209,21 @@ function installLazyEsmPolyfills() {
|
|
|
209
209
|
}
|
|
210
210
|
};
|
|
211
211
|
|
|
212
|
+
// navigator backfill, ordered BEFORE the navigator.locks installs below so locks
|
|
213
|
+
// always has a host. The fast tier floor is 22.15 (navigator is always native, >= 21),
|
|
214
|
+
// so installNavigatorShim() version-checks and returns WITHOUT reading globalThis
|
|
215
|
+
// .navigator — never triggering the 24.5+ lazy-navigator realization. It does real
|
|
216
|
+
// work only if this preload is ever reached on Node < 21; on the fast floor it is a
|
|
217
|
+
// cheap no-op, wired for symmetry with the compat tier. See navigator-shim.mjs.
|
|
218
|
+
try {
|
|
219
|
+
__require("./navigator-shim.mjs").installNavigatorShim();
|
|
220
|
+
} catch (err) {
|
|
221
|
+
// require(esm) disabled (--no-experimental-require-module): navigator is native at
|
|
222
|
+
// the fast floor, so skipping the no-op shim is harmless. Any OTHER error is a real
|
|
223
|
+
// fault in the module — surface it rather than swallow it.
|
|
224
|
+
if (!err || err.code !== "ERR_REQUIRE_ESM") throw err;
|
|
225
|
+
}
|
|
226
|
+
|
|
212
227
|
if (inWorkerThread) {
|
|
213
228
|
// Worker-side scope bootstrap must be present synchronously where possible.
|
|
214
229
|
loadEsmSideEffect("./worker-polyfill.mjs");
|
package/runtime/preload.mjs
CHANGED
|
@@ -112,6 +112,14 @@ if (__isFastTier) {
|
|
|
112
112
|
// modules is unreliable, so they load via dynamic `import()` here.
|
|
113
113
|
const __preloadedPolyfills = common.preloadPolyfillPackages(__require);
|
|
114
114
|
installSyncPolyfills(__preloadedPolyfills);
|
|
115
|
+
// navigator backfill (Node < 21: the `navigator` global is wholly absent). MUST run
|
|
116
|
+
// BEFORE navigator-locks so Web Locks has a host on the 18.19–20.x floor. On Node >= 21
|
|
117
|
+
// it is a no-op (navigator is native). Thread the floor's createRequire so the shim can
|
|
118
|
+
// fetch node:os where process.getBuiltinModule is absent (the narrow floor). See
|
|
119
|
+
// navigator-shim.mjs.
|
|
120
|
+
const __navShim = await import("./navigator-shim.mjs");
|
|
121
|
+
__navShim.setBootstrapCreateRequire(floorCreateRequire);
|
|
122
|
+
__navShim.installNavigatorShim();
|
|
115
123
|
if (typeof globalThis.navigator?.locks === "undefined") {
|
|
116
124
|
await import("./navigator-locks.mjs");
|
|
117
125
|
}
|
|
@@ -156,7 +156,21 @@ if (typeof process.getBuiltinModule === "function") __ensureBuiltins();
|
|
|
156
156
|
// natively now, and this file no longer needs to read version.mjs.
|
|
157
157
|
|
|
158
158
|
// ── Constants ───────────────────────────────────────────────────────
|
|
159
|
+
// TS/JSX exts ALWAYS transform (type-stripping is required), so they live in
|
|
160
|
+
// TRANSPILE_EXTS — the set every dispatch site checks to route a file to
|
|
161
|
+
// loadTranspile. Plain JS (.js/.mjs/.cjs) is DELIBERATELY NOT here: a plain-JS file
|
|
162
|
+
// is transpiled ONLY when it carries transformable syntax (`using`/`await using`,
|
|
163
|
+
// `v`-flag RegExp, decorators), and a no-op plain-JS file must take Node's OWN load
|
|
164
|
+
// path BYTE-FOR-BYTE — putting it in TRANSPILE_EXTS would route every `.js`/`.cjs`
|
|
165
|
+
// through nub's hook and change native CJS/ESM behavior (the `commonjs-sync` relabel,
|
|
166
|
+
// require.cache, the require-of-ESM-syntax-.cjs error). So plain JS is handled by a
|
|
167
|
+
// SEPARATE narrow path (`maybeTranspilePlainJs`) that fires only for transformable
|
|
168
|
+
// files and is a no-op (returns null) otherwise — see PLAIN_JS_EXTS below.
|
|
159
169
|
export const TRANSPILE_EXTS = new Set([".ts", ".tsx", ".mts", ".cts", ".jsx"]);
|
|
170
|
+
// Project-source plain JS. Routed to the transpiler ONLY when transformable (the
|
|
171
|
+
// `maybeTranspilePlainJs` gate); a no-op plain-JS file falls through to Node's
|
|
172
|
+
// native loader untouched, byte-identical. node_modules is excluded at the gate.
|
|
173
|
+
export const PLAIN_JS_EXTS = new Set([".js", ".mjs", ".cjs"]);
|
|
160
174
|
export const DATA_EXTS = { ".jsonc": "jsonc", ".json5": "json5", ".toml": "toml", ".yaml": "yaml", ".yml": "yaml", ".txt": "txt" };
|
|
161
175
|
export const TS_PARENT_EXTS = new Set([".ts", ".tsx", ".mts", ".cts"]);
|
|
162
176
|
|
|
@@ -328,7 +342,7 @@ export function resolveSpec(specifier, parentURL) {
|
|
|
328
342
|
// from a nub-dependency ESM entry) delegated to a strict user loader is exactly
|
|
329
343
|
// the R11 leak. See isNubInternalParent.
|
|
330
344
|
if (isNubInternalParent(parentURL)) {
|
|
331
|
-
if (specifier.startsWith("node:") || module.
|
|
345
|
+
if (specifier.startsWith("node:") || module.isBuiltin(specifier)) {
|
|
332
346
|
const url = specifier.startsWith("node:") ? specifier : `node:${specifier}`;
|
|
333
347
|
return { url, shortCircuit: true };
|
|
334
348
|
}
|
|
@@ -349,7 +363,7 @@ export function resolveSpec(specifier, parentURL) {
|
|
|
349
363
|
|
|
350
364
|
// node: and data: protocols, and bare Node built-ins, are never ours.
|
|
351
365
|
if (specifier.startsWith("node:") || specifier.startsWith("data:")) return null;
|
|
352
|
-
if (module.
|
|
366
|
+
if (module.isBuiltin(specifier)) return null;
|
|
353
367
|
|
|
354
368
|
// 1. Built-in modules provided by Nub.
|
|
355
369
|
if (BUILTIN_MODULES.has(specifier)) {
|
|
@@ -392,7 +406,7 @@ export function resolveSpec(specifier, parentURL) {
|
|
|
392
406
|
// file's absolute path (from the CJS parent Module), or null for the entry.
|
|
393
407
|
export function resolveCjsPath(request, parentPath) {
|
|
394
408
|
if (request.startsWith("node:") || request.startsWith("data:") ||
|
|
395
|
-
module.
|
|
409
|
+
module.isBuiltin(request)) {
|
|
396
410
|
return null;
|
|
397
411
|
}
|
|
398
412
|
// The SAME native additive resolver as resolveSpec, returning an absolute path
|
|
@@ -435,13 +449,15 @@ function detectModuleInfo(filePath, source, lang) {
|
|
|
435
449
|
// Addon missing (should never happen in a real install): default to ESM for
|
|
436
450
|
// format (the common case) and "no decorators" for the guard — the same fallback
|
|
437
451
|
// the old oxc-parser-unavailable branches used.
|
|
438
|
-
if (!nubNative) return { hasValueEsmSyntax: true, hasDecorators: false };
|
|
452
|
+
if (!nubNative) return { hasValueEsmSyntax: true, hasDecorators: false, transformableSyntax: false };
|
|
439
453
|
try {
|
|
440
454
|
return nubNative.detectModuleInfo(filePath, source, lang);
|
|
441
455
|
} catch {
|
|
442
456
|
// Unparseable → CJS for format + no decorators (the transpile/V8 surfaces the
|
|
443
|
-
// real error), matching the old per-call catch blocks.
|
|
444
|
-
|
|
457
|
+
// real error), matching the old per-call catch blocks. `transformableSyntax:
|
|
458
|
+
// false` is the SAFE plain-JS default — the verbatim path hands the raw bytes
|
|
459
|
+
// back, so V8 surfaces the real syntax error exactly where Node would.
|
|
460
|
+
return { hasValueEsmSyntax: false, hasDecorators: false, transformableSyntax: false };
|
|
445
461
|
}
|
|
446
462
|
}
|
|
447
463
|
|
|
@@ -450,13 +466,28 @@ function detectModuleInfo(filePath, source, lang) {
|
|
|
450
466
|
// `type` is authoritative; otherwise (ambiguous) we detect from source syntax —
|
|
451
467
|
// full Node parity (`--experimental-detect-module`), so a CJS-syntax `.ts` with
|
|
452
468
|
// no `type` runs as CJS on nub exactly as on Node. See wiki/runtime/module-format.md.
|
|
469
|
+
// `.mjs`→module / `.cjs`→commonjs are explicit (mirroring `.mts`/`.cts`), so the
|
|
470
|
+
// plain-JS gate gets the right format without a needless detect.
|
|
453
471
|
export function moduleFormatFor(ext, pkgType, filePath, source) {
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
472
|
+
return moduleFormatWithInfo(ext, pkgType, filePath, source).format;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Same format decision as moduleFormatFor, but ALSO returns the `ModuleInfo`
|
|
476
|
+
// (`detectModuleInfo`) result when a parse was needed — `{ format, info }`, with
|
|
477
|
+
// `info` null on the no-parse short-circuits (`.mts`/`.mjs`/`.cts`/`.cjs`, explicit
|
|
478
|
+
// `type`). loadTranspile uses this so its ONE parse serves BOTH readers — the
|
|
479
|
+
// format decision (`hasValueEsmSyntax`) and the Stage-3 decorator guard
|
|
480
|
+
// (`hasDecorators`) — instead of `moduleFormatFor` + `hasDecoratorSyntax` each
|
|
481
|
+
// parsing the same source. On a short-circuit (`info` null) no parse happened, so
|
|
482
|
+
// the decorator guard runs its own single parse: still ≤1 detect per file.
|
|
483
|
+
function moduleFormatWithInfo(ext, pkgType, filePath, source) {
|
|
484
|
+
if (ext === ".mts" || ext === ".mjs") return { format: "module", info: null };
|
|
485
|
+
if (ext === ".cts" || ext === ".cjs") return { format: "commonjs", info: null };
|
|
486
|
+
if (pkgType === "module") return { format: "module", info: null };
|
|
487
|
+
if (pkgType === "commonjs") return { format: "commonjs", info: null };
|
|
458
488
|
const lang = ext === ".tsx" ? "tsx" : ext === ".jsx" ? "jsx" : "ts";
|
|
459
|
-
|
|
489
|
+
const info = detectModuleInfo(filePath, source, lang);
|
|
490
|
+
return { format: info.hasValueEsmSyntax ? "module" : "commonjs", info };
|
|
460
491
|
}
|
|
461
492
|
|
|
462
493
|
// The Stage-3-decorator rejection diagnostic. oxc does not lower TC39 Stage 3
|
|
@@ -568,9 +599,13 @@ export function loadTranspile(url, ext) {
|
|
|
568
599
|
// (.ts/.tsx/.jsx); .mts/.cts are explicit so its lookup is skipped. The chosen
|
|
569
600
|
// format is folded into the cache key (and the entry's leading byte) by native.
|
|
570
601
|
const pkgType = ext === ".mts" || ext === ".cts" ? undefined : getPackageType(dir);
|
|
571
|
-
|
|
602
|
+
// ONE detectModuleInfo parse for both the format decision and the decorator
|
|
603
|
+
// guard below: `moduleInfo` is the parsed ModuleInfo when the format needed a
|
|
604
|
+
// parse (ambiguous ext, no explicit `type`), else null (a no-parse short-circuit).
|
|
605
|
+
const { format, info: moduleInfo } = moduleFormatWithInfo(ext, pkgType, filePath, source);
|
|
572
606
|
|
|
573
607
|
const lang = ext === ".tsx" ? "tsx" : ext === ".jsx" ? "jsx" : "ts";
|
|
608
|
+
|
|
574
609
|
const opts = {
|
|
575
610
|
lang,
|
|
576
611
|
sourceType: format === "commonjs" ? "commonjs" : "module",
|
|
@@ -604,9 +639,12 @@ export function loadTranspile(url, ext) {
|
|
|
604
639
|
// SyntaxError. When legacy mode is off and decorator syntax is present, reject
|
|
605
640
|
// with the documented Option-A diagnostic instead. (Cheap `source.includes("@")`
|
|
606
641
|
// pre-filter keeps decorator-free files off the native parser; runs BEFORE the
|
|
607
|
-
// cache so the diagnostic surfaces even on what would be a warm hit.)
|
|
642
|
+
// cache so the diagnostic surfaces even on what would be a warm hit.) Reuse the
|
|
643
|
+
// `hasDecorators` flag from the format parse above when it ran (`moduleInfo`
|
|
644
|
+
// non-null), so the ambiguous-ext + `@` path detects ONCE; on a no-parse
|
|
645
|
+
// short-circuit (`.mts`/`.cts`/explicit `type`) it does its own single parse.
|
|
608
646
|
if (co?.experimentalDecorators !== true && source.includes("@") &&
|
|
609
|
-
hasDecoratorSyntax(filePath, source, lang)) {
|
|
647
|
+
(moduleInfo ? moduleInfo.hasDecorators : hasDecoratorSyntax(filePath, source, lang))) {
|
|
610
648
|
throw stage3DecoratorError(filePath);
|
|
611
649
|
}
|
|
612
650
|
|
|
@@ -626,6 +664,40 @@ export function loadTranspile(url, ext) {
|
|
|
626
664
|
return { format: result.format, source: result.code, shortCircuit: true };
|
|
627
665
|
}
|
|
628
666
|
|
|
667
|
+
// Project-source plain JS (`.js`/`.mjs`/`.cjs`) gate. Returns a transpiled load
|
|
668
|
+
// result ONLY when the file carries syntax oxc lowers at nub's es2022 target
|
|
669
|
+
// (`using`/`await using`, a `v`-flag RegExp, or decorators); otherwise returns
|
|
670
|
+
// `null`, meaning "this file needs no transform — handle it with Node's OWN loader,
|
|
671
|
+
// exactly as a non-listed extension." This is why `.js`/`.mjs`/`.cjs` are NOT in
|
|
672
|
+
// TRANSPILE_EXTS: a no-op plain-JS file must take Node's native load path
|
|
673
|
+
// byte-for-byte (preserving the `commonjs-sync` relabel, require.cache, the
|
|
674
|
+
// require-of-ESM-syntax-`.cjs` error — all of which intercepting the file would
|
|
675
|
+
// break), and oxc would reformat it (quotes/semicolons/whitespace + a sourcemap
|
|
676
|
+
// footer) if we ran it through anyway. The verdict rides ONE parse (the same one
|
|
677
|
+
// `detectModuleInfo` does for format detection). node_modules is gated at the call
|
|
678
|
+
// sites (the byte-parity boundary). JSX-in-`.js` is out of scope (lang is "ts",
|
|
679
|
+
// which does not parse JSX); use `.jsx`.
|
|
680
|
+
export function maybeTranspilePlainJs(url, ext) {
|
|
681
|
+
__ensureBuiltins();
|
|
682
|
+
const filePath = fileURLToPath(url);
|
|
683
|
+
let source;
|
|
684
|
+
try {
|
|
685
|
+
source = readFileSync(filePath, "utf8");
|
|
686
|
+
} catch {
|
|
687
|
+
// Unreadable here → let Node's loader surface its own error.
|
|
688
|
+
return null;
|
|
689
|
+
}
|
|
690
|
+
// lang "ts" parses all JS (a TS superset) but NOT JSX — JSX-in-.js is out of scope.
|
|
691
|
+
const info = detectModuleInfo(filePath, source, "ts");
|
|
692
|
+
if (!info.transformableSyntax && !info.hasDecorators) {
|
|
693
|
+
return null; // no-op: Node's native loader handles it, byte-identical.
|
|
694
|
+
}
|
|
695
|
+
// Transformable: run the SAME pipeline as TS/JSX (target es2022 lowering, tsconfig,
|
|
696
|
+
// source maps, the Stage-3 decorator guard, format detection, cache). loadTranspile
|
|
697
|
+
// re-reads + re-parses, but only for the rare file that actually needs lowering.
|
|
698
|
+
return loadTranspile(url, ext);
|
|
699
|
+
}
|
|
700
|
+
|
|
629
701
|
// ── Data-format imports ─────────────────────────────────────────────
|
|
630
702
|
function lazyRequire(pkg) {
|
|
631
703
|
try { return __require(pkg); } catch {
|
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.2.
|
|
12
|
+
export const NUB_VERSION = "0.2.2";
|