FAQ
The short, indexed answers to common questions about Nub — what it is, how it works, and what it deliberately is not.
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/.binIt'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.tsimports, inline source maps) - Respects
tsconfig.json—paths,baseUrl,extendschains,jsxall applied at runtime - JSX support —
.jsx/.tsxfiles execute directly, runtime sourced fromcompilerOptions.jsx/jsxImportSource - Env files —
.envand.env.${NODE_ENV}loaded eagerly with Vite-compatible precedence - YAML / TOML / JSON5 / JSONC / text loaders —
import config from "./config.yaml"works the wayimport data from "./data.json"already does - Automatic polyfills —
Temporal,URLPattern, browser-shapeWorker, WinterTC gap globals, version-detected and feature-gated - Unflagged experimentals —
EventSource,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 runis 24× faster thanpnpm runon cold start.nubxis 19× faster thannpxon 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
--nodeflag) 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, typenodedirectly.
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@v0The 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).
| Lockfile | Behavior |
|---|---|
pnpm-lock.yaml | Round-trips in place |
package-lock.json | Round-trips in place |
bun.lock | Round-trips in place |
yarn.lock | Read-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 runreadspackage.jsonand its"scripts"field.nubxreadsnode_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 dependenciesTopological 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 bin —
nubxwalks up the directory tree checkingnode_modules/.bin/at each level (the standard resolution algorithm), in Rust, returning in single-digit milliseconds. - Not installed —
nubxfetches 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.jsonpaths,baseUrl,jsx,experimentalDecorators,extends→ ignored ("intentionally unsupported," per the Node 26 docs)- Extensionless
.tsimports → 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>.localExpansion 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:
- Resolves the
nodeon yourPATH. - Reads your nearest
package.json,tsconfig.json, and.env*files. - Spawns that Node with
--importpointing at Nub's preload bundle andNODE_OPTIONSset to whatever experimental flags are needed for your Node version. - 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 tooxc), installs polyfills (Temporal,URLPattern,WebSocket,Worker, WinterTC gap globals) where Node is behind, and hands control to your script. - Subprocesses inherit the same augmentation recursively —
node hello.jsfrom 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--loaderCLI dance.--importpreload (Node 19.0, October 2022, stable since 20.6) — run setup code before usermain, ESM-aware. Used to install polyfills and set up the load hook.--requirepreload (Node 1.6, 2012) — older CJS-style preload, still useful in pre-ESM corners.NODE_OPTIONSand 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
oxctranspiler 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/polyfillon older NodeURLPattern— WHATWG URL pattern matching. Native in Node 24+; Nub polyfills viaurlpattern-polyfillon older NodeWebSocket— Browser-standard WebSocket client. Available from Node 20.10+; Nub injects--experimental-websocketbelow 22.0 where Node stabilized itWorker— Browser-shape worker constructor overnode:worker_threads. Node has no native plan (nodejs/node#43583 open since 2022); Nub ships a ~150-LOC wrapper- WinterTC gap globals —
reportError,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 lockfileModern 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.10The 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/bunNub keeps application code on Node's public surface:
no globalThis.nub
no nub:* module namespace
no @nub/* npm scope
no "nub" field in package.jsonThe 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.readTextFileDeno 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/postlifecycle 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.
.envfiles needdotenv-cliorimport "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 offIt 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/nubThat 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@latestSame 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.
Package-manager shims
An opt-in for muscle memory. Install the shims and a bare pnpm, npm, or yarn command routes through Nub to the package manager your project pins, with no extra Node process in front.
GitHub Action
A drop-in replacement for the official setup-node action. Swap one line in your workflow and Nub installs itself, provisions the project's pinned Node, and fronts it on PATH so every step keeps working unchanged.