@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.
@@ -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
+ }
@@ -0,0 +1,5 @@
1
+ // `node --import <pkg>` — arms BOTH module systems (ESM hooks + CommonJS
2
+ // require() augmentation), tsx's default-entry shape.
3
+ import { arm } from "./loader-entry.mjs";
4
+
5
+ arm({ esm: true, cjs: true });
package/package.json CHANGED
@@ -1,7 +1,61 @@
1
1
  {
2
2
  "name": "@nubjs/loader",
3
- "version": "0.0.0",
4
- "description": "Placeholder the Nub loader ships here with the next Nub release.",
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
+ }