Nub is an all-in-one toolkit powered by Node.js that modernizes the developer experience of the Node.js ecosystem. Use it instead of node, npm run, and npx (or the equivalents in your preferred package manager).
$ npm install -g --ignore-scripts=false @nubjs/nub
$ nub index.ts # run a TypeScript file
$ nub run dev # run a package.json script
$ nub watch src/server.ts # restart on file changes
$ nubx prisma generate # run a CLI from node_modules/.bin
$ nub install # install dependencies, pnpm-compatible
$ nub pm shim # Corepack-style package-manager shims
$ nub node install 26 # manage Node.js versionsNode is the gold standard for server-side JavaScript. It defines the stdlib the whole ecosystem is built on and sets the bar for stability and trust. That reputation comes from strict compatibility guarantees and careful governance, which also means Node ships new DX features slowly. So the day-to-day toolchain accumulated around it, one tool at a time:
node— runs files. Recently added limited TypeScript support.npm run(orpnpm run,yarn run) — the "script runner." Node leaves package management to userland, so third-party package managers became the frontend for almost everything you do in a Node project.npx(orpnpm exec) — the "bin runner," for CLIs your dependencies ship intonode_modules/.bin.tsx/ts-node— TypeScript support, bolted on.dotenv—.envfile loading.nodemon— restart on file changes.
Bun and Deno proved there's enormous appetite for an all-in-one toolkit that replaces this pile. But neither has dented Node's production dominance — six years after Deno 1.0 and two and a half after Bun 1.0, neither is a first-class target on AWS, GCP, or Azure, and their Node.js conformance still trails the real thing by a wide margin. Both are from-scratch runtimes reimplementing Node's surface, and the gaps show up in production. The "next-generation" runtimes share a fundamental flaw: they're trying to replace Node.js.
Nub embraces it instead. It's a single Rust binary that transpiles your code and executes it on the stock node your project pins — so the runtime underneath is Node. On Deno's own cross-runtime Node-compatibility corpus it clears 98.8% of what real Node passes, versus 77.4% for Deno and 40.5% for Bun. It combines:
$ nub index.ts # file runner — full TypeScript, on stock Node
$ nub run build # script runner — 24× faster than pnpm run
$ nubx prisma generate # bin runner — 19× faster than npx
$ nub watch src/server.ts # watcher — driven by the dependency graph
$ nub install # package manager — pnpm-compatible, your lockfile
$ nub node install 26 # version manager — installs Node on demandThe top-level command runs files. It's a true drop-in replacement for node — same flags, same argv, same runtime behavior:
$ NODE_OPTIONS='--enable-source-maps' nub \
--max-old-space-size=4096 \
--import ./instrument.js \
app.ts --port 3000Nub transpiles your code in memory with oxc (compiled into a native Node addon) and runs the output on the stock node binary. There is no Nub runtime — just real Node. Everything it adds is layered on through Node's own extension surfaces:
- Transpiles TypeScript, JSX, and decorators in memory — no build step.
- Polyfills missing global APIs, like Temporal and Web Workers.
- Sets the V8 and Node flags that unflag experimental features.
- Extends Node's resolver for
tsconfig.jsonpaths, and loads.envfiles and YAML/TOML imports.
The transpile costs nothing measurable: running a TypeScript file adds negligible overhead over plain node, and runs about 2.9× faster than tsx — while supporting the non-erasable syntax Node's stripper rejects:
TypeScript-first
Node's built-in type stripping erases annotations and rejects everything else — most real TypeScript codebases won't run on it. Nub transpiles each file through its native addon instead, so the enums, namespaces, parameter properties, and extensionless imports that Node doesn't allow all just work — with inline source maps, so stack traces and breakpoints point at your TypeScript source.
import { Model } from "./base" // extensionless → ./base.ts
enum Status { Draft, Sent, Paid }
class Invoice extends Model {
constructor(public status = Status.Draft) {} // parameter property
}Respects tsconfig.json#paths
The most consistent frustration in Node + TypeScript: your editor, your bundler, and Node's ESM resolver disagree about what's a valid import. Nub reads tsconfig.json#paths — with baseUrl and full extends chains — and applies the aliases at runtime, so import { db } from "@/db" just works.
// tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@db": ["src/db/index.ts"]
}
}
}JSX
Files ending in .jsx and .tsx execute directly — no Vite plugin, no separate compile step. The JSX runtime is sourced the way the rest of your toolchain sources it: compilerOptions.jsx and jsxImportSource from your nearest tsconfig.json, with a /** @jsxImportSource preact */ pragma winning per-file. Defaults match oxc, Vite, and Bun: automatic runtime, react import source.
const view = <Hello name="world" /> // .tsx — no build stepDecorators
Legacy experimentalDecorators — the form NestJS, TypeORM, and Angular depend on, which V8 can't run natively — transpile out of the box. Switch the flag on in your tsconfig.json and decorated classes just work, emitDecoratorMetadata included.
// with "experimentalDecorators": true in your tsconfig.json
@logged
class API {
@memoize
async fetchUser(id: string) {
// ...
}
}TC39 decorators (TypeScript 5.0's default) are on the way, gated on oxc shipping the transform — Nub follows your
tsconfig.jsoneither way.
Explicit resource management
The using and await using declarations work on every supported Node: native where V8 has shipped it, safely down-transpiled otherwise.
await using db = await connect() // disposed at scope endYAML and TOML imports
Import a config file directly. No npm install yaml, no fs.readFile + parse plumbing — the load hook parses the file in Rust and hands you the value. Supported out of the box:
.json # Node-native
.jsonc
.json5
.toml
.yaml, .yml
.txtThese are extension-based loaders, not built-in modules — import { parse } from "yaml" still resolves to the npm package, and under --node the loaders are off entirely.
import config from "./config.yaml" // parsed object
import flags from "./feature.jsonc" // comments stripped
import pkg from "./Cargo.toml" // parsed object
import prompt from "./prompt.txt" // string
const { host, port } = config // destructure the defaultEnvironment files
Nub loads .env, .env.local, .env.<NODE_ENV>, and .env.<NODE_ENV>.local automatically — Vite/Bun precedence rules, ${VAR} expansion included — and injects them before Node starts. One deliberate exception: .env.local is skipped under NODE_ENV=test, following the Next.js convention, so local secrets don't leak into your test suite.
# .env
APP=acme
DATABASE_URL=postgres://localhost/${APP}_dev
# no --env-file, no dotenv
$ nub server.tsBuilt-in version management
Nub resolves the right Node for every project implicitly — from .node-version, .nvmrc, or package.json#engines — every time it runs a file. If that version isn't installed, it's downloaded from nodejs.org (checksum-verified, cached under ~/.cache/nub) and your code runs on it. No pin? The node on your PATH is used, however you manage it. It's the uv experience, for Node.
$ echo 26 > .node-version
$ nub hello.ts
Using Node.js 26.4.0 (resolved from .node-version)
Installing from nodejs.org... (29 MB)
Installed in 9.8s
Hello world!The explicit commands exist too — no shell hooks, no PATH munging:
nub node install 26 # install a version
nub node ls # what's installed
nub node pin 26 # write .node-version
nub node uninstall 22 # remove a versionEvery API in the table below works on every Node version Nub supports — same code, no migration. Below the version where Node stabilized each API, Nub injects the experimental flag or preloads a polyfill; above it the API is native and Nub steps aside entirely. Node has been closing this gap itself; as your Node floor rises, the shims feature-detect and step aside.
Native — Node ships it; Nub steps aside entirely. Unflagged — available from the listed floor via flag injection. Polyfilled — Nub preloads an established community polyfill until your Node floor reaches the native line.
Worker
Browser-shape worker constructor over node:worker_threads. Node has no native plan (nodejs/node#43583, open since 2022); Nub bridges EventTarget, MessageEvent, and ErrorEvent semantics in ~150 lines. The same Worker you'd use in a browser, in Bun, or in Deno.
const worker = new Worker(
new URL("./worker.ts", import.meta.url),
{ type: "module" },
);
worker.addEventListener("message", (e) => console.log(e.data));
worker.postMessage({ task: "compute" });Temporal
The TC39 date/time API. Native in Node 26+; older Nodes get the reference @js-temporal/polyfill by the proposal authors.
const now = Temporal.Now.plainDateTimeISO();
const tomorrow = now.add({ days: 1 });
const tz = "2026-06-01T09:00[America/New_York]";
const meeting = Temporal.ZonedDateTime.from(tz);URLPattern
WHATWG URL pattern matching. Native in Node 24+; polyfilled on older lines.
const route = new URLPattern({ pathname: "/users/:id" });
const match = route.exec("https://example.com/users/42");
match?.pathname.groups.id; // "42"WebSocket
Browser-standard WebSocket client. Available from Node 20.10+; Nub injects --experimental-websocket below 22.0, native above. Below 20.10 there is no WebSocket client.
const ws = new WebSocket("wss://api.example.com/stream");
ws.addEventListener("message", (e) => console.log(e.data));
ws.addEventListener("open", () => ws.send("hello"));EventSource
SSE client — handy for LLM streaming. Available from Node 20.18+; Nub injects --experimental-eventsource until Node stabilizes the API.
const stream = new EventSource("https://api.example.com/llm/stream");
stream.addEventListener("message", (e) => process.stdout.write(e.data));localStorage / sessionStorage
The sessionStorage global — in-memory, per-process — works out of the box. Persistent localStorage is opt-in: pass --localstorage-file=<path> and Node persists to that file. On the 22.4–24 band Node also gates the global behind --experimental-webstorage, which Nub injects for you whenever you supply the file, so a single --localstorage-file is enough on every supported version. Without that flag, localStorage stays undefined.
// run with: nub --localstorage-file=.localstorage app.ts
localStorage.setItem("auth.token", "abc123");
const token = localStorage.getItem("auth.token");vm.Module
Evaluate ESM with a custom resolver. Behind --experimental-vm-modules since 2017 and still flagged; Jest and Vitest depend on it. Nub auto-injects.
import { SourceTextModule } from "node:vm";
const mod = new SourceTextModule(`export const answer = 42;`);
await mod.link(() => {});
await mod.evaluate();
mod.namespace.answer; // 42node:sqlite
Embedded SQLite. Available from Node 22.5+; Nub injects --experimental-sqlite until 22.13 where Node stabilized it. Below 22.5 it isn't available.
import { DatabaseSync } from "node:sqlite";
const db = new DatabaseSync("app.db");
db.exec("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)");
db.prepare("INSERT INTO users (name) VALUES (?)").run("Ada");Stage 4 standard-library bridges
Finished TC39 methods — native on Node 24+, polyfilled on older lines, each one feature-detected and deferred the moment your floor rises:
RegExp.escape— escape text for literal use in aRegExp.Error.isError— cross-realmErrordetection, whereinstanceoflies.Promise.try— start a chain from a sync-or-async function.Float16Array— plusDataView.getFloat16/setFloat16andMath.f16round. One real caveat: the polyfill is ~3× slower than V8's hardware-FP16 path, so pin Node 24+ for ML-inference hot loops.
Restart on change — driven by the actual dependency graph, not a glob list:
$ nub watch src/server.ts
Listening on http://localhost:3000
↺ src/db.ts changed — restarting
Listening on http://localhost:3000The watch set is the resolved dependency graph of your entry file, plus the files that invalidate the process without appearing in it: the tsconfig.json chain (with extends), your .env* files, and package.json. Build outputs and coverage artifacts never enter the watch set — there is nothing to configure.
The engine underneath is Node's own --watch. Nub's preload reports every file the load hook touches — including .ts files transpiled in memory, which Node never sees on disk — to Node's watcher over its dependency-report IPC channel. That report is what makes --watch work for TypeScript at all.
A drop-in replacement for npm run and pnpm run, minus the JS startup these Node-based tools pay on every invocation:
$ nub run dev # any package.json script
$ nub run test --watch # trailing flags pass through — no -- separator
$ nub -r run build # every workspace package, topo-orderedPerformance
Both pnpm and npm are themselves Node.js programs: every pnpm run <script> boots a Node process, loads the package manager, parses its config, walks the workspace, and finally shells out to your script. That's hundreds of milliseconds of overhead before your script's first byte runs — and in a 30-package monorepo, pnpm -r run build pays it 30 times.
Script-runner overhead — a pure-shell echo noop, so the figure isolates the runner's dispatch overhead, removing the per-call JS startup that pnpm and npm pay before your script's first byte:
script dispatch · warm · 50 runs
node --run32.2 ms · 2.2× slower
npm run329.9 ms · 22× slower
pnpm run442.7 ms · 30× slower
The benchmarked script is identical on every row — the delta is pure wrapper overhead. The numbers are machine-specific; the ratios are the portable claim. Reproduce with tests/bench/script-runner.
Workspace-aware
Nub reads workspace topology from package.json#workspaces (npm, Yarn) and pnpm-workspace.yaml (pnpm), and lifts pnpm's --filter selector grammar verbatim — the most expressive of any package manager. The rest of the workspace flag surface comes along with it, npm spellings accepted as aliases, and packages always run in dependency order:
nub -r run build # all packages, topo-ordered
nub --filter @org/api dev # one package by name
nub --filter "./packages/*" test # glob match
nub --filter "@org/web..." build # @org/web and its dependencies
nub --filter "[main]" test # changed since main
nub -r --no-bail test # keep going past failures
nub -r --workspace-concurrency 4 build # cap parallel jobsDrop-in compatible
Everything your scripts depend on still works — npm_* env vars, pre/post lifecycle hooks, node_modules/.bin resolution in monorepos, and the .npmrc#script-shell config key.
Node 22 shipped its own script runner,
node --run— genuinely fast, but deliberately stripped down (no lifecycle hooks, nonpm_*env vars, no workspaces) in ways that break most real projects.
Subprocesses inherit augmentation
When nub run executes a script, nub is aliased as node for the duration — so spawned subprocesses (say, a CLI with a node shebang) get the augmentation too. Pass --node to turn this off:
nub run --node testRun any CLI your dependencies ship into node_modules/.bin, no matter which package manager put it there:
nubx eslint .
nubx prisma generate
nubx tsc --noEmitThe .bin walk-up happens in Rust and the binary is exec'd directly — no Node bootstrap in the wrapper, where pnpm exec and npx boot a full Node first. The binary is identical on both sides, so the entire delta is wrapper overhead:
esbuild --version · 20 runs
nubx esbuild --version11 ms
pnpm exec esbuild --version191 ms · 17× slower
npx esbuild --version226 ms · 20× slower
When the tool isn't installed, nubx fetches it from the registry and runs it — no prompt, the same local-first-then-DLX model as npx and pnpm dlx. A version specifier like nubx cowsay@1.5.0 goes straight through that fetch path.
Nub ships a full package manager, powered by the embedded aube engine, built by jdx, the creator of mise. Two guarantees define it.
First, whatever lockfile your project already has is respected bidirectionally — Nub reads it and writes it back in the same format. No migration, no foreign lockfile dropped beside yours:
$ nub install # pnpm-lock.yaml → read, written back
$ nub install # package-lock.json → read, written back
$ nub install # bun.lock → read, written back
$ nub install # yarn.lock → read-only (installed from, never rewritten)The npm, pnpm, and Bun lockfiles round-trip; Yarn is read-only. Run Nub and your old package manager side by side — same file, switch back any time.
Second, on a pnpm project, support is complete: Nub reads and honors every config surface pnpm does — workspace files, hooks, the "pnpm" package.json section, and the npm_config_* environment — so the install it produces is one real pnpm accepts unchanged.
Performance
Like pnpm, Nub keeps package files in a global content-addressed store and links them into node_modules with reflinks or hardlinks when the filesystem supports them. The benchmark below is the warm path for create-t3-app: packages are already on disk, node_modules is removed, and the installer rebuilds the project layout.
warm frozen install · create-t3-app · 222 deps · macOS
bun1444 ms · 1.29× slower
pnpm2847 ms · 2.5× slower
Secure by default
Nub install ships the strongest default security posture of any Node package manager — every protection below is on out of the box:
- Lifecycle scripts are deny-by-default. A dependency's
postinstallwon't run unless you approve that package, so a compromised transitive dep can't execute code just because you installed it. A curated set of well-known native packages (esbuild, sharp, and the like) is the one exception — it builds automatically, but only when registry provenance, an advisory check, and a release-age cooling window all pass. See the security posture for the gates. - Non-registry transitive deps are blocked. A dependency-of-a-dependency that resolves to a
git+,file:, or raw-tarball URL — a classic supply-chain vector — is refused rather than silently fetched. - New releases have to age. A default
minimumReleaseAgeof 24 hours keeps a just-published version out of your tree until it's sat in the registry a day — the simplest defense against the now-common "publish a malicious patch, hijack installs for an hour" attack. - Provenance can't silently downgrade. A version that loses its trusted-publisher or provenance evidence fails at resolve instead of installing quietly.
One paranoid: true line turns every soft gate into a hard failure and adds a build-script jail — approved scripts then run sandboxed, with no network and a scoped filesystem.
Run nub pm shim once and it drops npm, pnpm, yarn, and bun onto your PATH as shims. From then on, running pnpm install in a project reads the version that project pins in packageManager, provisions that exact version (fetched and verified from the registry), and execs it — corepack's job, but in Rust, with none of the per-call Node bootstrap corepack pays:
$ nub pm shim # install npm / pnpm / yarn / bun shims
$ pnpm install # routes to the pnpm the project pins, fetching it if missingTo set or change the pinned manager by hand, nub pm use resolves the version, verifies the tarball, writes packageManager (with a +sha512 hash), and aligns the lockfile — converting formats when you switch managers:
$ nub pm use pnpm@^9
using pnpm@9.15.9
package.json: packageManager = pnpm@9.15.9 (+sha512)
pnpm-lock.yaml: kept (already pnpm's format)There are no Nub-specific APIs.
- No
Nubglobal - No
nub:*module namespace - No
@nub/*npm scope - No
"nub"field inpackage.json - No nub-named lockfile
- No Nub-named config files
Everything Nub ships is one of:
- A web platform standard (WHATWG / W3C / WinterTC)
- A TC39 proposal
- An unflagged Node.js feature
- A pragmatic affordance for ecosystem compatibility — usually TypeScript
Remove Nub tomorrow and your code keeps working, unchanged.
- Is it faster than Node.js?
- Why not just use Node's built-in TypeScript support?
- Why hasn't anyone else done this?
- Is Nub a replacement for Node?
- What can I stop using once I install Nub?
- How is it different from Bun?
- How is it different from Deno?
- How is it different from
node --run? - Will my existing Node code work on Nub?
- What about CommonJS /
require()? - What about native (N-API) addons?
- Is it monorepo-friendly?
- What's the
--nodeflag? - What if I want to stop using Nub?
Nub is open-source and MIT-licensed.