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 i -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/.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. The "would a user on plain Node, plus the corresponding module.register() / --import / npm addon, get the same result?" question always answers yes.
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, nub run --node and nubx --node strip the hook/preload/flag-injection 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.
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. There's no separate compatibility matrix to consult.
The contract is: 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. Nub does not own the node: namespace and does not register synthetic specifiers.
What Node versions does it support?
Augmented modes require Node 18.19+ (Node 18 LTS). That's the floor for the loader-hook API Nub uses for the transpile-on-import path.
Compat mode (nub run --node / nubx --node) is best-effort on any Node version. In compat mode Nub is a pure orchestrator (script resolution, workspace bin PATH, env var injection) and spawns plain Node without registering augmentation. Works on Node 20, 18, whatever. For ad-hoc plain-Node behavior on a file, type node directly.
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 @nubjs/nub, with platform-specific N-API addons resolved at install time.
Does it replace my package manager?
It can. nub install is a full package manager, powered by the embedded aube engine — pnpm's CLI surface, your project's lockfile. It detects the lockfile your project already has and writes the same format back: pnpm-lock.yaml, package-lock.json, and bun.lock round-trip in place, never replaced with a foreign file (yarn.lock is honored read-only — an install that would rewrite it is refused, with the exact yarn command to run instead).
Because Nub writes the same lockfile your package manager does, nothing forces a migration: pnpm, npm, and bun keep working side by side — run either tool, commit the same file, switch back any time. And when you want the original tool itself, nub pm use provisions the exact pinned version — corepack's job, without the PATH shims.
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. Workspace topology (pnpm-workspace.yaml, npm workspaces, yarn workspaces) is honored by nub run and nubx directly. The source of truth for nub run is package.json and its "scripts" field; for nubx, it's node_modules/.bin.
One Yarn-specific caveat: Yarn Berry's Plug'n'Play mode replaces node_modules/.bin/ with a runtime resolution table managed by Yarn's own loader, and tools that walk the .bin/ tree — npx, pnpm exec, bunx, nubx — don't see it. Yarn Berry users who want compatibility with the standard CLI-runner ecosystem can opt out by setting nodeLinker: node-modules in .yarnrc.yml, which restores the conventional .bin/ layout.
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. Locally installed CLIs live in a standard place: node_modules/.bin/, with the resolution algorithm well-defined (walk up the directory tree, check .bin/ at each level) — and nubx is that lookup, in Rust. Local node_modules/.bin resolution returns in single-digit milliseconds. For binaries that aren't installed, Nub detects the package manager your project uses and delegates the fetch — so your lockfile stays consistent.
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: File runner → TypeScript.
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 .env, .env.local, .env.<NODE_ENV>, and .env.<NODE_ENV>.local automatically — with the Vite/Bun-style precedence rules — and injects them into the process environment before Node starts. Expansion of ${VAR} and $VAR is supported, including nested references and cycle detection. Under NODE_ENV=test, .env.local is intentionally skipped, following the Next.js convention — local secrets shouldn't leak into your test suite. Full details: File runner → 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.
No patched Node, no vendored libnode, no separate runtime to discover.
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 doesn't Node ship these defaults natively?
Node is an exceptionally well-managed open source project, with a release cadence and stability story few projects this load-bearing match. The Node team's caution about breaking changes is the right caution — Node is the substrate that thousands of production deployments rely on, and it has to stay predictable.
By the same logic, Node can't ship the kinds of changes that would bring plain node into alignment with modern developer-tooling expectations — opinionated defaults, integrated TypeScript, eager .env loading, a workspace-aware script runner. Collectively those would shift the meaning of "running on Node," and breaking that contract isn't worth it.
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. The Node Foundation, correctly, is not going to ship a 5× faster script runner in next month's LTS, and shouldn't try. Nub can. Nub is the rate of change layered on top of the rate of stability.
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. Native in Node 22.4+; Nub polyfills viawson older NodeWorker— 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 directly: dotenv, cross-env, tsx / ts-node, nodemon, tsconfig-paths, npx (replaced by nubx / nub exec), nvm / fnm (the pin file alone provisions the right Node), corepack (nub pm), and the package-manager CLI itself for installs and script runs (nub install / nub run, against the lockfile you already have). Modern Node already obsoletes; Nub lets you inherit: node-fetch / cross-fetch / whatwg-fetch (native fetch since 18), abort-controller (native since 15), uuid for the v4 case (crypto.randomUUID since 14.17), rimraf (fs.rmSync({ recursive: true, force: true }) since 14.14), mkdirp (fs.mkdirSync({ recursive: true }) since 10.12), glob / globby for basic cases (fs.glob since 22), form-data (native FormData since 18), ws client (native WebSocket since 22.5, Nub polyfills on older). 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.
What's nub-resolver?
The one Rust crate Nub ships of its own — nub-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 nub-resolver itself.
How is it different from Bun?
Bun is a from-scratch JavaScript runtime built on JavaScriptCore, with its own module loader, its own package manager, its own test runner, its own bundler, and its own Bun.* API surface. It's an impressive engineering effort and a real product. The tradeoff is that "I wrote this on Bun" eventually becomes "this only runs on Bun," and the path back to Node is a rewrite — Bun.serve(), bun:sqlite, bun:test, the Bun global, and @types/bun are baked into application code.
Nub is the inverse bet. The compatibility surface stays Node's — your code is plain Node code, no globalThis.nub, no nub:* module namespace, no @nub/* npm scope, no NUB_* environment variables. The augmentations Nub applies are all things that work on plain Node with the matching module.register() / --import / npm addon. Most of Bun's "all-in-one" pitch is now either native to Node or implementable on top of it; Nub assembles the rest.
Bun was designed in 2022, before Node exposed module.register() (Node 20.6, August 2023) and module.registerHooks() (Node 22.15, April 2025). The build-vs.-extend tradeoff has tipped since.
How is it different from Deno?
Deno is a from-scratch runtime built on V8, with its own permissions model, its own standard library, its own module resolution (URL imports, jsr: specifiers), and its own Deno.* API surface. Like Bun, it bakes the runtime name into application code (Deno.serve(), Deno.env, Deno.readTextFile). Like Bun, the path back to Node is a rewrite.
Deno's Node compatibility layer has improved a lot, but it's a compatibility layer — a translation of Node semantics into Deno's runtime — not the same execution surface. Nub runs on the actual Node binary you installed; if a package works on Node, it works on Nub, by construction.
Deno's first commit is from 2018, three years before module.register() and seven before module.registerHooks(). The build-vs.-extend tradeoff was different then.
How is it different from node --run?
Node itself shipped a built-in script runner in 22.0 (node --run, April 2024). It's fast — roughly 6× faster than npm run per Node's own benchmarks — but deliberately stripped down: no pre / post lifecycle hooks, no workspace-aware lookup, no package-manager env vars (npm_*), no implicit arg forwarding through --. Real projects routinely hit one of those gaps and fall back to pnpm run. The goal for nub run is node --run speed without the compatibility cliff — same script lookup, same npm_* environment variables, same node_modules/.bin PATH chain, same pre / post lifecycle hooks, plus pnpm-style workspace filters.
What if I want to stop using Nub?
Your codebase keeps working on plain Node, unchanged.
Nub adds no APIs to your code. No globalThis.nub. No nub:* module namespace. No @nub/* npm scope (with the single TypeScript-ecosystem exception of @types/nub on DefinitelyTyped). No NUB_* environment variables. No "nub" field in package.json. Your package.json doesn't mention Nub. Your imports don't reference Nub. Your runtime checks don't gate on Nub.
If Nub disappeared tomorrow, the worst case is: TypeScript files need a separate compile step or a tsx-equivalent loader; .env files need dotenv-cli or import "dotenv/config" again; you're back to whatever toolchain you had before. 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 offNo transpile hook, no .env loading into the spawned process, no polyfills, no experimental-flag injection, no PATH shim. 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.
There's no NODE_COMPAT=1 env var and no plain-Node passthrough verb — the per-invocation flag on the two orchestration verbs is the one mechanism. For ad-hoc plain-Node runs, type node.
Does Nub add anything to my package.json?
No. Nub doesn't consume a package.json-level config field. No "nub" key, no "nub.config", no "dependencies" shim. The fields Nub reads are the existing standard ones — "scripts", "workspaces", "bin", "type", "exports", "imports", "engines.node" — exactly as Node, npm, pnpm, and yarn read them.
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. It does not require, write, or interpret any Nub-specific field.
How do I install Nub?
npm install -g @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 @nubjs/nub@latestSame as installing. Because Nub adopts whatever node is on your PATH, upgrading Nub does not change your 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 @nubjs/nub (or pnpm add -g / yarn global add) does the same thing.
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.