@nubjs/nub-linux-arm64 0.0.31 → 0.0.33

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.0.31",
3
+ "version": "0.0.33",
4
4
  "description": "Nub binary for linux-arm64",
5
5
  "license": "MIT",
6
6
  "repository": "https://github.com/nubjs/nub",
Binary file
@@ -0,0 +1,57 @@
1
+ // Compat-tier floor bootstrap: threads `node:module`'s `createRequire` into the
2
+ // modules that fetch their `node:` builtins via `process.getBuiltinModule`
3
+ // (transform-core.mjs, worker-polyfill.mjs) on the narrow FLOOR where that API is
4
+ // absent — WITHOUT any globalThis surface.
5
+ //
6
+ // WHY those modules can't fetch the builtin themselves: both are loaded on the fast
7
+ // tier via Node's `require(esm)`, which instantiates an ES module by walking its
8
+ // STATIC IMPORT graph through whatever ESM loader chain is registered — including the
9
+ // USER's `--experimental-loader` / `module.register` hooks. A static `import {
10
+ // createRequire } from "node:module"` in either file therefore routed the builtin
11
+ // through the user chain, and a user resolve/load hook that rejects or rewrites
12
+ // `node:module` exploded nub's own load (observed against es-module/test-esm-example-
13
+ // loader and the loader-chaining corpus). So they fetch builtins via
14
+ // `process.getBuiltinModule` (synchronous, OFF the loader chain, no static import).
15
+ //
16
+ // `process.getBuiltinModule` only exists from Node 22.3 / 20.16 / 18.20.4. On the
17
+ // narrow FLOOR below that (18.19.x, 20.11–20.15, 22.0–22.2) it is `undefined`, so the
18
+ // floor needs another way to reach `node:module`'s `createRequire`. This file is that
19
+ // fallback: it holds the LONE static `import { createRequire } from "node:module"`
20
+ // and hands the value to transform-core / worker-polyfill through their module-scoped
21
+ // SETTERS (no globalThis surface — a `globalThis.__nub*` sentinel is the same brand
22
+ // leak as a NUB_* env var, enumerable in user code AND worker realms, so it is
23
+ // forbidden; this threading honors the same enumeration-invisibility contract every
24
+ // other nub polyfill keeps).
25
+ //
26
+ // WHY THIS IS LEAK-SAFE WHERE the static import in transform-core/worker-polyfill WAS
27
+ // NOT: this module is imported ONLY by the compat-tier entries (preload.mjs and
28
+ // preload-async-hooks.mjs), AHEAD of transform-core/worker-polyfill in their source
29
+ // order. The FAST tier (preload.cjs) loads those via `require(esm)` directly and never
30
+ // touches preload.mjs / preload-async-hooks.mjs — so this file's static `node:module`
31
+ // import never enters the fast-tier `require(esm)` graph, and the user loader chain
32
+ // can never observe it. On the compat tier the loader hooks run in nub's OWN worker
33
+ // (preload-async-hooks) or on the main thread before any user `--loader` could
34
+ // intercept a bare `node:` builtin, both off the user chain — so the static import is
35
+ // harmless exactly where it's reachable.
36
+ //
37
+ // IMPORT ORDERING (load-bearing): the compat entries import this file BEFORE
38
+ // transform-core/worker-polyfill, but ES modules evaluate the importEE before the
39
+ // importer's body — so transform-core's body has ALREADY run by the time the setter
40
+ // calls below fire (during THIS module's evaluation). That is fine: transform-core
41
+ // acquires its floor builtins lazily and `setBootstrapCreateRequire` triggers that
42
+ // acquisition immediately, so every binding is ready before the entry body and long
43
+ // before any hook fires. On Node WITH getBuiltinModule this file is a near no-op (the
44
+ // setters are called but those modules never consult `_bootstrapCreateRequire`).
45
+ import { createRequire } from "node:module";
46
+ import { setBootstrapCreateRequire as setTransformCoreCreateRequire } from "./transform-core.mjs";
47
+
48
+ // The floor's `createRequire`, exported so the compat entries (and any future floor
49
+ // consumer) can thread it elsewhere without re-importing `node:module` into their own
50
+ // static graph.
51
+ export { createRequire };
52
+
53
+ // Thread it into transform-core unconditionally (the setter no-ops the floor branch
54
+ // on Node WITH getBuiltinModule). worker-polyfill's setter is wired by the entries
55
+ // themselves, AFTER they import worker-polyfill, since worker-polyfill is loaded later
56
+ // in the entry's flow (via dynamic import) than this static import runs.
57
+ setTransformCoreCreateRequire(createRequire);
@@ -17,6 +17,16 @@
17
17
  // worker injects no watch hooks (watch IPC is main-thread only), so the core's
18
18
  // dependency reporters stay no-ops here, exactly as before the extraction.
19
19
 
20
+ // Floor bootstrap (Node < 22.3/20.16/18.20.4): threads node:module's createRequire
21
+ // into transform-core via a MODULE-SCOPE SETTER — never globalThis (brand boundary) —
22
+ // because transform-core has no process.getBuiltinModule on the floor. floor-builtin
23
+ // calls transform-core's setter during its own evaluation, so importing it FIRST
24
+ // (ESM evaluates imports in source order) means the value is threaded before any hook
25
+ // in this loader worker fires. No-op where getBuiltinModule exists. This worker runs
26
+ // OFF any user loader chain, so floor-builtin's static node:module import never leaks
27
+ // — see floor-builtin.mjs. (worker-polyfill is NOT loaded here: this is the dedicated
28
+ // loader worker, not a user realm, so it installs no browser globals.)
29
+ import "./floor-builtin.mjs";
20
30
  import {
21
31
  TRANSPILE_EXTS, DATA_EXTS,
22
32
  extname, resolveSpec, loadTranspile, loadData,
@@ -305,12 +305,18 @@ function installCjsRequireHooks(core, withClassicTranspile) {
305
305
  // to it. The one snag is that a registered customization hook makes Node thread a
306
306
  // `conditions` option that PnP rejects ("aren't supported by PnP yet
307
307
  // (conditions)"), so strip it first. The require/default condition PnP then
308
- // applies is exactly right for `require()`. (Stripping is a harmless no-op when
309
- // origResolveFilename is plain Node — i.e. not a PnP project.) This replaces the
310
- // former `pnpapi.resolveRequest` reimplementation: simpler, and with no
311
- // `findPnpApi` in the hot path there is no lookup-miss to leak a `conditions`
312
- // crash on Windows.
313
- if (options && "conditions" in options) {
308
+ // applies is exactly right for `require()`. This replaces the former
309
+ // `pnpapi.resolveRequest` reimplementation: simpler, and with no `findPnpApi` in
310
+ // the hot path there is no lookup-miss to leak a `conditions` crash on Windows.
311
+ //
312
+ // GATED ON PnP (`process.versions.pnp`). Off PnP the strip is NOT a harmless
313
+ // no-op: a user who passes custom `conditions` to require-side resolution via
314
+ // `module.registerHooks` (Node's module-hooks custom-conditions tests) relies on
315
+ // Node's own `_resolveFilename` honoring them, and unconditionally deleting the
316
+ // key silently dropped their conditions — breaking module-hooks/test-module-hooks-
317
+ // custom-conditions{,-cjs}. PnP is the only resolver that rejects `conditions`, so
318
+ // only strip when PnP is actually active; everywhere else conditions pass through.
319
+ if (process.versions.pnp && options && "conditions" in options) {
314
320
  options = { ...options };
315
321
  delete options.conditions;
316
322
  }
@@ -544,22 +550,44 @@ function wrapChildProcessCompileCache(cp) {
544
550
  };
545
551
 
546
552
  // Returns a possibly-rewritten options object with NODE_COMPILE_CACHE stripped
547
- // from its env, after writing the sentinel keyed on THIS process's pid (= the
548
- // grandchild's process.ppid). No-op unless the child env explicitly carries a
549
- // live (non-empty, != "0") NODE_COMPILE_CACHE an inherited (undefined env)
550
- // value was already stripped from this process by nub, so nothing to do there.
553
+ // from the child's env, after writing the sentinel keyed on THIS process's pid
554
+ // (= the grandchild's process.ppid). Two source cases, both stripped:
555
+ // EXPLICIT env (options.env carries NODE_COMPILE_CACHE) — strip from it.
556
+ // INHERITED env (no options.env, child inherits this process's env) when
557
+ // OUR process.env carries a live NODE_COMPILE_CACHE, materialize an explicit
558
+ // env from process.env with it removed. This case matters now that the
559
+ // DEFAULT (nub-owned) cache also travels via the sentinel and gets restored
560
+ // into process.env: a node child the user spawns with NO explicit env would
561
+ // otherwise inherit it and enable the cache AT BOOTSTRAP — before any preload
562
+ // gate — collapsing that child's V8 coverage if it runs under
563
+ // --experimental-test-coverage (the test-runner coverage-width fixtures, which
564
+ // are spawned with inherited env). Stripping here makes every node-target
565
+ // child boot cache-off; its own preload re-enables the cache post-bootstrap
566
+ // via reenableUserCompileCache UNLESS it's collecting coverage.
551
567
  const stripFromOptions = (options) => {
552
- if (!options || typeof options !== "object") return options;
553
- const env = options.env;
554
- if (!env || typeof env !== "object") return options;
555
- const dir = env.NODE_COMPILE_CACHE;
556
- if (!dir || dir === "0") return options;
568
+ const inheritedDir = process.env.NODE_COMPILE_CACHE;
569
+ const opts = options && typeof options === "object" ? options : {};
570
+ const env = opts.env;
571
+ if (env && typeof env === "object") {
572
+ const dir = env.NODE_COMPILE_CACHE;
573
+ if (!dir || dir === "0") return options;
574
+ try {
575
+ writeFileSync(join(tmpdirNoOs(), `nub-ccache-${process.pid}`), String(dir));
576
+ } catch { return options; }
577
+ const newEnv = { ...env };
578
+ delete newEnv.NODE_COMPILE_CACHE;
579
+ return { ...opts, env: newEnv };
580
+ }
581
+ // Inherited env path: only act when this process actually carries a live cache
582
+ // dir (otherwise there is nothing for the child to inherit and we leave the
583
+ // spawn's env untouched — `undefined` keeps Node's default inheritance).
584
+ if (!inheritedDir || inheritedDir === "0") return options;
557
585
  try {
558
- writeFileSync(join(tmpdirNoOs(), `nub-ccache-${process.pid}`), String(dir));
586
+ writeFileSync(join(tmpdirNoOs(), `nub-ccache-${process.pid}`), String(inheritedDir));
559
587
  } catch { return options; }
560
- const newEnv = { ...env };
588
+ const newEnv = { ...process.env };
561
589
  delete newEnv.NODE_COMPILE_CACHE;
562
- return { ...options, env: newEnv };
590
+ return { ...opts, env: newEnv };
563
591
  };
564
592
 
565
593
  // For (command, args?, options?) signatures the options object is the last arg
@@ -610,7 +638,75 @@ function wrapChildProcessCompileCache(cp) {
610
638
  };
611
639
  }
612
640
 
641
+ // True when V8 code coverage is active for THIS process — `--experimental-test-
642
+ // coverage` / a bare `--test-coverage*` flag in our own argv or execArgv, or a
643
+ // non-empty NODE_V8_COVERAGE env. A WARM compile cache makes V8 coverage imprecise
644
+ // (cached bytecode collapses/omits per-branch ranges, so a fixture's coverage
645
+ // JSON loses `functions[].ranges[1]` and the line/branch percentages drift from
646
+ // plain node). nub must therefore NOT (re)enable its compile cache for any process
647
+ // that is collecting coverage. This mirrors spawn.rs's coverage gate, but catches
648
+ // the case spawn.rs cannot see: a grandchild the USER's test code spawns directly
649
+ // (e.g. `spawnSync(execPath, [fixture], { env: { NODE_V8_COVERAGE } })`), which
650
+ // inherits nub's preload via NODE_OPTIONS but never goes through nub's Rust spawn
651
+ // path — so the gate has to live here too. (Observed against parallel/test-v8-
652
+ // coverage, test-runner-coverage-thresholds, and the test-runner coverage-width
653
+ // snapshot tests, all of which warm-cache then collect coverage in a child.)
654
+ function coverageActiveInProcess() {
655
+ if (process.env.NODE_V8_COVERAGE) return true;
656
+ const hasCovFlag = (a) =>
657
+ typeof a === "string" &&
658
+ (a === "--experimental-test-coverage" || a.startsWith("--test-coverage"));
659
+ return (process.execArgv || []).some(hasCovFlag) || (process.argv || []).some(hasCovFlag);
660
+ }
661
+
613
662
  function reenableUserCompileCache() {
663
+ // Coverage active: leave the compile cache OFF so V8 collects precise per-branch
664
+ // ranges (a warm cache collapses them — see coverageActiveInProcess). Two things
665
+ // matter, because Node's test runner spawns a SEPARATE isolated child to run the
666
+ // covered fixture and that child enables its cache at BOOTSTRAP from an inherited
667
+ // NODE_COMPILE_CACHE — too early for any preload gate to catch:
668
+ // (a) don't enableCompileCache in THIS process, and
669
+ // (b) set NODE_DISABLE_COMPILE_CACHE=1 in our env so EVERY descendant (incl. the
670
+ // test runner's isolated coverage child) boots with the cache off. Node
671
+ // honors NODE_DISABLE_COMPILE_CACHE at bootstrap and it travels via the env,
672
+ // reaching children nub never spawns itself. We do NOT clear NODE_COMPILE_CACHE
673
+ // (user code may read it); the disable var takes precedence at bootstrap.
674
+ // This is the JS half of the compile-cache/coverage fix; spawn.rs is the Rust half
675
+ // (it never sets the DEFAULT cache when it can see coverage in nub's own
676
+ // argv/NODE_OPTIONS/NODE_V8_COVERAGE).
677
+ //
678
+ // INTENTIONAL COVERAGE JUDGMENT CALL (b): NODE_DISABLE_COMPILE_CACHE=1 is set in
679
+ // process.env, so it propagates SUBTREE-WIDE for the rest of this coverage session —
680
+ // it is inherited by EVERY descendant, including non-coverage grandchildren the
681
+ // user's test code spawns mid-run (e.g. a build step a covered test shells out to).
682
+ // Those grandchildren therefore ALSO lose compile caching for the duration, even
683
+ // though they aren't themselves collecting coverage. We accept this: the disable var
684
+ // is the only mechanism that reaches the test runner's isolated coverage child
685
+ // (which boots its cache before any preload gate can fire), and scoping it more
686
+ // tightly than "the whole coverage subtree" isn't possible via an inherited env var.
687
+ // The cost is bounded (no caching during one coverage session) and self-healing (a
688
+ // grandchild spawned outside a coverage run is unaffected); surprising only to a
689
+ // user who expects an unrelated grandchild to keep caching while a parent collects
690
+ // coverage. Documented here and at the spawn.rs coverage branch (judgment call (a)).
691
+ if (coverageActiveInProcess()) {
692
+ const dir = process.env.NODE_COMPILE_CACHE;
693
+ if (!dir || dir === "0") {
694
+ // No explicit cache: keep coverage precise by disabling nub's default
695
+ // subtree-wide (the only mechanism that reaches the test runner's isolated
696
+ // coverage child, which boots its cache before any preload gate can fire).
697
+ process.env.NODE_DISABLE_COMPILE_CACHE = "1";
698
+ return;
699
+ }
700
+ // Explicit NODE_COMPILE_CACHE present: the user's choice wins over coverage
701
+ // precision (Colin, 2026-06-11) — same tradeoff they'd have on plain node.
702
+ // Narrow accepted caveat: a nub-DEFAULT dir restored from the sentinel is
703
+ // indistinguishable from a user dir here; that combination is reachable only
704
+ // when coverage is invisible to nub's own spawn (e.g. a c8-style grandchild
705
+ // setting NODE_V8_COVERAGE itself). Simple + documented beats provenance
706
+ // plumbing through the sentinel.
707
+ try { module_.enableCompileCache(dir); } catch {}
708
+ return;
709
+ }
614
710
  const dir = process.env.NODE_COMPILE_CACHE;
615
711
  // "0" is nub's disable signal (see transform-core); anything else is the user's
616
712
  // real cache dir, which we re-point Node's compile cache at for THEIR modules.
@@ -26,6 +26,18 @@
26
26
  // side-effecting import has to precede the transform-core import. See
27
27
  // compile-cache-restore.mjs.
28
28
  import "./compile-cache-restore.mjs";
29
+ // Floor bootstrap (Node < 22.3/20.16/18.20.4): threads node:module's createRequire
30
+ // into transform-core (and, below, worker-polyfill) via MODULE-SCOPE SETTERS — never
31
+ // globalThis (brand boundary) — so those fast-tier `require(esm)` modules can fetch
32
+ // node: builtins on the floor, where process.getBuiltinModule is absent. floor-builtin
33
+ // holds the lone static node:module import and calls transform-core's setter during
34
+ // ITS evaluation; importing it FIRST (ESM evaluates imports in source order) means
35
+ // that setter has fired before transform-core's hooks are used. No-op on Node with
36
+ // getBuiltinModule. See floor-builtin.mjs for why this is leak-safe. We capture its
37
+ // re-exported `createRequire` (the floor's, off any user chain) to thread into
38
+ // worker-polyfill below — that module is loaded later (dynamic import) so its setter
39
+ // can't be called from floor-builtin's own evaluation.
40
+ import { createRequire as floorCreateRequire } from "./floor-builtin.mjs";
29
41
  import module from "node:module";
30
42
  import { createRequire } from "node:module";
31
43
  import * as core from "./transform-core.mjs";
@@ -99,7 +111,18 @@ installSyncPolyfills(__preloadedPolyfills);
99
111
  if (typeof globalThis.navigator?.locks === "undefined") {
100
112
  await import("./navigator-locks.mjs");
101
113
  }
102
- await import("./worker-polyfill.mjs");
114
+ // worker-polyfill fetches its node: builtins via process.getBuiltinModule; on the
115
+ // floor (where that's absent) it needs the threaded createRequire BEFORE its install
116
+ // runs. The module no longer auto-installs on the floor, so thread the value in and
117
+ // install explicitly. On the fast/modern-compat tier the module already auto-installed
118
+ // at eval (getBuiltinModule present) and installWorkerPolyfill() is an idempotent
119
+ // no-op (its globalThis.Worker / worker-scope guards already fired). See
120
+ // floor-builtin.mjs + worker-polyfill.mjs for the brand-safe (globalThis-free)
121
+ // threading contract; this same path re-runs per worker realm (preload re-runs via
122
+ // NODE_OPTIONS), so each user worker thread gets the value threaded too.
123
+ const __workerPolyfill = await import("./worker-polyfill.mjs");
124
+ __workerPolyfill.setBootstrapCreateRequire(floorCreateRequire);
125
+ __workerPolyfill.installWorkerPolyfill();
103
126
 
104
127
  // ── Temporal: lazy global (A37) ─────────────────────────────────────
105
128
  common.installTemporalLazyGlobal(__require);
@@ -44,29 +44,76 @@
44
44
  // .getBuiltinModule` fetches node: builtins synchronously off the loader chain;
45
45
  // `createRequire(import.meta.url)` resolves the (now CommonJS-only) vendored
46
46
  // polyfills + the `@oxc-project/runtime` helpers from nub's distribution.
47
- // This file keeps its `export`s (it stays an ES module), but has ZERO static
48
- // imports, so `require(esm)` finds no dependency graph to route through the user.
47
+ // This file keeps its `export`s (it stays an ES module) but has ZERO static
48
+ // imports INCLUDING zero static `import` of any `node:` builtin so `require(esm)`
49
+ // of transform-core finds no dependency graph to route through the user loader.
50
+ // This is load-bearing, not cosmetic: transform-core previously carried a static
51
+ // `import { createRequire } from "node:module"`. That import sat in transform-core's
52
+ // static graph, so when nub's fast-tier preload.cjs does `require("./transform-core
53
+ // .mjs")` (a `require(esm)`), Node instantiated transform-core by walking its static
54
+ // import graph THROUGH the user's pre-registered `--experimental-loader` /
55
+ // `module.register` chain — and a user resolve hook that rejects or rewrites
56
+ // `node:module` (e.g. the example-loader that throws on any non-`./`/`../`/URL
57
+ // specifier) then exploded nub's own load, while resolve-count loaders saw a phantom
58
+ // `node:module` hit. (Observed against es-module/test-esm-example-loader,
59
+ // -loader-chaining, -initialization, -preserve-symlinks-not-found, and
60
+ // parallel/test-shadow-realm-custom-loaders.) The earlier comment here claimed the
61
+ // `node:module` import was "never routed through a user loader hook" — that was
62
+ // FALSE for the fast-tier `require(esm)` path, and is the bug this rewrite fixes.
63
+ //
49
64
  // `process.getBuiltinModule` (Node 22.3 / backported to 20.16 / 18.20.4) fetches a
50
- // node: builtin synchronously off the loader chain. On older floor Node (18.19,
51
- // 20.11–20.15, 22.0–22.2) it's `undefined` calling it threw `TypeError: process
52
- // .getBuiltinModule is not a function`, aborting every run. Fall back to a
53
- // createRequire bootstrapped from a single static `node:module` import. That import
54
- // is a BUILTIN specifier — resolved by Node natively, never routed through a user
55
- // loader hook (and resolved here at preload time, before any hook registers) — so
56
- // the "zero user-routable dependency graph for require(esm)" property still holds.
57
- import { createRequire as __bootstrapCreateRequire } from "node:module";
58
- const __getBuiltin =
59
- typeof process.getBuiltinModule === "function"
60
- ? (id) => process.getBuiltinModule(id)
61
- : ((__r) => (id) => __r(id))(__bootstrapCreateRequire(import.meta.url));
62
-
63
- const { createRequire } = __getBuiltin("node:module");
64
- const __require = createRequire(import.meta.url);
65
-
66
- const module = __getBuiltin("node:module");
67
- const { readFileSync, writeFileSync, mkdirSync, statSync } = __getBuiltin("node:fs");
68
- const { fileURLToPath, pathToFileURL } = __getBuiltin("node:url");
69
- const { join, dirname } = __getBuiltin("node:path");
65
+ // node: builtin synchronously OFF the loader chain, with no static import — so on
66
+ // the fast tier (22.15+, the only tier that loads transform-core via `require(esm)`,
67
+ // and where getBuiltinModule ALWAYS exists) there is nothing in the graph for a user
68
+ // loader to observe. On the narrow FLOOR below 22.3/20.16/18.20.4 (18.19.x,
69
+ // 20.11–20.15, 22.0–22.2) it's `undefined`; there, transform-core is loaded ONLY via
70
+ // static ESM `import` from the compat-tier entries (preload.mjs main thread /
71
+ // preload-async-hooks.mjs loader worker), both OFF any user loader chain — so the
72
+ // floor's `node:module` access cannot leak.
73
+ //
74
+ // BRAND BOUNDARY — the floor's `createRequire` is THREADED IN THROUGH MODULE SCOPE,
75
+ // not parked on `globalThis`. floor-builtin.mjs holds the lone static `import {
76
+ // createRequire } from "node:module"` and pushes the value in here via the
77
+ // `setBootstrapCreateRequire` setter below; nothing is ever written to the user's
78
+ // global object (a `globalThis.__nub*` sentinel is the same brand leak as a NUB_*
79
+ // env var — enumerable in user code AND worker realms — so it is forbidden). The
80
+ // floor's `node:module` import lives only in floor-builtin.mjs, which the fast tier
81
+ // never loads, so it never enters the fast-tier `require(esm)` graph.
82
+ //
83
+ // On the floor the threaded value isn't available at this module's top-level eval:
84
+ // the compat entry imports floor-builtin AHEAD of transform-core, but ES modules
85
+ // evaluate the importEE before the importer's body, so floor-builtin's setter call
86
+ // (made during ITS evaluation) lands AFTER transform-core's body has run. So on the
87
+ // floor every builtin is acquired LAZILY, on first hook use — by which point the
88
+ // setter has run. On the fast tier getBuiltinModule is present, so the builtins are
89
+ // acquired eagerly here at module eval (no setter, no floor path involved).
90
+ let _bootstrapCreateRequire = null;
91
+ // Called by floor-builtin.mjs (imported first by the compat entries) to hand in the
92
+ // floor's `createRequire` without any globalThis surface. NEVER called on the fast
93
+ // tier (getBuiltinModule covers it). The compat entries import floor-builtin ahead of
94
+ // transform-core, so by the time this fires transform-core's body has already
95
+ // evaluated (importEE before importer) — which is exactly why this setter also runs
96
+ // __ensureBuiltins() right now: it lands DURING floor-builtin's evaluation, before the
97
+ // entry's body and long before any hook fires, so every builtin binding is ready
98
+ // without ever consulting globalThis.
99
+ export function setBootstrapCreateRequire(fn) {
100
+ _bootstrapCreateRequire = fn;
101
+ __ensureBuiltins();
102
+ }
103
+
104
+ // node: builtins (lazy on the floor, eager on the fast tier — see above). On the
105
+ // floor `_bootstrapCreateRequire` is read at FETCH time (inside the thunk), never at
106
+ // definition time, so floor-builtin's setter has run before the first fetch fires.
107
+ function __getBuiltin(id) {
108
+ if (typeof process.getBuiltinModule === "function") return process.getBuiltinModule(id);
109
+ return _bootstrapCreateRequire(import.meta.url)(id);
110
+ }
111
+
112
+ // Builtin bindings + the native addon, populated by __ensureBuiltins(). They stay
113
+ // `let` (not `const`) because on the floor they are filled on first hook use rather
114
+ // than at module eval — see the lazy-vs-eager note above.
115
+ let createRequire, __require, module, readFileSync, writeFileSync, mkdirSync, statSync;
116
+ let fileURLToPath, pathToFileURL, join, dirname;
70
117
  // Nub's N-API addon — the in-process TS/JSX transpiler (`transform`,
71
118
  // `transformCached`, `detectModuleInfo`), the tsconfig reader + additive
72
119
  // TS-resolver (`loadTsconfig`, `resolveTs`), AND the data-format parsers
@@ -80,9 +127,27 @@ const { join, dirname } = __getBuiltin("node:path");
80
127
  // package, no static-import graph to route. nub now loads ZERO npm packages
81
128
  // internally, so the user ESM loader chain can never observe a nub dependency.
82
129
  let nubNative = null;
83
- for (const rel of ["./addons/nub-native.node", "../runtime/addons/nub-native.node"]) {
84
- try { nubNative = __require(fileURLToPath(new URL(rel, import.meta.url))); break; } catch {}
130
+
131
+ // Idempotent. Acquires the node: builtins + the native addon. Runs eagerly at module
132
+ // eval on the fast tier (getBuiltinModule present); on the floor it is invoked at the
133
+ // top of every exported entry point, where the threaded createRequire is ready.
134
+ let __builtinsReady = false;
135
+ function __ensureBuiltins() {
136
+ if (__builtinsReady) return;
137
+ __builtinsReady = true;
138
+ ({ createRequire } = __getBuiltin("node:module"));
139
+ __require = createRequire(import.meta.url);
140
+ module = __getBuiltin("node:module");
141
+ ({ readFileSync, writeFileSync, mkdirSync, statSync } = __getBuiltin("node:fs"));
142
+ ({ fileURLToPath, pathToFileURL } = __getBuiltin("node:url"));
143
+ ({ join, dirname } = __getBuiltin("node:path"));
144
+ for (const rel of ["./addons/nub-native.node", "../runtime/addons/nub-native.node"]) {
145
+ try { nubNative = __require(fileURLToPath(new URL(rel, import.meta.url))); break; } catch {}
146
+ }
85
147
  }
148
+ // Fast tier: getBuiltinModule is present, so acquire everything now (preserves the
149
+ // original eager-at-eval behavior). The floor defers to first-use — see above.
150
+ if (typeof process.getBuiltinModule === "function") __ensureBuiltins();
86
151
 
87
152
  // NOTE: the transpile-cache version component is no longer read here. nub's
88
153
  // version is baked into the native addon at compile time (`env!("CARGO_PKG_VERSION")`
@@ -455,27 +520,39 @@ function hasDecoratorSyntax(filePath, source, lang) {
455
520
  // nub-specific env var). Per wiki/runtime/transpile-cache.md (Colin 2026-05-18).
456
521
  const CACHE_DISABLED =
457
522
  process.permission?.has !== undefined || process.env.NODE_COMPILE_CACHE === "0";
523
+ // Resolved lazily (memoized) rather than at module eval, because on the floor the
524
+ // node:path builtins it needs aren't bound until __ensureBuiltins() runs on first
525
+ // hook use. `null` = disabled / no writable dir; `undefined` cacheDirResolved means
526
+ // "not yet computed".
458
527
  let cacheDir = null;
459
- if (!CACHE_DISABLED) {
528
+ let cacheDirResolved = false;
529
+ function getCacheDir() {
530
+ if (cacheDirResolved) return cacheDir;
531
+ cacheDirResolved = true;
532
+ if (CACHE_DISABLED) return cacheDir;
533
+ __ensureBuiltins();
460
534
  const base = process.env.XDG_CACHE_HOME || (process.env.HOME ? join(process.env.HOME, ".cache") : null);
461
535
  if (base) {
462
536
  cacheDir = join(base, "nub", "transpile");
463
537
  try { mkdirSync(cacheDir, { recursive: true }); } catch { cacheDir = null; }
464
538
  }
539
+ return cacheDir;
465
540
  }
466
541
 
467
542
  // ── Bounded-cache maintenance ───────────────────────────────────────
468
543
  const CACHE_MAX_BYTES = 512 * 1024 * 1024; // 512 MiB — bounds runaway growth, not normal use
469
544
  const SWEEP_INTERVAL_MS = 24 * 60 * 60 * 1000; // ≤ one sweep per day
470
545
  export function maybeSweepCache() {
471
- if (!cacheDir) return;
546
+ __ensureBuiltins();
547
+ const dir = getCacheDir();
548
+ if (!dir) return;
472
549
  // Workers inherit this preload (via execArgv); only the main thread sweeps.
473
550
  try {
474
551
  if (!__require("node:worker_threads").isMainThread) return;
475
552
  } catch {
476
553
  return;
477
554
  }
478
- const sentinel = join(cacheDir, ".sweep");
555
+ const sentinel = join(dir, ".sweep");
479
556
  const s = statSync(sentinel, { throwIfNoEntry: false });
480
557
  if (s && Date.now() - s.mtimeMs < SWEEP_INTERVAL_MS) return;
481
558
  try {
@@ -484,7 +561,7 @@ export function maybeSweepCache() {
484
561
  return;
485
562
  }
486
563
  import("./cache-evict.mjs")
487
- .then((m) => m.sweepCache(cacheDir, CACHE_MAX_BYTES))
564
+ .then((m) => m.sweepCache(dir, CACHE_MAX_BYTES))
488
565
  .catch(() => {});
489
566
  }
490
567
 
@@ -495,6 +572,7 @@ export function maybeSweepCache() {
495
572
  // fix that makes `require()` of a TS file work on the compat tier, where Node's
496
573
  // CJS translator loads it via this hook and keys on the returned format.
497
574
  export function loadTranspile(url, ext) {
575
+ __ensureBuiltins();
498
576
  const filePath = fileURLToPath(url);
499
577
  const source = readFileSync(filePath, "utf8");
500
578
  const dir = dirname(filePath);
@@ -555,7 +633,7 @@ export function loadTranspile(url, ext) {
555
633
  // JS enable/disable signal: native then skips all cache I/O and just transforms.
556
634
  const formatByte = format === "commonjs" ? "c" : "m";
557
635
  const result = nubNative.transformCached(
558
- filePath, source, opts, ext, tsconfigHash || "", pkgType || "", formatByte, cacheDir ?? undefined,
636
+ filePath, source, opts, ext, tsconfigHash || "", pkgType || "", formatByte, getCacheDir() ?? undefined,
559
637
  );
560
638
  if (result.errors.length > 0) {
561
639
  const details = result.errors.map((e) => e.codeframe || e.message).join("\n\n");
@@ -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.31";
12
+ export const NUB_VERSION = "0.0.33";
@@ -3,31 +3,48 @@
3
3
  // real MessageEvent/ErrorEvent, and URL-only constructor (Deno shape).
4
4
 
5
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
6
+ // `import`. This file is loaded via `require(esm)` from the preload (polyfills.cjs),
7
+ // and Node's `require(esm)` instantiates an ES module by walking its STATIC IMPORT
8
+ // graph through whatever ESM loader chain is registered — including the USER's
9
9
  // `--loader`/`register()` hooks. A static `import { Worker } from
10
10
  // "node:worker_threads"` therefore routes the builtin through the user chain; a
11
11
  // user load hook that returns SOURCE for node:worker_threads makes V8 see no
12
12
  // `Worker` export, so `new NodeWorker(...)` references an undefined binding and
13
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
- // `process.getBuiltinModule` doesn't exist below Node 22.3 / 20.16 / 18.20.4 (it
17
- // threw `TypeError: ... is not a function` there). Fall back to a createRequire
18
- // bootstrapped from a single static `node:module` import. The actual builtins below
19
- // are still fetched through `__getBuiltin` getBuiltinModule on new Node, CJS
20
- // `__require` on old and BOTH bypass the user ESM loader chain (the whole reason
21
- // this file avoids a static `import` of node:worker_threads). Only the bootstrap
22
- // touches node:module, which user loader hooks don't intercept (unlike the mockable
23
- // node:worker_threads the chaining test targets), so the bypass guarantee holds.
24
- import { createRequire as __bootstrapCreateRequire } from "node:module";
25
- const __getBuiltin =
26
- typeof process.getBuiltinModule === "function"
27
- ? (id) => process.getBuiltinModule(id)
28
- : ((__r) => (id) => __r(id))(__bootstrapCreateRequire(import.meta.url));
29
- const { Worker: NodeWorker, parentPort, isMainThread } = __getBuiltin("node:worker_threads");
30
- const { fileURLToPath } = __getBuiltin("node:url");
14
+ // .getBuiltinModule` fetches the real builtin synchronously off the loader graph,
15
+ // bypassing the user chain entirely — same fix transform-core.mjs uses.
16
+ //
17
+ // The bootstrap MUST avoid a static `node:module` import too: it has the IDENTICAL
18
+ // leak. The chaining corpus registers a user load hook (loader-load-foo-or-42.mjs)
19
+ // that rewrites the SOURCE of `node:module` so its compiled namespace no longer
20
+ // exports `createRequire` — so a static `import { createRequire } from "node:module"`
21
+ // here threw `does not provide an export named 'createRequire'` and crashed every
22
+ // run with that loader (the earlier comment claimed user hooks "don't intercept
23
+ // node:module" FALSE; this is the bug). So we use `process.getBuiltinModule` when
24
+ // present (fast tier + modern compat: no static import, nothing for the user chain
25
+ // to observe), and on the narrow FLOOR where it's absent (Node < 22.3/20.16/18.20.4,
26
+ // loaded only via the compat-tier entries OFF any user chain) the createRequire
27
+ // THREADED IN through `setBootstrapCreateRequire` below.
28
+ //
29
+ // BRAND BOUNDARY the floor's `createRequire` is threaded through MODULE SCOPE, never
30
+ // parked on `globalThis` (a `globalThis.__nub*` sentinel is the same brand leak as a
31
+ // NUB_* env var — enumerable in user code AND worker realms — so it is forbidden). On
32
+ // the floor this module is loaded ONLY via the compat-tier main-thread preload
33
+ // (preload.mjs), which imports floor-builtin first, then — AFTER importing this module
34
+ // — calls `setBootstrapCreateRequire(createRequire)` and `installWorkerPolyfill()`. So
35
+ // the install work is deferred (this module does NOT auto-run on the floor): its body
36
+ // fetches builtins, and on the floor those aren't reachable until the setter runs.
37
+ // On the fast tier (getBuiltinModule present) the install runs eagerly at module eval
38
+ // — see the auto-install at the bottom — so the existing side-effect-`require` call
39
+ // sites (preload.cjs, polyfills.cjs) are unchanged.
40
+ let _bootstrapCreateRequire = null;
41
+ export function setBootstrapCreateRequire(fn) {
42
+ _bootstrapCreateRequire = fn;
43
+ }
44
+ function __getBuiltin(id) {
45
+ if (typeof process.getBuiltinModule === "function") return process.getBuiltinModule(id);
46
+ return _bootstrapCreateRequire(import.meta.url)(id);
47
+ }
31
48
 
32
49
  // `ErrorEvent` only became a global in Node 26. On the 22/24 floor it is
33
50
  // undefined, so `new ErrorEvent(...)` below would throw a ReferenceError inside
@@ -62,7 +79,16 @@ function getErrorEventCtor() {
62
79
  });
63
80
  }
64
81
 
65
- if (typeof globalThis.Worker === "undefined") {
82
+ // Define the browser-shape `Worker` global (main thread) + the worker-side scope
83
+ // (self/postMessage/message wiring). Acquires its node: builtins on entry — on the
84
+ // floor that needs the threaded createRequire, so this runs only after the compat
85
+ // entry has called setBootstrapCreateRequire (or, on the fast tier, eagerly via the
86
+ // auto-install at the bottom).
87
+ export function installWorkerPolyfill() {
88
+ const { Worker: NodeWorker, parentPort, isMainThread } = __getBuiltin("node:worker_threads");
89
+ const { fileURLToPath } = __getBuiltin("node:url");
90
+
91
+ if (typeof globalThis.Worker === "undefined") {
66
92
  class Worker extends EventTarget {
67
93
  #worker;
68
94
 
@@ -288,4 +314,12 @@ if (!isMainThread && parentPort) {
288
314
  if (typeof scope.close !== "function") {
289
315
  defineGlobal("close", () => process.exit(0));
290
316
  }
317
+ }
291
318
  }
319
+
320
+ // Fast tier (and modern compat): getBuiltinModule is present, so the install needs no
321
+ // threaded createRequire — run it eagerly at module eval, preserving the side-effect-
322
+ // on-`require` contract the fast-tier call sites (preload.cjs, polyfills.cjs) rely on.
323
+ // On the FLOOR (getBuiltinModule absent) this is skipped; the compat main-thread
324
+ // preload calls setBootstrapCreateRequire(...) + installWorkerPolyfill() explicitly.
325
+ if (typeof process.getBuiltinModule === "function") installWorkerPolyfill();