What is Nub?

Nub is an all-in-one toolkit powered by Node.js that modernizes the developer experience in the Node.js ecosystem. Use it to run files, package.json scripts, and locally installed CLIs. 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
$ nubx prisma generate     # run a CLI from node_modules/.bin

It's a Rust CLI that orchestrates the Node you already have installed and augments it through Node's own public extension surfaces — module.registerHooks(), --import preloads, NODE_OPTIONS, N-API addons. It doesn't replace your runtime, your package manager, or your editor.

Is it a new runtime?

No. Nub runs on Node — your node binary, on your PATH. There is no Nub runtime, no Nub JavaScript engine, no separate compatibility surface. When nub index.ts runs, what's actually executing is the Node you installed via nvm, mise, brew, the official installer, or whatever else; Nub spawns it, registers a load hook for TypeScript / JSX / YAML / TOML, preloads polyfills where Node is behind, and gets out of the way. See How does it work under the hood? for the mechanism-level answer.

Is it a fork of Node?

No. Nub does not patch Node source, ship a custom-built Node binary, or embed libnode. Every augmentation rides on Node's public extension surfaces.

Is Nub a replacement for Node?

No — Nub is Node. Your code is transpiled and executed by the stock node binary; there's no Nub runtime to discover and nothing reimplemented. Nub is a layer of developer-experience defaults on top of Node, built entirely on Node's own extension surfaces.

How is it different from plain node?

Running nub <file> is flag-for-flag compatible with node <file> — same argv shape, same flag set, same behavior — and on top of that adds:

  • TypeScript first — full TS surface, not just type stripping (enums, namespaces, parameter properties, decorators, import = / export =, extensionless .ts imports, inline source maps)
  • Respects tsconfig.jsonpaths, baseUrl, extends chains, jsx all applied at runtime
  • JSX support.jsx / .tsx files execute directly, runtime sourced from compilerOptions.jsx / jsxImportSource
  • Env files.env and .env.${NODE_ENV} loaded eagerly with Vite-compatible precedence
  • YAML / TOML / JSON5 / JSONC / text loadersimport config from "./config.yaml" works the way import data from "./data.json" already does
  • Automatic polyfillsTemporal, URLPattern, browser-shape Worker, WinterTC gap globals, version-detected and feature-gated
  • Unflagged experimentalsEventSource, WebSocket, localStorage / sessionStorage, vm.Module, node:sqlite — flags Node already ships but keeps gated, auto-injected in exact per-version bands where each is still behind --experimental-*

All of that comes for free when you type nub instead of node. For plain Node behavior, type node — the PATH shim is per-invocation, so shell node is always the user's real Node. For orchestration without runtime augmentation, the --node flag on nub run and nubx strips the hook/preload/unflagging while keeping Nub's CLI work.

What's the elevator pitch?

If you've spent the last few years assembling a personal toolchain out of tsx, dotenv-cli, nodemon, npx, pnpm run, plus a handful of tsconfig loader shims — Nub replaces that pile with one binary that starts in milliseconds and doesn't ask you to change your runtime. Keep Node. Replace the toolchain.

Is it faster than Node.js?

No — your code runs in Node itself, so once it's executing, V8 is V8. What's dramatically faster is the CLI surface, because a native Rust binary skips the full Node bootstrap those tools pay just to do a script lookup:

  • nub run is 24× faster than pnpm run on cold start.
  • nubx is 19× faster than npx on cold start.

Transpile output lands in a content-addressed on-disk cache keyed on (source content, Nub version, resolved tsconfig), so the first run transpiles and every run after pays roughly nothing.

Will my existing Node code work on Nub?

Yes. The compatibility surface is Node's. If a package works on Node, it works on Nub — because what's actually running underneath is Node.

The contract

Code targeting Node runs on Nub byte-for-byte. If you hit a case where that's not true, it's a bug.

What about CommonJS / require()?

Works. CommonJS, require(), dynamic require(), require.cache, module.exports, the node: core modules in both ESM and CJS form — all of it works because all of it is just Node, running underneath. Nub's load hook covers TypeScript / JSX / YAML / TOML at the ESM and CJS entry points equivalently.

What about native (N-API) addons?

Works. Native addons compile against Node's N-API ABI, and Nub is running on your Node binary, so addons load exactly as they do on plain Node. (Nub itself ships its oxc transpiler as an N-API addon; that path is the same path your better-sqlite3 or @napi-rs/canvas uses.)

Does it support all Node core modules?

Yes. Every node: core module — node:fs, node:http, node:test, node:worker_threads, node:sqlite, node:crypto, all of them — works because they're provided by your Node binary, not by Nub.

What Node versions does it support?

  • Augmented modes require Node 18.19+ (Node 18 LTS) — the floor for the loader-hook API Nub uses for the transpile-on-import path.
  • Compat mode (the --node flag) is best-effort on any Node version. Here Nub is a pure orchestrator (script resolution, workspace bin PATH, env var injection) and spawns plain Node without runtime augmentation. For ad-hoc plain-Node behavior on a file, type node directly.

Below the floor

On Node older than 18.19, augmented commands emit a tagged error telling you to upgrade Node or use compat mode.

Does it work on Linux / macOS / Windows?

Yes — Linux (x64, arm64), macOS (x64, arm64), Windows (x64). Same platforms the underlying Node + napi-rs ecosystem already targets. Nub is distributed as prebuilt Rust binaries via npm install -g --ignore-scripts=false @nubjs/nub, with platform-specific N-API addons resolved at install time.

How do I use it in CI / GitHub Actions?

Swap actions/setup-node for nubjs/setup-nub:

- - uses: actions/setup-node@v4
+ - uses: nubjs/setup-nub@v0

The action installs the Nub CLI onto the runner, and from there Nub provisions the project's pinned Node itself. It also pre-provisions a warm-up Node version and caches Nub's store across runs. See the GitHub Action page for the full input surface.

Does it replace my package manager?

It can. The nub install command is a full package manager — pnpm's CLI surface, your project's lockfile — powered by the embedded aube engine. It detects the lockfile your project already has and writes the same format back, so nothing forces a migration: pnpm, npm, and Bun keep working side by side, and nub pm use provisions the original tool at its exact pinned version (corepack's job, without the PATH shims).

LockfileBehavior
pnpm-lock.yamlRound-trips in place
package-lock.jsonRound-trips in place
bun.lockRound-trips in place
yarn.lockRead-only — an install that would rewrite it is refused, with the exact yarn command to run instead

Does it work with pnpm / npm / yarn / bun?

Yes. All four keep working exactly as they do without Nub. Your lockfile stays in whatever format your package manager produces, and workspace topology (pnpm-workspace.yaml, npm workspaces, yarn workspaces) is honored by nub run and nubx directly. The sources of truth:

  • nub run reads package.json and its "scripts" field.
  • nubx reads node_modules/.bin.

Yarn Berry Plug'n'Play

PnP has no node_modules/.bin/ — just a runtime resolution table managed by Yarn's loader. Nub handles it anyway: the runtime detects a .pnp.cjs and runs the project through it, and nubx resolves PnP bins through pnpapi (the way yarn exec does). See module resolution.

Is it monorepo-friendly?

Yes. Workspace topology comes from package.json's "workspaces" field (npm, yarn) and pnpm-workspace.yaml (pnpm), with the -r / --filter semantics every workspace user already knows from pnpm:

nub run -r build                        # all workspace packages
nub run --filter @org/api dev           # one package by name
nub run --filter "./packages/*" test    # glob match
nub run --filter "@org/web..." build    # @org/web and its dependencies

Topological ordering, parallelism, and --workspace-root are honored. The point is what isn't honored: the ~150 ms pnpm bootstrap, paid per package. In a 30-package monorepo, pnpm -r run build pays that bootstrap 30 times before any of your code runs; nub run -r build pays it zero.

Does it replace npx?

Yes — nubx (also available as nub exec) is the equivalent, following the same local-first-then-DLX model as npx and pnpm dlx, with no interactive prompt:

  • Local binnubx walks up the directory tree checking node_modules/.bin/ at each level (the standard resolution algorithm), in Rust, returning in single-digit milliseconds.
  • Not installednubx fetches the binary from the registry and runs it on the fly. Nub is a complete package manager, so it does the fetch itself rather than shelling out to a foreign PM.

Does it replace tsx / ts-node?

Yes — nub script.ts runs TypeScript directly, with the full TS surface (not just type stripping), respecting your tsconfig.json, with inline source maps so stack traces point at your .ts source. Full details: Runtime → TypeScript.

Why not just use Node's built-in TypeScript support?

Node 22.18+ runs .ts files by default, but it strips types rather than transpiling them — so anything beyond erasable annotations is on its own:

  • enum, namespace, and parameter properties → syntax errors (Node 26 even removed --experimental-transform-types, the flag that used to handle them)
  • JSX and emitDecoratorMetadata → unsupported
  • tsconfig.json paths, baseUrl, jsx, experimentalDecorators, extends → ignored ("intentionally unsupported," per the Node 26 docs)
  • Extensionless .ts imports → unresolved, and no source maps

Nub transpiles each file through its native addon instead, so all of that just works. If your codebase is erasableSyntaxOnly-clean, Node's built-in support is fine; if it isn't, you need a real transpile pipeline — which is what Nub provides and Node deliberately doesn't.

Does it replace nodemon / tsx watch?

For restart-on-change, yes: nub watch script.ts (or nub --watch script.ts) restarts the process when anything in the resolved dependency graph (plus your .env* files and tsconfig.json) changes — no glob list to maintain. (A --watch after a script name — nub run dev --watch — is forwarded to the script itself, like every other argument.) Full details: Watch mode.

What about dotenv-cli?

Not needed. Nub loads your env files automatically — with the Vite/Bun-style precedence rules — and injects them into the process environment before Node starts:

.env
.env.local
.env.<NODE_ENV>
.env.<NODE_ENV>.local

Expansion of ${VAR} and $VAR is supported, including nested references and cycle detection.

NODE_ENV=test

Under NODE_ENV=test, .env.local is skipped, following the Next.js convention — local secrets shouldn't leak into your test suite.

Full details: Runtime → Environment variables.

How does it work under the hood?

Nub is a Rust CLI. When you type nub script.ts, the Rust binary:

  1. Resolves the node on your PATH.
  2. Reads your nearest package.json, tsconfig.json, and .env* files.
  3. Spawns that Node with --import pointing at Nub's preload bundle and NODE_OPTIONS set to whatever experimental flags are needed for your Node version.
  4. The preload bundle calls module.registerHooks() to install Nub's load hook (TS / JSX / YAML / TOML / JSON5 / JSONC transpile-and-parse path, backed by an N-API binding to oxc), installs polyfills (Temporal, URLPattern, WebSocket, Worker, WinterTC gap globals) where Node is behind, and hands control to your script.
  5. Subprocesses inherit the same augmentation recursively — node hello.js from inside a script, the Node process Vite or Next.js spawns for a dev server, the worker pool your test runner spins up, child_process.spawn("node", …) from your own code, all augmented.

Every step rides on a public Node extension surface — see What Node features does it rely on? for the full inventory.

What Node features does it rely on?

Every augmentation Nub applies is something a user with patience could wire up by hand on plain Node. The extension surface Node exposes for this work has grown substantially in the last three years, and the kit Nub composes is small and entirely public:

  • module.registerHooks() (Node 22.15, April 2025) — synchronous loader hooks for the transpile-on-import path.
  • module.register() (Node 20.6, August 2023) — programmatic loader registration, no --loader CLI dance.
  • --import preload (Node 19.0, October 2022, stable since 20.6) — run setup code before user main, ESM-aware. Used to install polyfills and set up the load hook.
  • --require preload (Node 1.6, 2012) — older CJS-style preload, still useful in pre-ESM corners.
  • NODE_OPTIONS and V8 flag injection — used to turn on --experimental-* flags Node already implements but keeps gated.
  • N-API addons (stable since Node 8.6, October 2017) — the ABI-stable native-addon contract. The oxc transpiler ships as a Node addon, called from inside the load hook.

That's the entire kit. There is no patched Node binary, no vendored libnode, no separate runtime to discover.

Why hasn't anyone else done this?

Because the extension surface that makes it possible is new — the public Node mechanisms Nub composes only landed over the last three years (module.registerHooks() in April 2025, module.register() in 2023). Deno and Bun predate most of that surface — back then, shipping integrated TypeScript meant writing a runtime around it; today the same layer composes on top of Node, with the entire compatibility surface preserved for free.

Why doesn't Node ship these defaults natively?

Node stays predictable on purpose — it's the substrate thousands of production deployments rely on. Shipping opinionated defaults, integrated TypeScript, eager .env loading, or a workspace-aware script runner into core would shift the meaning of "running on Node."

What Node has done instead, over the last three years, is expose enough public extension surface — module.registerHooks(), module.register(), --import, stable N-API — that the modern-defaults layer doesn't have to live inside Node anymore. It can live above. That's what Nub is.

What polyfills does it ship?

Each polyfill is a thin shim over an established community implementation, feature-detected, and deferred to native when Node ships it.

  • Temporal — TC39 date/time API. Native in Node 26+; Nub polyfills via @js-temporal/polyfill on older Node
  • URLPattern — WHATWG URL pattern matching. Native in Node 24+; Nub polyfills via urlpattern-polyfill on older Node
  • WebSocket — Browser-standard WebSocket client. Available from Node 20.10+; Nub injects --experimental-websocket below 22.0 where Node stabilized it
  • Worker — Browser-shape worker constructor over node:worker_threads. Node has no native plan (nodejs/node#43583 open since 2022); Nub ships a ~150-LOC wrapper
  • WinterTC gap globalsreportError, self, PromiseRejectionEvent. Node TSC has no opposition to these; PRs stalled to inactivity. ~50-LOC preload bundle

These are not Nub-specific. They're the Minimum Common Web API, a cross-runtime contract published by WinterTC — code written against it runs unchanged on every conformant runtime, including stock Node.

What npm packages can I stop using once I install Nub?

Two buckets — and the first one isn't limited to npm packages. Nub replaces these directly:

dotenv
cross-env
tsx, ts-node
nodemon
tsconfig-paths
npx                 # → nubx / nub exec
nvm, fnm            # the pin file alone provisions the right Node
corepack            # → nub pm
the PM CLI itself   # nub install / nub run, against your existing lockfile

Modern Node already obsoletes a second bucket, and Nub lets you inherit them by running on a modern-Node floor:

node-fetch, cross-fetch, whatwg-fetch   # native fetch since 18
abort-controller                        # native since 15
uuid (v4 case)                          # crypto.randomUUID since 14.17
rimraf                                  # fs.rmSync({ recursive, force }) since 14.14
mkdirp                                  # fs.mkdirSync({ recursive }) since 10.12
glob, globby (basic cases)              # fs.glob since 22
form-data                               # native FormData since 18
ws client                               # native WebSocket since 22.0, unflagged down to 20.10

The second bucket's credit is Node's — running on a modern-Node floor is what makes the inheritance practical without per-project Node-version verification.

How does Nub resolve modules?

Nub's resolver is a Rust implementation of Node's ESM resolution algorithm that layers in tsconfig.json paths and extensionless .ts import probing on top of vanilla Node's behavior. It builds on oxc_resolver for the primitives, is validated against a conformance suite ported from Node's own ESM resolver specification, and is also what powers the resolver inside nubx and nub run.

The rest of the hot path delegates to crates the Rust JavaScript-tooling ecosystem has already converged on — oxc for parsing, transformation, and source maps; napi-rs for Rust→Node bindings. Nub's own contribution is the orchestration: detecting the user's Node version, wiring up module.registerHooks(), prepending the PATH shim, managing the transpile cache, version-gating the polyfills, and the resolver itself.

How is it different from Bun?

Bun is a from-scratch JavaScript runtime built on JavaScriptCore, with its own module loader, package manager, test runner, bundler, and Bun.* API surface. Code that uses those surfaces is Bun-specific:

Bun.serve()
bun:sqlite
bun:test
the Bun global
@types/bun

Nub keeps application code on Node's public surface:

no globalThis.nub
no nub:* module namespace
no @nub/* npm scope
no "nub" field in package.json

The augmentations Nub applies all work on plain Node with the matching module.register() / --import / npm addon. Nub focuses on the pieces Node now exposes as extension surfaces: TypeScript loading, script and package orchestration, package management, and version provisioning.

How is it different from Deno?

Deno is a from-scratch runtime built on V8, with its own permissions model, standard library, module resolution (URL imports, jsr: specifiers), and Deno.* API surface. Code that uses those surfaces is Deno-specific:

Deno.serve()
Deno.env
Deno.readTextFile

Deno implements Node compatibility inside Deno. Nub runs on the actual Node binary you installed; if a package works on Node, it works on Nub, by construction.

How is it different from node --run?

Node shipped a built-in script runner in 22.0 — node --run — and it's genuinely fast. But it's deliberately minimal, and the gaps are why projects still reach for pnpm run:

  • No pre / post lifecycle hooks.
  • No workspaces — no recursive or filtered runs across a monorepo.
  • No npm_* environment variables (npm_package_*, npm_config_*, npm_lifecycle_event) that countless scripts read.

The nub run command is just as fast in the script-runner benchmark and closes all three, so scripts written for npm or pnpm keep working unchanged.

What if I want to stop using Nub?

Your codebase keeps working on plain Node, unchanged. Nub adds no APIs to your code — no nub global, no nub:* import namespace, no @nub/* scope, no "nub" field in your package.json.

If Nub disappeared tomorrow, the worst case is going back to whatever toolchain you had before:

  • TypeScript files need a separate compile step or a tsx-equivalent loader.
  • .env files need dotenv-cli or import "dotenv/config" again.

The code itself doesn't change.

What's the --node flag?

A compat flag on nub run and nubx. It disables Nub's runtime augmentation for that orchestration call while keeping the CLI work (workspace, scripts, npm_* env, lifecycle hooks).

node script.js                # plain Node, full stop (shim is per-invocation, shell `node` is always your real Node)
nub script.js                 # Nub augmentation active
nub run --node test           # Nub's CLI orchestration, runtime augmentation off
nubx --node prisma generate   # Nub's bin resolution, runtime augmentation off

It strips every runtime augmentation for the spawned process — the transpile hook, .env loading, polyfills, unflagging, and the PATH shim — leaving byte-exact Node runtime behavior; full contract at the runtime overview.

Useful when a script's #!/usr/bin/env node shebang chain expects real Node, when bisecting a Nub bug, or when CI wants byte-exact Node runtime behavior with Nub's --filter / workspace selection. For ad-hoc plain-Node runs, type node.

Does Nub add anything to my package.json?

No. The fields Nub reads are the existing standard ones, exactly as Node, npm, pnpm, and yarn read them:

"scripts"
"workspaces"
"bin"
"type"
"exports"
"imports"
"engines.node"

Does Nub add anything to my tsconfig.json?

No. Nub reads tsconfig.json the way tsc does — paths, baseUrl, extends chains, jsx, experimentalDecorators — and applies them at runtime.

How do I install Nub?

npm install -g --ignore-scripts=false @nubjs/nub

That installs the Rust binary (via the usual @<scope>/<platform-arch> npm distribution pattern that swc, esbuild, oxc, and napi-rs-based tools use) plus the N-API addons for your platform. The resulting nub and nubx commands are on your PATH.

How do I upgrade?

npm install -g --ignore-scripts=false @nubjs/nub@latest

Same as installing — match how you installed: brew upgrade nub for a Homebrew install, otherwise re-run your installer. Nub and Node version independently: Nub resolves the Node your project pins (.node-version / .nvmrc / engines.node) and provisions it if it's missing, falling back to the node on your PATH only when nothing is pinned. So upgrading Nub does not change your project's Node version, and upgrading Node does not change your Nub version.

Is there a curl install script?

Yes. On macOS and Linux, curl -fsSL https://nubjs.com/install.sh | bash; on Windows, powershell -c "irm https://nubjs.com/install.ps1 | iex". Both drop a native binary into ~/.nub and put it on your PATH. If you'd rather go through your package manager, npm install -g --ignore-scripts=false @nubjs/nub (or pnpm add -g / yarn global add) does the same thing, and on macOS and Linux brew install nubjs/tap/nub works too.

Does it have a dev server?

No. Vite already owns this; nub run dev runs whatever your project's dev script is — faster, because the nub run wrapper costs less than the pnpm run wrapper, but the dev server itself is still Vite (or Next.js, or whatever you're running).

Does it have a test runner?

No. Node's own node:test already owns this — and vitest and jest if you prefer those. Nub runs whichever you've chosen. Workers spawned by your test runner inherit Nub's augmentation, so TypeScript test files Just Work.

Does it have a bundler?

No. The bundlers your project already uses (Vite / Rollup / Rolldown / esbuild / webpack / tsup) keep working. Nub's transpile path is for execution, not for shipping production artifacts.

What's the hot-reload story?

Watch mode restarts the process when files change — the watch set is the resolved dependency graph plus your .env* files and tsconfig.json, so it Just Works for TypeScript projects with no glob hygiene. It does not preserve in-memory state or hot-swap modules in place; a restart is a restart. See Watch mode.