@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 +0 -0
- package/bin/nubx +0 -0
- package/package.json +1 -1
- package/runtime/addons/nub-native.node +0 -0
- package/runtime/floor-builtin.mjs +57 -0
- package/runtime/preload-async-hooks.mjs +10 -0
- package/runtime/preload-common.cjs +114 -18
- package/runtime/preload.mjs +24 -1
- package/runtime/transform-core.mjs +107 -29
- package/runtime/version.mjs +1 -1
- package/runtime/worker-polyfill.mjs +55 -21
package/bin/nub
CHANGED
|
Binary file
|
package/bin/nubx
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
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()`.
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
//
|
|
312
|
-
//
|
|
313
|
-
|
|
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
|
|
548
|
-
// grandchild's process.ppid).
|
|
549
|
-
//
|
|
550
|
-
//
|
|
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
|
-
|
|
553
|
-
const
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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(
|
|
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 { ...
|
|
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.
|
package/runtime/preload.mjs
CHANGED
|
@@ -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
|
-
|
|
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)
|
|
48
|
-
// imports
|
|
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
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
84
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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,
|
|
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");
|
package/runtime/version.mjs
CHANGED
|
@@ -9,4 +9,4 @@
|
|
|
9
9
|
// previously lived as a literal inside preload.mjs, which `make version` patched,
|
|
10
10
|
// while the worker carried a hand-maintained "…-compat" copy that `make version`
|
|
11
11
|
// never touched — a latent staleness bug this module closes.)
|
|
12
|
-
export const NUB_VERSION = "0.0.
|
|
12
|
+
export const NUB_VERSION = "0.0.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,
|
|
7
|
-
// `require(esm)` instantiates an ES module by walking its STATIC IMPORT
|
|
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
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
// `
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
// node:
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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();
|