NewIntroducing Nub

The all-in-one JavaScript toolkit that augments Node.js instead of trying to replace it

A TypeScript-first toolchain for Node.js. Run TypeScript files, package.json scripts, and local CLIs on the node and package manager you already have. No new runtime, no lock-in.

$ nub index.ts # run a TypeScript file
$ nub run dev # run a package.json script
$ nub watch src/server.ts # restart on changes
$ nubx prisma generate # run a local CLI, fast
$ nub node install 26 # manage Node.js versions

The toolchain

An all-in-one toolkit for Node.js

One Rust binary that runs your files, scripts, and local CLIs — and manages Node itself.

nub <file>

A TypeScript-first Node.js

Run .ts, .tsx, and .jsx on stock Node with full tsconfig.json support, .env loading, and unflagged support for modern syntax and APIs.

Replacestsxts-nodetsconfig-pathsdotenv
# run a TypeScript file
$ nub index.ts
# restart on changes
$ nub watch src/server.ts
nub <file>

A TypeScript-first Node.js

Nub adds support for TypeScript, JSX, decorators, .env files, YAML/TOML imports, and modern APIs syntax on top of stock Node. Flag-for-flag compatible with node. Powered by Rust and oxc.

Architecture

Transpiles in Rust, runs on real Node

Nub transpiles your code in memory with oxc (compiled into a native Node addon) and runs the output on the stock node binary. There’s no Nub runtime, just real Node. Your code is run by the version of Node your project expects. If unavailable, it’s installed on the fly. Runs on Node.js 18 LTS and newer.

$ nub app.ts
# oxc transpiles in memory, then stock node runs it
running on node v26.2.0

TypeScript-first

Full TypeScript support, not just type stripping

Recent versions of Node support type stripping, which erases annotations but rejects non-erasable syntax. Nub’s load hook transpiles each file through its native addon instead, so enums, parameter properties, and extensionless imports that Node doesn’t allow all just work.

import { Model } from "./base"   // extensionless → ./base.ts

enum Status { Draft, Sent, Paid }

class Invoice extends Model {
  constructor(public status = Status.Draft) {} // parameter property
}

tsconfig

Respects your tsconfig.json

Nub resolves your tsconfig.json (including "extends") and feeds its paths into Node’s own resolver through a module.registerHooks() resolve hook. No more tsconfig-paths or disagreement between Node.js and your editor.

// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@db": ["src/db/index.ts"]
    }
  }
}

Environment

Loads .env files automatically

Nub reads .env, .env.local, and .env.[NODE_ENV] and injects them before Node starts. No dotenv required. Automatic var expansion via ${VAR} just like Vite and Next.js.

# .env
APP=acme
DATABASE_URL=postgres://localhost/${APP}_dev

# No dotenv. No cross-env. No import "dotenv/config".
$ nub server.ts

Modern syntax

Decorators, JSX, and using

Nub supports decorators and JSX, transpiling it according to your tsconfig.json settings. Full support for emitDecoratorMetadata and explicit resource management, no build step required.

await using db = await connect()    // disposed at scope end

@sealed                             // legacy decorator
class User {}

const view = <Hello name="world" /> // JSX in .tsx

Loaders

Import JSON, YAML, and TOML

Import .yml, .yaml, .toml, .json5, and .jsonc files directly. A module.registerHooks() load hook routes them through fast Rust parsers in Nub’s native addon, resolving each import to a plain JavaScript object. (Oh, .txt works too)

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

import { host, port } from "./config.yaml" // named exports

Auto-restart

A dependency-aware watch mode

Powered by node --watch, Nub’s watch command watches for changes to your entrypoint or any file transitively imported. It also adds TypeScript/JSX sourcemap support and watches your package.json, tsconfigs, and .env files.

$ nub watch src/server.ts
Listening on http://localhost:3000
↺ src/db.ts changed — restarting
Listening on http://localhost:3000

Compatibility

100% runtime compatibility with Node

Nub passes Node’s test suite because it is Node. Your code is transpiled and executed with the stock node binary. It’s not a reimplementation; other Node alternatives continue to play catch-up.

Node 25.8
100%
4,375 / 4,375
Nub
98.7%
4,318 / 4,375
Deno 2.8
76.6%
3,351 / 4,375
Bun 1.3.14
40.1%
1,756 / 4,375

Deno’s Node-compat suite, node-relative. The gap is 57 documented deltas — module-hook and native-addon visibility, plus three intended divergences from default-on Web Storage and compile caching. View benchmark repo

Drop-in

Flag-for-flag compatible with node

Nub is a true drop-in replacement for node. Same flags, same argv, same runtime behavior.

$ nub \ --max-old-space-size=4096 \ --inspect \ --import ./instrument.js \ app.ts --port 3000

No Nub-specific APIs

Zero lock-in

Nub is not a runtime. Your code runs on the real node binary: no Nub engine, no reimplementation, no proprietary API surface. Everything Nub ships is a web standard, a TC39 proposal, an unflagged Node feature, or a pragmatic TypeScript affordance. Remove Nub tomorrow and your code keeps working, unchanged.

  • No Nub global
  • No nub:* module namespace
  • No @nub/* npm scope
  • No NUB_* environment variables
  • No "nub" field in package.json

Forward compatibility

Modern APIs and syntax, fully supported

Nub polyfills APIs like Temporal and Worker, adds support for new ECMAScript syntax like using, and unflags all experimental Node.js features.

Web Workers
Auto-polyfilled
Temporal
Polyfilled < 26
URLPattern
Polyfilled < 24
WebSocket
Unflagged < 22
navigator.locks
Auto-polyfilled
localStorage
Unflagged < 25
EventSource
Auto-unflagged
node:sqlite
Unflagged < 22.13
vm.Module
Auto-unflagged
RegExp.escape
Polyfilled < 24
Promise.try
Polyfilled < 24
Float16Array
Polyfilled < 24
nub run

An 18× faster pnpm run

A drop-in for npm run and pnpm run with lifecycle hooks, npm_* env vars, and arg forwarding all working, minus the per-call Node bootstrap.

Performance

Run package.json scripts at the speed of Rust

Scripts run with npm run or pnpm run feel perceptibly laggy due to the 100+ms of overhead introduced by these tools. They’re written in Node.js themselves, so they pay the Node.js bootstrap tax.

echo-hi script · hyperfine, 20 runs

nub run echo-hi9 ms
npm run echo-hi104 ms · 11× slower
pnpm run echo-hi161 ms · 18× slower
Reproduce →

Workspaces

Monorepo-friendly

Nub implements pnpm’s --filter grammar and -r, reading workspaces from package.json#workspaces or pnpm-workspace.yaml. Packages run in dependency order, without the per-package Node bootstrap.

$ nub -r run build # every package, topo-ordered
$ nub --filter @org/api dev # one package
$ nub --filter ...@org/web build # + its deps
$ nub --filter "[main]" test # changed since main
nubx

A 20× faster npx

nubx resolves node_modules/.bin in Rust and execs the binary directly; no Node process in the wrapper. A drop-in for npx and pnpm exec.

Performance

Makes commands feel instantaneous

When invoking native CLIs like esbuild, npx itself (written in JS) adds a noticeable 200ms of cold-start latency, even when running a CLI command that’s instantaneous. Nub walks node_modules/.bin and execs the binary directly.

esbuild --version · hyperfine, 20 runs

nubx esbuild --version11 ms
pnpm exec esbuild --version191 ms · 17× slower
npx esbuild --version226 ms · 20× slower
Reproduce →

Resolution

Works with any package manager

Nub resolves the CLI the way pnpm, yarn, and npm do, so it runs the exact binary your install put there, even in a monorepo. Add --node to run one under plain Node.

$ nubx eslint . # member's .bin first
$ nubx prisma generate # then workspace root
$ nubx tsc --noEmit # then ancestors
$ nubx --node some-cli # run under plain Node
nub node

A built-in Node version manager

Nub reads your .node-version or .nvmrc and, if that Node isn’t installed, downloads it from nodejs.org, verifies the checksum, and installs it. Replaces nvm and fnm.

Per-project

Resolves your project's Node version

Nub automatically resolves the right Node for each project from .node-version, .nvmrc, or package.json#engines before your code runs.

$ nub node which
~/.cache/nub/node/26.3.0/bin/node
» resolved from package.json#engines.node (>=26)

On demand

Auto-installs Node versions

If the resolved version isn’t on your machine, Nub downloads it from nodejs.org (checksum-verified), then runs your code on it. No nvm use, no prompt, no second step.

$ echo 26 > .node-version
$ nub hello.ts
Using Node.js 26.3.0 (resolved from .node-version)
Installed in 9.8s
Hello world!

Direct control

Or manage versions by hand

Install, list, pin, and remove Node versions directly with nub node. 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 version
nub install

A built-in package manager

Nub ships a pnpm-compatible package manager — powered by the embedded aube engine, built in partnership with jdx. It reads the lockfile your project already has and writes the same format back.

Your lockfile

Keeps the lockfile you already have

nub install detects your existing lockfile and writes the same format back — pnpm-lock.yaml, package-lock.json, and bun.lock are read and written in place, never replaced with a foreign file. Fresh projects get a standard pnpm-lock.yaml. yarn.lock is honored read-only for now: an install that would rewrite it is refused, with the exact yarn command to run instead.

$ 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 → honored read-only

Layout

Isolated installs, hoisted where you expect them

pnpm projects and fresh ones get strict, symlinked, pnpm-style installs, with the virtual store tucked under node_modules/.nub. Projects with an npm, yarn, or bun lockfile default to the flat hoisted layout those tools produce — so nothing about your tree surprises the code that walks it. One .npmrc line (node-linker) overrides either default.

$ nub install
node_modules/express → .nub/express@5.1.0/…
$ nub install --node-linker=hoisted # or one .npmrc line

Config

Reads .npmrc, honors your workspaces

Configuration comes from the files you already maintain: .npmrc (registry, auth, flags), pnpm-workspace.yaml, and package.json#workspaces. Nub’s own defaults rank below every user source — a CLI flag, env var, .npmrc entry, or workspace yaml always wins. No new config file, no "nub" field in package.json.

$ cat .npmrc
registry=https://npm.example.com
node-linker=hoisted
$ nub install # your config wins, always

Keep your tools

Your package manager still works

Because Nub writes the same lockfile your package manager does, pnpm, npm, and bun keep working side by side — run either tool, commit the same file, switch back any time. Registry, scoped, peer-heavy, and platform-specific dependency trees all round-trip through the real tools today — workspace: links and git dependencies included. And when you want the original tool itself, nub pm use declares and provisions the exact version for the whole team — Corepack’s job, without the PATH shims.

$ nub install # or pnpm install — same lockfile
$ 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)

The toolkit that augments Node.js