@nubjs/loader 0.8.3 → 0.9.0-canary.20260909.509
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 +13 -0
- package/preload-common.cjs +131 -25
- package/transform-core.mjs +163 -13
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-canary.20260909.509",
|
|
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-canary.20260909.509",
|
|
53
|
+
"@nubjs/loader-darwin-x64": "0.9.0-canary.20260909.509",
|
|
54
|
+
"@nubjs/loader-linux-x64": "0.9.0-canary.20260909.509",
|
|
55
|
+
"@nubjs/loader-linux-x64-musl": "0.9.0-canary.20260909.509",
|
|
56
|
+
"@nubjs/loader-linux-arm64": "0.9.0-canary.20260909.509",
|
|
57
|
+
"@nubjs/loader-linux-arm64-musl": "0.9.0-canary.20260909.509",
|
|
58
|
+
"@nubjs/loader-win32-x64": "0.9.0-canary.20260909.509",
|
|
59
|
+
"@nubjs/loader-win32-arm64": "0.9.0-canary.20260909.509"
|
|
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, outerHookOwnsFormat,
|
|
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
|
|
@@ -106,6 +114,11 @@ export async function load(url, context, nextLoad) {
|
|
|
106
114
|
// fast-tier hook.
|
|
107
115
|
if (context?.importAttributes?.type === "text" && isFileUrl(url)) return loadTextImport(url);
|
|
108
116
|
const ext = extname(url);
|
|
117
|
+
// A user loader above nub's assigned this TypeScript file a bare module format
|
|
118
|
+
// and will transform it itself from the raw source (tsx's pattern; see
|
|
119
|
+
// `outerHookOwnsFormat`). This tier has no registration counter like the fast
|
|
120
|
+
// tier's and needs none: nothing but such a hook produces the bare form here.
|
|
121
|
+
if (outerHookOwnsFormat(context?.format, ext) && !isDependency(url)) return nextLoad(url, context);
|
|
109
122
|
// node_modules deps are NEVER transpiled (the byte-parity boundary). This guard is
|
|
110
123
|
// make-or-break now that TRANSPILE_EXTS includes `.js`/`.mjs`/`.cjs`: without it,
|
|
111
124
|
// the compat tier would route every dependency `.js` through oxc. (loadTranspile's
|
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
|
|
@@ -190,6 +231,10 @@ function installWatchReporting(core) {
|
|
|
190
231
|
// `type` (transpile — there is no user hook to do it, and Node's strip-only mode
|
|
191
232
|
// can't handle enums/namespaces). See makeHooks().load.
|
|
192
233
|
let __userHooksRegistered = false;
|
|
234
|
+
// Narrower: a user registration that brought a `load` hook, so it can transform
|
|
235
|
+
// what nub hands back. A resolve-only user hook that labels a `.ts` file with a
|
|
236
|
+
// bare `commonjs`/`module` still relies on nub as the transformer.
|
|
237
|
+
let __userLoadHookRegistered = false;
|
|
193
238
|
function installUserHookDetector() {
|
|
194
239
|
if (typeof module_.registerHooks !== "function") return;
|
|
195
240
|
const orig = module_.registerHooks;
|
|
@@ -197,7 +242,10 @@ function installUserHookDetector() {
|
|
|
197
242
|
let seen = 0;
|
|
198
243
|
const wrapped = function (...args) {
|
|
199
244
|
// Call #1 is nub's own preload registration; #2+ are user hooks.
|
|
200
|
-
if (seen >= 1)
|
|
245
|
+
if (seen >= 1) {
|
|
246
|
+
__userHooksRegistered = true;
|
|
247
|
+
if (args[0] && typeof args[0].load === "function") __userLoadHookRegistered = true;
|
|
248
|
+
}
|
|
201
249
|
seen += 1;
|
|
202
250
|
return orig.apply(this, args);
|
|
203
251
|
};
|
|
@@ -252,6 +300,7 @@ function cliAsyncLoaderPresent() {
|
|
|
252
300
|
if (
|
|
253
301
|
a === "--loader" || a.startsWith("--loader=") ||
|
|
254
302
|
a === "--experimental-loader" || a.startsWith("--experimental-loader=") ||
|
|
303
|
+
a === "--experimental_loader" || a.startsWith("--experimental_loader=") ||
|
|
255
304
|
a === "--import" || a.startsWith("--import=")
|
|
256
305
|
) { present = true; break; }
|
|
257
306
|
}
|
|
@@ -275,9 +324,10 @@ function cliAsyncLoaderPresent() {
|
|
|
275
324
|
// against a user async loader, and Node rejected the `commonjs-sync`+null-source pair
|
|
276
325
|
// (#669). That helper also excludes nub's OWN preload chainer, which rides NODE_OPTIONS
|
|
277
326
|
// 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
|
-
|
|
327
|
+
// the relabel for every chained project. The standalone loader supplies its
|
|
328
|
+
// value-aware flag scan instead, excluding its own --import entrypoint too.
|
|
329
|
+
function userAsyncLoaderActive(foreignLoaderFlagPresent) {
|
|
330
|
+
return __userAsyncLoaderRegistered || foreignLoaderFlagPresent;
|
|
281
331
|
}
|
|
282
332
|
|
|
283
333
|
// The Node band where the async `module.register` loader's `resolveSync`/`loadSync`
|
|
@@ -328,7 +378,7 @@ function computeForeignAsyncLoaderFlagPresent() {
|
|
|
328
378
|
if (cliAsyncLoaderPresent()) return true; // execArgv channel
|
|
329
379
|
const opts = process.env.NODE_OPTIONS;
|
|
330
380
|
if (typeof opts !== "string" || opts === "") return false;
|
|
331
|
-
const re = /(?:^|\s)--(?:experimental-)?(?:import|loader)(?:=|\s)("[^"]*"|\S*)/g;
|
|
381
|
+
const re = /(?:^|\s)--(?:experimental[-_])?(?:import|loader)(?:=|\s)("[^"]*"|\S*)/g;
|
|
332
382
|
for (const match of opts.matchAll(re)) {
|
|
333
383
|
const value = (match[1] || "").replace(/^"|"$/g, "");
|
|
334
384
|
if (!NUB_CHAIN_MARKER.test(value)) return true;
|
|
@@ -593,7 +643,7 @@ function restoreSchemeOnlyBuiltinURL(result) {
|
|
|
593
643
|
}
|
|
594
644
|
}
|
|
595
645
|
|
|
596
|
-
function makeHooks(core, watchReporting) {
|
|
646
|
+
function makeHooks(core, watchReporting, foreignLoaderFlagPresent = foreignAsyncLoaderFlagPresent()) {
|
|
597
647
|
installUserHookDetector();
|
|
598
648
|
installUserAsyncLoaderDetector();
|
|
599
649
|
|
|
@@ -655,14 +705,22 @@ function makeHooks(core, watchReporting) {
|
|
|
655
705
|
const source = readFileSync(path);
|
|
656
706
|
const pkgType = core.getPackageType(dirname(path));
|
|
657
707
|
const format = core.moduleFormatFor(ext, pkgType, path, source.toString("utf8"));
|
|
658
|
-
if (format === "commonjs") return { format: "commonjs", source: null, shortCircuit: true };
|
|
659
|
-
return { format, source, shortCircuit: true };
|
|
708
|
+
if (format === "commonjs") return { format: "commonjs", source: null, responseURL: url, shortCircuit: true };
|
|
709
|
+
return { format, source, responseURL: url, shortCircuit: true };
|
|
660
710
|
} catch {
|
|
661
711
|
return null;
|
|
662
712
|
}
|
|
663
713
|
}
|
|
664
714
|
|
|
715
|
+
// Every load result passes through the runtime-V8-flag scan BEFORE Node compiles
|
|
716
|
+
// it, whichever branch of loadInner produced it — see transform-core
|
|
717
|
+
// `noteRuntimeV8FlagSource`. A no-op (one null check) unless the spawn layer armed
|
|
718
|
+
// a flag for this Node.
|
|
665
719
|
function load(url, context, nextLoad) {
|
|
720
|
+
return core.noteRuntimeV8FlagSource(loadInner(url, context, nextLoad));
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function loadInner(url, context, nextLoad) {
|
|
666
724
|
const ext = core.extname(url);
|
|
667
725
|
|
|
668
726
|
// Watch mode: surface this file's nearest config files (tsconfig.json,
|
|
@@ -679,7 +737,7 @@ function makeHooks(core, watchReporting) {
|
|
|
679
737
|
|
|
680
738
|
// Import Text (attribute-keyed): honor `with { type: "text" }` on ANY extension,
|
|
681
739
|
// 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 (
|
|
740
|
+
// returns raw text, not parsed YAML. Where Node knows the flag (`nativeImportText()`
|
|
683
741
|
// — 24.19+ on the 24.x line, 26.5+ on 26.x) step aside and let Node's own
|
|
684
742
|
// textStrategy own it — nub injects --experimental-import-text there, so the
|
|
685
743
|
// additive "would plain Node + the flag do the same?" test holds and users get
|
|
@@ -700,8 +758,8 @@ function makeHooks(core, watchReporting) {
|
|
|
700
758
|
// the unknown-data-URL-format trap below instead of Node's own text answer.
|
|
701
759
|
// A non-`file:` URL on the polyfill tier falls through to `nextLoad` with
|
|
702
760
|
// every other unclaimed URL.
|
|
703
|
-
if (context?.importAttributes?.type === "text" && (
|
|
704
|
-
return
|
|
761
|
+
if (context?.importAttributes?.type === "text" && (nativeImportText() || core.isFileUrl(url))) {
|
|
762
|
+
return nativeImportText() ? nextLoad(url, context) : core.loadTextImport(url);
|
|
705
763
|
}
|
|
706
764
|
|
|
707
765
|
// A USER resolve hook (a ts-node/tsx-style transpiler registered AFTER nub's
|
|
@@ -722,7 +780,22 @@ function makeHooks(core, watchReporting) {
|
|
|
722
780
|
// user's outer hook, which does the real ESM->CJS conversion, matching Node.
|
|
723
781
|
// Native 'module-typescript'/'commonjs-typescript' formats still fall through to
|
|
724
782
|
// nub's transpile below, so normal augmentation is unchanged.
|
|
725
|
-
|
|
783
|
+
//
|
|
784
|
+
// tsx 4.2x writes the bare `commonjs`/`module` form instead of 'typescript'
|
|
785
|
+
// (`outerHookOwnsFormat`), and the hook that wrote it must also be able to
|
|
786
|
+
// transform what it gets back: a user registration WITH a load hook
|
|
787
|
+
// (`__userLoadHookRegistered`), or a loader registered through
|
|
788
|
+
// `module.register` (`userAsyncLoaderActive`), which the registerHooks counter
|
|
789
|
+
// cannot see and which nub has no way to inspect. A resolve-only user hook
|
|
790
|
+
// that labels a `.ts` file keeps nub as its transformer. The 'typescript'
|
|
791
|
+
// branch keeps its own narrower gate: a non-transpiling async loader (a
|
|
792
|
+
// telemetry `--import`) must not turn a bare 'typescript' from Node's own
|
|
793
|
+
// CJS loader into a step-aside.
|
|
794
|
+
if (context && (
|
|
795
|
+
(__userHooksRegistered && context.format === "typescript") ||
|
|
796
|
+
((__userLoadHookRegistered || userAsyncLoaderActive(foreignLoaderFlagPresent)) &&
|
|
797
|
+
core.outerHookOwnsFormat(context.format, ext) && !core.isDependency(url))
|
|
798
|
+
)) {
|
|
726
799
|
return nextLoad(url, context);
|
|
727
800
|
}
|
|
728
801
|
|
|
@@ -845,7 +918,7 @@ function makeHooks(core, watchReporting) {
|
|
|
845
918
|
typeof url === "string" && url.startsWith("file:") &&
|
|
846
919
|
Array.isArray(context && context.conditions) &&
|
|
847
920
|
context.conditions.includes("import") &&
|
|
848
|
-
!__userHooksRegistered && !userAsyncLoaderActive()
|
|
921
|
+
!__userHooksRegistered && !userAsyncLoaderActive(foreignLoaderFlagPresent)
|
|
849
922
|
) {
|
|
850
923
|
return { ...r, format: "commonjs-sync" };
|
|
851
924
|
}
|
|
@@ -976,10 +1049,36 @@ function installCjsRequireHooks(core, withClassicTranspile) {
|
|
|
976
1049
|
// missing. An explicit `require("dep/x/sub.cjs")` is unaffected either way — an
|
|
977
1050
|
// exact path is found by stat, without consulting the extension list at all.
|
|
978
1051
|
const NUB_ADDED_EXTS = [".ts", ".cts", ".mts", ".tsx", ".jsx", ".cjs"];
|
|
1052
|
+
// A `require.extensions` handler that is ALREADY registered for one of these
|
|
1053
|
+
// when this runs belongs to a transpiler the user chose, and it stays in charge
|
|
1054
|
+
// of that extension: nub's classic shim neither replaces it nor pre-judges its
|
|
1055
|
+
// files as ES modules at resolve time. Every `--require` runs before any `--import`, so on
|
|
1056
|
+
// the compat tier — where nub's own preload is an `--import` — tsx's
|
|
1057
|
+
// `--require`d preflight installs its `.ts` handler FIRST; nub then overwrote
|
|
1058
|
+
// it, and a file tsx would have compiled to CJS (mixed `import` + `require`,
|
|
1059
|
+
// the shape its ESM hook hands to the CJS loader) died in nub's handler as
|
|
1060
|
+
// "Cannot require() this file — it is an ES module". Node itself registers only
|
|
1061
|
+
// `.js`/`.json`/`.node`, so nothing but a user transpiler holds one of these
|
|
1062
|
+
// keys here.
|
|
1063
|
+
const foreignExts = new Set(
|
|
1064
|
+
[...core.TRANSPILE_EXTS, ...core.allDataExts(), ...NUB_ADDED_EXTS].filter((ext) =>
|
|
1065
|
+
ext !== ".js" && ext !== ".json" && ext !== ".node" && Object.hasOwn(module_._extensions, ext)),
|
|
1066
|
+
);
|
|
1067
|
+
// Ownership of a key is decided per lookup, never frozen at start-up: user code
|
|
1068
|
+
// may install or replace a handler after nub's preload (`--import tsx` runs
|
|
1069
|
+
// after every `--require`), and from then on that key is the user's own
|
|
1070
|
+
// widening of LOAD_AS_FILE, exactly as under plain Node plus their handler —
|
|
1071
|
+
// `require("dep/sub")` finding a dependency's `sub.ts` through it is the answer
|
|
1072
|
+
// to preserve. A key is nub's only while BOTH hold: nub introduced it (it was
|
|
1073
|
+
// not in `foreignExts` — a user's `.cjs` handler that nub merely wraps still
|
|
1074
|
+
// counts as theirs) and it still holds a function nub registered below
|
|
1075
|
+
// (`nubHandlers`). Anything else is not nub's to strip, nor to pre-judge.
|
|
1076
|
+
const nubHandlers = new Set();
|
|
1077
|
+
const nubOwnsExt = (ext) => !foreignExts.has(ext) && nubHandlers.has(module_._extensions[ext]);
|
|
979
1078
|
const withoutNubAddedExtensions = (fn) => {
|
|
980
1079
|
const saved = [];
|
|
981
1080
|
for (const ext of NUB_ADDED_EXTS) {
|
|
982
|
-
if (
|
|
1081
|
+
if (nubOwnsExt(ext)) {
|
|
983
1082
|
saved.push([ext, module_._extensions[ext]]);
|
|
984
1083
|
delete module_._extensions[ext];
|
|
985
1084
|
}
|
|
@@ -992,7 +1091,7 @@ function installCjsRequireHooks(core, withClassicTranspile) {
|
|
|
992
1091
|
};
|
|
993
1092
|
const isDepAddedExtHit = (filename) =>
|
|
994
1093
|
typeof filename === "string" &&
|
|
995
|
-
|
|
1094
|
+
nubOwnsExt(pathExtname(filename)) &&
|
|
996
1095
|
core.isDependency(pathToFileURL(filename).href);
|
|
997
1096
|
|
|
998
1097
|
module_._resolveFilename = function (request, parent, isMain, options) {
|
|
@@ -1015,7 +1114,8 @@ function installCjsRequireHooks(core, withClassicTranspile) {
|
|
|
1015
1114
|
// and 4 from `require.resolve` up to 22.14, but 22.15 and 22.16 pass 4 for both
|
|
1016
1115
|
// while still lacking native TS — and the translator crash is present on
|
|
1017
1116
|
// exactly those versions. A clean error beats an opaque crash, so this stays.
|
|
1018
|
-
if (withClassicTranspile &&
|
|
1117
|
+
if (withClassicTranspile && nubOwnsExt(pathExtname(resolved)) &&
|
|
1118
|
+
core.requireTargetIsEsm(resolved, pathExtname(resolved))) {
|
|
1019
1119
|
throw requireEsmError(resolved);
|
|
1020
1120
|
}
|
|
1021
1121
|
return resolved;
|
|
@@ -1144,6 +1244,7 @@ function installCjsRequireHooks(core, withClassicTranspile) {
|
|
|
1144
1244
|
if (core.TRANSPILE_EXTS.has(ext)) return transpileExtension(mod, filename);
|
|
1145
1245
|
return nativeJs.call(module_._extensions, mod, filename);
|
|
1146
1246
|
};
|
|
1247
|
+
nubHandlers.add(nubExtension);
|
|
1147
1248
|
|
|
1148
1249
|
// Registered for every extension either path may claim, so the dispatcher is
|
|
1149
1250
|
// reached at all; `nubExtension` then decides. Node's own `.js`/`.json`/`.node`
|
|
@@ -1164,7 +1265,7 @@ function installCjsRequireHooks(core, withClassicTranspile) {
|
|
|
1164
1265
|
// resolution identical across tiers.
|
|
1165
1266
|
const CODE_EXTS = new Set([".ts", ".cts", ".mts", ".tsx", ".jsx"]);
|
|
1166
1267
|
for (const ext of new Set([...core.TRANSPILE_EXTS, ...core.allDataExts()])) {
|
|
1167
|
-
if (ext === ".js" || ext === ".json" || ext === ".node") continue;
|
|
1268
|
+
if (ext === ".js" || ext === ".json" || ext === ".node" || foreignExts.has(ext)) continue;
|
|
1168
1269
|
Object.defineProperty(module_._extensions, ext, {
|
|
1169
1270
|
value: nubExtension,
|
|
1170
1271
|
enumerable: CODE_EXTS.has(ext),
|
|
@@ -1192,7 +1293,7 @@ function installCjsRequireHooks(core, withClassicTranspile) {
|
|
|
1192
1293
|
// (`nativeJs` is captured above, before the TS handlers are registered.)
|
|
1193
1294
|
for (const ext of [".js", ".cjs"]) {
|
|
1194
1295
|
const origExtension = module_._extensions[ext] || nativeJs;
|
|
1195
|
-
|
|
1296
|
+
const plainJsExtension = (mod, filename) => {
|
|
1196
1297
|
// (0) The project pointed this extension at a data loader (`{".js":"text"}`),
|
|
1197
1298
|
// which the ESM path honors. These two extensions are skipped by the
|
|
1198
1299
|
// registration loop above because THIS wrapper owns them and runs after it,
|
|
@@ -1215,6 +1316,8 @@ function installCjsRequireHooks(core, withClassicTranspile) {
|
|
|
1215
1316
|
}
|
|
1216
1317
|
return origExtension.call(module_._extensions, mod, filename); // (3)
|
|
1217
1318
|
};
|
|
1319
|
+
module_._extensions[ext] = plainJsExtension;
|
|
1320
|
+
nubHandlers.add(plainJsExtension);
|
|
1218
1321
|
}
|
|
1219
1322
|
}
|
|
1220
1323
|
|
|
@@ -1717,6 +1820,9 @@ module.exports = {
|
|
|
1717
1820
|
preloadPolyfillPackages,
|
|
1718
1821
|
installTemporalGlobal,
|
|
1719
1822
|
installTemporalLazyGlobal,
|
|
1823
|
+
// The compiled preamble's lazy Temporal getter (compile-lazy-temporal.cjs)
|
|
1824
|
+
// installs the value on first access without reading the global back.
|
|
1825
|
+
installTemporalValue,
|
|
1720
1826
|
restoreCompileCacheEnv,
|
|
1721
1827
|
installCompiledChildProcess,
|
|
1722
1828
|
reenableUserCompileCache,
|
package/transform-core.mjs
CHANGED
|
@@ -207,6 +207,20 @@ export const TRANSPILE_EXTS = new Set([".ts", ".tsx", ".mts", ".cts", ".jsx"]);
|
|
|
207
207
|
// `maybeTranspilePlainJs` gate); a no-op plain-JS file falls through to Node's
|
|
208
208
|
// native loader untouched, byte-identical. node_modules is excluded at the gate.
|
|
209
209
|
export const PLAIN_JS_EXTS = new Set([".js", ".mjs", ".cjs"]);
|
|
210
|
+
// A bare `commonjs`/`module` format on a file nub would transpile can only have
|
|
211
|
+
// been assigned by a hook layered ABOVE nub's. Node's resolver labels a `.ts` file
|
|
212
|
+
// `commonjs-typescript`/`module-typescript` (or `typescript` when the package has
|
|
213
|
+
// no `type`) and leaves `.jsx` unlabelled, and nub's own resolve returns no format;
|
|
214
|
+
// the bare form is what tsx's resolve hook writes (getFormatFromFileUrl), and its
|
|
215
|
+
// load hook then expects the RAW source back from `nextLoad` so it can run its own
|
|
216
|
+
// module-format transform — a mixed `import` + `require` file becomes CJS there,
|
|
217
|
+
// where nub's syntax detection would make it ESM and `require` undefined. Both
|
|
218
|
+
// tiers step aside on this signal, the fast tier only once the user registration
|
|
219
|
+
// is known to carry a load hook (preload-common.cjs). Plain JS is excluded: Node
|
|
220
|
+
// assigns those the bare form itself.
|
|
221
|
+
export function outerHookOwnsFormat(format, ext) {
|
|
222
|
+
return (format === "commonjs" || format === "module") && TRANSPILE_EXTS.has(ext) && !PLAIN_JS_EXTS.has(ext);
|
|
223
|
+
}
|
|
210
224
|
// The data loaders nub SHIPS — a runtime feature, not a project setting, so they stay
|
|
211
225
|
// in force inside node_modules too (see dataExtsFor).
|
|
212
226
|
const BUILTIN_DATA_EXTS = { ".jsonc": "jsonc", ".json5": "json5", ".toml": "toml", ".yaml": "yaml", ".yml": "yaml", ".txt": "txt" };
|
|
@@ -785,16 +799,145 @@ export function maybeSweepCache() {
|
|
|
785
799
|
.catch(() => {});
|
|
786
800
|
}
|
|
787
801
|
|
|
802
|
+
// ── Runtime V8 flags (`Mitigation::RuntimeV8Flag`) ──────────────────
|
|
803
|
+
// A V8 syntax flag nub does NOT put on argv. The Rust spawn layer names it in
|
|
804
|
+
// `__NUB_RUNTIME_V8_FLAGS` (`<node-version> <flag>…`, flags.rs RUNTIME_V8_FLAGS_ENV)
|
|
805
|
+
// and both load hooks route every result through `noteRuntimeV8FlagSource`, which
|
|
806
|
+
// turns the flag on with `v8.setFlagsFromString` the first time a source uses the
|
|
807
|
+
// syntax. V8 consults such a flag per parse (`v8_flags.js_defer_import_eval` is read
|
|
808
|
+
// in the parser alone, with an empty bootstrapper hook) and Node runs V8 with
|
|
809
|
+
// `--no-freeze-flags-after-init`, so a flip made before the hook returns is in force
|
|
810
|
+
// when Node compiles that module — verified on 26.4.0 and 26.7.0, identical deferral
|
|
811
|
+
// to the argv flag.
|
|
812
|
+
//
|
|
813
|
+
// What it buys: a V8 flag that is non-default at STARTUP enters V8's flag hash, and
|
|
814
|
+
// the code cache Node embeds for its own internals is keyed on that hash, so every
|
|
815
|
+
// builtin compiled after startup (`node:http`, `crypto`, `zlib`, …) is rejected and
|
|
816
|
+
// rebuilt from source — ~20 ms for a program loading those, measured on 26.7.0. The
|
|
817
|
+
// flip charges that only to a program that actually uses the syntax, and only for
|
|
818
|
+
// the internals loaded after it. The flag also never appears in `process.execArgv`,
|
|
819
|
+
// where forwarding it into a Worker once killed a Next.js 16 + Turbopack build.
|
|
820
|
+
//
|
|
821
|
+
// Every Nub launch sets or removes the var, so a child that re-enters Nub carries its
|
|
822
|
+
// own decision; it is NOT deleted here, so a process that makes no such decision — a
|
|
823
|
+
// Worker, the `module.register` loader worker, a child spawned by absolute path —
|
|
824
|
+
// starts from a copy of this env and gets the feature too. Two guards close the gaps
|
|
825
|
+
// inheritance leaves: a polarity already on this process's own `process.execArgv`
|
|
826
|
+
// wins, either sign (V8 has the flag, or the user negated it, and the parent's signal
|
|
827
|
+
// must not override that); and the version stamp makes a descendant on a different
|
|
828
|
+
// Node (an inherited-NODE_OPTIONS grandchild) ignore a set computed for another
|
|
829
|
+
// binary, because a flag V8 does not know is an "Error: unrecognized flag" on stderr.
|
|
830
|
+
// `v8_flags` is process-global, so one flip from any thread serves every isolate.
|
|
831
|
+
// Node documents a post-init flag change as unsupported; for a flag the parser reads
|
|
832
|
+
// as one bool at the `import` token, the exposure is a benign race with a worker
|
|
833
|
+
// parsing concurrently, which sees the old value for that one parse.
|
|
834
|
+
//
|
|
835
|
+
// Detection is textual. The static form is always `import defer * as` — V8 allows
|
|
836
|
+
// `defer` with a namespace import only — so the pattern requires the `*`, which
|
|
837
|
+
// keeps prose that merely names the two words (a comment, a docs string) from
|
|
838
|
+
// arming the flag. Whitespace or comments between the tokens still match. A false
|
|
839
|
+
// positive that survives (the three tokens inside a string) only recreates the
|
|
840
|
+
// state every program had when the flag rode argv. The dynamic form
|
|
841
|
+
// `import.defer()` is NOT matched, on purpose: it aborts the process on every 26.x
|
|
842
|
+
// measured (a V8 fatal in Node's phase wiring), so a program that uses only that
|
|
843
|
+
// form keeps bare Node's catchable SyntaxError.
|
|
844
|
+
const TOKEN_GAP = String.raw`(?:\s|/\*[\s\S]*?\*/|//[^\n]*\n)`;
|
|
845
|
+
const IMPORT_DEFER_RE = new RegExp(String.raw`\bimport${TOKEN_GAP}+defer${TOKEN_GAP}*\*`);
|
|
846
|
+
const RUNTIME_V8_FLAG_DETECTORS = {
|
|
847
|
+
"--js-defer-import-eval": sourceUsesImportDefer,
|
|
848
|
+
};
|
|
849
|
+
// Formats whose source Node compiles as an ES module. `import defer` is module-only
|
|
850
|
+
// syntax, so a CommonJS, JSON or wasm result can never need the flip; a null format
|
|
851
|
+
// is still undecided and is scanned.
|
|
852
|
+
const ESM_FORMATS = new Set(["module", "module-typescript", "typescript"]);
|
|
853
|
+
|
|
854
|
+
// The source as a string, or null when a byte-level scan for `needle` misses — so a
|
|
855
|
+
// file that cannot match is never decoded.
|
|
856
|
+
function sourceText(source, needle) {
|
|
857
|
+
if (typeof source === "string") return source.includes(needle) ? source : null;
|
|
858
|
+
let buf = null;
|
|
859
|
+
if (ArrayBuffer.isView(source)) {
|
|
860
|
+
buf = Buffer.from(source.buffer, source.byteOffset, source.byteLength);
|
|
861
|
+
} else if (source instanceof ArrayBuffer) {
|
|
862
|
+
buf = Buffer.from(source);
|
|
863
|
+
}
|
|
864
|
+
return buf !== null && buf.includes(needle) ? buf.toString("utf8") : null;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// Whether `source` (string, Buffer, TypedArray or ArrayBuffer) carries a static
|
|
868
|
+
// `import defer` declaration.
|
|
869
|
+
export function sourceUsesImportDefer(source) {
|
|
870
|
+
const text = sourceText(source, "defer");
|
|
871
|
+
return text !== null && IMPORT_DEFER_RE.test(text);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// `-e`/`-p` code never passes through a load hook, so it is scanned once at
|
|
875
|
+
// arm time: the preload runs before Node compiles the eval string.
|
|
876
|
+
function evalSourceFromExecArgv(execArgv) {
|
|
877
|
+
if (!Array.isArray(execArgv)) return null;
|
|
878
|
+
for (let i = 0; i < execArgv.length; i++) {
|
|
879
|
+
const arg = execArgv[i];
|
|
880
|
+
if (typeof arg !== "string") continue;
|
|
881
|
+
if (arg === "-e" || arg === "--eval" || arg === "-p" || arg === "--print" || arg === "-pe" || arg === "-ep") {
|
|
882
|
+
return i + 1 < execArgv.length && typeof execArgv[i + 1] === "string" ? execArgv[i + 1] : null;
|
|
883
|
+
}
|
|
884
|
+
if (arg.startsWith("--eval=") || arg.startsWith("--print=")) return arg.slice(arg.indexOf("=") + 1);
|
|
885
|
+
}
|
|
886
|
+
return null;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// Flags still to turn on, or null once nothing is armed — the hot path is one null
|
|
890
|
+
// check per load.
|
|
891
|
+
let pendingRuntimeV8Flags = null;
|
|
892
|
+
|
|
893
|
+
function turnOnRuntimeV8Flag(flag) {
|
|
894
|
+
pendingRuntimeV8Flags.delete(flag);
|
|
895
|
+
if (pendingRuntimeV8Flags.size === 0) pendingRuntimeV8Flags = null;
|
|
896
|
+
try {
|
|
897
|
+
__getBuiltin("node:v8").setFlagsFromString(flag);
|
|
898
|
+
} catch {
|
|
899
|
+
// The module then fails exactly as it would on bare Node.
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// Route a load result through the runtime-flag scan and hand it back unchanged.
|
|
904
|
+
export function noteRuntimeV8FlagSource(result) {
|
|
905
|
+
if (pendingRuntimeV8Flags === null || result == null || result.source == null) return result;
|
|
906
|
+
if (result.format != null && !ESM_FORMATS.has(result.format)) return result;
|
|
907
|
+
for (const flag of [...pendingRuntimeV8Flags]) {
|
|
908
|
+
if (RUNTIME_V8_FLAG_DETECTORS[flag](result.source)) turnOnRuntimeV8Flag(flag);
|
|
909
|
+
}
|
|
910
|
+
return result;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
{
|
|
914
|
+
const raw = process.env.__NUB_RUNTIME_V8_FLAGS;
|
|
915
|
+
if (raw) {
|
|
916
|
+
const [stampedVersion, ...flags] = raw.split(" ").filter(Boolean);
|
|
917
|
+
const execArgv = Array.isArray(process.execArgv) ? process.execArgv : [];
|
|
918
|
+
const armed = stampedVersion === process.versions.node
|
|
919
|
+
? flags.filter((flag) =>
|
|
920
|
+
Object.hasOwn(RUNTIME_V8_FLAG_DETECTORS, flag) &&
|
|
921
|
+
!execArgv.includes(flag) && !execArgv.includes(`--no-${flag.slice(2)}`))
|
|
922
|
+
: [];
|
|
923
|
+
if (armed.length > 0) {
|
|
924
|
+
pendingRuntimeV8Flags = new Set(armed);
|
|
925
|
+
const evalSource = evalSourceFromExecArgv(execArgv);
|
|
926
|
+
if (evalSource !== null) noteRuntimeV8FlagSource({ format: "module", source: evalSource });
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
|
|
788
931
|
// ── Transpile ───────────────────────────────────────────────────────
|
|
789
932
|
// Transpile a TS/JSX file to JS, returning `{ format, source, shortCircuit }` in
|
|
790
933
|
// the shape both hook tiers hand back to Node. Format is detected (not derived
|
|
791
934
|
// from extension alone), so a CommonJS-syntax `.ts` is reported `commonjs` — the
|
|
792
935
|
// fix that makes `require()` of a TS file work on the compat tier, where Node's
|
|
793
936
|
// CJS translator loads it via this hook and keys on the returned format.
|
|
794
|
-
export function loadTranspile(url, ext) {
|
|
937
|
+
export function loadTranspile(url, ext, source) {
|
|
795
938
|
__ensureBuiltins();
|
|
796
939
|
const filePath = fileURLToPath(url);
|
|
797
|
-
|
|
940
|
+
source ??= readFileSync(filePath, "utf8");
|
|
798
941
|
const dir = dirname(filePath);
|
|
799
942
|
// The transform-relevant compilerOptions slice + the byte-for-byte cache-key
|
|
800
943
|
// component (`tsconfigHash`) both come from the native tsconfig reader.
|
|
@@ -878,7 +1021,15 @@ export function loadTranspile(url, ext) {
|
|
|
878
1021
|
const details = result.errors.map((e) => e.codeframe || e.message).join("\n\n");
|
|
879
1022
|
throw new Error(`Transpile error in ${filePath}:\n${details}`);
|
|
880
1023
|
}
|
|
881
|
-
|
|
1024
|
+
// `responseURL` is what an OUTER user hook keys on. Node's default load sets it
|
|
1025
|
+
// to the file URL, and tsx's load hook takes its CommonJS branch only when the
|
|
1026
|
+
// result `nextLoad` hands back carries a `file:` responseURL — without it, tsx
|
|
1027
|
+
// re-transformed a `.ts` file nub had already emitted as CJS in its ESM branch,
|
|
1028
|
+
// and `require` was undefined at run time (`tsx script.ts` under `nub run`).
|
|
1029
|
+
// Node itself defaults a missing responseURL to the URL, so only a hook layered
|
|
1030
|
+
// above nub's can observe the difference; every file-URL result nub
|
|
1031
|
+
// short-circuits carries it for that reason.
|
|
1032
|
+
return { format: result.format, source: result.code, responseURL: url, shortCircuit: true };
|
|
882
1033
|
}
|
|
883
1034
|
|
|
884
1035
|
// Project-source plain JS (`.js`/`.mjs`/`.cjs`) gate. Returns a transpiled load
|
|
@@ -895,7 +1046,7 @@ export function loadTranspile(url, ext) {
|
|
|
895
1046
|
// sites (the byte-parity boundary). JSX-in-`.js` is out of scope for the syntax
|
|
896
1047
|
// gate (lang is "ts", which does not parse JSX); use `.jsx`, or say so explicitly
|
|
897
1048
|
// with a `loader` entry, which takes the unconditional path below instead.
|
|
898
|
-
export function maybeTranspilePlainJs(url, ext) {
|
|
1049
|
+
export function maybeTranspilePlainJs(url, ext, source) {
|
|
899
1050
|
__ensureBuiltins();
|
|
900
1051
|
// An explicit `loader` entry pointing this extension at a code dialect moved it
|
|
901
1052
|
// into TRANSPILE_EXTS, which for every other member means "always compile". Only
|
|
@@ -906,11 +1057,10 @@ export function maybeTranspilePlainJs(url, ext) {
|
|
|
906
1057
|
// while the ESM path transpiles the same file on both tiers. The registration
|
|
907
1058
|
// loop deliberately skips `.js`/`.cjs` because this wrapper owns them, so there
|
|
908
1059
|
// is nothing else downstream to catch it.
|
|
909
|
-
if (TRANSPILE_EXTS.has(ext)) return loadTranspile(url, ext);
|
|
1060
|
+
if (TRANSPILE_EXTS.has(ext)) return loadTranspile(url, ext, source);
|
|
910
1061
|
const filePath = fileURLToPath(url);
|
|
911
|
-
let source;
|
|
912
1062
|
try {
|
|
913
|
-
source
|
|
1063
|
+
source ??= readFileSync(filePath, "utf8");
|
|
914
1064
|
} catch {
|
|
915
1065
|
// Unreadable here → let Node's loader surface its own error.
|
|
916
1066
|
return null;
|
|
@@ -921,10 +1071,10 @@ export function maybeTranspilePlainJs(url, ext) {
|
|
|
921
1071
|
return null; // no-op: Node's native loader handles it, byte-identical.
|
|
922
1072
|
}
|
|
923
1073
|
// Transformable: run the SAME pipeline as TS/JSX (target es2022 lowering, tsconfig,
|
|
924
|
-
// source maps, the Stage-3 decorator guard, format detection, cache)
|
|
925
|
-
//
|
|
1074
|
+
// source maps, the Stage-3 decorator guard, format detection, cache), reusing
|
|
1075
|
+
// the bytes already inspected by the gate.
|
|
926
1076
|
try {
|
|
927
|
-
return loadTranspile(url, ext);
|
|
1077
|
+
return loadTranspile(url, ext, source);
|
|
928
1078
|
} catch (err) {
|
|
929
1079
|
// #225: a plain-JS file the transformable verdict flagged (a `using` decl or
|
|
930
1080
|
// `v`-flag RegExp somewhere) but whose transform oxc then REJECTS — V8 tolerates
|
|
@@ -998,7 +1148,7 @@ export function loadData(url, ext) {
|
|
|
998
1148
|
const parsed = dataValue(url, ext);
|
|
999
1149
|
|
|
1000
1150
|
if (parsed == null) {
|
|
1001
|
-
return { format: "module", source: "export default undefined;\n", shortCircuit: true };
|
|
1151
|
+
return { format: "module", source: "export default undefined;\n", responseURL: url, shortCircuit: true };
|
|
1002
1152
|
}
|
|
1003
1153
|
|
|
1004
1154
|
// Default export only. Data modules deliberately do NOT emit per-key named
|
|
@@ -1008,7 +1158,7 @@ export function loadData(url, ext) {
|
|
|
1008
1158
|
// default — `import cfg from "./c.yaml"; const { host } = cfg;` — which the
|
|
1009
1159
|
// `@nubjs/types` `Record<string, unknown>` default type makes sound.
|
|
1010
1160
|
const code = `export default ${JSON.stringify(parsed)};\n`;
|
|
1011
|
-
return { format: "module", source: code, shortCircuit: true };
|
|
1161
|
+
return { format: "module", source: code, responseURL: url, shortCircuit: true };
|
|
1012
1162
|
}
|
|
1013
1163
|
|
|
1014
1164
|
// Import Text: `import s from "./any.file" with { type: "text" }` → the raw file
|
|
@@ -1024,5 +1174,5 @@ export function loadData(url, ext) {
|
|
|
1024
1174
|
const __textDecoder = new TextDecoder();
|
|
1025
1175
|
export function loadTextImport(url) {
|
|
1026
1176
|
const text = __textDecoder.decode(readFileSync(fileURLToPath(url)));
|
|
1027
|
-
return { format: "module", source: `export default ${JSON.stringify(text)};\n`, shortCircuit: true };
|
|
1177
|
+
return { format: "module", source: `export default ${JSON.stringify(text)};\n`, responseURL: url, shortCircuit: true };
|
|
1028
1178
|
}
|