@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,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// `node --require <pkg>` — the CommonJS delivery of the full loader (ESM hooks +
|
|
3
|
+
// CommonJS require() augmentation). On Node 22.15+ this is strictly the better
|
|
4
|
+
// consumption shape: a `--require` CJS preload keeps Node's synchronous CJS entry
|
|
5
|
+
// path (the mere presence of `--import` forces eager async ESM-loader init that
|
|
6
|
+
// routes even a CJS entry through the async module-job — see preload.cjs, R1),
|
|
7
|
+
// which is exactly why the nub CLI injects its own fast-tier preload this way.
|
|
8
|
+
//
|
|
9
|
+
// require(esm) loads the shared ES-module arming logic synchronously (TLA-free by
|
|
10
|
+
// construction). Where require(esm) is unavailable — `--no-experimental-require-
|
|
11
|
+
// module`, or a compat-tier Node below 22.12/20.19 — fall back to registering the
|
|
12
|
+
// loader-worker hooks directly (preload-common is CommonJS, so that registration
|
|
13
|
+
// needs no require(esm)): `import`-side TS still transpiles through the worker,
|
|
14
|
+
// and only require()'d TS is inactive, matching the CLI preload's own degradation
|
|
15
|
+
// under that flag.
|
|
16
|
+
try {
|
|
17
|
+
require("./loader-entry.mjs").arm({ esm: true, cjs: true });
|
|
18
|
+
} catch (err) {
|
|
19
|
+
if (!err || err.code !== "ERR_REQUIRE_ESM") throw err;
|
|
20
|
+
if (process.versions.electron) return;
|
|
21
|
+
// The loader worker's transform-core needs the addon path in the inherited env
|
|
22
|
+
// — loader-entry.mjs (which normally sets it) could not load here.
|
|
23
|
+
require("./loader-platform.cjs").ensureAddonEnv(require);
|
|
24
|
+
const { pathToFileURL } = require("node:url");
|
|
25
|
+
const common = require("./preload-common.cjs");
|
|
26
|
+
common.registerLoaderWorker(
|
|
27
|
+
"./preload-async-hooks.mjs",
|
|
28
|
+
pathToFileURL(__filename).href,
|
|
29
|
+
// Same payload the ESM entry sends: the worker clears its own realm's
|
|
30
|
+
// clobber map (see initialize in preload-async-hooks.mjs).
|
|
31
|
+
{ data: { standaloneLoader: true } },
|
|
32
|
+
);
|
|
33
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,61 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nubjs/loader",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.8.2",
|
|
4
|
+
"description": "Standalone TypeScript loader for Node.js from the Nub project — TypeScript, JSX, tsconfig paths, and data-format imports through a native transform, registered the way tsx and ts-node are",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"repository": "https://github.com/nubjs/nub"
|
|
6
|
+
"repository": "https://github.com/nubjs/nub",
|
|
7
|
+
"homepage": "https://nubjs.com",
|
|
8
|
+
"bugs": {
|
|
9
|
+
"url": "https://github.com/nubjs/nub/issues"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"typescript",
|
|
13
|
+
"loader",
|
|
14
|
+
"esm",
|
|
15
|
+
"nodejs",
|
|
16
|
+
"tsx",
|
|
17
|
+
"ts-node",
|
|
18
|
+
"transpiler",
|
|
19
|
+
"jsx"
|
|
20
|
+
],
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18.19.0"
|
|
23
|
+
},
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"import": "./loader-register.mjs",
|
|
27
|
+
"require": "./loader-register.cjs"
|
|
28
|
+
},
|
|
29
|
+
"./esm": "./loader-esm.mjs",
|
|
30
|
+
"./package.json": "./package.json"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"loader-register.mjs",
|
|
34
|
+
"loader-register.cjs",
|
|
35
|
+
"loader-esm.mjs",
|
|
36
|
+
"loader-entry.mjs",
|
|
37
|
+
"loader-addon-env.mjs",
|
|
38
|
+
"loader-platform.cjs",
|
|
39
|
+
"transform-core.mjs",
|
|
40
|
+
"preload-common.cjs",
|
|
41
|
+
"preload-async-hooks.mjs",
|
|
42
|
+
"pnp-util.cjs",
|
|
43
|
+
"floor-builtin.mjs",
|
|
44
|
+
"cache-evict.mjs",
|
|
45
|
+
"README.md",
|
|
46
|
+
"LICENSE"
|
|
47
|
+
],
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@oxc-project/runtime": "0.140.0"
|
|
50
|
+
},
|
|
51
|
+
"optionalDependencies": {
|
|
52
|
+
"@nubjs/loader-darwin-arm64": "0.8.2",
|
|
53
|
+
"@nubjs/loader-darwin-x64": "0.8.2",
|
|
54
|
+
"@nubjs/loader-linux-x64": "0.8.2",
|
|
55
|
+
"@nubjs/loader-linux-x64-musl": "0.8.2",
|
|
56
|
+
"@nubjs/loader-linux-arm64": "0.8.2",
|
|
57
|
+
"@nubjs/loader-linux-arm64-musl": "0.8.2",
|
|
58
|
+
"@nubjs/loader-win32-x64": "0.8.2",
|
|
59
|
+
"@nubjs/loader-win32-arm64": "0.8.2"
|
|
60
|
+
}
|
|
7
61
|
}
|
package/pnp-util.cjs
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Shared Yarn PnP ESM-resolution helper, used by BOTH realms that resolve ESM
|
|
2
|
+
// imports: the main-thread preload (preload-common.cjs, fast tier) and the
|
|
3
|
+
// compat-tier loader worker (preload-async-hooks.mjs). CJS resolution does NOT live
|
|
4
|
+
// here — it just strips the `conditions` option and delegates to PnP's own patched
|
|
5
|
+
// `_resolveFilename` (see installCjsRequireHooks). ESM is different: PnP does not
|
|
6
|
+
// patch the ESM loader, so `import` of a PnP dep must be resolved explicitly, AND it
|
|
7
|
+
// must pass the *import* conditions — otherwise a dual package (separate `import` /
|
|
8
|
+
// `require` exports) resolves to its CJS build and `import { x }` fails. So we go
|
|
9
|
+
// through `pnpapi.resolveRequest` (the only PnP resolver that accepts conditions).
|
|
10
|
+
const { readFileSync } = require("node:fs");
|
|
11
|
+
const { dirname, join, sep } = require("node:path");
|
|
12
|
+
const { fileURLToPath, pathToFileURL } = require("node:url");
|
|
13
|
+
|
|
14
|
+
// A directory issuer for resolveRequest: cwd with a trailing separator so PnP treats
|
|
15
|
+
// it as a directory. `path.sep` (not a literal "/") keeps it correct on Windows.
|
|
16
|
+
const cwdIssuer = () => process.cwd() + sep;
|
|
17
|
+
|
|
18
|
+
// Module format of a resolved file. `.mjs`/`.cjs` are unambiguous; a `.js` file
|
|
19
|
+
// inherits its nearest package's `type`, read by walking up to package.json via the
|
|
20
|
+
// zip-patched `fs` (`.pnp.cjs` patches `fs`, so reads inside `.zip` work in both
|
|
21
|
+
// realms). No pnpapi needed. Defaults to "commonjs" if detection fails.
|
|
22
|
+
function formatOf(resolvedPath) {
|
|
23
|
+
if (resolvedPath.endsWith(".mjs")) return "module";
|
|
24
|
+
if (resolvedPath.endsWith(".cjs")) return "commonjs";
|
|
25
|
+
let dir = dirname(resolvedPath);
|
|
26
|
+
for (let i = 0; i < 16; i++) {
|
|
27
|
+
try {
|
|
28
|
+
const pj = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
|
|
29
|
+
return pj.type === "module" ? "module" : "commonjs";
|
|
30
|
+
} catch {}
|
|
31
|
+
const up = dirname(dir);
|
|
32
|
+
if (up === dir) break;
|
|
33
|
+
dir = up;
|
|
34
|
+
}
|
|
35
|
+
return "commonjs";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Resolve a `specifier` through PnP for a hook `context`, applying the correct
|
|
39
|
+
// exports conditions so a dual package resolves to the right build. Returns a Node
|
|
40
|
+
// resolve-hook result `{ url, format, shortCircuit }`, or `null` if PnP can't resolve
|
|
41
|
+
// it (the caller then delegates). Throwing is also a fall-through signal.
|
|
42
|
+
//
|
|
43
|
+
// Conditions: trust `context.conditions` when Node populates it (Node 24+/26 for both
|
|
44
|
+
// import and require; Node 22.15 for import). But Node 22.15 hands the resolve hook
|
|
45
|
+
// an EMPTY `conditions` for a `require()` — so when it's empty, infer the side from
|
|
46
|
+
// `importAttributes` (`undefined` ⇒ a require, an object ⇒ an import) and apply the
|
|
47
|
+
// matching default. Without this a 22.15 `require()` of a dual package would wrongly
|
|
48
|
+
// get the `import` build.
|
|
49
|
+
function pnpResolveEsm(api, specifier, context) {
|
|
50
|
+
const parentURL = context && context.parentURL;
|
|
51
|
+
const issuer = parentURL ? fileURLToPath(parentURL) : cwdIssuer();
|
|
52
|
+
let conds = context && context.conditions;
|
|
53
|
+
if (!conds || !conds.length) {
|
|
54
|
+
const isImport = !!context && context.importAttributes !== undefined;
|
|
55
|
+
conds = isImport ? ["node", "import"] : ["node", "require"];
|
|
56
|
+
}
|
|
57
|
+
const resolved = api.resolveRequest(specifier, issuer, { conditions: new Set(conds) });
|
|
58
|
+
if (!resolved) return null;
|
|
59
|
+
return { url: pathToFileURL(resolved).href, format: formatOf(resolved), shortCircuit: true };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = { pnpResolveEsm, cwdIssuer };
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// Nub compat-tier hooks module — Node 18.19 through 22.14.
|
|
2
|
+
//
|
|
3
|
+
// On Node 22.15+, runtime/preload.mjs registers its hooks synchronously via
|
|
4
|
+
// `module.registerHooks({ resolve, load })`. That API didn't exist before 22.15,
|
|
5
|
+
// so on 18.19..22.14 the main-thread bootstrap calls
|
|
6
|
+
// `module.register('./preload-async-hooks.mjs', parentURL)` instead, which loads
|
|
7
|
+
// THIS file into a dedicated loader worker thread and uses its async
|
|
8
|
+
// `resolve` / `load` exports. (CommonJS `require()` is augmented separately, on
|
|
9
|
+
// the main thread, by preload.mjs's installCjsRequireHooks — `module.register`
|
|
10
|
+
// hooks the ESM loader only.)
|
|
11
|
+
//
|
|
12
|
+
// There is NO logic of its own here: resolution + transpilation come verbatim
|
|
13
|
+
// from runtime/transform-core.mjs, the single source of truth shared with the
|
|
14
|
+
// fast path. The two tiers can no longer drift — the only difference is the
|
|
15
|
+
// async function signatures Node's loader-worker protocol requires (it awaits
|
|
16
|
+
// the returned values, so returning the core's synchronous results is fine). The
|
|
17
|
+
// worker injects no watch hooks (watch IPC is main-thread only), so the core's
|
|
18
|
+
// dependency reporters stay no-ops here, exactly as before the extraction.
|
|
19
|
+
|
|
20
|
+
// Floor bootstrap (Node < 22.3/20.16/18.20.4): threads node:module's createRequire
|
|
21
|
+
// into transform-core via a MODULE-SCOPE SETTER — never globalThis (brand boundary) —
|
|
22
|
+
// because transform-core has no process.getBuiltinModule on the floor. floor-builtin
|
|
23
|
+
// calls transform-core's setter during its own evaluation, so importing it FIRST
|
|
24
|
+
// (ESM evaluates imports in source order) means the value is threaded before any hook
|
|
25
|
+
// in this loader worker fires. No-op where getBuiltinModule exists. This worker runs
|
|
26
|
+
// OFF any user loader chain, so floor-builtin's static node:module import never leaks
|
|
27
|
+
// — see floor-builtin.mjs. (worker-polyfill is NOT loaded here: this is the dedicated
|
|
28
|
+
// loader worker, not a user realm, so it installs no browser globals.)
|
|
29
|
+
import "./floor-builtin.mjs";
|
|
30
|
+
import {
|
|
31
|
+
TRANSPILE_EXTS, PLAIN_JS_EXTS, CLOBBER_MAP, dataExtsFor,
|
|
32
|
+
extname, isFileUrl, resolveSpec, loadTranspile, maybeTranspilePlainJs, loadData, loadTextImport, isDependency,
|
|
33
|
+
} from "./transform-core.mjs";
|
|
34
|
+
import { createRequire, isBuiltin } from "node:module";
|
|
35
|
+
import { existsSync } from "node:fs";
|
|
36
|
+
import { join, dirname } from "node:path";
|
|
37
|
+
import { pnpResolveEsm } from "./pnp-util.cjs";
|
|
38
|
+
|
|
39
|
+
// Yarn PnP handle for this loader worker. The worker runs in its own thread where
|
|
40
|
+
// `.pnp.cjs` was never --require'd, so neither the `pnpapi` builtin nor
|
|
41
|
+
// `module.findPnpApi` is installed here (the main-thread preload uses findPnpApi; it
|
|
42
|
+
// can't reach across to this realm). So bootstrap PnP for this thread directly: walk
|
|
43
|
+
// up from cwd to the `.pnp.cjs` Rust located and require it by absolute path — that
|
|
44
|
+
// returns the pnpapi object. nub then resolves PnP specifiers via
|
|
45
|
+
// `pnpapi.resolveRequest` (its public, conditions-free resolver), mirroring the main
|
|
46
|
+
// thread, so there is no need to register Yarn's `.pnp.loader.mjs` (which deadlocks
|
|
47
|
+
// against the fast tier's `module.registerHooks`). `null` when not a PnP run.
|
|
48
|
+
const __pnp = (() => {
|
|
49
|
+
if (!process.versions.pnp) return null;
|
|
50
|
+
const req = createRequire(import.meta.url);
|
|
51
|
+
try {
|
|
52
|
+
let dir = process.cwd();
|
|
53
|
+
for (;;) {
|
|
54
|
+
const candidate = join(dir, ".pnp.cjs");
|
|
55
|
+
if (existsSync(candidate)) return req(candidate);
|
|
56
|
+
const parent = dirname(dir);
|
|
57
|
+
if (parent === dir) return null;
|
|
58
|
+
dir = parent;
|
|
59
|
+
}
|
|
60
|
+
} catch { return null; }
|
|
61
|
+
})();
|
|
62
|
+
|
|
63
|
+
// Node calls this once per worker when the main thread invokes
|
|
64
|
+
// `module.register(url, parentURL, { data })`, and register() blocks until it
|
|
65
|
+
// resolves — so anything done here lands before the first resolve/load fires.
|
|
66
|
+
// The STANDALONE LOADER (loader-entry.mjs / loader-register.cjs) sends
|
|
67
|
+
// `{ standaloneLoader: true }`: this worker imports its OWN transform-core
|
|
68
|
+
// instance (a separate module registry from the main thread), so the entry's
|
|
69
|
+
// main-thread CLOBBER_MAP.clear() cannot reach the map THIS realm's resolveSpec
|
|
70
|
+
// reads — without the clear here, `import "@js-temporal/polyfill"` on the
|
|
71
|
+
// compat tier resolved to a synthetic re-export of a global the loader never
|
|
72
|
+
// installs, silently binding `undefined` (verified on Node 20.19). The nub CLI
|
|
73
|
+
// registers this worker with no data, keeping its clobbers intact.
|
|
74
|
+
export async function initialize(data) {
|
|
75
|
+
if (data && data.standaloneLoader) CLOBBER_MAP.clear();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── Resolve hook ────────────────────────────────────────────────────
|
|
79
|
+
export async function resolve(specifier, context, nextResolve) {
|
|
80
|
+
const r = resolveSpec(specifier, context.parentURL);
|
|
81
|
+
if (r) return r;
|
|
82
|
+
// Yarn PnP: resolve deps through PnP's own resolver — identical to the fast tier,
|
|
83
|
+
// via the shared helper (resolveRequest with the import conditions + format
|
|
84
|
+
// detection), so dual packages resolve to their `import` build.
|
|
85
|
+
if (__pnp && !isBuiltin(specifier) && !specifier.startsWith("node:")) {
|
|
86
|
+
try {
|
|
87
|
+
const res = pnpResolveEsm(__pnp, specifier, context);
|
|
88
|
+
if (res) return res;
|
|
89
|
+
} catch { /* fall through to Node's resolver */ }
|
|
90
|
+
}
|
|
91
|
+
return nextResolve(specifier, context);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── Load hook ───────────────────────────────────────────────────────
|
|
95
|
+
export async function load(url, context, nextLoad) {
|
|
96
|
+
// Import Text (attribute-keyed): honor `with { type: "text" }` on ANY extension,
|
|
97
|
+
// checked BEFORE extension dispatch so `import s from "./c.yaml" with {type:"text"}`
|
|
98
|
+
// returns the raw text, not parsed YAML. shortCircuits, so Node never runs its own
|
|
99
|
+
// unknown-'text'-attribute validation. (Node 18.20+ parses the `with` syntax; the
|
|
100
|
+
// 18.19.x floor cannot parse it at all — see the import-text thread.) Unlike the
|
|
101
|
+
// fast-tier hook (preload-common.cjs) this ALWAYS polyfills, with no native
|
|
102
|
+
// fall-through gate: native import-text is Node 26.5+, but the compat tier tops out
|
|
103
|
+
// at Node 22.14, so a native-capable Node never reaches this loader worker.
|
|
104
|
+
// `isFileUrl` because this branch precedes extension dispatch and so is not covered
|
|
105
|
+
// by `extname`'s scheme gate; the polyfill reads the bytes off disk. Mirrors the
|
|
106
|
+
// fast-tier hook.
|
|
107
|
+
if (context?.importAttributes?.type === "text" && isFileUrl(url)) return loadTextImport(url);
|
|
108
|
+
const ext = extname(url);
|
|
109
|
+
// node_modules deps are NEVER transpiled (the byte-parity boundary). This guard is
|
|
110
|
+
// make-or-break now that TRANSPILE_EXTS includes `.js`/`.mjs`/`.cjs`: without it,
|
|
111
|
+
// the compat tier would route every dependency `.js` through oxc. (loadTranspile's
|
|
112
|
+
// own skip-gate handles the project-source no-op case; this keeps deps off the
|
|
113
|
+
// pipeline entirely.) Mirrors the fast-tier sync hook's `!isDependency` gate.
|
|
114
|
+
if (TRANSPILE_EXTS.has(ext) && !isDependency(url)) {
|
|
115
|
+
// Module-format + decorator detection inside loadTranspile is a synchronous
|
|
116
|
+
// native call (nub's addon), available on every supported Node — no parser
|
|
117
|
+
// warm-up needed (the old `await ensureParser()` for the ESM-only oxc-parser
|
|
118
|
+
// is gone with the package).
|
|
119
|
+
return loadTranspile(url, ext);
|
|
120
|
+
}
|
|
121
|
+
// Project-source plain JS: transpile ONLY when it carries transformable syntax. A
|
|
122
|
+
// no-op plain-JS file returns null and falls through to `nextLoad` — Node's own
|
|
123
|
+
// loader handles it byte-identically, preserving every native CJS/ESM behavior.
|
|
124
|
+
// node_modules excluded (the byte-parity boundary).
|
|
125
|
+
if (PLAIN_JS_EXTS.has(ext) && !isDependency(url)) {
|
|
126
|
+
const r = maybeTranspilePlainJs(url, ext);
|
|
127
|
+
if (r) return r;
|
|
128
|
+
}
|
|
129
|
+
// Data-format imports. dataExtsFor pins node_modules to nub's BUILT-IN loaders, so
|
|
130
|
+
// the project's `loader` config can't redefine how a dependency's imports load.
|
|
131
|
+
if (ext in dataExtsFor(url)) return loadData(url, ext);
|
|
132
|
+
return nextLoad(url, context);
|
|
133
|
+
}
|