@nubjs/nub-linux-x64 0.0.12 → 0.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -20,18 +20,119 @@
20
20
  // top-level hook registration here — importing this module never augments the
21
21
  // realm; the tier files do that.
22
22
 
23
- import module from "node:module";
24
- import { readFileSync, writeFileSync, mkdirSync, statSync, renameSync, unlinkSync, readdirSync } from "node:fs";
25
- import { fileURLToPath, pathToFileURL } from "node:url";
26
- import { createRequire } from "node:module";
27
- import { createHash } from "node:crypto";
28
- import { join, dirname, resolve as pathResolve, extname as pathExtname } from "node:path";
29
- import { transformSync } from "oxc-transform";
30
- import { getTsconfig, createPathsMatcher } from "get-tsconfig";
31
- import { NUB_VERSION } from "./version.mjs";
32
-
23
+ // EVERY dependency of this module is pulled in via CJS `require()` (below), NOT
24
+ // via static ESM `import`. This is load-bearing for loader compatibility (R11):
25
+ // nub loads transform-core through `require(esm)`, and Node's `require(esm)`
26
+ // instantiates the module by walking its STATIC IMPORT graph through whatever ESM
27
+ // loader hooks are registered — including the USER's `--loader`/`register()`
28
+ // chain. Static `import oxc-transform`/`get-tsconfig`/`./version.mjs`/`node:*`
29
+ // here therefore leaked nub's entire internal graph (transform-core, version.mjs,
30
+ // oxc-transform, get-tsconfig, their transitive node_modules deps, and the node:
31
+ // builtins) THROUGH the user's resolve/load hooks, which observed and corrupted
32
+ // it (a user load hook returning `source: 1` for version.mjs, a resolve hook
33
+ // appending `?foo` to `oxc-transform`, a strict loader throwing on the bare
34
+ // `oxc-transform` specifier — see test-esm-loader-chaining, -example-loader,
35
+ // -preserve-symlinks-not-found, test-shadow-realm-custom-loaders). Verified: a CJS
36
+ // `require()` of a package/builtin does NOT route through the ESM loader chain, so
37
+ // loading the graph this way bypasses the user chain entirely. `process
38
+ // .getBuiltinModule` fetches node: builtins synchronously off the loader chain;
39
+ // `createRequire(import.meta.url)` resolves the bare deps from nub's distribution.
40
+ // This file keeps its `export`s (it stays an ES module), but has ZERO static
41
+ // imports, so `require(esm)` finds no dependency graph to route through the user.
42
+ const { createRequire } = process.getBuiltinModule("node:module");
33
43
  const __require = createRequire(import.meta.url);
34
44
 
45
+ const module = process.getBuiltinModule("node:module");
46
+ const { readFileSync, writeFileSync, mkdirSync, statSync, renameSync, unlinkSync, readdirSync } = process.getBuiltinModule("node:fs");
47
+ const { fileURLToPath, pathToFileURL } = process.getBuiltinModule("node:url");
48
+ const { join, dirname, resolve: pathResolve, extname: pathExtname } = process.getBuiltinModule("node:path");
49
+ // oxc-transform is `type: module`, so a plain `require("oxc-transform")` uses
50
+ // `require(esm)` and Node instantiates its ESM entry by walking ITS static
51
+ // imports through the registered ESM loader chain — i.e. through the USER's
52
+ // `--loader` worker (which nub's main-thread sync hooks do NOT chain into, so the
53
+ // resolveSpec short-circuit can't catch it). oxc-transform's entry has exactly one
54
+ // ESM-only construct that leaks: `import { createRequire } from 'node:module'`
55
+ // (the rest is CJS `require`/`module.exports`-shaped). To keep it off the chain
56
+ // entirely we load it AS CommonJS: read the entry source, neutralize its tiny ESM
57
+ // header (the `import { createRequire }` line, the `const require =
58
+ // createRequire(import.meta.url)` shadow, the `import.meta.url`-based `__dirname`)
59
+ // and convert its `export { X }` footer to `module.exports`, then compile it with
60
+ // `module._compile` at its real path so its internal `require('./binding.node')`
61
+ // and `__dirname` resolve correctly. A CJS-compiled module has no static-import
62
+ // graph for Node to route through any loader. Falls back to plain require(esm) if
63
+ // the source shape ever changes (then the leak returns, but transpilation works).
64
+ function requireOxcAsCjs() {
65
+ const Module = module;
66
+ let entry;
67
+ try {
68
+ entry = __require.resolve("oxc-transform");
69
+ } catch {
70
+ return __require("oxc-transform");
71
+ }
72
+ // Only the .js NAPI-RS wrapper has the mixed ESM-header/CJS-body shape we can
73
+ // safely transform; anything else, defer to the normal loader.
74
+ if (!entry.endsWith(".js")) return __require("oxc-transform");
75
+ try {
76
+ let src = readFileSync(entry, "utf8");
77
+ // Strip the ESM header: `import { createRequire } from 'node:module'`,
78
+ // `const require = createRequire(import.meta.url)`, and any
79
+ // `const __dirname = new URL('.', import.meta.url).pathname`. In a CJS module
80
+ // `require`, `__dirname`, `__filename` are already injected by _compile.
81
+ src = src
82
+ .replace(/^\s*import\s*\{[^}]*\}\s*from\s*['"]node:module['"];?\s*$/m, "")
83
+ .replace(/^\s*const\s+require\s*=\s*createRequire\([^)]*\);?\s*$/m, "")
84
+ .replace(/^\s*const\s+__dirname\s*=\s*new URL\([^)]*\)\.pathname;?\s*$/m, "");
85
+ // Convert the `export { Name }` footer lines to a single CJS exports block.
86
+ const names = [];
87
+ src = src.replace(/^\s*export\s*\{\s*([A-Za-z0-9_$]+)\s*\}\s*;?\s*$/gm, (_m, n) => {
88
+ names.push(n);
89
+ return "";
90
+ });
91
+ // Guard: if any `import`/`export`/`import.meta` survived, we don't understand
92
+ // this version's shape — bail to the safe loader rather than ship broken code.
93
+ if (/^\s*(import|export)\s/m.test(src) || /\bimport\.meta\b/.test(src)) {
94
+ return __require("oxc-transform");
95
+ }
96
+ if (names.length === 0) return __require("oxc-transform");
97
+ src += `\nmodule.exports = { ${names.join(", ")} };\n`;
98
+ const m = new Module(entry, null);
99
+ m.filename = entry;
100
+ m.paths = Module._nodeModulePaths(dirname(entry));
101
+ m._compile(src, entry);
102
+ return m.exports;
103
+ } catch {
104
+ return __require("oxc-transform");
105
+ }
106
+ }
107
+ const { transformSync } = requireOxcAsCjs();
108
+ const { getTsconfig, createPathsMatcher } = __require("get-tsconfig");
109
+
110
+ // NUB_VERSION is the single source of truth in runtime/version.mjs. We must NOT
111
+ // `import` it (that would route version.mjs through the user loader chain — see
112
+ // above; a user load hook returning bogus source corrupts it), and we cannot
113
+ // `require()` it either (it is an ES module, so `require()` uses require(esm),
114
+ // which re-routes version.mjs's own load through the chain). Instead read its
115
+ // text directly and extract the literal — `make version` keeps the assignment on
116
+ // one line (`export const NUB_VERSION = "x.y.z";`), so a tight regex is stable.
117
+ const NUB_VERSION = (() => {
118
+ try {
119
+ const text = readFileSync(fileURLToPath(new URL("./version.mjs", import.meta.url)), "utf8");
120
+ const m = text.match(/NUB_VERSION\s*=\s*["']([^"']+)["']/);
121
+ if (m) return m[1];
122
+ } catch {}
123
+ return "0.0.0";
124
+ })();
125
+
126
+ // `node:crypto` is used ONLY to hash the transpile-cache key, so it loads lazily
127
+ // on first transpile rather than at module top level. Importing it eagerly pulls
128
+ // in the crypto/tls native tree (~dozens of builtins) on EVERY startup — including
129
+ // a plain-JS run that never transpiles anything (R7). The first `.ts` transpile
130
+ // pays the one-time require; a no-TS run never touches it. Memoized.
131
+ let _createHash = null;
132
+ function getCreateHash() {
133
+ return (_createHash ??= __require("node:crypto").createHash);
134
+ }
135
+
35
136
  // ── Constants ───────────────────────────────────────────────────────
36
137
  export const TRANSPILE_EXTS = new Set([".ts", ".tsx", ".mts", ".cts", ".jsx"]);
37
138
  export const DATA_EXTS = { ".jsonc": "jsonc", ".json5": "json5", ".toml": "toml", ".yaml": "yaml", ".yml": "yaml", ".txt": "txt" };
@@ -263,10 +364,102 @@ export function getProbeOrder(parentExt) {
263
364
  }
264
365
  }
265
366
 
367
+ // nub's own runtime directory (this file's dir, as a file: URL prefix). Any
368
+ // resolution whose IMPORTER lives here is one of nub's internal requires — the
369
+ // preload loading transform-core, transform-core requiring oxc, the Temporal lazy
370
+ // getter resolving @js-temporal/polyfill — and must NEVER be routed through nub's
371
+ // own clobber/vendored/tsconfig logic: those are user-code conveniences, and
372
+ // applying them to nub's internals both breaks them (e.g. the Temporal clobber
373
+ // re-exports globalThis.Temporal, which IS the getter → a require of the polyfill
374
+ // from the getter would recurse into the clobber) and amplifies the user loader
375
+ // chain by re-walking nub's internal graph through user hooks (R11). Short-circuit
376
+ // to native resolution for these.
377
+ const RUNTIME_DIR_URL = new URL(".", import.meta.url).href;
378
+
379
+ // nub's internal-graph package roots — the file: URL prefixes of the npm packages
380
+ // nub itself loads (oxc-transform, get-tsconfig) and their transitive deps. Any
381
+ // resolution whose IMPORTER lives under one of these is part of nub's internal
382
+ // graph, NOT user code. `oxc-transform` is `type: module`, so even when nub pulls
383
+ // it in via CJS `require()`, Node loads its `index.js` through `require(esm)` and
384
+ // walks ITS static `import "node:module"` graph through the registered ESM loader
385
+ // chain — including the USER's loader. That re-leaks nub internals one hop down
386
+ // (R11): test-esm-example-loader's strict loader throws on `node:module`,
387
+ // loader-resolve-shortcircuit appends `?foo` to oxc's specifiers, etc. So a
388
+ // nub-internal-graph resolution must FULLY short-circuit (resolve natively, return
389
+ // shortCircuit:true) — never delegate to nextResolve — for EVERY specifier,
390
+ // including node: builtins and the package's own relative imports. Computed lazily
391
+ // (and pinned even on resolve failure) so a missing dep can't wedge startup.
392
+ let _nubGraphRoots = null;
393
+ function nubGraphRoots() {
394
+ if (_nubGraphRoots) return _nubGraphRoots;
395
+ const roots = [];
396
+ for (const pkg of ["oxc-transform", "get-tsconfig"]) {
397
+ try {
398
+ const entry = __require.resolve(pkg);
399
+ // Package root = the directory two levels up does not work generically;
400
+ // instead key on the package-name segment: everything under
401
+ // `.../node_modules/<pkg>/` is that package. Use the entry's dir-with-pkg.
402
+ const idx = entry.lastIndexOf(`${sep()}node_modules${sep()}`);
403
+ if (idx !== -1) {
404
+ // Keep through the package-name segment (handles scoped names too).
405
+ const afterNM = entry.slice(idx + (`${sep()}node_modules${sep()}`).length);
406
+ const firstSeg = afterNM.startsWith("@")
407
+ ? afterNM.split(sep()).slice(0, 2).join(sep())
408
+ : afterNM.split(sep())[0];
409
+ const pkgRoot = entry.slice(0, idx) + `${sep()}node_modules${sep()}` + firstSeg + sep();
410
+ roots.push(pathToFileURL(pkgRoot).href);
411
+ }
412
+ } catch {}
413
+ }
414
+ return (_nubGraphRoots = roots);
415
+ }
416
+ function sep() {
417
+ return process.platform === "win32" ? "\\" : "/";
418
+ }
419
+
420
+ // Is this importer part of nub's own internal module graph (runtime dir or a nub
421
+ // dependency package)? Such imports must bypass the user ESM loader chain entirely.
422
+ function isNubInternalParent(parentURL) {
423
+ if (!parentURL) return false;
424
+ const p = String(parentURL);
425
+ if (p.startsWith(RUNTIME_DIR_URL)) return true;
426
+ for (const root of nubGraphRoots()) {
427
+ if (p.startsWith(root)) return true;
428
+ }
429
+ return false;
430
+ }
431
+
266
432
  // Resolve a specifier the way both hook tiers do. Returns `{ url, shortCircuit }`
267
433
  // to short-circuit Node's resolver, or `null` to fall through to `nextResolve`.
268
434
  // `parentURL` is the importer (a file: URL string), or "" for the entry.
269
435
  export function resolveSpec(specifier, parentURL) {
436
+ // nub's own internal graph (importer inside nub's runtime dir OR a nub
437
+ // dependency package): resolve natively and SHORT-CIRCUIT so nextResolve (the
438
+ // user's loader chain) never observes nub's internals. This MUST run before the
439
+ // node:/data:/builtin early-returns below, because those `return null` =
440
+ // DELEGATE to the user loader — and a nub-internal `import "node:module"` (e.g.
441
+ // from oxc-transform's ESM entry) delegated to a strict user loader is exactly
442
+ // the R11 leak. See isNubInternalParent / nubGraphRoots.
443
+ if (isNubInternalParent(parentURL)) {
444
+ if (specifier.startsWith("node:") || module.builtinModules.includes(specifier)) {
445
+ const url = specifier.startsWith("node:") ? specifier : `node:${specifier}`;
446
+ return { url, shortCircuit: true };
447
+ }
448
+ if (specifier.startsWith("data:")) return { url: specifier, shortCircuit: true };
449
+ // A relative/bare import from inside nub's graph: resolve it natively from the
450
+ // parent's own require() resolver (NOT nub's tsconfig/clobber/probe logic) and
451
+ // short-circuit. Bare specifiers resolve from the parent package's location.
452
+ try {
453
+ const parentReq = createRequire(parentURL);
454
+ const resolved = parentReq.resolve(specifier);
455
+ return { url: pathToFileURL(resolved).href, shortCircuit: true };
456
+ } catch {
457
+ // Couldn't resolve from the parent (e.g. a non-file: parent): still short-
458
+ // circuit by handing the specifier back as-is, so the user chain is bypassed.
459
+ return null;
460
+ }
461
+ }
462
+
270
463
  // node: and data: protocols, and bare Node built-ins, are never ours.
271
464
  if (specifier.startsWith("node:") || specifier.startsWith("data:")) return null;
272
465
  if (module.builtinModules.includes(specifier)) return null;
@@ -544,7 +737,7 @@ if (!CACHE_DISABLED) {
544
737
  }
545
738
 
546
739
  function cacheKey(source) {
547
- return createHash("sha256")
740
+ return getCreateHash()("sha256")
548
741
  .update(NUB_VERSION).update("\0")
549
742
  .update(CACHE_SCHEMA).update("\0")
550
743
  .update(source)
@@ -557,7 +750,7 @@ function cacheKey(source) {
557
750
  // garbage to V8.
558
751
  const CACHE_INTEGRITY_LEN = 16;
559
752
  function cacheIntegrity(body) {
560
- return createHash("sha256").update(body).digest("hex").slice(0, CACHE_INTEGRITY_LEN);
753
+ return getCreateHash()("sha256").update(body).digest("hex").slice(0, CACHE_INTEGRITY_LEN);
561
754
  }
562
755
  function cacheGet(key) {
563
756
  if (!cacheDir) return null;
@@ -695,6 +888,14 @@ export function loadTranspile(url, ext) {
695
888
  map.sourcesContent = [source];
696
889
  code += `\n//# sourceMappingURL=data:application/json;base64,${Buffer.from(JSON.stringify(map)).toString("base64")}\n`;
697
890
  }
891
+ // Append a `//# sourceURL=` magic comment, matching Node's native strip-types
892
+ // (lib/internal/modules/typescript.js: `return ${code}\n\n//# sourceURL=${filename}`).
893
+ // This is the marker V8/the inspector reads to set `scriptParsed.hasSourceURL =
894
+ // true` — the signal that a script is generated/transpiled rather than read
895
+ // verbatim from disk (test-inspector-strip-types asserts it). It coexists with
896
+ // the inline sourceMappingURL above (maps still drive stack frames); sourceURL
897
+ // only names the origin. Use the absolute file path, exactly as Node does.
898
+ code += `\n//# sourceURL=${filePath}\n`;
698
899
 
699
900
  // Store the chosen format as a leading byte so cache hits skip re-detection.
700
901
  cacheSet(key, (format === "commonjs" ? "c" : "m") + code);
@@ -9,4 +9,4 @@
9
9
  // previously lived as a literal inside preload.mjs, which `make version` patched,
10
10
  // while the worker carried a hand-maintained "…-compat" copy that `make version`
11
11
  // never touched — a latent staleness bug this module closes.)
12
- export const NUB_VERSION = "0.0.12";
12
+ export const NUB_VERSION = "0.0.14";
@@ -2,28 +2,52 @@
2
2
  // Wraps node:worker_threads.Worker with EventTarget inheritance,
3
3
  // real MessageEvent/ErrorEvent, and URL-only constructor (Deno shape).
4
4
 
5
- import { Worker as NodeWorker, parentPort, isMainThread } from "node:worker_threads";
6
- import { fileURLToPath } from "node:url";
5
+ // node: builtins are fetched via `process.getBuiltinModule` rather than static
6
+ // `import`. This file is loaded via `require(esm)` from the preload, and Node's
7
+ // `require(esm)` instantiates an ES module by walking its STATIC IMPORT graph
8
+ // through whatever ESM loader chain is registered — including the USER's
9
+ // `--loader`/`register()` hooks. A static `import { Worker } from
10
+ // "node:worker_threads"` therefore routes the builtin through the user chain; a
11
+ // user load hook that returns SOURCE for node:worker_threads makes V8 see no
12
+ // `Worker` export, so `new NodeWorker(...)` references an undefined binding and
13
+ // the child crashes (observed against test-esm-loader-chaining). `process
14
+ // .getBuiltinModule` fetches the real builtin synchronously off the loader
15
+ // graph, bypassing the user chain entirely — same fix transform-core.mjs uses.
16
+ const { Worker: NodeWorker, parentPort, isMainThread } = process.getBuiltinModule("node:worker_threads");
17
+ const { fileURLToPath } = process.getBuiltinModule("node:url");
7
18
 
8
19
  // `ErrorEvent` only became a global in Node 26. On the 22/24 floor it is
9
20
  // undefined, so `new ErrorEvent(...)` below would throw a ReferenceError inside
10
21
  // the worker's "error" handler — crashing the PARENT thread on every worker
11
- // that throws. Resolve the constructor once at load: use the native global when
12
- // present, otherwise a minimal Event subclass carrying the standard ErrorEvent
13
- // fields (message/error/filename/lineno/colno). See wiki/research/worker-polyfill.md.
14
- const ErrorEventCtor =
15
- typeof globalThis.ErrorEvent === "function"
16
- ? globalThis.ErrorEvent
17
- : class ErrorEvent extends Event {
18
- constructor(type, init = {}) {
19
- super(type, init);
20
- this.message = init.message ?? "";
21
- this.error = init.error ?? null;
22
- this.filename = init.filename ?? "";
23
- this.lineno = init.lineno ?? 0;
24
- this.colno = init.colno ?? 0;
25
- }
26
- };
22
+ // that throws. Resolve the constructor lazily and memoize on first use: use the
23
+ // native global when present, otherwise a minimal Event subclass carrying the
24
+ // standard ErrorEvent fields (message/error/filename/lineno/colno).
25
+ // See wiki/research/worker-polyfill.md.
26
+ //
27
+ // LAZY (not resolved at module load) on purpose: reading `globalThis.ErrorEvent`
28
+ // at top level trips Node's lazy `ErrorEvent` getter, which eagerly realizes
29
+ // ~100+ builtins (http2/tls/crypto/zlib/perf_hooks/webstreams) on EVERY startup
30
+ // — a cold-start regression (process.moduleLoadList ~230 vs node's ~110) that
31
+ // contradicts nub's fast-runner premise for the common "run a plain file, never
32
+ // touch Workers" case. The constructors are only ever needed inside the
33
+ // post-construction error/message handlers, so deferring resolution there costs
34
+ // nothing for non-Worker programs and nothing measurable for Worker ones.
35
+ let _ErrorEventCtor;
36
+ function getErrorEventCtor() {
37
+ return (_ErrorEventCtor ??=
38
+ typeof globalThis.ErrorEvent === "function"
39
+ ? globalThis.ErrorEvent
40
+ : class ErrorEvent extends Event {
41
+ constructor(type, init = {}) {
42
+ super(type, init);
43
+ this.message = init.message ?? "";
44
+ this.error = init.error ?? null;
45
+ this.filename = init.filename ?? "";
46
+ this.lineno = init.lineno ?? 0;
47
+ this.colno = init.colno ?? 0;
48
+ }
49
+ });
50
+ }
27
51
 
28
52
  if (typeof globalThis.Worker === "undefined") {
29
53
  class Worker extends EventTarget {
@@ -66,6 +90,7 @@ if (typeof globalThis.Worker === "undefined") {
66
90
  });
67
91
 
68
92
  this.#worker.on("error", (err) => {
93
+ const ErrorEventCtor = getErrorEventCtor();
69
94
  this.dispatchEvent(new ErrorEventCtor("error", { error: err, message: err.message }));
70
95
  });
71
96
 
@@ -107,7 +132,16 @@ if (typeof globalThis.Worker === "undefined") {
107
132
  }
108
133
  }
109
134
 
110
- globalThis.Worker = Worker;
135
+ // NON-ENUMERABLE: invisible to `Object.keys(globalThis)` / for-in is the
136
+ // additive contract — vanilla-Node code that enumerates the global object must
137
+ // not observe nub's injected `Worker`. Node defines its own globals the same
138
+ // way. Writable+configurable so user code can still override it.
139
+ Object.defineProperty(globalThis, "Worker", {
140
+ value: Worker,
141
+ enumerable: false,
142
+ writable: true,
143
+ configurable: true,
144
+ });
111
145
  }
112
146
 
113
147
  // Worker-side bootstrap: emulate the DedicatedWorkerGlobalScope on top of
@@ -118,7 +152,22 @@ if (typeof globalThis.Worker === "undefined") {
118
152
  // and a parent→worker round-trip hangs — see wiki/research/worker-polyfill.md.
119
153
  if (!isMainThread && parentPort) {
120
154
  const scope = globalThis;
121
- scope.self = scope;
155
+ // All of nub's worker-scope global injections below (self, addEventListener,
156
+ // removeEventListener, dispatchEvent, postMessage, close) are defined
157
+ // NON-ENUMERABLE. Node's worker global is not an EventTarget and exposes none
158
+ // of these, so a worker doing `Object.keys(globalThis)` / for-in must not see
159
+ // nub's additions — invisibility-to-enumeration is the additive contract.
160
+ // writable+configurable mirrors Node's own global descriptors. (`onmessage`/
161
+ // `onmessageerror` below already use Object.defineProperty, whose enumerable
162
+ // defaults to false.)
163
+ const defineGlobal = (name, value) =>
164
+ Object.defineProperty(scope, name, {
165
+ value,
166
+ enumerable: false,
167
+ writable: true,
168
+ configurable: true,
169
+ });
170
+ defineGlobal("self", scope);
122
171
 
123
172
  // `message`/`messageerror` are DELEGATED straight onto the native `parentPort`
124
173
  // (a real Node MessagePort) so Node's own C++ event-loop ref-counting governs
@@ -191,15 +240,15 @@ if (!isMainThread && parentPort) {
191
240
  }
192
241
  }
193
242
 
194
- scope.addEventListener = (type, listener, opts) =>
243
+ defineGlobal("addEventListener", (type, listener, opts) =>
195
244
  DELEGATED.has(type)
196
245
  ? addDelegated(type, listener, opts)
197
- : otherAdd(type, listener, opts);
198
- scope.removeEventListener = (type, listener, opts) =>
246
+ : otherAdd(type, listener, opts));
247
+ defineGlobal("removeEventListener", (type, listener, opts) =>
199
248
  DELEGATED.has(type)
200
249
  ? removeDelegated(type, listener)
201
- : otherRemove(type, listener, opts);
202
- scope.dispatchEvent = (ev) => otherDispatch(ev);
250
+ : otherRemove(type, listener, opts));
251
+ defineGlobal("dispatchEvent", (ev) => otherDispatch(ev));
203
252
 
204
253
  // `onmessage` / `onmessageerror` register via the delegating add/remove above,
205
254
  // mirroring the web API and the main-side Worker. Assigning `null` removes the
@@ -221,9 +270,9 @@ if (!isMainThread && parentPort) {
221
270
 
222
271
  // Outbound + lifecycle.
223
272
  if (typeof scope.postMessage !== "function") {
224
- scope.postMessage = (data, transfer) => parentPort.postMessage(data, transfer);
273
+ defineGlobal("postMessage", (data, transfer) => parentPort.postMessage(data, transfer));
225
274
  }
226
275
  if (typeof scope.close !== "function") {
227
- scope.close = () => process.exit(0);
276
+ defineGlobal("close", () => process.exit(0));
228
277
  }
229
278
  }
@@ -1,136 +0,0 @@
1
- // Polyfill preloads for Nub v0.1. All feature-detect and bow out
2
- // if the global is already present. Loaded via the main preload.
3
- //
4
- // Node 22.15+ (our floor) already has: navigator, navigator.locks,
5
- // navigator.hardwareConcurrency, WebSocket. No polyfills needed.
6
- //
7
- // Node 24+ adds: URLPattern, RegExp.escape, Error.isError, Promise.try.
8
- // We polyfill those on Node 22.x only.
9
- //
10
- // No Node version ships: Temporal, reportError, browser-shape Worker.
11
- // These need polyfills on all supported versions.
12
-
13
- // ── reportError (WinterTC min-common-API, not in any Node) ──────────
14
- if (typeof globalThis.reportError !== "function") {
15
- globalThis.reportError = (err) => {
16
- queueMicrotask(() => {
17
- throw err;
18
- });
19
- };
20
- }
21
-
22
- // ── URLPattern (native on Node 24+, missing on 22.x) ───────────────
23
- if (typeof globalThis.URLPattern === "undefined") {
24
- const mod = globalThis.__nubPreloaded?.urlpattern;
25
- const URLPattern = mod?.URLPattern;
26
- if (URLPattern) globalThis.URLPattern = URLPattern;
27
- }
28
-
29
- // Temporal (in no Node version) is installed as a LAZY global by the main
30
- // preload after this module runs — see preload.mjs (A37). Touching
31
- // globalThis.Temporal here would defeat that laziness, so we must not.
32
-
33
- // ── Stage 4 polyfills (native on Node 24+, missing on 22.x) ────────
34
-
35
- // RegExp.escape — spec-faithful port of the TC39 proposal (native on Node 24+),
36
- // so the 22.x floor behaves byte-for-byte like native: a leading digit/letter is
37
- // control-escaped, syntax chars are backslashed, control chars use \t\n\v\f\r, and
38
- // the "other punctuators" + whitespace set is hex-escaped. Verified byte-identical
39
- // to Node's native RegExp.escape across every ASCII char + leading/whitespace/
40
- // astral cases (so a concatenated `escape(s)` is safe too, not just
41
- // `new RegExp(escape(s))`). The earlier reduced-fidelity version only escaped the
42
- // syntax chars.
43
- if (typeof RegExp.escape !== "function") {
44
- const SYNTAX = new Set(["^", "$", "\\", ".", "*", "+", "?", "(", ")", "[", "]", "{", "}", "|", "/"]);
45
- const CONTROL = { "\t": "\\t", "\n": "\\n", "\v": "\\v", "\f": "\\f", "\r": "\\r" };
46
- // ASCII "other punctuators" the spec escapes by code, plus SPACE.
47
- const OTHER = new Set([..." ,-=<>#&!%:;@~'\"`"]);
48
- const isWhiteSpace = (cp) =>
49
- cp === 0x09 || cp === 0x0a || cp === 0x0b || cp === 0x0c || cp === 0x0d ||
50
- cp === 0x20 || cp === 0xa0 || cp === 0x1680 || (cp >= 0x2000 && cp <= 0x200a) ||
51
- cp === 0x2028 || cp === 0x2029 || cp === 0x202f || cp === 0x205f || cp === 0x3000 ||
52
- cp === 0xfeff;
53
- const hexEscape = (cp) => {
54
- if (cp <= 0xff) return "\\x" + cp.toString(16).padStart(2, "0");
55
- if (cp <= 0xffff) return "\\u" + cp.toString(16).padStart(4, "0");
56
- const h = cp - 0x10000;
57
- const hi = 0xd800 + (h >> 10);
58
- const lo = 0xdc00 + (h & 0x3ff);
59
- return "\\u" + hi.toString(16).padStart(4, "0") + "\\u" + lo.toString(16).padStart(4, "0");
60
- };
61
- const encode = (ch, cp) =>
62
- SYNTAX.has(ch)
63
- ? "\\" + ch
64
- : CONTROL[ch] ?? ((OTHER.has(ch) || isWhiteSpace(cp)) ? hexEscape(cp) : ch);
65
- RegExp.escape = (s) => {
66
- if (typeof s !== "string") throw new TypeError("RegExp.escape argument must be a string");
67
- const cps = [...s]; // iterate by code point (astral-safe)
68
- let out = "";
69
- for (let i = 0; i < cps.length; i++) {
70
- const ch = cps[i];
71
- const cp = ch.codePointAt(0);
72
- // A leading decimal-digit/ASCII-letter is control-escaped so a preceding `\`
73
- // in a concatenated pattern can't form an escape sequence.
74
- if (i === 0 && ((cp >= 0x30 && cp <= 0x39) || (cp >= 0x41 && cp <= 0x5a) || (cp >= 0x61 && cp <= 0x7a))) {
75
- out += "\\x" + cp.toString(16).padStart(2, "0");
76
- } else {
77
- out += encode(ch, cp);
78
- }
79
- }
80
- return out;
81
- };
82
- }
83
-
84
- // Error.isError (~95% fidelity — cross-realm internal-slot unreachable)
85
- if (typeof Error.isError !== "function") {
86
- Error.isError = (value) => {
87
- if (value == null || typeof value !== "object") return false;
88
- return value instanceof Error;
89
- };
90
- }
91
-
92
- // Promise.try
93
- if (typeof Promise.try !== "function") {
94
- Promise.try = (fn, ...args) => {
95
- return new Promise((resolve) => resolve(fn(...args)));
96
- };
97
- }
98
-
99
- // Float16Array (TC39 Stage 4, native on Node 24+; absent on our 22.x floor).
100
- // Installed from the spec-compliant @petamoriken/float16 polyfill (vendored,
101
- // preloaded by preload.mjs). It provides the full TypedArray method surface
102
- // (map/filter/subarray/set/reduce/…) and correct round-to-nearest-even,
103
- // including subnormals — unlike the prior hand-rolled Proxy shim, which had
104
- // ~30 methods missing and truncating/denormal-flushing conversion.
105
- //
106
- // INHERENT userland limitation (not fixable by any JS polyfill): a polyfilled
107
- // Float16Array isn't recognized by `ArrayBuffer.isView()` (it has no V8 internal
108
- // [[TypedArrayName]] slot). Code needing that check should use the polyfill's
109
- // `isFloat16Array`. See wiki/runtime/float16array-polyfill.md.
110
- if (typeof globalThis.Float16Array === "undefined") {
111
- const f16 = globalThis.__nubPreloaded?.float16;
112
- if (f16?.Float16Array) {
113
- globalThis.Float16Array = f16.Float16Array;
114
-
115
- if (typeof DataView.prototype.getFloat16 !== "function") {
116
- DataView.prototype.getFloat16 = function (offset, littleEndian) {
117
- return f16.getFloat16(this, offset, littleEndian);
118
- };
119
- DataView.prototype.setFloat16 = function (offset, value, littleEndian) {
120
- f16.setFloat16(this, offset, value, littleEndian);
121
- };
122
- }
123
-
124
- if (typeof Math.f16round !== "function") {
125
- Math.f16round = f16.f16round;
126
- }
127
- }
128
- }
129
-
130
- // ── navigator.locks (native on Node 24+, missing on 22.x) ──────────
131
- if (typeof globalThis.navigator?.locks === "undefined") {
132
- await import("./navigator-locks.mjs");
133
- }
134
-
135
- // ── Worker (browser-shape global, not in any Node) ──────────────────
136
- await import("./worker-polyfill.mjs");