@nubjs/loader 0.8.3 → 0.9.0
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/loader-entry.mjs +15 -5
- package/package.json +9 -9
- package/preload-async-hooks.mjs +8 -0
- package/preload-common.cjs +70 -16
- package/transform-core.mjs +137 -9
package/loader-entry.mjs
CHANGED
|
@@ -87,13 +87,16 @@ function isOwnLoaderToken(value) {
|
|
|
87
87
|
// delivery channel (execArgv or NODE_OPTIONS). Same two-channel scan as
|
|
88
88
|
// preload-common's computeForeignAsyncLoaderFlagPresent, but value-aware so our
|
|
89
89
|
// own token is excluded.
|
|
90
|
-
function foreignAsyncLoaderPresent() {
|
|
90
|
+
function foreignAsyncLoaderPresent(includeRequire = false) {
|
|
91
91
|
const tokens = [];
|
|
92
|
+
const flags = ["--import", "--loader", "--experimental-loader", "--experimental_loader"];
|
|
93
|
+
if (includeRequire) flags.push("--require", "-r");
|
|
92
94
|
const argv = Array.isArray(process.execArgv) ? process.execArgv : [];
|
|
93
95
|
for (let i = 0; i < argv.length; i++) {
|
|
94
96
|
const a = argv[i];
|
|
95
97
|
if (typeof a !== "string") continue;
|
|
96
|
-
|
|
98
|
+
if (includeRequire && a.startsWith("-r") && a.length > 2) tokens.push(a.slice(2));
|
|
99
|
+
for (const flag of flags) {
|
|
97
100
|
if (a === flag) {
|
|
98
101
|
if (typeof argv[i + 1] === "string") tokens.push(argv[i + 1]);
|
|
99
102
|
} else if (a.startsWith(`${flag}=`)) {
|
|
@@ -103,7 +106,9 @@ function foreignAsyncLoaderPresent() {
|
|
|
103
106
|
}
|
|
104
107
|
const opts = process.env.NODE_OPTIONS;
|
|
105
108
|
if (typeof opts === "string" && opts !== "") {
|
|
106
|
-
const re =
|
|
109
|
+
const re = includeRequire
|
|
110
|
+
? /(?:^|\s)(?:--(?:(?:experimental[-_])?(?:import|loader)|require)(?:=|\s)|-r(?:\s|=)?)("[^"]*"|\S*)/g
|
|
111
|
+
: /(?:^|\s)--(?:experimental[-_])?(?:import|loader)(?:=|\s)("[^"]*"|\S*)/g;
|
|
107
112
|
for (const match of opts.matchAll(re)) {
|
|
108
113
|
tokens.push((match[1] || "").replace(/^"|"$/g, ""));
|
|
109
114
|
}
|
|
@@ -153,11 +158,16 @@ export function arm({ esm = true, cjs = true } = {}) {
|
|
|
153
158
|
// loader (tsx, ts-node, an OTel ESM attach) would crash resolution. Register
|
|
154
159
|
// via the async path there instead so both loaders compose all-async — the same
|
|
155
160
|
// tier decision the CLI preload makes, minus counting ourselves as foreign.
|
|
156
|
-
const
|
|
161
|
+
const foreignLoaderFlagPresent = foreignAsyncLoaderPresent();
|
|
162
|
+
const forceAsync = common.nodeHookComposeBroken() && foreignLoaderFlagPresent;
|
|
157
163
|
|
|
158
164
|
if (wantEsm) {
|
|
159
165
|
if (hasSyncHooks && !forceAsync) {
|
|
160
|
-
|
|
166
|
+
// The standalone --import is our own loader, not a foreign async hook.
|
|
167
|
+
// Earlier --require preloads may have registered hooks before our detectors
|
|
168
|
+
// were installed. Decline the cache repair for them too, without assuming
|
|
169
|
+
// their hooks are async when choosing the composition tier above.
|
|
170
|
+
const { resolve, load } = common.makeHooks(core, watchReporting, foreignAsyncLoaderPresent(true));
|
|
161
171
|
module_.registerHooks({ resolve, load });
|
|
162
172
|
armed.esmMode = "sync";
|
|
163
173
|
} else {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nubjs/loader",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Standalone TypeScript loader for Node.js from the Nub project — TypeScript, JSX, tsconfig paths, and data-format imports through a native transform, registered the way tsx and ts-node are",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": "https://github.com/nubjs/nub",
|
|
@@ -49,13 +49,13 @@
|
|
|
49
49
|
"@oxc-project/runtime": "0.140.0"
|
|
50
50
|
},
|
|
51
51
|
"optionalDependencies": {
|
|
52
|
-
"@nubjs/loader-darwin-arm64": "0.
|
|
53
|
-
"@nubjs/loader-darwin-x64": "0.
|
|
54
|
-
"@nubjs/loader-linux-x64": "0.
|
|
55
|
-
"@nubjs/loader-linux-x64-musl": "0.
|
|
56
|
-
"@nubjs/loader-linux-arm64": "0.
|
|
57
|
-
"@nubjs/loader-linux-arm64-musl": "0.
|
|
58
|
-
"@nubjs/loader-win32-x64": "0.
|
|
59
|
-
"@nubjs/loader-win32-arm64": "0.
|
|
52
|
+
"@nubjs/loader-darwin-arm64": "0.9.0",
|
|
53
|
+
"@nubjs/loader-darwin-x64": "0.9.0",
|
|
54
|
+
"@nubjs/loader-linux-x64": "0.9.0",
|
|
55
|
+
"@nubjs/loader-linux-x64-musl": "0.9.0",
|
|
56
|
+
"@nubjs/loader-linux-arm64": "0.9.0",
|
|
57
|
+
"@nubjs/loader-linux-arm64-musl": "0.9.0",
|
|
58
|
+
"@nubjs/loader-win32-x64": "0.9.0",
|
|
59
|
+
"@nubjs/loader-win32-arm64": "0.9.0"
|
|
60
60
|
}
|
|
61
61
|
}
|
package/preload-async-hooks.mjs
CHANGED
|
@@ -30,6 +30,7 @@ import "./floor-builtin.mjs";
|
|
|
30
30
|
import {
|
|
31
31
|
TRANSPILE_EXTS, PLAIN_JS_EXTS, CLOBBER_MAP, dataExtsFor,
|
|
32
32
|
extname, isFileUrl, resolveSpec, loadTranspile, maybeTranspilePlainJs, loadData, loadTextImport, isDependency,
|
|
33
|
+
noteRuntimeV8FlagSource,
|
|
33
34
|
} from "./transform-core.mjs";
|
|
34
35
|
import { createRequire, isBuiltin } from "node:module";
|
|
35
36
|
import { existsSync } from "node:fs";
|
|
@@ -92,7 +93,14 @@ export async function resolve(specifier, context, nextResolve) {
|
|
|
92
93
|
}
|
|
93
94
|
|
|
94
95
|
// ── Load hook ───────────────────────────────────────────────────────
|
|
96
|
+
// Every result passes through the runtime-V8-flag scan before Node compiles it —
|
|
97
|
+
// see transform-core `noteRuntimeV8FlagSource`. Awaited here because the
|
|
98
|
+
// `nextLoad` branch of loadInner hands back a promise on this tier.
|
|
95
99
|
export async function load(url, context, nextLoad) {
|
|
100
|
+
return noteRuntimeV8FlagSource(await loadInner(url, context, nextLoad));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function loadInner(url, context, nextLoad) {
|
|
96
104
|
// Import Text (attribute-keyed): honor `with { type: "text" }` on ANY extension,
|
|
97
105
|
// checked BEFORE extension dispatch so `import s from "./c.yaml" with {type:"text"}`
|
|
98
106
|
// returns the raw text, not parsed YAML. shortCircuits, so Node never runs its own
|
package/preload-common.cjs
CHANGED
|
@@ -34,14 +34,47 @@ const { join, dirname, extname: pathExtname } = getBuiltin("node:path");
|
|
|
34
34
|
// 16 + Turbopack build died on `--js-defer-import-eval`. V8 parses these flags at
|
|
35
35
|
// startup, so dropping them here keeps the feature ON while restoring the execArgv a
|
|
36
36
|
// plain-Node user would have seen. Only flags NUB injected are removed; a user's own
|
|
37
|
-
// `v8Flags` stay visible, because those are the user's choice to reason about.
|
|
37
|
+
// `v8Flags` stay visible, because those are the user's choice to reason about. (The
|
|
38
|
+
// set is empty today — `--js-defer-import-eval` moved to a runtime flip, see
|
|
39
|
+
// transform-core `noteRuntimeV8FlagSource` — but the hygiene stays for any future
|
|
40
|
+
// argv-only row.)
|
|
41
|
+
// The flags have to be hidden on two boundaries, and no single channel spans both.
|
|
42
|
+
//
|
|
43
|
+
// The ENV VAR crosses a PROCESS boundary: the Rust spawn layer sets it on a Node it
|
|
44
|
+
// starts. Deleting it after use is what stops a descendant from hiding a flag its own
|
|
45
|
+
// user passed, so that hygiene stays.
|
|
46
|
+
//
|
|
47
|
+
// WORKER ENVIRONMENT DATA crosses a THREAD boundary, which the env var cannot. Node
|
|
48
|
+
// starts a worker from the process's REAL exec argv — flags and all — whatever the main
|
|
49
|
+
// thread filtered, so the worker has to filter again, and this preload runs there to do
|
|
50
|
+
// it. Three measured properties make this the right channel and an env copy the wrong
|
|
51
|
+
// one (verified on 18.19 and 26.7): it survives `new Worker(…, { env: {} })`, which
|
|
52
|
+
// REPLACES the environment outright and would otherwise strand that worker with the flags
|
|
53
|
+
// visible; it is transitive to nested workers; and it does NOT cross a process boundary,
|
|
54
|
+
// so a thread of this process is separated from a descendant structurally rather than by
|
|
55
|
+
// guesswork.
|
|
56
|
+
const ARGV_ONLY_FLAGS_KEY = "nub.argv-only-flags";
|
|
57
|
+
// A compiled artifact whose sealed graph cannot reach Worker or worker_threads has
|
|
58
|
+
// no second thread, so nothing can have written this channel and nobody can read
|
|
59
|
+
// what we publish to it — only the env var below carries flags into such a process.
|
|
60
|
+
// Asking anyway loads the builtin, and its subgraph is eight internal modules on
|
|
61
|
+
// every run. This module's EVALUATION is part of the compile preamble's static
|
|
62
|
+
// import graph, so no call-gating reaches that cost; the same signal and the same
|
|
63
|
+
// deliberately over-detecting build-time scan gate the preamble's own Worker branch.
|
|
64
|
+
// The record is published by the bootstrap's `--require`, ahead of any ESM here,
|
|
65
|
+
// and is absent outside a compiled artifact — so this reads false there and the
|
|
66
|
+
// load stays eager, which is the only behaviour an ordinary run ever had.
|
|
67
|
+
const workerless = compileBootstrap?.needsWorker === false;
|
|
38
68
|
try {
|
|
39
|
-
const
|
|
69
|
+
const workerThreads = workerless ? null : getBuiltin("node:worker_threads");
|
|
70
|
+
const fromEnv = process.env.__NUB_ARGV_ONLY_FLAGS;
|
|
71
|
+
const injectedArgvFlags = fromEnv || workerThreads?.getEnvironmentData(ARGV_ONLY_FLAGS_KEY);
|
|
40
72
|
if (injectedArgvFlags) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
73
|
+
if (fromEnv) {
|
|
74
|
+
delete process.env.__NUB_ARGV_ONLY_FLAGS;
|
|
75
|
+
workerThreads?.setEnvironmentData(ARGV_ONLY_FLAGS_KEY, fromEnv);
|
|
76
|
+
}
|
|
77
|
+
const injected = new Set(String(injectedArgvFlags).split(" ").filter(Boolean));
|
|
45
78
|
if (Array.isArray(process.execArgv)) {
|
|
46
79
|
process.execArgv = process.execArgv.filter((arg) => !injected.has(arg));
|
|
47
80
|
}
|
|
@@ -64,7 +97,15 @@ const VERSION_ENV = "__NUB_VERSION";
|
|
|
64
97
|
// feature-matrix `import-text` bands cover every release that KNOWS the flag: on a
|
|
65
98
|
// version nub steps aside on but does not inject for, the import falls through to
|
|
66
99
|
// Node's default loader and dies with ERR_UNKNOWN_FILE_EXTENSION (#688).
|
|
67
|
-
|
|
100
|
+
// Read LAZILY. The first touch of `process.allowedNodeEnvironmentFlags` materialises
|
|
101
|
+
// Node's entire accepted-flag set, and as a top-level const that cost 0.355 ms of
|
|
102
|
+
// this module's evaluation (child CPU, 200 runs, against a 0.000 ms control) on
|
|
103
|
+
// every nub process and inside every compiled artifact — where this module is a
|
|
104
|
+
// static import of the preamble, so nothing call-gated can reach it. Both readers
|
|
105
|
+
// sit inside the load hook's `type: "text"` arm, which most programs never take.
|
|
106
|
+
let __nativeImportText;
|
|
107
|
+
const nativeImportText = () =>
|
|
108
|
+
(__nativeImportText ??= process.allowedNodeEnvironmentFlags.has("--experimental-import-text"));
|
|
68
109
|
|
|
69
110
|
// ── data: URL unknown-format fidelity helpers ───────────────────────
|
|
70
111
|
// Mirror Node's internal/modules/esm/get_format.js so nub's sync registerHooks load
|
|
@@ -252,6 +293,7 @@ function cliAsyncLoaderPresent() {
|
|
|
252
293
|
if (
|
|
253
294
|
a === "--loader" || a.startsWith("--loader=") ||
|
|
254
295
|
a === "--experimental-loader" || a.startsWith("--experimental-loader=") ||
|
|
296
|
+
a === "--experimental_loader" || a.startsWith("--experimental_loader=") ||
|
|
255
297
|
a === "--import" || a.startsWith("--import=")
|
|
256
298
|
) { present = true; break; }
|
|
257
299
|
}
|
|
@@ -275,9 +317,10 @@ function cliAsyncLoaderPresent() {
|
|
|
275
317
|
// against a user async loader, and Node rejected the `commonjs-sync`+null-source pair
|
|
276
318
|
// (#669). That helper also excludes nub's OWN preload chainer, which rides NODE_OPTIONS
|
|
277
319
|
// as `--import`; a raw scan here would read it as a user loader and silently decline
|
|
278
|
-
// the relabel for every chained project.
|
|
279
|
-
|
|
280
|
-
|
|
320
|
+
// the relabel for every chained project. The standalone loader supplies its
|
|
321
|
+
// value-aware flag scan instead, excluding its own --import entrypoint too.
|
|
322
|
+
function userAsyncLoaderActive(foreignLoaderFlagPresent) {
|
|
323
|
+
return __userAsyncLoaderRegistered || foreignLoaderFlagPresent;
|
|
281
324
|
}
|
|
282
325
|
|
|
283
326
|
// The Node band where the async `module.register` loader's `resolveSync`/`loadSync`
|
|
@@ -328,7 +371,7 @@ function computeForeignAsyncLoaderFlagPresent() {
|
|
|
328
371
|
if (cliAsyncLoaderPresent()) return true; // execArgv channel
|
|
329
372
|
const opts = process.env.NODE_OPTIONS;
|
|
330
373
|
if (typeof opts !== "string" || opts === "") return false;
|
|
331
|
-
const re = /(?:^|\s)--(?:experimental-)?(?:import|loader)(?:=|\s)("[^"]*"|\S*)/g;
|
|
374
|
+
const re = /(?:^|\s)--(?:experimental[-_])?(?:import|loader)(?:=|\s)("[^"]*"|\S*)/g;
|
|
332
375
|
for (const match of opts.matchAll(re)) {
|
|
333
376
|
const value = (match[1] || "").replace(/^"|"$/g, "");
|
|
334
377
|
if (!NUB_CHAIN_MARKER.test(value)) return true;
|
|
@@ -593,7 +636,7 @@ function restoreSchemeOnlyBuiltinURL(result) {
|
|
|
593
636
|
}
|
|
594
637
|
}
|
|
595
638
|
|
|
596
|
-
function makeHooks(core, watchReporting) {
|
|
639
|
+
function makeHooks(core, watchReporting, foreignLoaderFlagPresent = foreignAsyncLoaderFlagPresent()) {
|
|
597
640
|
installUserHookDetector();
|
|
598
641
|
installUserAsyncLoaderDetector();
|
|
599
642
|
|
|
@@ -662,7 +705,15 @@ function makeHooks(core, watchReporting) {
|
|
|
662
705
|
}
|
|
663
706
|
}
|
|
664
707
|
|
|
708
|
+
// Every load result passes through the runtime-V8-flag scan BEFORE Node compiles
|
|
709
|
+
// it, whichever branch of loadInner produced it — see transform-core
|
|
710
|
+
// `noteRuntimeV8FlagSource`. A no-op (one null check) unless the spawn layer armed
|
|
711
|
+
// a flag for this Node.
|
|
665
712
|
function load(url, context, nextLoad) {
|
|
713
|
+
return core.noteRuntimeV8FlagSource(loadInner(url, context, nextLoad));
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function loadInner(url, context, nextLoad) {
|
|
666
717
|
const ext = core.extname(url);
|
|
667
718
|
|
|
668
719
|
// Watch mode: surface this file's nearest config files (tsconfig.json,
|
|
@@ -679,7 +730,7 @@ function makeHooks(core, watchReporting) {
|
|
|
679
730
|
|
|
680
731
|
// Import Text (attribute-keyed): honor `with { type: "text" }` on ANY extension,
|
|
681
732
|
// ahead of extension dispatch so `import s from "./c.yaml" with {type:"text"}`
|
|
682
|
-
// returns raw text, not parsed YAML. Where Node knows the flag (
|
|
733
|
+
// returns raw text, not parsed YAML. Where Node knows the flag (`nativeImportText()`
|
|
683
734
|
// — 24.19+ on the 24.x line, 26.5+ on 26.x) step aside and let Node's own
|
|
684
735
|
// textStrategy own it — nub injects --experimental-import-text there, so the
|
|
685
736
|
// additive "would plain Node + the flag do the same?" test holds and users get
|
|
@@ -700,8 +751,8 @@ function makeHooks(core, watchReporting) {
|
|
|
700
751
|
// the unknown-data-URL-format trap below instead of Node's own text answer.
|
|
701
752
|
// A non-`file:` URL on the polyfill tier falls through to `nextLoad` with
|
|
702
753
|
// every other unclaimed URL.
|
|
703
|
-
if (context?.importAttributes?.type === "text" && (
|
|
704
|
-
return
|
|
754
|
+
if (context?.importAttributes?.type === "text" && (nativeImportText() || core.isFileUrl(url))) {
|
|
755
|
+
return nativeImportText() ? nextLoad(url, context) : core.loadTextImport(url);
|
|
705
756
|
}
|
|
706
757
|
|
|
707
758
|
// A USER resolve hook (a ts-node/tsx-style transpiler registered AFTER nub's
|
|
@@ -845,7 +896,7 @@ function makeHooks(core, watchReporting) {
|
|
|
845
896
|
typeof url === "string" && url.startsWith("file:") &&
|
|
846
897
|
Array.isArray(context && context.conditions) &&
|
|
847
898
|
context.conditions.includes("import") &&
|
|
848
|
-
!__userHooksRegistered && !userAsyncLoaderActive()
|
|
899
|
+
!__userHooksRegistered && !userAsyncLoaderActive(foreignLoaderFlagPresent)
|
|
849
900
|
) {
|
|
850
901
|
return { ...r, format: "commonjs-sync" };
|
|
851
902
|
}
|
|
@@ -1717,6 +1768,9 @@ module.exports = {
|
|
|
1717
1768
|
preloadPolyfillPackages,
|
|
1718
1769
|
installTemporalGlobal,
|
|
1719
1770
|
installTemporalLazyGlobal,
|
|
1771
|
+
// The compiled preamble's lazy Temporal getter (compile-lazy-temporal.cjs)
|
|
1772
|
+
// installs the value on first access without reading the global back.
|
|
1773
|
+
installTemporalValue,
|
|
1720
1774
|
restoreCompileCacheEnv,
|
|
1721
1775
|
installCompiledChildProcess,
|
|
1722
1776
|
reenableUserCompileCache,
|
package/transform-core.mjs
CHANGED
|
@@ -785,16 +785,145 @@ export function maybeSweepCache() {
|
|
|
785
785
|
.catch(() => {});
|
|
786
786
|
}
|
|
787
787
|
|
|
788
|
+
// ── Runtime V8 flags (`Mitigation::RuntimeV8Flag`) ──────────────────
|
|
789
|
+
// A V8 syntax flag nub does NOT put on argv. The Rust spawn layer names it in
|
|
790
|
+
// `__NUB_RUNTIME_V8_FLAGS` (`<node-version> <flag>…`, flags.rs RUNTIME_V8_FLAGS_ENV)
|
|
791
|
+
// and both load hooks route every result through `noteRuntimeV8FlagSource`, which
|
|
792
|
+
// turns the flag on with `v8.setFlagsFromString` the first time a source uses the
|
|
793
|
+
// syntax. V8 consults such a flag per parse (`v8_flags.js_defer_import_eval` is read
|
|
794
|
+
// in the parser alone, with an empty bootstrapper hook) and Node runs V8 with
|
|
795
|
+
// `--no-freeze-flags-after-init`, so a flip made before the hook returns is in force
|
|
796
|
+
// when Node compiles that module — verified on 26.4.0 and 26.7.0, identical deferral
|
|
797
|
+
// to the argv flag.
|
|
798
|
+
//
|
|
799
|
+
// What it buys: a V8 flag that is non-default at STARTUP enters V8's flag hash, and
|
|
800
|
+
// the code cache Node embeds for its own internals is keyed on that hash, so every
|
|
801
|
+
// builtin compiled after startup (`node:http`, `crypto`, `zlib`, …) is rejected and
|
|
802
|
+
// rebuilt from source — ~20 ms for a program loading those, measured on 26.7.0. The
|
|
803
|
+
// flip charges that only to a program that actually uses the syntax, and only for
|
|
804
|
+
// the internals loaded after it. The flag also never appears in `process.execArgv`,
|
|
805
|
+
// where forwarding it into a Worker once killed a Next.js 16 + Turbopack build.
|
|
806
|
+
//
|
|
807
|
+
// Every Nub launch sets or removes the var, so a child that re-enters Nub carries its
|
|
808
|
+
// own decision; it is NOT deleted here, so a process that makes no such decision — a
|
|
809
|
+
// Worker, the `module.register` loader worker, a child spawned by absolute path —
|
|
810
|
+
// starts from a copy of this env and gets the feature too. Two guards close the gaps
|
|
811
|
+
// inheritance leaves: a polarity already on this process's own `process.execArgv`
|
|
812
|
+
// wins, either sign (V8 has the flag, or the user negated it, and the parent's signal
|
|
813
|
+
// must not override that); and the version stamp makes a descendant on a different
|
|
814
|
+
// Node (an inherited-NODE_OPTIONS grandchild) ignore a set computed for another
|
|
815
|
+
// binary, because a flag V8 does not know is an "Error: unrecognized flag" on stderr.
|
|
816
|
+
// `v8_flags` is process-global, so one flip from any thread serves every isolate.
|
|
817
|
+
// Node documents a post-init flag change as unsupported; for a flag the parser reads
|
|
818
|
+
// as one bool at the `import` token, the exposure is a benign race with a worker
|
|
819
|
+
// parsing concurrently, which sees the old value for that one parse.
|
|
820
|
+
//
|
|
821
|
+
// Detection is textual. The static form is always `import defer * as` — V8 allows
|
|
822
|
+
// `defer` with a namespace import only — so the pattern requires the `*`, which
|
|
823
|
+
// keeps prose that merely names the two words (a comment, a docs string) from
|
|
824
|
+
// arming the flag. Whitespace or comments between the tokens still match. A false
|
|
825
|
+
// positive that survives (the three tokens inside a string) only recreates the
|
|
826
|
+
// state every program had when the flag rode argv. The dynamic form
|
|
827
|
+
// `import.defer()` is NOT matched, on purpose: it aborts the process on every 26.x
|
|
828
|
+
// measured (a V8 fatal in Node's phase wiring), so a program that uses only that
|
|
829
|
+
// form keeps bare Node's catchable SyntaxError.
|
|
830
|
+
const TOKEN_GAP = String.raw`(?:\s|/\*[\s\S]*?\*/|//[^\n]*\n)`;
|
|
831
|
+
const IMPORT_DEFER_RE = new RegExp(String.raw`\bimport${TOKEN_GAP}+defer${TOKEN_GAP}*\*`);
|
|
832
|
+
const RUNTIME_V8_FLAG_DETECTORS = {
|
|
833
|
+
"--js-defer-import-eval": sourceUsesImportDefer,
|
|
834
|
+
};
|
|
835
|
+
// Formats whose source Node compiles as an ES module. `import defer` is module-only
|
|
836
|
+
// syntax, so a CommonJS, JSON or wasm result can never need the flip; a null format
|
|
837
|
+
// is still undecided and is scanned.
|
|
838
|
+
const ESM_FORMATS = new Set(["module", "module-typescript", "typescript"]);
|
|
839
|
+
|
|
840
|
+
// The source as a string, or null when a byte-level scan for `needle` misses — so a
|
|
841
|
+
// file that cannot match is never decoded.
|
|
842
|
+
function sourceText(source, needle) {
|
|
843
|
+
if (typeof source === "string") return source.includes(needle) ? source : null;
|
|
844
|
+
let buf = null;
|
|
845
|
+
if (ArrayBuffer.isView(source)) {
|
|
846
|
+
buf = Buffer.from(source.buffer, source.byteOffset, source.byteLength);
|
|
847
|
+
} else if (source instanceof ArrayBuffer) {
|
|
848
|
+
buf = Buffer.from(source);
|
|
849
|
+
}
|
|
850
|
+
return buf !== null && buf.includes(needle) ? buf.toString("utf8") : null;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// Whether `source` (string, Buffer, TypedArray or ArrayBuffer) carries a static
|
|
854
|
+
// `import defer` declaration.
|
|
855
|
+
export function sourceUsesImportDefer(source) {
|
|
856
|
+
const text = sourceText(source, "defer");
|
|
857
|
+
return text !== null && IMPORT_DEFER_RE.test(text);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// `-e`/`-p` code never passes through a load hook, so it is scanned once at
|
|
861
|
+
// arm time: the preload runs before Node compiles the eval string.
|
|
862
|
+
function evalSourceFromExecArgv(execArgv) {
|
|
863
|
+
if (!Array.isArray(execArgv)) return null;
|
|
864
|
+
for (let i = 0; i < execArgv.length; i++) {
|
|
865
|
+
const arg = execArgv[i];
|
|
866
|
+
if (typeof arg !== "string") continue;
|
|
867
|
+
if (arg === "-e" || arg === "--eval" || arg === "-p" || arg === "--print" || arg === "-pe" || arg === "-ep") {
|
|
868
|
+
return i + 1 < execArgv.length && typeof execArgv[i + 1] === "string" ? execArgv[i + 1] : null;
|
|
869
|
+
}
|
|
870
|
+
if (arg.startsWith("--eval=") || arg.startsWith("--print=")) return arg.slice(arg.indexOf("=") + 1);
|
|
871
|
+
}
|
|
872
|
+
return null;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// Flags still to turn on, or null once nothing is armed — the hot path is one null
|
|
876
|
+
// check per load.
|
|
877
|
+
let pendingRuntimeV8Flags = null;
|
|
878
|
+
|
|
879
|
+
function turnOnRuntimeV8Flag(flag) {
|
|
880
|
+
pendingRuntimeV8Flags.delete(flag);
|
|
881
|
+
if (pendingRuntimeV8Flags.size === 0) pendingRuntimeV8Flags = null;
|
|
882
|
+
try {
|
|
883
|
+
__getBuiltin("node:v8").setFlagsFromString(flag);
|
|
884
|
+
} catch {
|
|
885
|
+
// The module then fails exactly as it would on bare Node.
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// Route a load result through the runtime-flag scan and hand it back unchanged.
|
|
890
|
+
export function noteRuntimeV8FlagSource(result) {
|
|
891
|
+
if (pendingRuntimeV8Flags === null || result == null || result.source == null) return result;
|
|
892
|
+
if (result.format != null && !ESM_FORMATS.has(result.format)) return result;
|
|
893
|
+
for (const flag of [...pendingRuntimeV8Flags]) {
|
|
894
|
+
if (RUNTIME_V8_FLAG_DETECTORS[flag](result.source)) turnOnRuntimeV8Flag(flag);
|
|
895
|
+
}
|
|
896
|
+
return result;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
{
|
|
900
|
+
const raw = process.env.__NUB_RUNTIME_V8_FLAGS;
|
|
901
|
+
if (raw) {
|
|
902
|
+
const [stampedVersion, ...flags] = raw.split(" ").filter(Boolean);
|
|
903
|
+
const execArgv = Array.isArray(process.execArgv) ? process.execArgv : [];
|
|
904
|
+
const armed = stampedVersion === process.versions.node
|
|
905
|
+
? flags.filter((flag) =>
|
|
906
|
+
Object.hasOwn(RUNTIME_V8_FLAG_DETECTORS, flag) &&
|
|
907
|
+
!execArgv.includes(flag) && !execArgv.includes(`--no-${flag.slice(2)}`))
|
|
908
|
+
: [];
|
|
909
|
+
if (armed.length > 0) {
|
|
910
|
+
pendingRuntimeV8Flags = new Set(armed);
|
|
911
|
+
const evalSource = evalSourceFromExecArgv(execArgv);
|
|
912
|
+
if (evalSource !== null) noteRuntimeV8FlagSource({ format: "module", source: evalSource });
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
788
917
|
// ── Transpile ───────────────────────────────────────────────────────
|
|
789
918
|
// Transpile a TS/JSX file to JS, returning `{ format, source, shortCircuit }` in
|
|
790
919
|
// the shape both hook tiers hand back to Node. Format is detected (not derived
|
|
791
920
|
// from extension alone), so a CommonJS-syntax `.ts` is reported `commonjs` — the
|
|
792
921
|
// fix that makes `require()` of a TS file work on the compat tier, where Node's
|
|
793
922
|
// CJS translator loads it via this hook and keys on the returned format.
|
|
794
|
-
export function loadTranspile(url, ext) {
|
|
923
|
+
export function loadTranspile(url, ext, source) {
|
|
795
924
|
__ensureBuiltins();
|
|
796
925
|
const filePath = fileURLToPath(url);
|
|
797
|
-
|
|
926
|
+
source ??= readFileSync(filePath, "utf8");
|
|
798
927
|
const dir = dirname(filePath);
|
|
799
928
|
// The transform-relevant compilerOptions slice + the byte-for-byte cache-key
|
|
800
929
|
// component (`tsconfigHash`) both come from the native tsconfig reader.
|
|
@@ -895,7 +1024,7 @@ export function loadTranspile(url, ext) {
|
|
|
895
1024
|
// sites (the byte-parity boundary). JSX-in-`.js` is out of scope for the syntax
|
|
896
1025
|
// gate (lang is "ts", which does not parse JSX); use `.jsx`, or say so explicitly
|
|
897
1026
|
// with a `loader` entry, which takes the unconditional path below instead.
|
|
898
|
-
export function maybeTranspilePlainJs(url, ext) {
|
|
1027
|
+
export function maybeTranspilePlainJs(url, ext, source) {
|
|
899
1028
|
__ensureBuiltins();
|
|
900
1029
|
// An explicit `loader` entry pointing this extension at a code dialect moved it
|
|
901
1030
|
// into TRANSPILE_EXTS, which for every other member means "always compile". Only
|
|
@@ -906,11 +1035,10 @@ export function maybeTranspilePlainJs(url, ext) {
|
|
|
906
1035
|
// while the ESM path transpiles the same file on both tiers. The registration
|
|
907
1036
|
// loop deliberately skips `.js`/`.cjs` because this wrapper owns them, so there
|
|
908
1037
|
// is nothing else downstream to catch it.
|
|
909
|
-
if (TRANSPILE_EXTS.has(ext)) return loadTranspile(url, ext);
|
|
1038
|
+
if (TRANSPILE_EXTS.has(ext)) return loadTranspile(url, ext, source);
|
|
910
1039
|
const filePath = fileURLToPath(url);
|
|
911
|
-
let source;
|
|
912
1040
|
try {
|
|
913
|
-
source
|
|
1041
|
+
source ??= readFileSync(filePath, "utf8");
|
|
914
1042
|
} catch {
|
|
915
1043
|
// Unreadable here → let Node's loader surface its own error.
|
|
916
1044
|
return null;
|
|
@@ -921,10 +1049,10 @@ export function maybeTranspilePlainJs(url, ext) {
|
|
|
921
1049
|
return null; // no-op: Node's native loader handles it, byte-identical.
|
|
922
1050
|
}
|
|
923
1051
|
// Transformable: run the SAME pipeline as TS/JSX (target es2022 lowering, tsconfig,
|
|
924
|
-
// source maps, the Stage-3 decorator guard, format detection, cache)
|
|
925
|
-
//
|
|
1052
|
+
// source maps, the Stage-3 decorator guard, format detection, cache), reusing
|
|
1053
|
+
// the bytes already inspected by the gate.
|
|
926
1054
|
try {
|
|
927
|
-
return loadTranspile(url, ext);
|
|
1055
|
+
return loadTranspile(url, ext, source);
|
|
928
1056
|
} catch (err) {
|
|
929
1057
|
// #225: a plain-JS file the transformable verdict flagged (a `using` decl or
|
|
930
1058
|
// `v`-flag RegExp somewhere) but whose transform oxc then REJECTS — V8 tolerates
|