@nubjs/nub-linux-arm64 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 CHANGED
Binary file
package/bin/nubx CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nubjs/nub-linux-arm64",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Nub binary for linux-arm64",
5
5
  "license": "MIT",
6
6
  "repository": "https://github.com/nubjs/nub",
Binary file
@@ -1,10 +1,21 @@
1
- // Web Locks API polyfill for Node 22.x (native on Node 24.5+).
2
- // Single-process only — locks don't coordinate across workers.
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
- // Track held locks: name → { mode, count } where count > 1 means shared holders
6
- const held = new Map();
7
- const queue = new Map();
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 current = held.get(name);
22
- if (!current) return true;
23
- if (mode === "shared" && current.mode === "shared") return true;
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
- function acquire(name, mode) {
28
- const current = held.get(name);
29
- if (current && mode === "shared" && current.mode === "shared") {
30
- current.count++;
31
- } else {
32
- held.set(name, { mode, count: 1 });
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 release(name) {
37
- const current = held.get(name);
38
- if (!current) return;
39
- current.count--;
40
- if (current.count <= 0) {
41
- held.delete(name);
42
- drainQueue(name);
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
- // Try to grant as many queued requests as possible.
51
- // If the first queued is shared, grant all consecutive shared requests.
52
- // If the first queued is exclusive, grant only that one.
53
- const first = q[0];
54
- if (canAcquire(name, first.mode)) {
55
- if (first.mode === "exclusive") {
56
- q.shift();
57
- first.resolve();
58
- } else {
59
- // Grant all consecutive shared requests.
60
- while (q.length > 0 && q[0].mode === "shared") {
61
- const req = q.shift();
62
- req.resolve();
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, callback) {
70
- let options = {};
71
- if (typeof optionsOrCallback === "function") {
72
- callback = optionsOrCallback;
73
- } else {
74
- options = optionsOrCallback || {};
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 || "exclusive";
78
- const ifAvailable = options.ifAvailable || false;
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
- if (signal?.aborted) {
82
- throw signal.reason || new DOMException("Lock request aborted", "AbortError");
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 (!canAcquire(name, mode)) {
86
- if (ifAvailable) {
87
- return callback(null);
88
- }
89
- await new Promise((resolve, reject) => {
90
- if (!queue.has(name)) queue.set(name, []);
91
- queue.get(name).push({ resolve, reject, mode });
92
- if (signal) {
93
- signal.addEventListener("abort", () => {
94
- const q = queue.get(name) || [];
95
- const idx = q.findIndex((e) => e.resolve === resolve);
96
- if (idx !== -1) q.splice(idx, 1);
97
- reject(signal.reason || new DOMException("Lock request aborted", "AbortError"));
98
- }, { once: true });
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
- acquire(name, mode);
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
- const heldLocks = [];
115
- for (const [name, info] of held) {
116
- for (let i = 0; i < info.count; i++) {
117
- heldLocks.push({ name, mode: info.mode, clientId: "" });
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 req of q) {
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: heldLocks, pending };
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();
@@ -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
- if (TRANSPILE_EXTS.has(ext)) {
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 ────────────
@@ -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");
@@ -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.builtinModules.includes(specifier)) {
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.builtinModules.includes(specifier)) return null;
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.builtinModules.includes(request)) {
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
- return { hasValueEsmSyntax: false, hasDecorators: false };
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
- if (ext === ".mts") return "module";
455
- if (ext === ".cts") return "commonjs";
456
- if (pkgType === "module") return "module";
457
- if (pkgType === "commonjs") return "commonjs";
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
- return detectModuleInfo(filePath, source, lang).hasValueEsmSyntax ? "module" : "commonjs";
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
- const format = moduleFormatFor(ext, pkgType, filePath, source);
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 {
@@ -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.1";
12
+ export const NUB_VERSION = "0.2.2";