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 @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 runs on stock Node, provisioning the version your project pins or using the node on your PATH when nothing is pinned. It augments that Node through Node's own public extension surfaces: module.registerHooks(), --import preloads, NODE_OPTIONS, and N-API addons.

Is it a new runtime?

No. Nub runs on stock Node. There is no Nub runtime, no Nub JavaScript engine, no separate compatibility surface. When nub index.ts runs, the process is a stock node binary: Nub spawns it, registers a load hook for TypeScript / JSX / YAML / TOML, and preloads polyfills where Node lacks them. See How does it work? for the mechanism.

Is it a fork of Node?

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

Is Nub a replacement for Node?

No. Your code is transpiled and executed by the stock node binary; there is no Nub runtime. Nub is a layer of developer-experience defaults built 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.[mode] (mode from APP_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-*

For plain Node behavior, run node directly; Nub's per-invocation PATH shim only affects descendants of a nub call. 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 does it replace?

One binary replaces tsx, dotenv-cli, nodemon, npx, pnpm run, and the tsconfig loader shims around them, and runs on the Node you already have.

Is it faster than Node.js?

No. Your code runs in Node itself, so execution speed is Node's. The CLI surface is faster, because a native Rust binary skips the Node bootstrap that pnpm run and npx pay for 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 later run reads the cache.

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.

The contract

Code targeting Node runs on Nub byte-for-byte. Any divergence is a bug.

What about CommonJS / require()?

Yes. CommonJS, require(), dynamic require(), require.cache, module.exports, and the node: core modules in both ESM and CJS form all run, because the process is stock Node. Nub's load hook covers TypeScript / JSX / YAML / TOML at the ESM and CJS entry points equivalently.

What about native (N-API) addons?

Yes. Native addons compile against Node's N-API ABI, and Nub is running on a stock Node binary, so addons load 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.) Compiling one from source carries one upstream caveat under the default install layout — see GYP-based native builds.

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 — is provided by the stock Node binary.

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.

Below the floor

On Node older than 18.19, augmented commands fail with an error naming the floor and 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 @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, 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. 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/, only a runtime resolution table managed by Yarn's loader. 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 pnpm's -r / --filter semantics:

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

Read the full docs on pnpm.io for the filter syntax.

Topological ordering, parallelism, and --workspace-root are honored. Nub skips the ~150 ms pnpm bootstrap, which pnpm -r run build pays once per package.

Does it replace npx?

Yes — nubx is the equivalent, following the same local-first-then-registry model as npx and pnpm dlx:

  • 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. Nub is a complete package manager, so it does the fetch itself rather than shelling out to a foreign PM.

The one behavioral difference from npx is that the fetch is not silent: the first fetch of a tool prompts, and CI fails closed unless you pass -y. See the runner docs for the consent model.

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 Node's built-in TypeScript support?

Node 22.18+ runs .ts files by default, but only strips types. Anything beyond erasable annotations is rejected or ignored:

  • 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.

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. Full details: Watch mode.

What about dotenv-cli?

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

.env.[mode].local
.env.local
.env.[mode]
.env

The [mode] slot comes from APP_ENV — not NODE_ENV. To load a specific file directly, pass --env-file (repeatable); its values arrive verbatim, as on Node. In the automatic .env* files, expansion of ${VAR} and $VAR is supported, including nested references and cycle detection.

Test mode

Under the test mode (APP_ENV=test), .env.local is skipped, following the Next.js convention.

Full details: Runtime → Environment variables.

How does it work?

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

  1. Resolves the Node version your project pins — devEngines.runtime, .node-version, .nvmrc, or engines.node — provisioning it from nodejs.org if it's missing, and falling back to the node on your PATH when nothing's pinned.
  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 that 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 — a node hello.js from inside a script, or the worker pool your test runner spins up.

See What Node features does it rely on? for the extension surfaces each step uses.

What Node features does it rely on?

Every augmentation Nub applies can be set up by hand on plain Node, through public surfaces:

  • 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 without the --loader flag.
  • --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, for CJS-only setups.
  • 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.

There is no patched Node binary, no vendored libnode, no separate runtime.

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 it composes on top of Node, so Node's compatibility surface is preserved.

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. Nub is that layer.

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
  • reportError — the WinterTC gap global Node never shipped. Node TSC has no opposition; the PR stalled to inactivity. A few lines in Nub's preload

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 sets. 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 set, which you inherit 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
# native WebSocket since 22.0, unflagged down to 20.10
ws client

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.

How is it different from node --run?

Node shipped a built-in script runner in 22.0, node --run, and it is fast. It is deliberately minimal; three gaps keep projects on 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 as fast in the script-runner benchmark and closes all three.

What if I want to stop using Nub?

Your codebase runs 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.

Removing Nub restores the previous toolchain's requirements:

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

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).

# plain Node, full stop — shell `node` is always your real Node
node script.js
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 plain Node, when bisecting a Nub bug, or when CI needs byte-exact Node runtime behavior with Nub's --filter / workspace selection.

Does Nub add anything to my package.json?

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

scripts
workspaces
bin
type
exports
imports
engines.node

The fields that shape dependency resolution — overrides, resolutions, packageExtensions, patchedDependencies, and the rest — are standard too, and each is listed with an example under resolution.

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 @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 @nubjs/nub@latest

Same as installing — match how you installed: brew update && brew upgrade nub for a Homebrew install, otherwise re-run your installer. On a script install, nub upgrade resolves the published release before it downloads anything: when nothing is newer it prints already on the latest release and stops. Pass --version to reinstall the version you are already running. 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 place a native binary in ~/.nub and put it on your PATH. The scripts verify the release archive against its SHA-256 sidecar before replacing an existing installation. This detects corrupt, truncated, stale-cache, or mismatched assets; it is not an independent authenticity guarantee because the archive and checksum share the same release origin. Alternatively, npm install -g @nubjs/nub (or pnpm add -g / yarn global add) does the same, as does brew install nub on macOS and Linux.

Two environment variables customize the script, following the same convention as rustup and uv:

  • NUB_INSTALL_DIR — install somewhere other than ~/.nub. nub upgrade still updates it in place.
  • NUB_NO_MODIFY_PATH — set it truthy (1/yes/true/on) to skip the shell-profile edit; the script prints the PATH line to add manually.

Set them on the shell that runs the script — the right side of the pipe, not on curl:

curl -fsSL https://nubjs.com/install.sh | NUB_INSTALL_DIR="$HOME/.local/nub" NUB_NO_MODIFY_PATH=1 bash

On Windows, set $env:NUB_INSTALL_DIR / $env:NUB_NO_MODIFY_PATH before the irm | iex.

Can I run the canary build?

Yes. A nightly build of main publishes a full 8-platform build under the rolling canary release and to npm under the canary dist-tag. Nights when main gained no code change are skipped. Install it with the install script:

curl -fsSL https://nubjs.com/install.sh | bash -s canary

On Windows:

iex "& { $(irm https://nubjs.com/install.ps1) } canary"

Or through npm:

npm install -g @nubjs/nub@canary

An existing script install switches channels in place with nub upgrade --canary; nub upgrade --stable returns to the latest stable release. On a canary build, a plain nub upgrade stays on the canary channel. Canary versions are date-stamped — nub --version prints something like v0.5.1-canary.20260724.117.

Canary may break, and each build replaces the last. A fix that just landed reaches the channel on the next successful nightly rather than immediately. Homebrew and winget carry only stable releases.

Does it have a dev server?

No. nub run dev runs your project's dev script; the dev server itself is Vite, Next.js, or whatever that script starts.

Does it have a test runner?

No. Node's own node:test, vitest, and jest all run under Nub. Workers spawned by your test runner inherit Nub's augmentation, so TypeScript test files run directly.

Does it have a bundler?

No. Use the bundler your project already has (Vite / Rollup / Rolldown / esbuild / webpack / tsup); Nub's transpile path is for execution, not for shipping production artifacts.

Does it hot-reload?

Watch mode restarts the process when files change — the watch set is the resolved dependency graph plus your .env* files and tsconfig.json, so a TypeScript project needs no glob list. It does not preserve in-memory state or hot-swap modules in place. See Watch mode.