Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -907,6 +907,23 @@ jobs:
name: nub-${{ matrix.platform }}
path: npm/nub-${{ matrix.platform }}

- name: Assemble loader platform package (addon only)
shell: bash
env:
MATRIX_PLATFORM: ${{ matrix.platform }}
run: |
# The standalone loader (npm/loader) ships the SAME nub-native addon
# staged above, as its own per-platform package — the addon is the only
# platform-specific part of the loader; its JS is a verbatim slice of
# runtime/ that the publish job stages once (scripts/build-loader-npm.mjs).
cp runtime/addons/nub-native.node "npm/loader-$MATRIX_PLATFORM/nub-native.node"

- name: Upload loader platform package
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: loader-${{ matrix.platform }}
path: npm/loader-${{ matrix.platform }}

glibc-floor-guard:
# The real glibc-floor gate. test-install runs on glibc-2.39 hosts and would
# NOT catch a floor regression (0.0.44 shipped a GLIBC_2.39-requiring
Expand Down Expand Up @@ -1650,6 +1667,54 @@ jobs:
(cd npm/nub-types && npm publish --access public)
fi

- name: Prepare and publish loader packages (idempotent)
run: |
# The standalone loader: 8 addon-only platform packages first, the root
# package LAST — the same ordering safety as the CLI's packages, since the
# root exact-pins its platform deps and must never resolve to a version
# whose addon packages have not all published. Same idempotent skip on an
# already-published version. Each platform leg uploaded its addon package
# (loader-<platform> artifact); the root's JS is staged here from this
# checkout by build-loader-npm.mjs (no addon on this runner, by design).
for platform in darwin-arm64 darwin-x64 linux-x64 linux-x64-musl linux-arm64 linux-arm64-musl win32-x64 win32-arm64; do
PKG="artifacts/loader-$platform"
if [ ! -d "$PKG" ]; then
echo "⚠️ Skipping loader-$platform (not built)"
continue
fi
cp "npm/loader-$platform/package.json" "$PKG/package.json"
node -e "
const fs = require('fs');
const p = JSON.parse(fs.readFileSync('$PKG/package.json', 'utf8'));
p.version = process.env.VERSION;
fs.writeFileSync('$PKG/package.json', JSON.stringify(p, null, 2));
"
NAME="@nubjs/loader-$platform"
if [ "$(npm view "$NAME@$VERSION" version 2>/dev/null)" = "$VERSION" ]; then
echo "✓ $NAME@$VERSION already published — skipping"
continue
fi
echo "→ $NAME@$VERSION"
(cd "$PKG" && npm publish --access public)
done
node scripts/build-loader-npm.mjs --addon /nonexistent
node -e "
const fs = require('fs');
const p = JSON.parse(fs.readFileSync('npm/loader/package.json', 'utf8'));
p.version = process.env.VERSION;
for (const k of Object.keys(p.optionalDependencies || {})) {
p.optionalDependencies[k] = process.env.VERSION;
}
fs.writeFileSync('npm/loader/package.json', JSON.stringify(p, null, 2) + '\n');
"
NAME="$(node -p "require('./npm/loader/package.json').name")"
if [ "$(npm view "$NAME@$VERSION" version 2>/dev/null)" = "$VERSION" ]; then
echo "✓ $NAME@$VERSION already published — skipping"
else
echo "→ $NAME@$VERSION"
(cd npm/loader && npm publish --access public)
fi

stable-immutable-release:
name: Stage immutable stable release (+ verify assets)
# Publish the exact versioned release and verify its complete 32-asset set
Expand Down Expand Up @@ -2322,6 +2387,49 @@ jobs:
(cd npm/nub-types && npm publish --access public --tag canary)
fi

- name: Prepare and publish loader packages (idempotent)
run: |
# Mirrors publish-npm's loader step (keep the two in sync): platform
# addon packages first, root last, every publish tagged canary.
for platform in darwin-arm64 darwin-x64 linux-x64 linux-x64-musl linux-arm64 linux-arm64-musl win32-x64 win32-arm64; do
PKG="artifacts/loader-$platform"
if [ ! -d "$PKG" ]; then
echo "⚠️ Skipping loader-$platform (not built)"
continue
fi
cp "npm/loader-$platform/package.json" "$PKG/package.json"
node -e "
const fs = require('fs');
const p = JSON.parse(fs.readFileSync('$PKG/package.json', 'utf8'));
p.version = process.env.VERSION;
fs.writeFileSync('$PKG/package.json', JSON.stringify(p, null, 2));
"
NAME="@nubjs/loader-$platform"
if [ "$(npm view "$NAME@$VERSION" version 2>/dev/null)" = "$VERSION" ]; then
echo "✓ $NAME@$VERSION already published — skipping"
continue
fi
echo "→ $NAME@$VERSION (canary)"
(cd "$PKG" && npm publish --access public --tag canary)
done
node scripts/build-loader-npm.mjs --addon /nonexistent
node -e "
const fs = require('fs');
const p = JSON.parse(fs.readFileSync('npm/loader/package.json', 'utf8'));
p.version = process.env.VERSION;
for (const k of Object.keys(p.optionalDependencies || {})) {
p.optionalDependencies[k] = process.env.VERSION;
}
fs.writeFileSync('npm/loader/package.json', JSON.stringify(p, null, 2) + '\n');
"
NAME="$(node -p "require('./npm/loader/package.json').name")"
if [ "$(npm view "$NAME@$VERSION" version 2>/dev/null)" = "$VERSION" ]; then
echo "✓ $NAME@$VERSION already published — skipping"
else
echo "→ $NAME@$VERSION (canary)"
(cd npm/loader && npm publish --access public --tag canary)
fi


canary-release:
name: Update rolling canary release
Expand Down
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,16 @@ npm/nub-*/bin/
npm/nub/*.tgz
npm/nub-*/*.tgz

# Standalone-loader packages (npm/loader, npm/loader-<platform>): the JS is a
# verbatim slice of runtime/ staged in by scripts/build-loader-npm.mjs, and the
# addon comes from the platform build — only package.json + README are source.
npm/loader/*.mjs
npm/loader/*.cjs
npm/loader/LICENSE
npm/loader/*.tgz
npm/loader-*/nub-native.node
npm/loader-*/*.tgz

# nub's localStorage/cache leaking into cwd as `0/<node-version>-<arch>-…` when
# XDG_CACHE_HOME is unset and the store path resolves relative (a real nub bug —
# the cache should always live under ~/.cache/nub). Ignore the stray dirs until
Expand Down
14 changes: 14 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,20 @@ version-check:
const types = JSON.parse(fs.readFileSync('npm/nub-types/package.json', 'utf8')); \
if (types.version !== v) errors.push('npm/nub-types/package.json has ' + types.version + ', expected ' + v); \
} catch { errors.push('missing or unreadable npm/nub-types/package.json'); } \
try { \
const loader = JSON.parse(fs.readFileSync('npm/loader/package.json', 'utf8')); \
if (loader.version !== v) errors.push('npm/loader/package.json has ' + loader.version + ', expected ' + v); \
for (const [dep, ver] of Object.entries(loader.optionalDependencies || {})) { \
if (ver !== v) errors.push(dep + ' optionalDependency pinned at ' + ver + ', expected ' + v); \
const pkg = 'npm/' + dep.replace('@nubjs/', '') + '/package.json'; \
try { \
const p = JSON.parse(fs.readFileSync(pkg, 'utf8')); \
if (p.version !== v) errors.push(pkg + ' has ' + p.version + ', expected ' + v); \
} catch { errors.push('missing or unreadable ' + pkg); } \
} \
const lrt = (loader.dependencies || {})['@oxc-project/runtime']; \
if (!lrt) errors.push('npm/loader/package.json: @oxc-project/runtime missing from dependencies'); \
} catch { errors.push('missing or unreadable npm/loader/package.json'); } \
const cargo = fs.readFileSync('Cargo.toml', 'utf8'); \
const cm = cargo.match(/^version = \x22([^\x22]*)\x22/m); \
if (!cm) errors.push('Cargo.toml: workspace version line not found'); \
Expand Down
16 changes: 16 additions & 0 deletions npm/loader-darwin-arm64/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "@nubjs/loader-darwin-arm64",
"version": "0.8.0",
"description": "Nub loader native addon for macOS ARM64 (Apple Silicon)",
"license": "MIT",
"repository": "https://github.com/nubjs/nub",
"os": [
"darwin"
],
"cpu": [
"arm64"
],
"files": [
"nub-native.node"
]
}
16 changes: 16 additions & 0 deletions npm/loader-darwin-x64/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "@nubjs/loader-darwin-x64",
"version": "0.8.0",
"description": "Nub loader native addon for macOS x64 (Intel)",
"license": "MIT",
"repository": "https://github.com/nubjs/nub",
"os": [
"darwin"
],
"cpu": [
"x64"
],
"files": [
"nub-native.node"
]
}
19 changes: 19 additions & 0 deletions npm/loader-linux-arm64-musl/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "@nubjs/loader-linux-arm64-musl",
"version": "0.8.0",
"description": "Nub loader native addon for Linux ARM64 (musl)",
"license": "MIT",
"repository": "https://github.com/nubjs/nub",
"os": [
"linux"
],
"cpu": [
"arm64"
],
"files": [
"nub-native.node"
],
"libc": [
"musl"
]
}
19 changes: 19 additions & 0 deletions npm/loader-linux-arm64/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "@nubjs/loader-linux-arm64",
"version": "0.8.0",
"description": "Nub loader native addon for Linux ARM64 (glibc)",
"license": "MIT",
"repository": "https://github.com/nubjs/nub",
"os": [
"linux"
],
"cpu": [
"arm64"
],
"files": [
"nub-native.node"
],
"libc": [
"glibc"
]
}
19 changes: 19 additions & 0 deletions npm/loader-linux-x64-musl/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "@nubjs/loader-linux-x64-musl",
"version": "0.8.0",
"description": "Nub loader native addon for Linux x64 (musl)",
"license": "MIT",
"repository": "https://github.com/nubjs/nub",
"os": [
"linux"
],
"cpu": [
"x64"
],
"files": [
"nub-native.node"
],
"libc": [
"musl"
]
}
19 changes: 19 additions & 0 deletions npm/loader-linux-x64/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "@nubjs/loader-linux-x64",
"version": "0.8.0",
"description": "Nub loader native addon for Linux x64 (glibc)",
"license": "MIT",
"repository": "https://github.com/nubjs/nub",
"os": [
"linux"
],
"cpu": [
"x64"
],
"files": [
"nub-native.node"
],
"libc": [
"glibc"
]
}
16 changes: 16 additions & 0 deletions npm/loader-win32-arm64/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "@nubjs/loader-win32-arm64",
"version": "0.8.0",
"description": "Nub loader native addon for Windows ARM64",
"license": "MIT",
"repository": "https://github.com/nubjs/nub",
"os": [
"win32"
],
"cpu": [
"arm64"
],
"files": [
"nub-native.node"
]
}
16 changes: 16 additions & 0 deletions npm/loader-win32-x64/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "@nubjs/loader-win32-x64",
"version": "0.8.0",
"description": "Nub loader native addon for Windows x64",
"license": "MIT",
"repository": "https://github.com/nubjs/nub",
"os": [
"win32"
],
"cpu": [
"x64"
],
"files": [
"nub-native.node"
]
}
48 changes: 48 additions & 0 deletions npm/loader/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# nubjs

Standalone TypeScript loader for Node.js, from the [Nub](https://nubjs.com) project. Register it the way tsx or ts-node is registered, and TypeScript works in `import`, in `require()`, and in worker threads — powered by the same native oxc-based transform the Nub CLI uses.

```sh
npm install --save-dev nubjs
node --import nubjs app.ts
```

Any way Node accepts a preload works:

```sh
node --import nubjs app.ts # one run
NODE_OPTIONS="--import nubjs" vitest # tools that spawn node themselves
node --require nubjs app.ts # CommonJS delivery (see below)
```

## What it does

- Transpiles `.ts` / `.tsx` / `.mts` / `.cts` / `.jsx` on the fly — full TypeScript, including enums, namespaces, and legacy decorators, not just type stripping.
- Resolves TypeScript conventions: tsconfig `paths` and `baseUrl`, extensionless imports, the `.js` → `.ts` emit-convention swap, directory index files.
- Augments CommonJS `require()` with the same resolution and transpile, not only `import`.
- Loads data formats as modules: `.yaml`, `.toml`, `.json5`, `.jsonc`, `.txt`, and `with { type: "text" }` imports.
- Lowers `using` / `await using` and other syntax newer than the running Node.
- Inline source maps, on for every transpiled file.
- Applies inside worker threads automatically (Node inherits the preload).

Dependencies under `node_modules` are never transpiled, and files Node handles natively load byte-for-byte unchanged — the loader adds behavior, it does not modify Node's.

## Entry points

```sh
node --import nubjs app.ts # ESM hooks + CommonJS require() augmentation
node --require nubjs app.ts # same, delivered as a CommonJS preload (Node 20.19+)
node --import nubjs/esm app.ts # ESM hooks only
```

Module formats follow Node's own rules: a `.cts` file is CommonJS and a `.mts` file is an ES module, and the loader transpiles types and syntax without converting one format into the other.

## Node support

Node 18.19 and newer. On Node 22.15+ hooks register synchronously in-thread (`module.registerHooks`); older versions run them in Node's loader worker (`module.register`). The `--require` delivery needs `require(esm)` (Node 20.19+ / 22.12+); below that use `--import`.

## Relationship to the Nub CLI

The [`@nubjs/nub`](https://www.npmjs.com/package/@nubjs/nub) CLI is a complete TypeScript-first toolchain — runner, package manager, Node version management — and does everything this loader does without any flags. This package is the loader alone, for cases where the `node` invocation itself is fixed: existing tooling, test runners, other CLIs that spawn `node`.

Platform binaries ship as `optionalDependencies` (`@nubjs/loader-*`) for macOS, Linux (glibc and musl), and Windows, on x64 and arm64.
Loading
Loading