@nubjs/nub-win32-arm64 0.0.4 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/nub.exe +0 -0
- package/package.json +1 -1
- package/runtime/addons/nub-native.node +0 -0
- package/runtime/cache-evict.mjs +69 -0
- package/runtime/node_modules/@oxc-parser/binding-win32-arm64-msvc/README.md +3 -0
- package/runtime/node_modules/@oxc-parser/binding-win32-arm64-msvc/package.json +39 -0
- package/runtime/node_modules/@oxc-parser/binding-win32-arm64-msvc/parser.win32-arm64-msvc.node +0 -0
- package/runtime/node_modules/@oxc-transform/binding-win32-arm64-msvc/README.md +3 -0
- package/runtime/node_modules/@oxc-transform/binding-win32-arm64-msvc/package.json +41 -0
- package/runtime/node_modules/@oxc-transform/binding-win32-arm64-msvc/transform.win32-arm64-msvc.node +0 -0
- package/runtime/polyfills.mjs +73 -97
- package/runtime/preload-async-hooks.mjs +50 -0
- package/runtime/preload.mjs +274 -320
- package/runtime/transform-core.mjs +762 -0
- package/runtime/version.mjs +12 -0
- package/runtime/worker-polyfill.mjs +147 -9
- package/runtime/node_modules/get-tsconfig/LICENSE +0 -21
- package/runtime/node_modules/get-tsconfig/README.md +0 -268
- package/runtime/node_modules/get-tsconfig/dist/index.cjs +0 -7
- package/runtime/node_modules/get-tsconfig/dist/index.d.cts +0 -2116
- package/runtime/node_modules/get-tsconfig/dist/index.d.mts +0 -2116
- package/runtime/node_modules/get-tsconfig/dist/index.mjs +0 -7
- package/runtime/node_modules/get-tsconfig/package.json +0 -46
- package/runtime/node_modules/oxc-transform/LICENSE +0 -22
- package/runtime/node_modules/oxc-transform/README.md +0 -84
- package/runtime/node_modules/oxc-transform/browser.js +0 -1
- package/runtime/node_modules/oxc-transform/index.d.ts +0 -658
- package/runtime/node_modules/oxc-transform/index.js +0 -598
- package/runtime/node_modules/oxc-transform/package.json +0 -114
- package/runtime/node_modules/oxc-transform/webcontainer-fallback.cjs +0 -21
- package/runtime/node_modules/resolve-pkg-maps/LICENSE +0 -21
- package/runtime/node_modules/resolve-pkg-maps/README.md +0 -216
- package/runtime/node_modules/resolve-pkg-maps/dist/index.cjs +0 -1
- package/runtime/node_modules/resolve-pkg-maps/dist/index.d.cts +0 -11
- package/runtime/node_modules/resolve-pkg-maps/dist/index.d.mts +0 -11
- package/runtime/node_modules/resolve-pkg-maps/dist/index.mjs +0 -1
- package/runtime/node_modules/resolve-pkg-maps/package.json +0 -42
|
@@ -0,0 +1,762 @@
|
|
|
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
|
+
// oxc-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 data addon, lazily loading
|
|
19
|
+
// oxc-parser/oxc-transform, and reading/writing the transpile cache. There is no
|
|
20
|
+
// top-level hook registration here — importing this module never augments the
|
|
21
|
+
// realm; the tier files do that.
|
|
22
|
+
|
|
23
|
+
import module from "node:module";
|
|
24
|
+
import { readFileSync, writeFileSync, mkdirSync, statSync, renameSync, unlinkSync, readdirSync } from "node:fs";
|
|
25
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
26
|
+
import { createRequire } from "node:module";
|
|
27
|
+
import { createHash } from "node:crypto";
|
|
28
|
+
import { join, dirname, resolve as pathResolve, extname as pathExtname } from "node:path";
|
|
29
|
+
import { transformSync } from "oxc-transform";
|
|
30
|
+
import { getTsconfig, createPathsMatcher } from "get-tsconfig";
|
|
31
|
+
import { NUB_VERSION } from "./version.mjs";
|
|
32
|
+
|
|
33
|
+
const __require = createRequire(import.meta.url);
|
|
34
|
+
|
|
35
|
+
// ── Constants ───────────────────────────────────────────────────────
|
|
36
|
+
export const TRANSPILE_EXTS = new Set([".ts", ".tsx", ".mts", ".cts", ".jsx"]);
|
|
37
|
+
export const DATA_EXTS = { ".jsonc": "jsonc", ".json5": "json5", ".toml": "toml", ".yaml": "yaml", ".yml": "yaml", ".txt": "txt" };
|
|
38
|
+
export const TS_PARENT_EXTS = new Set([".ts", ".tsx", ".mts", ".cts"]);
|
|
39
|
+
|
|
40
|
+
// Reserved words / literals that cannot be a lexical binding name in a module
|
|
41
|
+
// (modules are strict mode). A data file with a top-level key like `package`
|
|
42
|
+
// (e.g. a Cargo.toml `[package]` table) must NOT emit `export const package = …`
|
|
43
|
+
// — that is a SyntaxError that takes down the whole module, default export
|
|
44
|
+
// included. Such keys stay reachable via the default export. Matches bun, which
|
|
45
|
+
// deoptimizes invalid-identifier keys rather than failing the whole module.
|
|
46
|
+
const RESERVED_EXPORT_NAMES = new Set([
|
|
47
|
+
"break", "case", "catch", "class", "const", "continue", "debugger", "default",
|
|
48
|
+
"delete", "do", "else", "enum", "export", "extends", "false", "finally", "for",
|
|
49
|
+
"function", "if", "import", "in", "instanceof", "new", "null", "return", "super",
|
|
50
|
+
"switch", "this", "throw", "true", "try", "typeof", "var", "void", "while", "with",
|
|
51
|
+
// Strict-mode (modules are always strict) future-reserved + restricted names:
|
|
52
|
+
"implements", "interface", "let", "package", "private", "protected", "public",
|
|
53
|
+
"static", "yield", "await", "eval", "arguments",
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
// Packages resolved from Nub's distribution, not the user's.
|
|
57
|
+
export const VENDORED_PACKAGES = new Set(["@oxc-project/runtime"]);
|
|
58
|
+
|
|
59
|
+
// Built-in modules provided by Nub (resolved to files in this distribution).
|
|
60
|
+
// connect() sockets deferred per design decision — "sockets" specifier not clobbered.
|
|
61
|
+
export const BUILTIN_MODULES = new Map();
|
|
62
|
+
|
|
63
|
+
// Package clobbering: specifiers that resolve to a synthetic module re-exporting
|
|
64
|
+
// the native global instead of the userland package.
|
|
65
|
+
export const CLOBBER_MAP = new Map([
|
|
66
|
+
// Reading globalThis.Temporal triggers the lazy getter the tier file installs,
|
|
67
|
+
// which loads the polyfill by resolved path — that load is what installs
|
|
68
|
+
// Date.prototype.toTemporalInstant, so Temporal MUST be read first.
|
|
69
|
+
// @js-temporal/polyfill exports { Temporal, Intl, toTemporalInstant }; mirror
|
|
70
|
+
// all three so `import { Temporal, Intl, toTemporalInstant } from ...` binds.
|
|
71
|
+
["@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;`],
|
|
72
|
+
["urlpattern-polyfill", () => `export const URLPattern = globalThis.URLPattern;`],
|
|
73
|
+
["abort-controller", () => `export const AbortController = globalThis.AbortController; export const AbortSignal = globalThis.AbortSignal; export default globalThis.AbortController;`],
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
// Nub's N-API addon for data-format parsing (Rust-native YAML/TOML/JSON5/JSONC).
|
|
77
|
+
// Loaded once per module instance (= once per thread: the main thread and the
|
|
78
|
+
// loader worker each import this module separately).
|
|
79
|
+
let nubNative = null;
|
|
80
|
+
for (const rel of ["./addons/nub-native.node", "../runtime/addons/nub-native.node"]) {
|
|
81
|
+
try { nubNative = __require(fileURLToPath(new URL(rel, import.meta.url))); break; } catch {}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ── Watch-mode hooks (injected by the main-thread tier) ─────────────
|
|
85
|
+
// `nub watch` needs config files (tsconfig.json, package.json) and `.env*` —
|
|
86
|
+
// which are not in any import graph — surfaced to Node's FilesWatcher. The main
|
|
87
|
+
// thread (preload.mjs) injects reporters; the loader worker injects nothing
|
|
88
|
+
// (watch IPC is main-thread only), so these default to no-ops.
|
|
89
|
+
let _reportDep = null;
|
|
90
|
+
let _reportEnvDir = null;
|
|
91
|
+
export function setWatchHooks({ reportDep, reportEnvDir } = {}) {
|
|
92
|
+
if (reportDep) _reportDep = reportDep;
|
|
93
|
+
if (reportEnvDir) _reportEnvDir = reportEnvDir;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ── tsconfig + package-type caches ──────────────────────────────────
|
|
97
|
+
const tsconfigCache = new Map();
|
|
98
|
+
export function getTsconfigForDir(dir) {
|
|
99
|
+
if (tsconfigCache.has(dir)) return tsconfigCache.get(dir);
|
|
100
|
+
const result = getTsconfig(dir);
|
|
101
|
+
const matcher = result ? createPathsMatcher(result) : null;
|
|
102
|
+
const entry = { tsconfig: result, matcher };
|
|
103
|
+
tsconfigCache.set(dir, entry);
|
|
104
|
+
if (result?.path) _reportDep?.(result.path);
|
|
105
|
+
return entry;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// The NEAREST package.json's `type` decides the format of ambiguous extensions
|
|
109
|
+
// (.ts/.tsx/.jsx, like Node's .js). The nearest one wins even when its `type`
|
|
110
|
+
// is absent — Node does not skip a typeless package.json to find a typed
|
|
111
|
+
// ancestor — so we stop at the first package.json found. Returns "module",
|
|
112
|
+
// "commonjs", or undefined.
|
|
113
|
+
const packageTypeCache = new Map();
|
|
114
|
+
export function getPackageType(dir) {
|
|
115
|
+
if (packageTypeCache.has(dir)) return packageTypeCache.get(dir);
|
|
116
|
+
let type;
|
|
117
|
+
let current = dir;
|
|
118
|
+
for (;;) {
|
|
119
|
+
const pkgPath = join(current, "package.json");
|
|
120
|
+
if (fileExists(pkgPath)) {
|
|
121
|
+
try { type = JSON.parse(readFileSync(pkgPath, "utf8")).type; } catch {}
|
|
122
|
+
// Watch this package.json (a `type`/script edit should restart) and the
|
|
123
|
+
// `.env*` files alongside it (the package root is where they live).
|
|
124
|
+
_reportDep?.(pkgPath);
|
|
125
|
+
_reportEnvDir?.(current);
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
const parent = dirname(current);
|
|
129
|
+
if (parent === current) break;
|
|
130
|
+
current = parent;
|
|
131
|
+
}
|
|
132
|
+
packageTypeCache.set(dir, type);
|
|
133
|
+
return type;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ── Filesystem helpers ──────────────────────────────────────────────
|
|
137
|
+
export function extname(url) {
|
|
138
|
+
const path = url.includes("?") ? url.slice(0, url.indexOf("?")) : url;
|
|
139
|
+
const dot = path.lastIndexOf(".");
|
|
140
|
+
return dot === -1 ? "" : path.slice(dot);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function isNodeModules(url) {
|
|
144
|
+
return url.includes("/node_modules/") || url.includes("\\node_modules\\");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function fileExists(filePath) {
|
|
148
|
+
const s = statSync(filePath, { throwIfNoEntry: false });
|
|
149
|
+
return s !== undefined && s.isFile();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function dirExists(filePath) {
|
|
153
|
+
const s = statSync(filePath, { throwIfNoEntry: false });
|
|
154
|
+
return s !== undefined && s.isDirectory();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function safeRequireResolve(specifier) {
|
|
158
|
+
try { return __require.resolve(specifier); } catch { return null; }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function barePkg(specifier) {
|
|
162
|
+
return specifier.startsWith("@")
|
|
163
|
+
? specifier.split("/").slice(0, 2).join("/")
|
|
164
|
+
: specifier.split("/")[0];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ── Resolution ──────────────────────────────────────────────────────
|
|
168
|
+
// Read a directory's package.json `main` (its legacy CJS entry point), or null.
|
|
169
|
+
// `exports` is deliberately NOT consulted: Node honors `exports` only for
|
|
170
|
+
// package-name/self-reference resolution, never for a relative/absolute import
|
|
171
|
+
// of a directory path (verified against Node 24 — a relative dir import with
|
|
172
|
+
// `exports` but no `main` falls through to index, not the export). So matching
|
|
173
|
+
// Node here means `main` only.
|
|
174
|
+
function readPackageMain(dir) {
|
|
175
|
+
const pkgPath = join(dir, "package.json");
|
|
176
|
+
if (!fileExists(pkgPath)) return null;
|
|
177
|
+
try {
|
|
178
|
+
const main = JSON.parse(readFileSync(pkgPath, "utf8")).main;
|
|
179
|
+
return typeof main === "string" && main.trim() ? main : null;
|
|
180
|
+
} catch {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Try to resolve a file path with extensionless probing + .js→.ts swap.
|
|
186
|
+
// `allowDirMain` honors a resolved directory's package.json `main` before its
|
|
187
|
+
// index; it is cleared on the recursive main-target probe because Node's
|
|
188
|
+
// LOAD_AS_DIRECTORY resolves `main` with file+index probing only and does not
|
|
189
|
+
// recurse into the target's own nested `main` (verified against Node 24).
|
|
190
|
+
export function tryResolveFile(target, parentExt, allowDirMain = true) {
|
|
191
|
+
// If the target already has an extension and exists, use it.
|
|
192
|
+
const existingExt = pathExtname(target);
|
|
193
|
+
if (existingExt && fileExists(target)) return target;
|
|
194
|
+
|
|
195
|
+
// .js → .ts swap (tsc emit convention reversal).
|
|
196
|
+
if (existingExt === ".js") {
|
|
197
|
+
const tsSwap = target.slice(0, -3) + ".ts";
|
|
198
|
+
if (fileExists(tsSwap)) return tsSwap;
|
|
199
|
+
const tsxSwap = target.slice(0, -3) + ".tsx";
|
|
200
|
+
if (fileExists(tsxSwap)) return tsxSwap;
|
|
201
|
+
}
|
|
202
|
+
if (existingExt === ".jsx") {
|
|
203
|
+
const tsxSwap = target.slice(0, -4) + ".tsx";
|
|
204
|
+
if (fileExists(tsxSwap)) return tsxSwap;
|
|
205
|
+
}
|
|
206
|
+
// .mjs → .mts swap (Bun does this).
|
|
207
|
+
if (existingExt === ".mjs") {
|
|
208
|
+
const mtsSwap = target.slice(0, -4) + ".mts";
|
|
209
|
+
if (fileExists(mtsSwap)) return mtsSwap;
|
|
210
|
+
}
|
|
211
|
+
// .cjs → .cts swap — the CommonJS analog of .mjs→.mts. tsc resolves
|
|
212
|
+
// `import "./foo.cjs"` to foo.cts (it strips the .cjs and finds the .cts
|
|
213
|
+
// source — verified via --traceResolution), so a TS file using the emitted
|
|
214
|
+
// extension to reference a .cts source must resolve at runtime. (Bun omits
|
|
215
|
+
// this swap even though it does .mjs→.mts; we match tsc, not that gap.)
|
|
216
|
+
if (existingExt === ".cjs") {
|
|
217
|
+
const ctsSwap = target.slice(0, -4) + ".cts";
|
|
218
|
+
if (fileExists(ctsSwap)) return ctsSwap;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Extensionless: probe in parent-ext-aware order.
|
|
222
|
+
if (!existingExt) {
|
|
223
|
+
const probeOrder = getProbeOrder(parentExt);
|
|
224
|
+
for (const ext of probeOrder) {
|
|
225
|
+
if (fileExists(target + ext)) return target + ext;
|
|
226
|
+
}
|
|
227
|
+
// Directory: honor package.json `main` (Node's legacy LOAD_AS_DIRECTORY)
|
|
228
|
+
// before falling back to index probing. The main target is resolved with
|
|
229
|
+
// the same extensionless/TS-swap probing (so a TS package can point `main`
|
|
230
|
+
// at a `.ts`, or `.js`→`.ts` swaps apply), but without re-reading a nested
|
|
231
|
+
// `main` — matching Node. If `main` is absent or unresolvable, index wins
|
|
232
|
+
// (Node falls back to index too, with a DEP0128 warning we needn't emit).
|
|
233
|
+
if (dirExists(target)) {
|
|
234
|
+
if (allowDirMain) {
|
|
235
|
+
const main = readPackageMain(target);
|
|
236
|
+
if (main) {
|
|
237
|
+
const resolved = tryResolveFile(pathResolve(target, main), parentExt, false);
|
|
238
|
+
if (resolved) return resolved;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
for (const ext of probeOrder) {
|
|
242
|
+
const idx = join(target, "index" + ext);
|
|
243
|
+
if (fileExists(idx)) return idx;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function getProbeOrder(parentExt) {
|
|
252
|
+
switch (parentExt) {
|
|
253
|
+
case ".tsx": return [".tsx", ".ts", ".jsx", ".js", ".json"];
|
|
254
|
+
// .mts/.cts prefer their own module system first, but STILL fall through to
|
|
255
|
+
// the general TS (`.ts`) and JS extensions: tsc and Node resolve an
|
|
256
|
+
// extensionless `./foo` from a .mts/.cts parent to foo.ts / foo.js too, not
|
|
257
|
+
// only foo.mts / foo.cts. Omitting `.ts` here is what made `require('./config')`
|
|
258
|
+
// — and a tsconfig-paths alias — from a .cts (or .mts) parent miss a `.ts`
|
|
259
|
+
// target (works from .js/.cjs, which use the default order below).
|
|
260
|
+
case ".mts": return [".mts", ".ts", ".mjs", ".js", ".json"];
|
|
261
|
+
case ".cts": return [".cts", ".ts", ".cjs", ".js", ".json"];
|
|
262
|
+
default: return [".ts", ".tsx", ".js", ".jsx", ".json"];
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Resolve a specifier the way both hook tiers do. Returns `{ url, shortCircuit }`
|
|
267
|
+
// to short-circuit Node's resolver, or `null` to fall through to `nextResolve`.
|
|
268
|
+
// `parentURL` is the importer (a file: URL string), or "" for the entry.
|
|
269
|
+
export function resolveSpec(specifier, parentURL) {
|
|
270
|
+
// node: and data: protocols, and bare Node built-ins, are never ours.
|
|
271
|
+
if (specifier.startsWith("node:") || specifier.startsWith("data:")) return null;
|
|
272
|
+
if (module.builtinModules.includes(specifier)) return null;
|
|
273
|
+
|
|
274
|
+
// 1. Built-in modules provided by Nub.
|
|
275
|
+
if (BUILTIN_MODULES.has(specifier)) {
|
|
276
|
+
return { url: BUILTIN_MODULES.get(specifier), shortCircuit: true };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// 2. Vendored packages (e.g. @oxc-project/runtime).
|
|
280
|
+
const bare = barePkg(specifier);
|
|
281
|
+
if (VENDORED_PACKAGES.has(bare)) {
|
|
282
|
+
const resolved = safeRequireResolve(specifier);
|
|
283
|
+
if (resolved) return { url: pathToFileURL(resolved).href, shortCircuit: true };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// 3. Package clobbering.
|
|
287
|
+
if (CLOBBER_MAP.has(bare) && !isNodeModules(parentURL || "")) {
|
|
288
|
+
return { url: `data:text/javascript,${encodeURIComponent(CLOBBER_MAP.get(bare)())}`, shortCircuit: true };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const parent = String(parentURL || "");
|
|
292
|
+
const parentExt = extname(parent);
|
|
293
|
+
|
|
294
|
+
// 4. tsconfig-paths (only for bare/aliased specifiers, not relative).
|
|
295
|
+
if (!specifier.startsWith(".") && !specifier.startsWith("/") && !specifier.startsWith("file:") && !isNodeModules(parent)) {
|
|
296
|
+
const parentDir = parent.startsWith("file:") ? dirname(fileURLToPath(parent)) : process.cwd();
|
|
297
|
+
const { matcher } = getTsconfigForDir(parentDir);
|
|
298
|
+
if (matcher) {
|
|
299
|
+
const mapped = matcher(specifier);
|
|
300
|
+
if (mapped && mapped.length > 0) {
|
|
301
|
+
for (const candidate of mapped) {
|
|
302
|
+
const resolved = tryResolveFile(candidate, parentExt);
|
|
303
|
+
if (resolved) return { url: pathToFileURL(resolved).href, shortCircuit: true };
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// 5. Extensionless probing (only when parent is a TS file).
|
|
310
|
+
if (TS_PARENT_EXTS.has(parentExt) && (specifier.startsWith("./") || specifier.startsWith("../"))) {
|
|
311
|
+
const parentDir = dirname(fileURLToPath(parent));
|
|
312
|
+
const target = pathResolve(parentDir, specifier);
|
|
313
|
+
const resolved = tryResolveFile(target, parentExt);
|
|
314
|
+
if (resolved) return { url: pathToFileURL(resolved).href, shortCircuit: true };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// CommonJS `require()` resolution for the compat-tier Module._resolveFilename
|
|
321
|
+
// patch. Returns an absolute file path for a require specifier nub should
|
|
322
|
+
// redirect (tsconfig `paths`, extensionless `.ts`, `.js`→`.ts` swap), or null to
|
|
323
|
+
// defer to Node's resolver. Mirrors resolveSpec steps 4–5 but returns a path (not
|
|
324
|
+
// a URL) and never handles clobber/vendored/builtin — those are import-only, and
|
|
325
|
+
// a clobber's data: URL can't be a require target. `parentPath` is the requiring
|
|
326
|
+
// file's absolute path (from the CJS parent Module), or null for the entry.
|
|
327
|
+
export function resolveCjsPath(request, parentPath) {
|
|
328
|
+
if (request.startsWith("node:") || request.startsWith("data:") ||
|
|
329
|
+
module.builtinModules.includes(request)) {
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
const parentExt = parentPath ? pathExtname(parentPath) : "";
|
|
333
|
+
|
|
334
|
+
// tsconfig `paths` — bare/aliased specifiers from a file outside node_modules
|
|
335
|
+
// (not gated on a TS parent: a plain .js with a paths alias resolves too).
|
|
336
|
+
if (!request.startsWith(".") && !request.startsWith("/") && !request.startsWith("file:") &&
|
|
337
|
+
!isNodeModules(parentPath || "")) {
|
|
338
|
+
const parentDir = parentPath ? dirname(parentPath) : process.cwd();
|
|
339
|
+
const { matcher } = getTsconfigForDir(parentDir);
|
|
340
|
+
if (matcher) {
|
|
341
|
+
const mapped = matcher(request);
|
|
342
|
+
if (mapped && mapped.length > 0) {
|
|
343
|
+
for (const candidate of mapped) {
|
|
344
|
+
const resolved = tryResolveFile(candidate, parentExt);
|
|
345
|
+
if (resolved) return resolved;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return null; // a plain bare package → let Node resolve it from node_modules
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Extensionless probing + .js→.ts swap for a relative specifier — only when the
|
|
353
|
+
// requiring file is itself TS (same TS_PARENT_EXTS gate as resolveSpec step 5).
|
|
354
|
+
if (parentPath && TS_PARENT_EXTS.has(parentExt) &&
|
|
355
|
+
(request.startsWith("./") || request.startsWith("../"))) {
|
|
356
|
+
const target = pathResolve(dirname(parentPath), request);
|
|
357
|
+
const resolved = tryResolveFile(target, parentExt);
|
|
358
|
+
if (resolved) return resolved;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Would `require()`-ing this resolved TS file need Node's require(esm)? An
|
|
365
|
+
// ESM-syntax `.ts`/`.mts` (or a `.ts` in a `type: module` package) transpiles to
|
|
366
|
+
// ESM, which `require()` can only load via require(esm). On the compat tier that
|
|
367
|
+
// path is the loader-worker's CJS translator, which on Node below the #60380 fix
|
|
368
|
+
// crashes cryptically (`cjsCache.get(job.url)` is undefined) instead of erroring.
|
|
369
|
+
// The compat CJS shim calls this so it can surface a clean ERR_REQUIRE_ESM
|
|
370
|
+
// instead. (`.cts` is always CommonJS → false; non-transpiled extensions → false.)
|
|
371
|
+
export function requireTargetIsEsm(filePath, ext) {
|
|
372
|
+
if (ext === ".cts") return false;
|
|
373
|
+
if (ext === ".mts") return true;
|
|
374
|
+
if (!TRANSPILE_EXTS.has(ext)) return false;
|
|
375
|
+
let source;
|
|
376
|
+
try { source = readFileSync(filePath, "utf8"); } catch { return false; }
|
|
377
|
+
const pkgType = getPackageType(dirname(filePath));
|
|
378
|
+
return moduleFormatFor(ext, pkgType, filePath, source) === "module";
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// ── Module-format detection ─────────────────────────────────────────
|
|
382
|
+
// oxc-parser loads a native binding (~8 ms) and is needed ONLY for absent-`type`
|
|
383
|
+
// module detection + the Stage-3 decorator guard, so it loads lazily — `nub
|
|
384
|
+
// script.js` and explicit-`type` files never pay for it.
|
|
385
|
+
//
|
|
386
|
+
// oxc-parser is ESM-only, so `require()` of it needs require(esm), which only
|
|
387
|
+
// exists on Node 20.19+ / 22.12+. The fast tier (>= 22.15) has it, so a lazy
|
|
388
|
+
// `require` there keeps the load off plain-JS startups. The compat tier reaches
|
|
389
|
+
// down to 18.19, where `require("oxc-parser")` throws ERR_REQUIRE_ESM — so the
|
|
390
|
+
// compat-tier callers (the loader worker's async load hook, and preload.mjs's
|
|
391
|
+
// main-thread compat branch) `await ensureParser()` first, which loads it via
|
|
392
|
+
// dynamic `import()` (native ESM, works on every supported Node). Without this,
|
|
393
|
+
// detection silently fell back to "ESM", so a CJS-content `.ts` mis-loaded as ESM
|
|
394
|
+
// and decorator syntax slipped past the guard on Node 18.19 / 20.x.
|
|
395
|
+
let _parseSync = null;
|
|
396
|
+
let _requireTried = false;
|
|
397
|
+
let _importTried = false;
|
|
398
|
+
|
|
399
|
+
/// Async, idempotent: ensure oxc-parser is loaded via dynamic import (the only
|
|
400
|
+
/// form that works below require(esm)). Compat-tier callers await this before the
|
|
401
|
+
/// synchronous detection below runs.
|
|
402
|
+
export async function ensureParser() {
|
|
403
|
+
if (_parseSync || _importTried) return;
|
|
404
|
+
_importTried = true;
|
|
405
|
+
try { _parseSync = (await import("oxc-parser")).parseSync; } catch { /* unavailable */ }
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function getParseSync() {
|
|
409
|
+
if (!_parseSync && !_requireTried) {
|
|
410
|
+
_requireTried = true;
|
|
411
|
+
try { _parseSync = __require("oxc-parser").parseSync; } catch { /* try ensureParser instead */ }
|
|
412
|
+
}
|
|
413
|
+
return _parseSync;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Does the source carry VALUE-level ESM syntax? Mirrors Node's
|
|
417
|
+
// `--experimental-detect-module`: type-only imports/exports are erased by oxc
|
|
418
|
+
// and must NOT count, but a value import/export, a bare `import "x"`, an
|
|
419
|
+
// `export {}` marker, `import.meta`, or top-level `await` all force ESM.
|
|
420
|
+
// Used only for the ambiguous extensions when package.json has no `type`.
|
|
421
|
+
function hasEsmSyntax(filePath, source, lang) {
|
|
422
|
+
const parse = getParseSync();
|
|
423
|
+
if (!parse) return true; // detection unavailable → default ESM (the common case)
|
|
424
|
+
let mod;
|
|
425
|
+
try {
|
|
426
|
+
mod = parse(filePath, source, { lang }).module;
|
|
427
|
+
} catch {
|
|
428
|
+
return false; // unparseable → default CJS; the transpile surfaces the real error
|
|
429
|
+
}
|
|
430
|
+
const valueImport = mod.staticImports.some(
|
|
431
|
+
(si) => si.entries.length === 0 || si.entries.some((e) => !e.isType),
|
|
432
|
+
);
|
|
433
|
+
const valueExport = mod.staticExports.some(
|
|
434
|
+
(se) => se.entries.length === 0 || se.entries.some((e) => !e.isType),
|
|
435
|
+
);
|
|
436
|
+
if (valueImport || valueExport || mod.importMetas.length > 0) return true;
|
|
437
|
+
// Top-level await: `hasModuleSyntax` is set with no static import/export/meta.
|
|
438
|
+
return (
|
|
439
|
+
mod.hasModuleSyntax &&
|
|
440
|
+
mod.staticImports.length === 0 &&
|
|
441
|
+
mod.staticExports.length === 0 &&
|
|
442
|
+
mod.importMetas.length === 0
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Map a transpiled file's extension + nearest package.json "type" to the module
|
|
447
|
+
// format Node's loader should use. `.mts`/`.cts` are explicit; an explicit
|
|
448
|
+
// `type` is authoritative; otherwise (ambiguous) we detect from source syntax —
|
|
449
|
+
// full Node parity (`--experimental-detect-module`), so a CJS-syntax `.ts` with
|
|
450
|
+
// no `type` runs as CJS on nub exactly as on Node. See wiki/runtime/module-format.md.
|
|
451
|
+
export function moduleFormatFor(ext, pkgType, filePath, source) {
|
|
452
|
+
if (ext === ".mts") return "module";
|
|
453
|
+
if (ext === ".cts") return "commonjs";
|
|
454
|
+
if (pkgType === "module") return "module";
|
|
455
|
+
if (pkgType === "commonjs") return "commonjs";
|
|
456
|
+
const lang = ext === ".tsx" ? "tsx" : ext === ".jsx" ? "jsx" : "ts";
|
|
457
|
+
return hasEsmSyntax(filePath, source, lang) ? "module" : "commonjs";
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// The Stage-3-decorator rejection diagnostic. oxc does not lower TC39 Stage 3
|
|
461
|
+
// decorators yet (oxc-project/oxc#9170) — it passes the `@decorator` syntax
|
|
462
|
+
// through verbatim with errors:[], so without this check V8 throws a bare
|
|
463
|
+
// `SyntaxError: Invalid or unexpected token`. See wiki/runtime/stage3-decorators.md.
|
|
464
|
+
function stage3DecoratorError(filePath) {
|
|
465
|
+
return new Error(
|
|
466
|
+
`Nub: Stage 3 decorators are not supported by the transpiler yet.\n` +
|
|
467
|
+
`This is an upstream limitation in oxc (oxc-project/oxc#9170).\n` +
|
|
468
|
+
` in ${filePath}\n\n` +
|
|
469
|
+
`Workarounds:\n` +
|
|
470
|
+
` 1. Set "experimentalDecorators": true in tsconfig.json to use legacy decorators\n` +
|
|
471
|
+
` (the shape NestJS / TypeORM / class-validator are written against).\n` +
|
|
472
|
+
` 2. Wait for Stage 3 decorator support in oxc; tracked upstream at\n` +
|
|
473
|
+
` https://github.com/oxc-project/oxc/issues/9170.\n\n` +
|
|
474
|
+
`See: https://www.typescriptlang.org/tsconfig/#experimentalDecorators`,
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Does the source contain TC39 decorator syntax (`@expr` on a class or class
|
|
479
|
+
// member)? Used ONLY when legacy decorators are off, to surface a clear
|
|
480
|
+
// diagnostic instead of oxc's verbatim passthrough → V8 SyntaxError. The cheap
|
|
481
|
+
// `source.includes("@")` pre-filter in the caller keeps decorator-free files off
|
|
482
|
+
// the parser; decorators only attach to ClassDeclaration/ClassExpression and
|
|
483
|
+
// their members (incl. accessors), and to `export`/`export default` wrappers.
|
|
484
|
+
function hasDecoratorSyntax(filePath, source, lang) {
|
|
485
|
+
const parse = getParseSync();
|
|
486
|
+
if (!parse) return false; // parser unavailable → let oxc/V8 surface the error
|
|
487
|
+
let program;
|
|
488
|
+
try {
|
|
489
|
+
program = parse(filePath, source, { lang }).program;
|
|
490
|
+
} catch {
|
|
491
|
+
return false; // unparseable → the transpile/V8 surfaces the real error
|
|
492
|
+
}
|
|
493
|
+
let found = false;
|
|
494
|
+
const visit = (node) => {
|
|
495
|
+
if (found || !node || typeof node !== "object") return;
|
|
496
|
+
if (Array.isArray(node)) {
|
|
497
|
+
for (const child of node) visit(child);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
if (Array.isArray(node.decorators) && node.decorators.length > 0) {
|
|
501
|
+
found = true;
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
for (const k in node) {
|
|
505
|
+
if (k === "type" || k === "start" || k === "end") continue;
|
|
506
|
+
visit(node[k]);
|
|
507
|
+
if (found) return;
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
visit(program.body);
|
|
511
|
+
return found;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Drop a trailing bare `export {};` — oxc injects it to preserve module-ness
|
|
515
|
+
// after stripping a file's only module syntax (e.g. a lone `import type`).
|
|
516
|
+
const EMPTY_EXPORT_MARKER = /(?:^|\n)[ \t]*export[ \t]*\{[ \t]*\}[ \t]*;?\s*$/;
|
|
517
|
+
function stripEmptyExportMarker(code) {
|
|
518
|
+
return code.replace(EMPTY_EXPORT_MARKER, "");
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// ── Transpile cache ─────────────────────────────────────────────────
|
|
522
|
+
// NUB_VERSION (from version.mjs) is the SOLE version component of the cache key:
|
|
523
|
+
// oxc-transform is pinned exact + vendored per release, so any emit change ships
|
|
524
|
+
// only in a new nub version, which `make version` bumps. CACHE_SCHEMA busts the
|
|
525
|
+
// cache when the on-disk ENTRY FORMAT changes (v3 = integrity prefix + leading
|
|
526
|
+
// format byte). The fast and compat tiers share this cache: post-extraction they
|
|
527
|
+
// emit byte-identical output for the same (source, ext, tsconfig, pkgType), so a
|
|
528
|
+
// single cache under one key is correct and maximizes hits.
|
|
529
|
+
const CACHE_SCHEMA = "3";
|
|
530
|
+
// Disable the transpile cache when (a) the permission model is active (writing a
|
|
531
|
+
// cache file may not be granted), or (b) the user set `NODE_COMPILE_CACHE=0` —
|
|
532
|
+
// Node's compile-cache disable signal, which nub honors as "no caching in this
|
|
533
|
+
// pipeline" (one knob for both V8's compile cache and nub's transpile cache; no
|
|
534
|
+
// nub-specific env var). Per wiki/runtime/transpile-cache.md (Colin 2026-05-18).
|
|
535
|
+
const CACHE_DISABLED =
|
|
536
|
+
process.permission?.has !== undefined || process.env.NODE_COMPILE_CACHE === "0";
|
|
537
|
+
let cacheDir = null;
|
|
538
|
+
if (!CACHE_DISABLED) {
|
|
539
|
+
const base = process.env.XDG_CACHE_HOME || (process.env.HOME ? join(process.env.HOME, ".cache") : null);
|
|
540
|
+
if (base) {
|
|
541
|
+
cacheDir = join(base, "nub", "transpile");
|
|
542
|
+
try { mkdirSync(cacheDir, { recursive: true }); } catch { cacheDir = null; }
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function cacheKey(source) {
|
|
547
|
+
return createHash("sha256")
|
|
548
|
+
.update(NUB_VERSION).update("\0")
|
|
549
|
+
.update(CACHE_SCHEMA).update("\0")
|
|
550
|
+
.update(source)
|
|
551
|
+
.digest("hex");
|
|
552
|
+
}
|
|
553
|
+
// Each entry is stored as `<16-hex integrity prefix><body>`, where the prefix is
|
|
554
|
+
// the first 8 bytes of sha256(body). cacheGet re-checks it and treats ANY
|
|
555
|
+
// mismatch — truncation, on-disk corruption, bit-rot, external edits — as a miss,
|
|
556
|
+
// so the entry is re-transpiled and overwritten (self-heals) instead of feeding
|
|
557
|
+
// garbage to V8.
|
|
558
|
+
const CACHE_INTEGRITY_LEN = 16;
|
|
559
|
+
function cacheIntegrity(body) {
|
|
560
|
+
return createHash("sha256").update(body).digest("hex").slice(0, CACHE_INTEGRITY_LEN);
|
|
561
|
+
}
|
|
562
|
+
function cacheGet(key) {
|
|
563
|
+
if (!cacheDir) return null;
|
|
564
|
+
let raw;
|
|
565
|
+
try {
|
|
566
|
+
raw = readFileSync(join(cacheDir, key), "utf8");
|
|
567
|
+
} catch {
|
|
568
|
+
return null;
|
|
569
|
+
}
|
|
570
|
+
if (raw.length < CACHE_INTEGRITY_LEN) return null;
|
|
571
|
+
const body = raw.slice(CACHE_INTEGRITY_LEN);
|
|
572
|
+
if (raw.slice(0, CACHE_INTEGRITY_LEN) !== cacheIntegrity(body)) return null;
|
|
573
|
+
return body;
|
|
574
|
+
}
|
|
575
|
+
let cacheTmpCounter = 0;
|
|
576
|
+
function cacheSet(key, value) {
|
|
577
|
+
if (!cacheDir) return;
|
|
578
|
+
const finalPath = join(cacheDir, key);
|
|
579
|
+
// Atomic write: temp file in the same dir, then rename (atomic on POSIX +
|
|
580
|
+
// Windows same-volume), so a concurrent reader sees old-or-complete, never torn.
|
|
581
|
+
const tmpPath = `${finalPath}.${process.pid}.${cacheTmpCounter++}.tmp`;
|
|
582
|
+
try {
|
|
583
|
+
writeFileSync(tmpPath, cacheIntegrity(value) + value);
|
|
584
|
+
renameSync(tmpPath, finalPath);
|
|
585
|
+
} catch {
|
|
586
|
+
try { unlinkSync(tmpPath); } catch {}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// ── Bounded-cache maintenance ───────────────────────────────────────
|
|
591
|
+
const CACHE_MAX_BYTES = 512 * 1024 * 1024; // 512 MiB — bounds runaway growth, not normal use
|
|
592
|
+
const SWEEP_INTERVAL_MS = 24 * 60 * 60 * 1000; // ≤ one sweep per day
|
|
593
|
+
export function maybeSweepCache() {
|
|
594
|
+
if (!cacheDir) return;
|
|
595
|
+
// Workers inherit this preload (via execArgv); only the main thread sweeps.
|
|
596
|
+
try {
|
|
597
|
+
if (!__require("node:worker_threads").isMainThread) return;
|
|
598
|
+
} catch {
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
const sentinel = join(cacheDir, ".sweep");
|
|
602
|
+
const s = statSync(sentinel, { throwIfNoEntry: false });
|
|
603
|
+
if (s && Date.now() - s.mtimeMs < SWEEP_INTERVAL_MS) return;
|
|
604
|
+
try {
|
|
605
|
+
writeFileSync(sentinel, "");
|
|
606
|
+
} catch {
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
import("./cache-evict.mjs")
|
|
610
|
+
.then((m) => m.sweepCache(cacheDir, CACHE_MAX_BYTES))
|
|
611
|
+
.catch(() => {});
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// ── Transpile ───────────────────────────────────────────────────────
|
|
615
|
+
// Transpile a TS/JSX file to JS, returning `{ format, source, shortCircuit }` in
|
|
616
|
+
// the shape both hook tiers hand back to Node. Format is detected (not derived
|
|
617
|
+
// from extension alone), so a CommonJS-syntax `.ts` is reported `commonjs` — the
|
|
618
|
+
// fix that makes `require()` of a TS file work on the compat tier, where Node's
|
|
619
|
+
// CJS translator loads it via this hook and keys on the returned format.
|
|
620
|
+
export function loadTranspile(url, ext) {
|
|
621
|
+
const filePath = fileURLToPath(url);
|
|
622
|
+
const source = readFileSync(filePath, "utf8");
|
|
623
|
+
const dir = dirname(filePath);
|
|
624
|
+
const { tsconfig } = getTsconfigForDir(dir);
|
|
625
|
+
const co = tsconfig?.config?.compilerOptions;
|
|
626
|
+
|
|
627
|
+
// Cache key folds in ext, the resolved tsconfig, and the nearest package.json
|
|
628
|
+
// type — the same source can transpile to a different format under a different
|
|
629
|
+
// type. The cached entry's leading byte ('c'/'m') records the chosen format,
|
|
630
|
+
// so a hit needs no re-detection.
|
|
631
|
+
const pkgType = ext === ".mts" || ext === ".cts" ? undefined : getPackageType(dir);
|
|
632
|
+
const tsconfigHash = co ? JSON.stringify(co) : "";
|
|
633
|
+
const key = cacheKey(source + "\0" + ext + "\0" + tsconfigHash + "\0" + (pkgType || ""));
|
|
634
|
+
const cached = cacheGet(key);
|
|
635
|
+
if (cached) {
|
|
636
|
+
return {
|
|
637
|
+
format: cached[0] === "c" ? "commonjs" : "module",
|
|
638
|
+
source: cached.slice(1),
|
|
639
|
+
shortCircuit: true,
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const format = moduleFormatFor(ext, pkgType, filePath, source);
|
|
644
|
+
|
|
645
|
+
const lang = ext === ".tsx" ? "tsx" : ext === ".jsx" ? "jsx" : "ts";
|
|
646
|
+
const opts = {
|
|
647
|
+
lang,
|
|
648
|
+
sourceType: format === "commonjs" ? "commonjs" : "module",
|
|
649
|
+
sourcemap: true,
|
|
650
|
+
// Lower syntax newer than the 22.15 floor. Critically this downlevels
|
|
651
|
+
// `using`/`await using` (Explicit Resource Management) — unparseable on Node
|
|
652
|
+
// 22's V8 — into the vendored `@oxc-project/runtime/helpers/usingCtx` shape,
|
|
653
|
+
// which resolves via VENDORED_PACKAGES. Without a target, oxc leaves `using`
|
|
654
|
+
// verbatim and Node 22 throws a SyntaxError. es2022 is the highest target
|
|
655
|
+
// that still lowers `using` while leaving everything Node 22 already supports
|
|
656
|
+
// (top-level await, class fields, private methods) untouched.
|
|
657
|
+
target: "es2022",
|
|
658
|
+
typescript: {},
|
|
659
|
+
// Decorators default to OFF (Stage-3 mode), matching tsc: legacy semantics
|
|
660
|
+
// and metadata are opt-in via tsconfig. See wiki/runtime/non-erasable-syntax.md.
|
|
661
|
+
decorator: co?.experimentalDecorators === true
|
|
662
|
+
? { legacy: true, emitDecoratorMetadata: co?.emitDecoratorMetadata === true }
|
|
663
|
+
: undefined,
|
|
664
|
+
};
|
|
665
|
+
if (ext === ".tsx" || ext === ".jsx") {
|
|
666
|
+
opts.jsx = {
|
|
667
|
+
runtime: co?.jsx === "react" ? "classic" : "automatic",
|
|
668
|
+
importSource: co?.jsxImportSource || "react",
|
|
669
|
+
};
|
|
670
|
+
if (co?.jsxFactory) opts.jsx.pragma = co.jsxFactory;
|
|
671
|
+
if (co?.jsxFragmentFactory) opts.jsx.pragmaFrag = co.jsxFragmentFactory;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// Stage-3 decorators: oxc returns errors:[] and emits the `@decorator` syntax
|
|
675
|
+
// verbatim, so the result-error check below never fires and V8 throws a bare
|
|
676
|
+
// SyntaxError. When legacy mode is off and decorator syntax is present, reject
|
|
677
|
+
// with the documented Option-A diagnostic instead.
|
|
678
|
+
if (co?.experimentalDecorators !== true && source.includes("@") &&
|
|
679
|
+
hasDecoratorSyntax(filePath, source, lang)) {
|
|
680
|
+
throw stage3DecoratorError(filePath);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
const result = transformSync(filePath, source, opts);
|
|
684
|
+
if (result.errors.length > 0) {
|
|
685
|
+
const details = result.errors.map((e) => e.codeframe || e.message).join("\n\n");
|
|
686
|
+
throw new Error(`Transpile error in ${filePath}:\n${details}`);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
let code = result.code;
|
|
690
|
+
// A CommonJS file must not carry oxc's injected ESM `export {};` marker (CJS
|
|
691
|
+
// body + ESM marker won't run). Node's strip-types emits no such marker.
|
|
692
|
+
if (format === "commonjs") code = stripEmptyExportMarker(code);
|
|
693
|
+
if (result.map) {
|
|
694
|
+
const map = typeof result.map === "string" ? JSON.parse(result.map) : result.map;
|
|
695
|
+
map.sourcesContent = [source];
|
|
696
|
+
code += `\n//# sourceMappingURL=data:application/json;base64,${Buffer.from(JSON.stringify(map)).toString("base64")}\n`;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// Store the chosen format as a leading byte so cache hits skip re-detection.
|
|
700
|
+
cacheSet(key, (format === "commonjs" ? "c" : "m") + code);
|
|
701
|
+
return { format, source: code, shortCircuit: true };
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// ── Data-format imports ─────────────────────────────────────────────
|
|
705
|
+
function lazyRequire(pkg) {
|
|
706
|
+
try { return __require(pkg); } catch {
|
|
707
|
+
throw new Error(`Nub: importing this file requires the "${pkg}" package.\nInstall it: npm install ${pkg}`);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function stripJsonComments(text) {
|
|
712
|
+
let result = "", i = 0, inString = false, escape = false;
|
|
713
|
+
while (i < text.length) {
|
|
714
|
+
const ch = text[i];
|
|
715
|
+
if (escape) { result += ch; escape = false; i++; continue; }
|
|
716
|
+
if (inString) { if (ch === "\\") escape = true; if (ch === '"') inString = false; result += ch; i++; continue; }
|
|
717
|
+
if (ch === '"') { inString = true; result += ch; i++; continue; }
|
|
718
|
+
if (ch === "/" && text[i + 1] === "/") { while (i < text.length && text[i] !== "\n") i++; continue; }
|
|
719
|
+
if (ch === "/" && text[i + 1] === "*") { i += 2; while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i++; i += 2; continue; }
|
|
720
|
+
result += ch; i++;
|
|
721
|
+
}
|
|
722
|
+
return result;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
export function loadData(url, ext) {
|
|
726
|
+
const filePath = fileURLToPath(url);
|
|
727
|
+
const raw = readFileSync(filePath, "utf8");
|
|
728
|
+
const kind = DATA_EXTS[ext];
|
|
729
|
+
|
|
730
|
+
if (kind === "txt") {
|
|
731
|
+
return { format: "module", source: `export default ${JSON.stringify(raw)};\n`, shortCircuit: true };
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
let parsed;
|
|
735
|
+
if (nubNative) {
|
|
736
|
+
if (kind === "yaml") parsed = nubNative.parseYaml(raw);
|
|
737
|
+
else if (kind === "toml") parsed = nubNative.parseToml(raw);
|
|
738
|
+
else if (kind === "json5") parsed = nubNative.parseJson5(raw);
|
|
739
|
+
else if (kind === "jsonc") parsed = nubNative.parseJsonc(raw);
|
|
740
|
+
} else {
|
|
741
|
+
if (kind === "yaml") parsed = lazyRequire("yaml").parse(raw);
|
|
742
|
+
else if (kind === "toml") parsed = lazyRequire("@iarna/toml").parse(raw);
|
|
743
|
+
else if (kind === "json5") parsed = lazyRequire("json5").parse(raw);
|
|
744
|
+
else if (kind === "jsonc") parsed = JSON.parse(stripJsonComments(raw));
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
if (parsed == null) {
|
|
748
|
+
return { format: "module", source: "export default undefined;\n", shortCircuit: true };
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
let code = `const _data = ${JSON.stringify(parsed)};\nexport default _data;\n`;
|
|
752
|
+
if (typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
753
|
+
for (const key of Object.keys(parsed)) {
|
|
754
|
+
// Emit a named export only for keys that are valid, non-reserved binding
|
|
755
|
+
// identifiers; everything else remains reachable via the default export.
|
|
756
|
+
if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) && !RESERVED_EXPORT_NAMES.has(key)) {
|
|
757
|
+
code += `export const ${key} = _data[${JSON.stringify(key)}];\n`;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
return { format: "module", source: code, shortCircuit: true };
|
|
762
|
+
}
|