@nubjs/loader 0.0.0 → 0.8.2
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/LICENSE +21 -0
- package/README.md +48 -1
- package/cache-evict.mjs +180 -0
- package/floor-builtin.mjs +57 -0
- package/loader-addon-env.mjs +24 -0
- package/loader-entry.mjs +197 -0
- package/loader-esm.mjs +6 -0
- package/loader-platform.cjs +109 -0
- package/loader-register.cjs +33 -0
- package/loader-register.mjs +5 -0
- package/package.json +57 -3
- package/pnp-util.cjs +62 -0
- package/preload-async-hooks.mjs +133 -0
- package/preload-common.cjs +1725 -0
- package/transform-core.mjs +1028 -0
|
@@ -0,0 +1,1028 @@
|
|
|
1
|
+
// Nub transform core — the single source of truth shared by both hook tiers.
|
|
2
|
+
//
|
|
3
|
+
// runtime/preload.mjs (fast path, Node 22.15+, sync `module.registerHooks`) and
|
|
4
|
+
// the compat-tier loader worker (Node 18.19–22.14, async `module.register` →
|
|
5
|
+
// runtime/preload-async-hooks.mjs) both import every resolution + transpile
|
|
6
|
+
// primitive from here. The tier files own only the parts that genuinely differ:
|
|
7
|
+
// hook registration (sync vs async signatures), polyfill preloading, the
|
|
8
|
+
// Temporal lazy global, watch-mode IPC, and the compat-tier CJS `require()`
|
|
9
|
+
// shim. EVERYTHING about how a file is resolved and transpiled — extension
|
|
10
|
+
// probing, the `.js`→`.ts` swap, tsconfig `paths`, module-format detection,
|
|
11
|
+
// transform options (including `target: 'es2022'` `using`-lowering), the
|
|
12
|
+
// Stage-3 decorator guard, the on-disk cache, data-format imports, package
|
|
13
|
+
// clobbering — lives here, so the two tiers can never drift. (They used to:
|
|
14
|
+
// separate copies diverged on probe order, `target` lowering, the decorator
|
|
15
|
+
// guard, module-format detection, the Temporal clobber's named exports, and the
|
|
16
|
+
// reserved-export filter — every one a real compat bug. This module is the fix.)
|
|
17
|
+
//
|
|
18
|
+
// Side effects are confined to: loading the N-API addon (data parsers + the
|
|
19
|
+
// in-process TS/JSX transpiler), and reading/writing the transpile cache. There is
|
|
20
|
+
// no top-level hook registration here — importing this module never augments the
|
|
21
|
+
// realm; the tier files do that.
|
|
22
|
+
|
|
23
|
+
// EVERY node: builtin this module needs is pulled in via CJS `require()` / `process
|
|
24
|
+
// .getBuiltinModule` (below), NOT via static ESM `import`. This is load-bearing for
|
|
25
|
+
// loader compatibility (R11): nub loads transform-core through `require(esm)`, and
|
|
26
|
+
// Node's `require(esm)` instantiates the module by walking its STATIC IMPORT graph
|
|
27
|
+
// through whatever ESM loader hooks are registered — including the USER's
|
|
28
|
+
// `--loader`/`register()` chain. Static `import get-tsconfig`/`./version.mjs`/`node:*`
|
|
29
|
+
// here therefore once leaked nub's entire internal graph (transform-core,
|
|
30
|
+
// version.mjs, get-tsconfig, their transitive node_modules deps, and the node:
|
|
31
|
+
// builtins) THROUGH the user's resolve/load hooks, which observed and corrupted it
|
|
32
|
+
// (a user load hook returning `source: 1` for version.mjs, a strict loader throwing
|
|
33
|
+
// on a bare specifier — see test-esm-loader-chaining, -example-loader,
|
|
34
|
+
// -preserve-symlinks-not-found, test-shadow-realm-custom-loaders). Verified: a CJS
|
|
35
|
+
// `require()` of a builtin does NOT route through the ESM loader chain, so loading
|
|
36
|
+
// off it bypasses the user chain entirely. As of this migration the point is
|
|
37
|
+
// stronger: transform-core `require()`s ZERO npm packages — the transpiler, TS/JSX
|
|
38
|
+
// detection, tsconfig discovery/parse, the additive TS-resolver, AND the transpile
|
|
39
|
+
// cache are ALL native calls into nub's own N-API addon (loaded by absolute `.node`
|
|
40
|
+
// path, off the loader chain), and the version.mjs text read is gone (the cache
|
|
41
|
+
// version is baked into the addon). So the worst historical leaks — oxc-transform's
|
|
42
|
+
// and then get-tsconfig's graphs pulled through the user chain — are gone by
|
|
43
|
+
// construction; only node: builtins remain, fetched off the chain. `process
|
|
44
|
+
// .getBuiltinModule` fetches node: builtins synchronously off the loader chain;
|
|
45
|
+
// `createRequire(import.meta.url)` resolves the (now CommonJS-only) vendored
|
|
46
|
+
// polyfills + the `@oxc-project/runtime` helpers from nub's distribution.
|
|
47
|
+
// This file keeps its `export`s (it stays an ES module) but has ZERO static
|
|
48
|
+
// imports — 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
|
+
//
|
|
64
|
+
// `process.getBuiltinModule` (Node 22.3 / backported to 20.16 / 18.20.4) fetches a
|
|
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, realpathSync;
|
|
116
|
+
let fileURLToPath, pathToFileURL, join, dirname;
|
|
117
|
+
// Nub's N-API addon — the in-process TS/JSX transpiler (`transform`,
|
|
118
|
+
// `transformCached`, `detectModuleInfo`), the tsconfig reader + additive
|
|
119
|
+
// TS-resolver (`loadTsconfig`, `resolveTs`), AND the data-format parsers
|
|
120
|
+
// (`parseYaml`/`parseToml`/`parseJson5`/`parseJsonc`), all native. Loaded once
|
|
121
|
+
// per module instance (= once per thread: the main thread and the loader worker
|
|
122
|
+
// each import this module separately). It is a `.node` binary resolved by absolute
|
|
123
|
+
// path off this file's dir, so it never touches the ESM loader chain — the
|
|
124
|
+
// historical require(esm)-of-an-ESM-npm-package leak (oxc-transform, and before
|
|
125
|
+
// this migration get-tsconfig) is gone: transpilation, tsconfig discovery, the
|
|
126
|
+
// additive resolution, and the transpile cache are synchronous native calls, no JS
|
|
127
|
+
// package, no static-import graph to route. nub now loads ZERO npm packages
|
|
128
|
+
// internally, so the user ESM loader chain can never observe a nub dependency.
|
|
129
|
+
let nubNative = null;
|
|
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, realpathSync } = __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
|
+
}
|
|
147
|
+
// Standalone-loader distribution (`node --import <pkg>`): the addon rides a
|
|
148
|
+
// per-platform npm package rather than a sibling addons/ dir; the loader entry
|
|
149
|
+
// resolves it and hands the absolute path over via internal env plumbing
|
|
150
|
+
// (loader-platform.cjs ensureAddonEnv). LAST in probe order, deliberately: a
|
|
151
|
+
// nub-CLI process nested under the standalone loader inherits the env var, and
|
|
152
|
+
// probing it first would load the outer loader's (possibly differently-
|
|
153
|
+
// versioned) addon over the CLI's own bundled one.
|
|
154
|
+
if (!nubNative && process.env.__NUB_ADDON_PATH) {
|
|
155
|
+
try { nubNative = __require(process.env.__NUB_ADDON_PATH); } catch {}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// Fast tier: getBuiltinModule is present, so acquire everything now (preserves the
|
|
159
|
+
// original eager-at-eval behavior). The floor defers to first-use — see above.
|
|
160
|
+
if (typeof process.getBuiltinModule === "function") __ensureBuiltins();
|
|
161
|
+
|
|
162
|
+
// The resolved `nub.jsonc` snapshot. The Rust frontend resolves it once, after
|
|
163
|
+
// the final cwd is known, and transports it unchanged through nested shim
|
|
164
|
+
// launches, so every process in a run transpiles against the same config. This
|
|
165
|
+
// is internal process plumbing, not a user-facing environment knob.
|
|
166
|
+
let runtimeConfig = {};
|
|
167
|
+
try { runtimeConfig = JSON.parse(process.env.__NUB_RUNTIME_CONFIG || "{}"); } catch {}
|
|
168
|
+
const RUNTIME_LOADER = runtimeConfig.loader || {};
|
|
169
|
+
const RUNTIME_TSCONFIG = runtimeConfig.tsconfig || undefined;
|
|
170
|
+
// Transform-only TypeScript options may live directly in `nub.jsonc`. They
|
|
171
|
+
// override the selected/nearest tsconfig because the project runtime config is
|
|
172
|
+
// the more specific source for what Nub executes; `baseUrl`/`paths` stay in the
|
|
173
|
+
// tsconfig reader, where editors and the resolver share them.
|
|
174
|
+
const RUNTIME_COMPILER_OPTIONS = {};
|
|
175
|
+
for (const key of [
|
|
176
|
+
"jsx",
|
|
177
|
+
"jsxFactory",
|
|
178
|
+
"jsxFragmentFactory",
|
|
179
|
+
"jsxImportSource",
|
|
180
|
+
"experimentalDecorators",
|
|
181
|
+
"emitDecoratorMetadata",
|
|
182
|
+
]) {
|
|
183
|
+
if (runtimeConfig[key] !== null && runtimeConfig[key] !== undefined) {
|
|
184
|
+
RUNTIME_COMPILER_OPTIONS[key] = runtimeConfig[key];
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// NOTE: the transpile-cache version component is no longer read here. nub's
|
|
189
|
+
// version is baked into the native addon at compile time (`env!("CARGO_PKG_VERSION")`
|
|
190
|
+
// in nub-native's cache.rs), which `make version` keeps in lockstep with
|
|
191
|
+
// runtime/version.mjs and Cargo.toml — so the cache key's version component lives
|
|
192
|
+
// natively now, and this file no longer needs to read version.mjs.
|
|
193
|
+
|
|
194
|
+
// ── Constants ───────────────────────────────────────────────────────
|
|
195
|
+
// TS/JSX exts ALWAYS transform (type-stripping is required), so they live in
|
|
196
|
+
// TRANSPILE_EXTS — the set every dispatch site checks to route a file to
|
|
197
|
+
// loadTranspile. Plain JS (.js/.mjs/.cjs) is DELIBERATELY NOT here: a plain-JS file
|
|
198
|
+
// is transpiled ONLY when it carries transformable syntax (`using`/`await using`,
|
|
199
|
+
// `v`-flag RegExp, decorators), and a no-op plain-JS file must take Node's OWN load
|
|
200
|
+
// path BYTE-FOR-BYTE — putting it in TRANSPILE_EXTS would route every `.js`/`.cjs`
|
|
201
|
+
// through nub's hook and change native CJS/ESM behavior (the `commonjs-sync` relabel,
|
|
202
|
+
// require.cache, the require-of-ESM-syntax-.cjs error). So plain JS is handled by a
|
|
203
|
+
// SEPARATE narrow path (`maybeTranspilePlainJs`) that fires only for transformable
|
|
204
|
+
// files and is a no-op (returns null) otherwise — see PLAIN_JS_EXTS below.
|
|
205
|
+
export const TRANSPILE_EXTS = new Set([".ts", ".tsx", ".mts", ".cts", ".jsx"]);
|
|
206
|
+
// Project-source plain JS. Routed to the transpiler ONLY when transformable (the
|
|
207
|
+
// `maybeTranspilePlainJs` gate); a no-op plain-JS file falls through to Node's
|
|
208
|
+
// native loader untouched, byte-identical. node_modules is excluded at the gate.
|
|
209
|
+
export const PLAIN_JS_EXTS = new Set([".js", ".mjs", ".cjs"]);
|
|
210
|
+
// The data loaders nub SHIPS — a runtime feature, not a project setting, so they stay
|
|
211
|
+
// in force inside node_modules too (see dataExtsFor).
|
|
212
|
+
const BUILTIN_DATA_EXTS = { ".jsonc": "jsonc", ".json5": "json5", ".toml": "toml", ".yaml": "yaml", ".yml": "yaml", ".txt": "txt" };
|
|
213
|
+
// The built-ins with this project's `loader` config layered on top: an extension
|
|
214
|
+
// pointed at a TS/JSX dialect moves to TRANSPILE_EXTS, anything else becomes (or
|
|
215
|
+
// overrides) a data loader.
|
|
216
|
+
const PROJECT_DATA_EXTS = { ...BUILTIN_DATA_EXTS };
|
|
217
|
+
for (const [ext, loader] of Object.entries(RUNTIME_LOADER)) {
|
|
218
|
+
if (loader === "ts" || loader === "tsx" || loader === "jsx") {
|
|
219
|
+
delete PROJECT_DATA_EXTS[ext];
|
|
220
|
+
TRANSPILE_EXTS.add(ext);
|
|
221
|
+
} else {
|
|
222
|
+
TRANSPILE_EXTS.delete(ext);
|
|
223
|
+
PROJECT_DATA_EXTS[ext] = loader === "text" ? "txt" : loader;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
export const TS_PARENT_EXTS = new Set([".ts", ".tsx", ".mts", ".cts"]);
|
|
227
|
+
|
|
228
|
+
// Which data-loader map governs `url`. A project's `loader` config must not reach code
|
|
229
|
+
// the project didn't write — `{".json": "text"}` would otherwise turn every dependency's
|
|
230
|
+
// JSON import into a string, and pointing a built-in extension at a transpile loader
|
|
231
|
+
// would DELETE a loader a dependency relies on. Same project/dependency boundary the
|
|
232
|
+
// TRANSPILE_EXTS and PLAIN_JS_EXTS dispatches draw with `!isNodeModules`, but as a map
|
|
233
|
+
// SWAP rather than a bail: the built-in half must keep serving deps. Both the dispatch
|
|
234
|
+
// sites and loadData() route through here, so "does this load?" and "as what?" can
|
|
235
|
+
// never disagree.
|
|
236
|
+
export function dataExtsFor(url) {
|
|
237
|
+
return isNodeModules(url) ? BUILTIN_DATA_EXTS : PROJECT_DATA_EXTS;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Packages resolved from Nub's distribution, not the user's.
|
|
241
|
+
export const VENDORED_PACKAGES = new Set(["@oxc-project/runtime"]);
|
|
242
|
+
|
|
243
|
+
// Built-in modules provided by Nub (resolved to files in this distribution).
|
|
244
|
+
// connect() sockets deferred per design decision — "sockets" specifier not clobbered.
|
|
245
|
+
export const BUILTIN_MODULES = new Map();
|
|
246
|
+
|
|
247
|
+
// Package clobbering: specifiers that resolve to a synthetic module re-exporting
|
|
248
|
+
// the native global instead of the userland package.
|
|
249
|
+
export const CLOBBER_MAP = new Map([
|
|
250
|
+
// Reading globalThis.Temporal triggers the lazy getter the tier file installs,
|
|
251
|
+
// which loads the polyfill by resolved path — that load is what installs
|
|
252
|
+
// Date.prototype.toTemporalInstant, so Temporal MUST be read first.
|
|
253
|
+
// @js-temporal/polyfill exports { Temporal, Intl, toTemporalInstant }; mirror
|
|
254
|
+
// all three so `import { Temporal, Intl, toTemporalInstant } from ...` binds.
|
|
255
|
+
["@js-temporal/polyfill", () => `const T = globalThis.Temporal; export default T; export const Temporal = T; export const Intl = globalThis.Intl; export const toTemporalInstant = Date.prototype.toTemporalInstant;`],
|
|
256
|
+
["urlpattern-polyfill", () => `export const URLPattern = globalThis.URLPattern;`],
|
|
257
|
+
["abort-controller", () => `export const AbortController = globalThis.AbortController; export const AbortSignal = globalThis.AbortSignal; export default globalThis.AbortController;`],
|
|
258
|
+
]);
|
|
259
|
+
|
|
260
|
+
// ── Watch-mode hooks (injected by the main-thread tier) ─────────────
|
|
261
|
+
// `nub watch` needs config files (tsconfig.json, package.json) and `.env*` —
|
|
262
|
+
// which are not in any import graph — surfaced to Node's FilesWatcher. The main
|
|
263
|
+
// thread (preload.mjs) injects reporters; the loader worker injects nothing
|
|
264
|
+
// (watch IPC is main-thread only), so these default to no-ops.
|
|
265
|
+
let _reportDep = null;
|
|
266
|
+
let _reportEnvDir = null;
|
|
267
|
+
export function setWatchHooks({ reportDep, reportEnvDir } = {}) {
|
|
268
|
+
if (reportDep) _reportDep = reportDep;
|
|
269
|
+
if (reportEnvDir) _reportEnvDir = reportEnvDir;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ── tsconfig + package-type caches ──────────────────────────────────
|
|
273
|
+
// tsconfig discovery / parse / `extends` resolution + the `paths` matcher all
|
|
274
|
+
// happen natively (nub-native `loadTsconfig`, the get-tsconfig@4.14.0 port). This
|
|
275
|
+
// JS wrapper exists only to (a) memoize per importer-dir — native ALSO memoizes,
|
|
276
|
+
// but a JS-side Map skips the napi boundary on a hit and lets watch-mode report
|
|
277
|
+
// the dep exactly once per dir — and (b) surface the resolved tsconfig path to the
|
|
278
|
+
// watch FilesWatcher. The returned shape exposes the transform-relevant
|
|
279
|
+
// `compilerOptions` slice and the `tsconfigHash` cache-key component; the `paths`
|
|
280
|
+
// matcher lives entirely in native (`resolveTs` runs it), so there is no JS matcher.
|
|
281
|
+
const tsconfigCache = new Map();
|
|
282
|
+
export function getTsconfigForDir(dir) {
|
|
283
|
+
if (tsconfigCache.has(dir)) return tsconfigCache.get(dir);
|
|
284
|
+
// { path: string|null, compilerOptions: object|null, tsconfigHash: string }
|
|
285
|
+
const result = nubNative
|
|
286
|
+
? nubNative.loadTsconfig(dir, RUNTIME_TSCONFIG)
|
|
287
|
+
: { path: null, compilerOptions: null, tsconfigHash: "" };
|
|
288
|
+
const compilerOptions = Object.keys(RUNTIME_COMPILER_OPTIONS).length > 0
|
|
289
|
+
? { ...(result.compilerOptions || {}), ...RUNTIME_COMPILER_OPTIONS }
|
|
290
|
+
: result.compilerOptions;
|
|
291
|
+
const resolved = { ...result, compilerOptions };
|
|
292
|
+
tsconfigCache.set(dir, resolved);
|
|
293
|
+
if (resolved.path) _reportDep?.(resolved.path);
|
|
294
|
+
return resolved;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// The NEAREST package.json's `type` decides the format of ambiguous extensions
|
|
298
|
+
// (.ts/.tsx/.jsx, like Node's .js). The nearest one wins even when its `type`
|
|
299
|
+
// is absent — Node does not skip a typeless package.json to find a typed
|
|
300
|
+
// ancestor — so we stop at the first package.json found. Returns "module",
|
|
301
|
+
// "commonjs", or undefined.
|
|
302
|
+
const packageTypeCache = new Map();
|
|
303
|
+
export function getPackageType(dir) {
|
|
304
|
+
if (packageTypeCache.has(dir)) return packageTypeCache.get(dir);
|
|
305
|
+
let type;
|
|
306
|
+
let current = dir;
|
|
307
|
+
for (;;) {
|
|
308
|
+
const pkgPath = join(current, "package.json");
|
|
309
|
+
if (fileExists(pkgPath)) {
|
|
310
|
+
// Keep the runtime package-type read aligned with the Rust tsconfig reader:
|
|
311
|
+
// Windows editors and PowerShell may prefix valid JSON with one UTF-8 BOM.
|
|
312
|
+
try { type = JSON.parse(readFileSync(pkgPath, "utf8").replace(/^\uFEFF/, "")).type; } catch {}
|
|
313
|
+
// Watch this package.json (a `type`/script edit should restart) and the
|
|
314
|
+
// `.env*` files alongside it (the package root is where they live).
|
|
315
|
+
_reportDep?.(pkgPath);
|
|
316
|
+
_reportEnvDir?.(current);
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
const parent = dirname(current);
|
|
320
|
+
if (parent === current) break;
|
|
321
|
+
current = parent;
|
|
322
|
+
}
|
|
323
|
+
packageTypeCache.set(dir, type);
|
|
324
|
+
return type;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ── Filesystem helpers ──────────────────────────────────────────────
|
|
328
|
+
// Is this a URL whose bytes nub may read off disk? Every branch of either load
|
|
329
|
+
// hook that claims a module ends in `fileURLToPath` + `readFileSync`, so `file:`
|
|
330
|
+
// is the whole answer. A load hook sees whatever scheme resolution produced:
|
|
331
|
+
// `node:`, `data:`, and — because a user `module.register` loader may serve any
|
|
332
|
+
// protocol it likes — `custom://x.js`, `byop://1/index.mjs`, an http-loader's
|
|
333
|
+
// `https://…/x.js`. None of those are nub's to claim.
|
|
334
|
+
export function isFileUrl(url) {
|
|
335
|
+
return typeof url === "string" && url.startsWith("file:");
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export function extname(url) {
|
|
339
|
+
// Report an extension ONLY for a `file:` URL: the extension is what dispatches
|
|
340
|
+
// both load hooks into their transpile/data branches, and a non-`file:` URL that
|
|
341
|
+
// merely ENDS in something extension-shaped used to enter them anyway, where the
|
|
342
|
+
// unguarded `fileURLToPath` threw ERR_INVALID_URL_SCHEME — masking Node's own
|
|
343
|
+
// ERR_UNSUPPORTED_ESM_URL_SCHEME and killing every custom-protocol ESM loader
|
|
344
|
+
// that plain Node runs fine. `data:` was the first face of this (its payload is
|
|
345
|
+
// INLINE, so a trailing `//x.ts` comment or a sourceMappingURL reads as an
|
|
346
|
+
// extension); testing for `file:` positively covers it and every other scheme at
|
|
347
|
+
// once, and — unlike a "does this look like a scheme" test — cannot mistake a
|
|
348
|
+
// Windows drive letter for one.
|
|
349
|
+
if (!isFileUrl(url)) return "";
|
|
350
|
+
const path = url.includes("?") ? url.slice(0, url.indexOf("?")) : url;
|
|
351
|
+
const dot = path.lastIndexOf(".");
|
|
352
|
+
return dot === -1 ? "" : path.slice(dot);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export function isNodeModules(url) {
|
|
356
|
+
return url.includes("/node_modules/") || url.includes("\\node_modules\\");
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// "Is this really a dependency?" — the ONE definition every gate that decides how a
|
|
360
|
+
// file LOADS uses, so the resolve step and each load step cannot disagree about the
|
|
361
|
+
// same file. (Watch-mode config reporting still uses the plain check: it asks which
|
|
362
|
+
// files to watch, not how they load.)
|
|
363
|
+
//
|
|
364
|
+
// A `/node_modules/` segment normally settles it: Node realpaths a resolved module,
|
|
365
|
+
// so the path we are handed IS the real one. Under `--preserve-symlinks` it does not
|
|
366
|
+
// — a workspace package symlinked into node_modules keeps that segment while its
|
|
367
|
+
// files genuinely live in the project, and calling it a dependency would refuse the
|
|
368
|
+
// TypeScript that is its build output. The stat therefore happens only under the
|
|
369
|
+
// flag, and only for paths that already look like a dependency; the default path
|
|
370
|
+
// stays the pure substring test it always was.
|
|
371
|
+
export function isDependency(url) {
|
|
372
|
+
if (!isNodeModules(url)) return false;
|
|
373
|
+
if (!PRESERVE_SYMLINKS) return true;
|
|
374
|
+
try {
|
|
375
|
+
return isNodeModules(pathToFileURL(realpathSync(fileURLToPath(url))).href);
|
|
376
|
+
} catch {
|
|
377
|
+
return true; // unreadable: trust the literal path
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export function fileExists(filePath) {
|
|
382
|
+
const s = statSync(filePath, { throwIfNoEntry: false });
|
|
383
|
+
return s !== undefined && s.isFile();
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function safeRequireResolve(specifier) {
|
|
387
|
+
try { return __require.resolve(specifier); } catch { return null; }
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export function barePkg(specifier) {
|
|
391
|
+
return specifier.startsWith("@")
|
|
392
|
+
? specifier.split("/").slice(0, 2).join("/")
|
|
393
|
+
: specifier.split("/")[0];
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// ── Resolution ──────────────────────────────────────────────────────
|
|
397
|
+
// The ADDITIVE TS resolution — tsconfig `paths` aliases, `.ts/.tsx/.mts/.cts/.jsx`
|
|
398
|
+
// extension probing, the `.js`→`.ts` (and `.jsx→.tsx`, `.mjs→.mts`, `.cjs→.cts`)
|
|
399
|
+
// emit-convention swap, directory-index probing, and reading a directory's
|
|
400
|
+
// `package.json#main` — all happens natively now (nub-native `resolveTs`). It
|
|
401
|
+
// returns an absolute path for the additive cases nub owns, or `null` for
|
|
402
|
+
// EVERYTHING Node owns (node_modules, `exports`/`imports`, conditions, scoped/bare
|
|
403
|
+
// specifiers), which the resolve hooks below turn into a fall-through to Node. That
|
|
404
|
+
// `null` is the byte-for-byte compat boundary; reimplementing Node's resolution in
|
|
405
|
+
// nub is forbidden. The `node:`/`data:`/builtin guards, the nub-internal-graph
|
|
406
|
+
// bypass, vendored packages, and the clobber map all stay in JS and run BEFORE the
|
|
407
|
+
// native resolver (see resolveSpec / resolveCjsPath).
|
|
408
|
+
// Node's `--preserve-symlinks` decides whether a resolved module is keyed by its
|
|
409
|
+
// real path or the path it was reached through, and the native resolver has to hand
|
|
410
|
+
// back whichever one Node itself would — otherwise a symlinked workspace package
|
|
411
|
+
// reached two ways instantiates twice. The flag arrives by argv OR `NODE_OPTIONS`,
|
|
412
|
+
// and only one of those shows up in `execArgv`, so both are checked. The word
|
|
413
|
+
// boundary matters: `--preserve-symlinks-main` is a DIFFERENT flag that governs only
|
|
414
|
+
// the entry point.
|
|
415
|
+
const PRESERVE_SYMLINKS =
|
|
416
|
+
process.execArgv.includes("--preserve-symlinks") ||
|
|
417
|
+
/(^|\s)--preserve-symlinks(\s|$)/.test(process.env.NODE_OPTIONS || "");
|
|
418
|
+
|
|
419
|
+
function resolveTs(specifier, parentPath) {
|
|
420
|
+
if (!nubNative) return null;
|
|
421
|
+
try {
|
|
422
|
+
return nubNative.resolveTs(specifier, parentPath || "", RUNTIME_TSCONFIG, PRESERVE_SYMLINKS);
|
|
423
|
+
} catch {
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// nub's own runtime directory (this file's dir, as a file: URL prefix). Any
|
|
429
|
+
// resolution whose IMPORTER lives here is one of nub's internal requires — the
|
|
430
|
+
// preload loading transform-core, the Temporal lazy getter resolving
|
|
431
|
+
// @js-temporal/polyfill — and must NEVER be routed through nub's own
|
|
432
|
+
// clobber/vendored/tsconfig logic: those are user-code conveniences, and applying
|
|
433
|
+
// them to nub's internals both breaks them (e.g. the Temporal clobber re-exports
|
|
434
|
+
// globalThis.Temporal, which IS the getter → a require of the polyfill from the
|
|
435
|
+
// getter would recurse into the clobber) and amplifies the user loader chain by
|
|
436
|
+
// re-walking nub's internal graph through user hooks (R11). Short-circuit to native
|
|
437
|
+
// resolution for these.
|
|
438
|
+
const RUNTIME_DIR_URL = new URL(".", import.meta.url).href;
|
|
439
|
+
|
|
440
|
+
// Is this importer part of nub's own internal module graph? Such imports must
|
|
441
|
+
// bypass the user ESM loader chain entirely (R11). nub now loads ZERO npm packages
|
|
442
|
+
// internally — tsconfig, the additive resolver, the transpile cache, the
|
|
443
|
+
// transpiler, and module detection are ALL native nub-native calls, and the only
|
|
444
|
+
// remaining JS deps (@oxc-project/runtime helpers, the polyfills) are CommonJS,
|
|
445
|
+
// whose `require()` graph already bypasses the ESM loader chain by construction. So
|
|
446
|
+
// the only nub-internal ESM importer left is nub's own runtime directory (this
|
|
447
|
+
// file, the preload tiers, the Temporal lazy getter resolving @js-temporal/
|
|
448
|
+
// polyfill). The historical "nub-dependency package roots" walk — which existed
|
|
449
|
+
// solely to catch an ESM hop into get-tsconfig (and before that oxc-transform) — is
|
|
450
|
+
// gone with those packages.
|
|
451
|
+
function isNubInternalParent(parentURL) {
|
|
452
|
+
if (!parentURL) return false;
|
|
453
|
+
return String(parentURL).startsWith(RUNTIME_DIR_URL);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Set while resolveSpec is inside its own `require.resolve` for a nub-internal
|
|
457
|
+
// importer; see the re-entrancy guard there. Module-scoped, so each realm (the
|
|
458
|
+
// main thread, each worker, the async tier's loader worker) carries its own — and
|
|
459
|
+
// resolve hooks are synchronous, so a single flag cannot interleave.
|
|
460
|
+
let resolvingInternal = false;
|
|
461
|
+
|
|
462
|
+
// Resolve a specifier the way both hook tiers do. Returns `{ url, shortCircuit }`
|
|
463
|
+
// to short-circuit Node's resolver, or `null` to fall through to `nextResolve`.
|
|
464
|
+
// `parentURL` is the importer (a file: URL string), or "" for the entry.
|
|
465
|
+
export function resolveSpec(specifier, parentURL) {
|
|
466
|
+
// nub's own internal graph (importer inside nub's runtime dir OR a nub
|
|
467
|
+
// dependency package): resolve natively and SHORT-CIRCUIT so nextResolve (the
|
|
468
|
+
// user's loader chain) never observes nub's internals. This MUST run before the
|
|
469
|
+
// node:/data:/builtin early-returns below, because those `return null` =
|
|
470
|
+
// DELEGATE to the user loader — and a nub-internal `import "node:module"` (e.g.
|
|
471
|
+
// from a nub-dependency ESM entry) delegated to a strict user loader is exactly
|
|
472
|
+
// the R11 leak. See isNubInternalParent.
|
|
473
|
+
if (isNubInternalParent(parentURL)) {
|
|
474
|
+
if (specifier.startsWith("node:") || module.isBuiltin(specifier)) {
|
|
475
|
+
const url = specifier.startsWith("node:") ? specifier : `node:${specifier}`;
|
|
476
|
+
return { url, shortCircuit: true };
|
|
477
|
+
}
|
|
478
|
+
if (specifier.startsWith("data:")) return { url: specifier, shortCircuit: true };
|
|
479
|
+
// Re-entrancy guard. The `require.resolve` below runs through Node's CJS
|
|
480
|
+
// resolver, which invokes the registered resolve hook — i.e. back into this
|
|
481
|
+
// function with the same specifier and parent. Unguarded, the two call each
|
|
482
|
+
// other until V8 exhausts the stack and the RangeError lands in the catch: every
|
|
483
|
+
// nub-internal relative require cost ~849 nested hook invocations (measured on
|
|
484
|
+
// the fast-tier preload's own `./navigator-shim.mjs` / `./worker-blob-url.cjs`),
|
|
485
|
+
// ~50 CPU-ms per process, and produced the right answer only by accident of that
|
|
486
|
+
// unwind. Delegating the RE-ENTRANT call is not a behavior change: the outer
|
|
487
|
+
// frame still short-circuits the user chain, and Node's default resolver is what
|
|
488
|
+
// `require.resolve` was going to consult anyway.
|
|
489
|
+
if (resolvingInternal) return null;
|
|
490
|
+
// A relative/bare import from inside nub's graph: resolve it natively from the
|
|
491
|
+
// parent's own require() resolver (NOT nub's tsconfig/clobber/probe logic) and
|
|
492
|
+
// short-circuit. Bare specifiers resolve from the parent package's location.
|
|
493
|
+
resolvingInternal = true;
|
|
494
|
+
try {
|
|
495
|
+
const parentReq = createRequire(parentURL);
|
|
496
|
+
const resolved = parentReq.resolve(specifier);
|
|
497
|
+
return { url: pathToFileURL(resolved).href, shortCircuit: true };
|
|
498
|
+
} catch {
|
|
499
|
+
// Couldn't resolve from the parent (e.g. a non-file: parent): still short-
|
|
500
|
+
// circuit by handing the specifier back as-is, so the user chain is bypassed.
|
|
501
|
+
return null;
|
|
502
|
+
} finally {
|
|
503
|
+
resolvingInternal = false;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// node: and data: protocols, and bare Node built-ins, are never ours.
|
|
508
|
+
if (specifier.startsWith("node:") || specifier.startsWith("data:")) return null;
|
|
509
|
+
if (module.isBuiltin(specifier)) return null;
|
|
510
|
+
|
|
511
|
+
// 1. Built-in modules provided by Nub.
|
|
512
|
+
if (BUILTIN_MODULES.has(specifier)) {
|
|
513
|
+
return { url: BUILTIN_MODULES.get(specifier), shortCircuit: true };
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// 2. Vendored packages (e.g. @oxc-project/runtime).
|
|
517
|
+
const bare = barePkg(specifier);
|
|
518
|
+
if (VENDORED_PACKAGES.has(bare)) {
|
|
519
|
+
const resolved = safeRequireResolve(specifier);
|
|
520
|
+
if (resolved) return { url: pathToFileURL(resolved).href, shortCircuit: true };
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// 3. Package clobbering.
|
|
524
|
+
if (CLOBBER_MAP.has(bare) && !isDependency(parentURL || "")) {
|
|
525
|
+
return { url: `data:text/javascript,${encodeURIComponent(CLOBBER_MAP.get(bare)())}`, shortCircuit: true };
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const parent = String(parentURL || "");
|
|
529
|
+
|
|
530
|
+
// 4. The ADDITIVE TS resolution (tsconfig `paths`, extension probing, `.js`→`.ts`
|
|
531
|
+
// swap, directory index/`main`) — native. `resolveTs` is handed the parent's
|
|
532
|
+
// absolute FS path (or "" for a non-file: parent / the entry, where it falls back
|
|
533
|
+
// to cwd, matching the old `process.cwd()` parentDir). A non-null result is an
|
|
534
|
+
// additive hit nub owns; null falls through to Node's resolver (the compat
|
|
535
|
+
// boundary — node_modules, `exports`, bare/scoped specifiers stay Node's).
|
|
536
|
+
const parentPath = parent.startsWith("file:") ? fileURLToPath(parent) : "";
|
|
537
|
+
const resolved = resolveTs(specifier, parentPath);
|
|
538
|
+
if (resolved) return { url: pathToFileURL(resolved).href, shortCircuit: true };
|
|
539
|
+
|
|
540
|
+
return null;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// CommonJS `require()` resolution for the compat-tier Module._resolveFilename
|
|
544
|
+
// patch. Returns an absolute file path for a require specifier nub should
|
|
545
|
+
// redirect (tsconfig `paths`, extensionless `.ts`, `.js`→`.ts` swap), or null to
|
|
546
|
+
// defer to Node's resolver. Mirrors resolveSpec steps 4–5 but returns a path (not
|
|
547
|
+
// a URL) and never handles clobber/vendored/builtin — those are import-only, and
|
|
548
|
+
// a clobber's data: URL can't be a require target. `parentPath` is the requiring
|
|
549
|
+
// file's absolute path (from the CJS parent Module), or null for the entry.
|
|
550
|
+
export function resolveCjsPath(request, parentPath) {
|
|
551
|
+
if (request.startsWith("node:") || request.startsWith("data:") ||
|
|
552
|
+
module.isBuiltin(request)) {
|
|
553
|
+
return null;
|
|
554
|
+
}
|
|
555
|
+
// The SAME native additive resolver as resolveSpec, returning an absolute path
|
|
556
|
+
// (not a URL). Vendored/clobber/builtin are import-only and never reach here. A
|
|
557
|
+
// null result (node_modules / `exports` / a plain bare package) falls through to
|
|
558
|
+
// Node's CJS resolver — the compat boundary.
|
|
559
|
+
return resolveTs(request, parentPath || "");
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Would `require()`-ing this resolved TS file need Node's require(esm)? An
|
|
563
|
+
// ESM-syntax `.ts`/`.mts` (or a `.ts` in a `type: module` package) transpiles to
|
|
564
|
+
// ESM, which `require()` can only load via require(esm). On the compat tier that
|
|
565
|
+
// path is the loader-worker's CJS translator, which on Node below the #60380 fix
|
|
566
|
+
// crashes cryptically (`cjsCache.get(job.url)` is undefined) instead of erroring.
|
|
567
|
+
// The compat CJS shim calls this so it can surface a clean ERR_REQUIRE_ESM
|
|
568
|
+
// instead. (`.cts` is always CommonJS → false; non-transpiled extensions → false.)
|
|
569
|
+
export function requireTargetIsEsm(filePath, ext) {
|
|
570
|
+
if (ext === ".cts") return false;
|
|
571
|
+
if (ext === ".mts") return true;
|
|
572
|
+
if (!TRANSPILE_EXTS.has(ext)) return false;
|
|
573
|
+
let source;
|
|
574
|
+
try { source = readFileSync(filePath, "utf8"); } catch { return false; }
|
|
575
|
+
const pkgType = getPackageType(dirname(filePath));
|
|
576
|
+
return moduleFormatFor(ext, pkgType, filePath, source) === "module";
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// ── Module-format detection ─────────────────────────────────────────
|
|
580
|
+
// The oxc `lang` for a transpiled extension: the project's `loader` config wins
|
|
581
|
+
// where it turns JSX on (`tsx`/`jsx`), otherwise the extension decides. Shared by
|
|
582
|
+
// the format probe and the transform itself so the parse that DECIDES the format
|
|
583
|
+
// and the parse that PRODUCES the output can never disagree about what a file is.
|
|
584
|
+
// The one pairing this would silently discard — `ts` on `.tsx`/`.jsx`, which
|
|
585
|
+
// still parses as JSX — is refused by the config parser (`validate_loader`), so
|
|
586
|
+
// reaching the extension fallback here always means the config asked for nothing
|
|
587
|
+
// different, never that a request was dropped.
|
|
588
|
+
function langFor(ext) {
|
|
589
|
+
const configuredLoader = RUNTIME_LOADER[ext];
|
|
590
|
+
return configuredLoader === "tsx" || configuredLoader === "jsx"
|
|
591
|
+
? configuredLoader
|
|
592
|
+
: ext === ".tsx" ? "tsx" : ext === ".jsx" ? "jsx" : "ts";
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Both signals nub needs to read off a file's syntax — the absent-`type` module
|
|
596
|
+
// format and the Stage-3-decorator guard — come from ONE native call into nub's
|
|
597
|
+
// N-API addon (`detectModuleInfo`, the oxc parser compiled in-process). There is
|
|
598
|
+
// no JS parser package anymore: `oxc-parser` (ESM-only, which used to need
|
|
599
|
+
// `require(esm)` on the fast tier and a dynamic-`import()` `ensureParser()` dance
|
|
600
|
+
// on the 18.19 compat tier) is gone, and with it the whole "is require(esm)
|
|
601
|
+
// available here?" fork. The native call is synchronous and works identically on
|
|
602
|
+
// every supported Node, so there is nothing to preload and no async warm-up — the
|
|
603
|
+
// former `ensureParser()` export is removed (its compat-tier callers just stop
|
|
604
|
+
// calling it). Used only for ambiguous extensions / the decorator guard; explicit
|
|
605
|
+
// `type` and `.mts`/`.cts` short-circuit before the parser runs.
|
|
606
|
+
function detectModuleInfo(filePath, source, lang) {
|
|
607
|
+
// Addon missing (should never happen in a real install): default to ESM for
|
|
608
|
+
// format (the common case) and "no decorators" for the guard — the same fallback
|
|
609
|
+
// the old oxc-parser-unavailable branches used.
|
|
610
|
+
if (!nubNative) return { hasValueEsmSyntax: true, hasDecorators: false, transformableSyntax: false };
|
|
611
|
+
try {
|
|
612
|
+
return nubNative.detectModuleInfo(filePath, source, lang);
|
|
613
|
+
} catch {
|
|
614
|
+
// Unparseable → CJS for format + no decorators (the transpile/V8 surfaces the
|
|
615
|
+
// real error), matching the old per-call catch blocks. `transformableSyntax:
|
|
616
|
+
// false` is the SAFE plain-JS default — the verbatim path hands the raw bytes
|
|
617
|
+
// back, so V8 surfaces the real syntax error exactly where Node would.
|
|
618
|
+
return { hasValueEsmSyntax: false, hasDecorators: false, transformableSyntax: false };
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Map a transpiled file's extension + nearest package.json "type" to the module
|
|
623
|
+
// format Node's loader should use. `.mts`/`.cts` are explicit; an explicit
|
|
624
|
+
// `type` is authoritative; otherwise (ambiguous) we detect from source syntax —
|
|
625
|
+
// full Node parity (`--experimental-detect-module`), so a CJS-syntax `.ts` with
|
|
626
|
+
// no `type` runs as CJS on nub exactly as on Node. See internal/runtime/module-format.md.
|
|
627
|
+
// `.mjs`→module / `.cjs`→commonjs are explicit (mirroring `.mts`/`.cts`), so the
|
|
628
|
+
// plain-JS gate gets the right format without a needless detect.
|
|
629
|
+
export function moduleFormatFor(ext, pkgType, filePath, source) {
|
|
630
|
+
return moduleFormatWithInfo(ext, pkgType, filePath, source).format;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// Same format decision as moduleFormatFor, but ALSO returns the `ModuleInfo`
|
|
634
|
+
// (`detectModuleInfo`) result when a parse was needed — `{ format, info }`, with
|
|
635
|
+
// `info` null on the no-parse short-circuits (`.mts`/`.mjs`/`.cts`/`.cjs`, explicit
|
|
636
|
+
// `type`). loadTranspile uses this so its ONE parse serves BOTH readers — the
|
|
637
|
+
// format decision (`hasValueEsmSyntax`) and the Stage-3 decorator guard
|
|
638
|
+
// (`hasDecorators`) — instead of `moduleFormatFor` + `hasDecoratorSyntax` each
|
|
639
|
+
// parsing the same source. On a short-circuit (`info` null) no parse happened, so
|
|
640
|
+
// the decorator guard runs its own single parse: still ≤1 detect per file.
|
|
641
|
+
function moduleFormatWithInfo(ext, pkgType, filePath, source) {
|
|
642
|
+
if (ext === ".mts" || ext === ".mjs") return { format: "module", info: null };
|
|
643
|
+
if (ext === ".cts" || ext === ".cjs") return { format: "commonjs", info: null };
|
|
644
|
+
if (pkgType === "module") return { format: "module", info: null };
|
|
645
|
+
if (pkgType === "commonjs") return { format: "commonjs", info: null };
|
|
646
|
+
const info = detectModuleInfo(filePath, source, langFor(ext));
|
|
647
|
+
return { format: info.hasValueEsmSyntax ? "module" : "commonjs", info };
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// The Stage-3-decorator rejection diagnostic. oxc does not lower TC39 Stage 3
|
|
651
|
+
// decorators yet (oxc-project/oxc#9170) — it passes the `@decorator` syntax
|
|
652
|
+
// through verbatim with errors:[], so without this check V8 throws a bare
|
|
653
|
+
// `SyntaxError: Invalid or unexpected token`. See internal/runtime/stage3-decorators.md.
|
|
654
|
+
function stage3DecoratorError(filePath) {
|
|
655
|
+
return new Error(
|
|
656
|
+
`Nub: Stage 3 decorators are not supported by the transpiler yet.\n` +
|
|
657
|
+
`This is an upstream limitation in oxc (oxc-project/oxc#9170).\n` +
|
|
658
|
+
` in ${filePath}\n\n` +
|
|
659
|
+
`Workarounds:\n` +
|
|
660
|
+
` 1. Set "decorators": "legacy" in nub.jsonc, or set\n` +
|
|
661
|
+
` "experimentalDecorators": true in tsconfig.json\n` +
|
|
662
|
+
` (the shape NestJS / TypeORM / class-validator are written against).\n` +
|
|
663
|
+
` 2. Wait for Stage 3 decorator support in oxc; tracked upstream at\n` +
|
|
664
|
+
` https://github.com/oxc-project/oxc/issues/9170.\n\n` +
|
|
665
|
+
`See: https://www.typescriptlang.org/tsconfig/#experimentalDecorators`,
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// Does the source contain TC39 decorator syntax (`@expr` on a class or class
|
|
670
|
+
// member)? Used ONLY when legacy decorators are off, to surface a clear
|
|
671
|
+
// diagnostic instead of oxc's verbatim passthrough → V8 SyntaxError. The cheap
|
|
672
|
+
// `source.includes("@")` pre-filter in the caller keeps decorator-free files off
|
|
673
|
+
// the native parser. The walk now happens in Rust (detectModuleInfo's AST visit).
|
|
674
|
+
function hasDecoratorSyntax(filePath, source, lang) {
|
|
675
|
+
return detectModuleInfo(filePath, source, lang).hasDecorators;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// ── Transpile cache ─────────────────────────────────────────────────
|
|
679
|
+
// The transpile cache — `cacheGet` + transform-on-miss + post-processing
|
|
680
|
+
// (CJS empty-export strip, inline sourceMap, `//# sourceURL=`) + `cacheSet` — is
|
|
681
|
+
// ONE native call now (nub-native `transformCached`): the cache key (NUB_VERSION
|
|
682
|
+
// is the sole version component — a new release ships any emit change + a rebuilt
|
|
683
|
+
// addon), the 16-hex integrity prefix, the `c`/`m` format byte, and the atomic
|
|
684
|
+
// `*.tmp`-then-rename write all live in Rust, byte-identical to the old JS cache so
|
|
685
|
+
// warm caches survive. This JS file keeps only (a) the cache enable/disable signal
|
|
686
|
+
// and (b) the cache directory it passes IN, so the policy stays in JS and native
|
|
687
|
+
// just does the I/O against the dir nub hands it.
|
|
688
|
+
//
|
|
689
|
+
// Disable the transpile cache when (a) the permission model is active (writing a
|
|
690
|
+
// cache file may not be granted), or (b) the user set `NODE_COMPILE_CACHE=0` —
|
|
691
|
+
// Node's compile-cache disable signal, which nub honors as "no caching in this
|
|
692
|
+
// pipeline" (one knob for both V8's compile cache and nub's transpile cache; no
|
|
693
|
+
// nub-specific env var). Per internal/runtime/transpile-cache.md (the maintainer 2026-05-18).
|
|
694
|
+
const CACHE_DISABLED =
|
|
695
|
+
process.permission?.has !== undefined || process.env.NODE_COMPILE_CACHE === "0";
|
|
696
|
+
// Resolved lazily (memoized) rather than at module eval, because on the floor the
|
|
697
|
+
// node:path builtins it needs aren't bound until __ensureBuiltins() runs on first
|
|
698
|
+
// hook use. `null` = disabled / no writable dir; `undefined` cacheDirResolved means
|
|
699
|
+
// "not yet computed".
|
|
700
|
+
let cacheDir = null;
|
|
701
|
+
let cacheDirResolved = false;
|
|
702
|
+
// nub's cache ROOT (`<cache>/nub`), computed WITHOUT creating anything, so the
|
|
703
|
+
// sweep-due probe and the compile-cache check can name a directory without a
|
|
704
|
+
// mkdir side effect on every startup.
|
|
705
|
+
function cacheRoot() {
|
|
706
|
+
const base = process.env.XDG_CACHE_HOME || (process.env.HOME ? join(process.env.HOME, ".cache") : null);
|
|
707
|
+
return base ? join(base, "nub") : null;
|
|
708
|
+
}
|
|
709
|
+
function getCacheDir() {
|
|
710
|
+
if (cacheDirResolved) return cacheDir;
|
|
711
|
+
cacheDirResolved = true;
|
|
712
|
+
if (CACHE_DISABLED) return cacheDir;
|
|
713
|
+
__ensureBuiltins();
|
|
714
|
+
const root = cacheRoot();
|
|
715
|
+
if (root) {
|
|
716
|
+
cacheDir = join(root, "transpile");
|
|
717
|
+
try { mkdirSync(cacheDir, { recursive: true }); } catch { cacheDir = null; }
|
|
718
|
+
}
|
|
719
|
+
return cacheDir;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// ── Bounded-cache maintenance ───────────────────────────────────────
|
|
723
|
+
const CACHE_MAX_BYTES = 512 * 1024 * 1024; // 512 MiB — bounds runaway growth, not normal use
|
|
724
|
+
const SWEEP_INTERVAL_MS = 24 * 60 * 60 * 1000; // ≤ one sweep per day
|
|
725
|
+
|
|
726
|
+
// Is a sweep DUE right now? Deliberately cheap and side-effect-free: one
|
|
727
|
+
// `statSync` against a path built without `mkdir`, loading no module the preload
|
|
728
|
+
// has not already loaded. The caller uses this to decide whether to schedule the
|
|
729
|
+
// sweep AT ALL, which is what lets the scheduled work be ref'd instead of
|
|
730
|
+
// unref'd — see preload.cjs for why that mattered.
|
|
731
|
+
//
|
|
732
|
+
// It deliberately does NOT test for the main thread. The tempting cheap test —
|
|
733
|
+
// "is worker_threads in `process.moduleLoadList`?" — is simply WRONG here:
|
|
734
|
+
// nub's own preload already pulls worker_threads in on the MAIN thread
|
|
735
|
+
// (verified), so it reports every run as a worker and nothing ever sweeps.
|
|
736
|
+
// `maybeSweepCache` asks `isMainThread` authoritatively, so the worst a worker
|
|
737
|
+
// thread costs is one statSync and a scheduled immediate that no-ops.
|
|
738
|
+
export function sweepDue() {
|
|
739
|
+
if (CACHE_DISABLED) return false;
|
|
740
|
+
__ensureBuiltins();
|
|
741
|
+
const root = cacheRoot();
|
|
742
|
+
if (!root) return false;
|
|
743
|
+
const s = statSync(join(root, "transpile", ".sweep"), { throwIfNoEntry: false });
|
|
744
|
+
return !s || Date.now() - s.mtimeMs >= SWEEP_INTERVAL_MS;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
export function maybeSweepCache() {
|
|
748
|
+
__ensureBuiltins();
|
|
749
|
+
const dir = getCacheDir();
|
|
750
|
+
if (!dir) return;
|
|
751
|
+
// Workers inherit this preload (via execArgv); only the main thread sweeps.
|
|
752
|
+
try {
|
|
753
|
+
if (!__require("node:worker_threads").isMainThread) return;
|
|
754
|
+
} catch {
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
const sentinel = join(dir, ".sweep");
|
|
758
|
+
const s = statSync(sentinel, { throwIfNoEntry: false });
|
|
759
|
+
if (s && Date.now() - s.mtimeMs < SWEEP_INTERVAL_MS) return;
|
|
760
|
+
try {
|
|
761
|
+
writeFileSync(sentinel, "");
|
|
762
|
+
} catch {
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
// nub's OWN default V8 compile-cache dir gets the same daily treatment. The
|
|
766
|
+
// Rust spawn layer creates it and points NODE_COMPILE_CACHE at it for every
|
|
767
|
+
// augmented run (spawn.rs `default_compile_cache_dir`), it gains an entry per
|
|
768
|
+
// distinct module path plus a whole subdirectory per Node build, and nothing
|
|
769
|
+
// ever removed any of it — 6.9 GB across ~594k files after ~12 days on a
|
|
770
|
+
// working machine. Swept ONLY when NODE_COMPILE_CACHE is exactly nub's own
|
|
771
|
+
// dir: a dir the USER chose is theirs, and nub must not evict from it.
|
|
772
|
+
const root = cacheRoot();
|
|
773
|
+
const ownCompileCache = root ? join(root, "v8-compile-cache") : null;
|
|
774
|
+
const compileDir =
|
|
775
|
+
ownCompileCache && process.env.NODE_COMPILE_CACHE === ownCompileCache ? ownCompileCache : null;
|
|
776
|
+
import("./cache-evict.mjs")
|
|
777
|
+
.then((m) => {
|
|
778
|
+
// Below Node 22.3 the module cannot reach `process.getBuiltinModule`; hand it
|
|
779
|
+
// the same createRequire-backed getter this file uses. See cache-evict.mjs's
|
|
780
|
+
// no-static-imports note.
|
|
781
|
+
m.setBuiltinGetter(__getBuiltin);
|
|
782
|
+
m.sweepCache(dir, CACHE_MAX_BYTES);
|
|
783
|
+
if (compileDir) m.sweepCompileCache(compileDir, CACHE_MAX_BYTES);
|
|
784
|
+
})
|
|
785
|
+
.catch(() => {});
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// ── Transpile ───────────────────────────────────────────────────────
|
|
789
|
+
// Transpile a TS/JSX file to JS, returning `{ format, source, shortCircuit }` in
|
|
790
|
+
// the shape both hook tiers hand back to Node. Format is detected (not derived
|
|
791
|
+
// from extension alone), so a CommonJS-syntax `.ts` is reported `commonjs` — the
|
|
792
|
+
// fix that makes `require()` of a TS file work on the compat tier, where Node's
|
|
793
|
+
// CJS translator loads it via this hook and keys on the returned format.
|
|
794
|
+
export function loadTranspile(url, ext) {
|
|
795
|
+
__ensureBuiltins();
|
|
796
|
+
const filePath = fileURLToPath(url);
|
|
797
|
+
const source = readFileSync(filePath, "utf8");
|
|
798
|
+
const dir = dirname(filePath);
|
|
799
|
+
// The transform-relevant compilerOptions slice + the byte-for-byte cache-key
|
|
800
|
+
// component (`tsconfigHash`) both come from the native tsconfig reader.
|
|
801
|
+
const { compilerOptions: co, tsconfigHash } = getTsconfigForDir(dir);
|
|
802
|
+
|
|
803
|
+
// The nearest package.json `type` decides the format of an ambiguous extension
|
|
804
|
+
// (.ts/.tsx/.jsx); .mts/.cts are explicit so its lookup is skipped. The chosen
|
|
805
|
+
// format is folded into the cache key (and the entry's leading byte) by native.
|
|
806
|
+
const pkgType = ext === ".mts" || ext === ".cts" ? undefined : getPackageType(dir);
|
|
807
|
+
// ONE detectModuleInfo parse for both the format decision and the decorator
|
|
808
|
+
// guard below: `moduleInfo` is the parsed ModuleInfo when the format needed a
|
|
809
|
+
// parse (ambiguous ext, no explicit `type`), else null (a no-parse short-circuit).
|
|
810
|
+
const { format, info: moduleInfo } = moduleFormatWithInfo(ext, pkgType, filePath, source);
|
|
811
|
+
|
|
812
|
+
const lang = langFor(ext);
|
|
813
|
+
|
|
814
|
+
const opts = {
|
|
815
|
+
lang,
|
|
816
|
+
sourceType: format === "commonjs" ? "commonjs" : "module",
|
|
817
|
+
sourcemap: true,
|
|
818
|
+
// Lower syntax newer than the 22.15 floor. Critically this downlevels
|
|
819
|
+
// `using`/`await using` (Explicit Resource Management) — unparseable on Node
|
|
820
|
+
// 22's V8 — into the vendored `@oxc-project/runtime/helpers/usingCtx` shape,
|
|
821
|
+
// which resolves via VENDORED_PACKAGES. Without a target, oxc leaves `using`
|
|
822
|
+
// verbatim and Node 22 throws a SyntaxError. es2022 is the highest target
|
|
823
|
+
// that still lowers `using` while leaving everything Node 22 already supports
|
|
824
|
+
// (top-level await, class fields, private methods) untouched.
|
|
825
|
+
target: "es2022",
|
|
826
|
+
typescript: {},
|
|
827
|
+
// Decorators default to OFF (Stage-3 mode), matching tsc: legacy semantics
|
|
828
|
+
// and metadata are opt-in via tsconfig. See internal/runtime/non-erasable-syntax.md.
|
|
829
|
+
decorator: co?.experimentalDecorators === true
|
|
830
|
+
? { legacy: true, emitDecoratorMetadata: co?.emitDecoratorMetadata === true }
|
|
831
|
+
: undefined,
|
|
832
|
+
};
|
|
833
|
+
if (lang === "tsx" || lang === "jsx") {
|
|
834
|
+
opts.jsx = {
|
|
835
|
+
runtime: co?.jsx === "react" ? "classic" : "automatic",
|
|
836
|
+
development: co?.jsx === "react-jsxdev",
|
|
837
|
+
importSource: co?.jsxImportSource || "react",
|
|
838
|
+
};
|
|
839
|
+
if (co?.jsxFactory) opts.jsx.pragma = co.jsxFactory;
|
|
840
|
+
if (co?.jsxFragmentFactory) opts.jsx.pragmaFrag = co.jsxFragmentFactory;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// Stage-3 decorators: oxc returns errors:[] and emits the `@decorator` syntax
|
|
844
|
+
// verbatim, so the result-error check below never fires and V8 throws a bare
|
|
845
|
+
// SyntaxError. When legacy mode is off and decorator syntax is present, reject
|
|
846
|
+
// with the documented Option-A diagnostic instead. (Cheap `source.includes("@")`
|
|
847
|
+
// pre-filter keeps decorator-free files off the native parser; runs BEFORE the
|
|
848
|
+
// cache so the diagnostic surfaces even on what would be a warm hit.) Reuse the
|
|
849
|
+
// `hasDecorators` flag from the format parse above when it ran (`moduleInfo`
|
|
850
|
+
// non-null), so the ambiguous-ext + `@` path detects ONCE; on a no-parse
|
|
851
|
+
// short-circuit (`.mts`/`.cts`/explicit `type`) it does its own single parse.
|
|
852
|
+
if (co?.experimentalDecorators !== true && source.includes("@") &&
|
|
853
|
+
(moduleInfo ? moduleInfo.hasDecorators : hasDecoratorSyntax(filePath, source, lang))) {
|
|
854
|
+
throw stage3DecoratorError(filePath);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// cacheGet + transform-on-miss + post-process (CJS empty-export strip, inline
|
|
858
|
+
// sourceMap, sourceURL append) + cacheSet — ALL native, byte-identical on-disk.
|
|
859
|
+
// The cache key folds in ext + tsconfigHash + pkgType (same source, different
|
|
860
|
+
// type → different format → distinct entry). `cacheDir: null/undefined` is the
|
|
861
|
+
// JS enable/disable signal: native then skips all cache I/O and just transforms.
|
|
862
|
+
const formatByte = format === "commonjs" ? "c" : "m";
|
|
863
|
+
// The RAW configured loader, not `lang`: a non-TS/JSX loader (`text`, `json5`)
|
|
864
|
+
// changes the output without changing `lang`, so the key must see it.
|
|
865
|
+
const runtimeHash = JSON.stringify({
|
|
866
|
+
loader: RUNTIME_LOADER[ext] || null,
|
|
867
|
+
tsconfig: RUNTIME_TSCONFIG || null,
|
|
868
|
+
compilerOptions: RUNTIME_COMPILER_OPTIONS,
|
|
869
|
+
});
|
|
870
|
+
// process.version decides how the appended `//# sourceURL` is percent-encoded:
|
|
871
|
+
// it is spelled to match THIS host's pathToFileURL, whose escape set widened
|
|
872
|
+
// mid-release-line, so the same file has two valid spellings across hosts.
|
|
873
|
+
// Native derives the band from it and folds that band into the cache key.
|
|
874
|
+
const result = nubNative.transformCached(
|
|
875
|
+
filePath, source, opts, ext, `${tsconfigHash || ""}\0${runtimeHash}`, pkgType || "", formatByte, getCacheDir() ?? undefined, process.version,
|
|
876
|
+
);
|
|
877
|
+
if (result.errors.length > 0) {
|
|
878
|
+
const details = result.errors.map((e) => e.codeframe || e.message).join("\n\n");
|
|
879
|
+
throw new Error(`Transpile error in ${filePath}:\n${details}`);
|
|
880
|
+
}
|
|
881
|
+
return { format: result.format, source: result.code, shortCircuit: true };
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// Project-source plain JS (`.js`/`.mjs`/`.cjs`) gate. Returns a transpiled load
|
|
885
|
+
// result ONLY when the file carries syntax oxc lowers at nub's es2022 target
|
|
886
|
+
// (`using`/`await using`, a `v`-flag RegExp, or decorators); otherwise returns
|
|
887
|
+
// `null`, meaning "this file needs no transform — handle it with Node's OWN loader,
|
|
888
|
+
// exactly as a non-listed extension." This is why `.js`/`.mjs`/`.cjs` are NOT in
|
|
889
|
+
// TRANSPILE_EXTS: a no-op plain-JS file must take Node's native load path
|
|
890
|
+
// byte-for-byte (preserving the `commonjs-sync` relabel, require.cache, the
|
|
891
|
+
// require-of-ESM-syntax-`.cjs` error — all of which intercepting the file would
|
|
892
|
+
// break), and oxc would reformat it (quotes/semicolons/whitespace + a sourcemap
|
|
893
|
+
// footer) if we ran it through anyway. The verdict rides ONE parse (the same one
|
|
894
|
+
// `detectModuleInfo` does for format detection). node_modules is gated at the call
|
|
895
|
+
// sites (the byte-parity boundary). JSX-in-`.js` is out of scope for the syntax
|
|
896
|
+
// gate (lang is "ts", which does not parse JSX); use `.jsx`, or say so explicitly
|
|
897
|
+
// with a `loader` entry, which takes the unconditional path below instead.
|
|
898
|
+
export function maybeTranspilePlainJs(url, ext) {
|
|
899
|
+
__ensureBuiltins();
|
|
900
|
+
// An explicit `loader` entry pointing this extension at a code dialect moved it
|
|
901
|
+
// into TRANSPILE_EXTS, which for every other member means "always compile". Only
|
|
902
|
+
// a plain-JS extension can reach here, so this is true ONLY when the project
|
|
903
|
+
// configured one, and it must not fall through to the syntax gate below: that
|
|
904
|
+
// gate asks "does this file NEED lowering", answers no for JSX (it detects with
|
|
905
|
+
// lang "ts", which cannot parse it), and hands raw JSX to Node for V8 to reject —
|
|
906
|
+
// while the ESM path transpiles the same file on both tiers. The registration
|
|
907
|
+
// loop deliberately skips `.js`/`.cjs` because this wrapper owns them, so there
|
|
908
|
+
// is nothing else downstream to catch it.
|
|
909
|
+
if (TRANSPILE_EXTS.has(ext)) return loadTranspile(url, ext);
|
|
910
|
+
const filePath = fileURLToPath(url);
|
|
911
|
+
let source;
|
|
912
|
+
try {
|
|
913
|
+
source = readFileSync(filePath, "utf8");
|
|
914
|
+
} catch {
|
|
915
|
+
// Unreadable here → let Node's loader surface its own error.
|
|
916
|
+
return null;
|
|
917
|
+
}
|
|
918
|
+
// lang "ts" parses all JS (a TS superset) but NOT JSX — JSX-in-.js is out of scope.
|
|
919
|
+
const info = detectModuleInfo(filePath, source, "ts");
|
|
920
|
+
if (!info.transformableSyntax && !info.hasDecorators) {
|
|
921
|
+
return null; // no-op: Node's native loader handles it, byte-identical.
|
|
922
|
+
}
|
|
923
|
+
// Transformable: run the SAME pipeline as TS/JSX (target es2022 lowering, tsconfig,
|
|
924
|
+
// source maps, the Stage-3 decorator guard, format detection, cache). loadTranspile
|
|
925
|
+
// re-reads + re-parses, but only for the rare file that actually needs lowering.
|
|
926
|
+
try {
|
|
927
|
+
return loadTranspile(url, ext);
|
|
928
|
+
} catch (err) {
|
|
929
|
+
// #225: a plain-JS file the transformable verdict flagged (a `using` decl or
|
|
930
|
+
// `v`-flag RegExp somewhere) but whose transform oxc then REJECTS — V8 tolerates
|
|
931
|
+
// constructs oxc's stricter ES grammar forbids, e.g. `set x(v = []) {}` in pnpm
|
|
932
|
+
// 11.x's bundled `pnpm.mjs`. Falling back to `null` hands the file to Node's
|
|
933
|
+
// native loader running the ORIGINAL source: a V8-tolerated file runs, and a
|
|
934
|
+
// GENUINELY broken one still surfaces V8's own SyntaxError at the same spot Node
|
|
935
|
+
// would — so no real error is masked. Plain-JS only (the `.ts`/`.tsx`/`.jsx` path
|
|
936
|
+
// keeps hard-erroring, since those MUST transpile to run); the cost is that
|
|
937
|
+
// down-leveling is forfeited for THIS one file. The Stage-3 decorator diagnostic
|
|
938
|
+
// is a deliberate nub error and must not be swallowed — a decorator file can't
|
|
939
|
+
// run on V8 raw regardless, so re-throw nub's guidance.
|
|
940
|
+
if (info.hasDecorators) throw err;
|
|
941
|
+
return null;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// ── Data-format imports ─────────────────────────────────────────────
|
|
946
|
+
function lazyRequire(pkg) {
|
|
947
|
+
try { return __require(pkg); } catch {
|
|
948
|
+
throw new Error(`Nub: importing this file requires the "${pkg}" package.\nInstall it: npm install ${pkg}`);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
function stripJsonComments(text) {
|
|
953
|
+
let result = "", i = 0, inString = false, escape = false;
|
|
954
|
+
while (i < text.length) {
|
|
955
|
+
const ch = text[i];
|
|
956
|
+
if (escape) { result += ch; escape = false; i++; continue; }
|
|
957
|
+
if (inString) { if (ch === "\\") escape = true; if (ch === '"') inString = false; result += ch; i++; continue; }
|
|
958
|
+
if (ch === '"') { inString = true; result += ch; i++; continue; }
|
|
959
|
+
if (ch === "/" && text[i + 1] === "/") { while (i < text.length && text[i] !== "\n") i++; continue; }
|
|
960
|
+
if (ch === "/" && text[i + 1] === "*") { i += 2; while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i++; i += 2; continue; }
|
|
961
|
+
result += ch; i++;
|
|
962
|
+
}
|
|
963
|
+
return result;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
/// Every data extension either tier may serve — the built-ins plus whatever this
|
|
967
|
+
/// project's `loader` added. The classic `require.extensions` shim registers from
|
|
968
|
+
/// this so the CJS path covers exactly what the ESM path does; `dataExtsFor` still
|
|
969
|
+
/// decides per-URL which of them is live inside `node_modules`.
|
|
970
|
+
export function allDataExts() {
|
|
971
|
+
return new Set([...Object.keys(BUILTIN_DATA_EXTS), ...Object.keys(PROJECT_DATA_EXTS)]);
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
/// The value a data module exposes as its default export. Split out of
|
|
975
|
+
/// [`loadData`] so the classic `require()` handler resolves a file through the
|
|
976
|
+
/// SAME parser dispatch and the same `dataExtsFor` node_modules pinning — two
|
|
977
|
+
/// tiers cannot disagree about what a document means if only one function reads it.
|
|
978
|
+
export function dataValue(url, ext) {
|
|
979
|
+
const raw = readFileSync(fileURLToPath(url), "utf8");
|
|
980
|
+
const kind = dataExtsFor(url)[ext];
|
|
981
|
+
if (kind === "txt") return raw;
|
|
982
|
+
|
|
983
|
+
if (nubNative) {
|
|
984
|
+
if (kind === "yaml") return nubNative.parseYaml(raw);
|
|
985
|
+
if (kind === "toml") return nubNative.parseToml(raw);
|
|
986
|
+
if (kind === "json5") return nubNative.parseJson5(raw);
|
|
987
|
+
if (kind === "jsonc") return nubNative.parseJsonc(raw);
|
|
988
|
+
} else {
|
|
989
|
+
if (kind === "yaml") return lazyRequire("yaml").parse(raw);
|
|
990
|
+
if (kind === "toml") return lazyRequire("@iarna/toml").parse(raw);
|
|
991
|
+
if (kind === "json5") return lazyRequire("json5").parse(raw);
|
|
992
|
+
if (kind === "jsonc") return JSON.parse(stripJsonComments(raw));
|
|
993
|
+
}
|
|
994
|
+
return undefined;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
export function loadData(url, ext) {
|
|
998
|
+
const parsed = dataValue(url, ext);
|
|
999
|
+
|
|
1000
|
+
if (parsed == null) {
|
|
1001
|
+
return { format: "module", source: "export default undefined;\n", shortCircuit: true };
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// Default export only. Data modules deliberately do NOT emit per-key named
|
|
1005
|
+
// exports: named imports of data are categorically un-typeable in TypeScript
|
|
1006
|
+
// (a `declare module "*.yaml"` wildcard has no per-key export index signature),
|
|
1007
|
+
// and default-only matches Node's own JSON modules. Consumers destructure the
|
|
1008
|
+
// default — `import cfg from "./c.yaml"; const { host } = cfg;` — which the
|
|
1009
|
+
// `@nubjs/types` `Record<string, unknown>` default type makes sound.
|
|
1010
|
+
const code = `export default ${JSON.stringify(parsed)};\n`;
|
|
1011
|
+
return { format: "module", source: code, shortCircuit: true };
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// Import Text: `import s from "./any.file" with { type: "text" }` → the raw file
|
|
1015
|
+
// contents as a default-export string, on ANY extension. This is nub's own
|
|
1016
|
+
// implementation of the import-attribute text feature Node standardized upstream
|
|
1017
|
+
// (translators.js textStrategy); it is attribute-KEYED (the load hooks call this
|
|
1018
|
+
// when `context.importAttributes?.type === "text"`), orthogonal to the
|
|
1019
|
+
// EXTENSION-keyed `.txt` data loader above. Semantics match Node's textStrategy:
|
|
1020
|
+
// decode via TextDecoder (UTF-8, strips a leading BOM — unlike readFileSync utf8,
|
|
1021
|
+
// which keeps it) and expose ONLY a `default` export (a named import errors, as on
|
|
1022
|
+
// Node). shortCircuit so this fully owns the module and Node's own
|
|
1023
|
+
// unknown-'text'-attribute validation never runs.
|
|
1024
|
+
const __textDecoder = new TextDecoder();
|
|
1025
|
+
export function loadTextImport(url) {
|
|
1026
|
+
const text = __textDecoder.decode(readFileSync(fileURLToPath(url)));
|
|
1027
|
+
return { format: "module", source: `export default ${JSON.stringify(text)};\n`, shortCircuit: true };
|
|
1028
|
+
}
|