@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.
Files changed (37) hide show
  1. package/bin/nub.exe +0 -0
  2. package/package.json +1 -1
  3. package/runtime/addons/nub-native.node +0 -0
  4. package/runtime/cache-evict.mjs +69 -0
  5. package/runtime/node_modules/@oxc-parser/binding-win32-arm64-msvc/README.md +3 -0
  6. package/runtime/node_modules/@oxc-parser/binding-win32-arm64-msvc/package.json +39 -0
  7. package/runtime/node_modules/@oxc-parser/binding-win32-arm64-msvc/parser.win32-arm64-msvc.node +0 -0
  8. package/runtime/node_modules/@oxc-transform/binding-win32-arm64-msvc/README.md +3 -0
  9. package/runtime/node_modules/@oxc-transform/binding-win32-arm64-msvc/package.json +41 -0
  10. package/runtime/node_modules/@oxc-transform/binding-win32-arm64-msvc/transform.win32-arm64-msvc.node +0 -0
  11. package/runtime/polyfills.mjs +73 -97
  12. package/runtime/preload-async-hooks.mjs +50 -0
  13. package/runtime/preload.mjs +274 -320
  14. package/runtime/transform-core.mjs +762 -0
  15. package/runtime/version.mjs +12 -0
  16. package/runtime/worker-polyfill.mjs +147 -9
  17. package/runtime/node_modules/get-tsconfig/LICENSE +0 -21
  18. package/runtime/node_modules/get-tsconfig/README.md +0 -268
  19. package/runtime/node_modules/get-tsconfig/dist/index.cjs +0 -7
  20. package/runtime/node_modules/get-tsconfig/dist/index.d.cts +0 -2116
  21. package/runtime/node_modules/get-tsconfig/dist/index.d.mts +0 -2116
  22. package/runtime/node_modules/get-tsconfig/dist/index.mjs +0 -7
  23. package/runtime/node_modules/get-tsconfig/package.json +0 -46
  24. package/runtime/node_modules/oxc-transform/LICENSE +0 -22
  25. package/runtime/node_modules/oxc-transform/README.md +0 -84
  26. package/runtime/node_modules/oxc-transform/browser.js +0 -1
  27. package/runtime/node_modules/oxc-transform/index.d.ts +0 -658
  28. package/runtime/node_modules/oxc-transform/index.js +0 -598
  29. package/runtime/node_modules/oxc-transform/package.json +0 -114
  30. package/runtime/node_modules/oxc-transform/webcontainer-fallback.cjs +0 -21
  31. package/runtime/node_modules/resolve-pkg-maps/LICENSE +0 -21
  32. package/runtime/node_modules/resolve-pkg-maps/README.md +0 -216
  33. package/runtime/node_modules/resolve-pkg-maps/dist/index.cjs +0 -1
  34. package/runtime/node_modules/resolve-pkg-maps/dist/index.d.cts +0 -11
  35. package/runtime/node_modules/resolve-pkg-maps/dist/index.d.mts +0 -11
  36. package/runtime/node_modules/resolve-pkg-maps/dist/index.mjs +0 -1
  37. 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
+ }