Runtime
What Nub adds when it runs your code on stock Node — TypeScript, resolution, env loading, data imports, and modern globals — and the plain-Node escape hatch that turns it all off.
The top-level nub <file> command runs a file on the node you already have installed. It is a true drop-in for node <file> — same argv shape, same flag set, same behavior — and on top of that it augments the runtime: TypeScript and JSX execute directly, imports resolve the way your editor resolves them, .env* files load automatically, data files import as parsed values, and modern web/TC39 globals are present even on older Node.
Nub registers a module.registerHooks() load hook and injects a small preload. Every augmentation rides on Node's own extension surfaces.
nub index.ts
nub server.tsx
nub script.jsSupported Node versions
Nub runs your code on stock Node — it resolves which Node your project pins and fetches one if it's missing, but never ships a runtime of its own. The supported floor is Node 18.19+; below that Nub refuses to run. Node 18.19–22.14 use the compatibility tier (async module.register() loader-worker) and 22.15+ use the fast path (sync module.registerHooks()) — functionally equivalent, differing only in startup overhead. See Managing Node versions for pinning and pre-installing.
Node version resolution
Nub resolves the project's pinned Node before running anything, walking up from the working directory and taking the first source that yields a version. Highest precedence first:
NODE_EXECUTABLE— an explicit path to a Node binarypackage.json→devEngines.runtime.node-version.nvmrcpackage.json→engines.node(a range)- the
nodeon yourPATH, when nothing is pinned
When a pinned version isn't installed, Nub fetches it — see Managing Node versions.
A drop-in for node
Anything node <args> accepts, nub <args> accepts too. Pass-through is the default for the entire flag space — every flag reaches Node verbatim:
# diagnostics
--prof --cpu-prof --report-*
# module resolution
--conditions --preserve-symlinks
# inspector
--inspect --inspect-brk
# warnings
--no-warnings --trace-deprecationReading a program from stdin works the same way node - does.
nub --max-old-space-size=4096 build.ts
nub --import ./instrument.js server.ts
nub script.ts --port 3000 --verbose # everything after the file is your script's argv
echo 'console.log(1 + 1)' | nub -Augmentations
In brief:
- TypeScript — the whole TypeScript surface runs directly, including non-erasable syntax (
enum,namespace, parameter properties). Types are stripped, not checked. - JSX —
.jsxand.tsxfiles run with the modern automatic runtime by default, configured throughtsconfig.jsonor a per-file@jsxImportSourcepragma. - Decorators — legacy decorators run with no build step under
experimentalDecorators, includingemitDecoratorMetadatafor the DI / ORM ecosystem. - Resolution — extensionless imports,
.js → .tsrewriting, andtsconfig.jsonpath aliases resolve at runtime, the waytscand your editor resolve them. - Environment variables —
.env*files load into the environment before Node starts, with${VAR}expansion. Nodotenv, no--env-file. - Loaders — import
.yaml,.toml,.jsonc,.json5, and.txtfiles directly as parsed values, with no npm parser. - Modern APIs — modern globals and built-ins (Temporal, URLPattern, WebSocket, EventSource, node:sqlite, and more) work out of the box, polyfilled or unflagged per Node-version band.
- Workers — the browser-shape
Workerglobal runs your.tsentry points, wrappingnode:worker_threads. - Web Storage —
localStorage/sessionStoragebehavior and the opt-in that backs persistent storage.
Modern APIs
Modern globals — TC39, web-platform, and newer Node built-ins — work out of the box under Nub. Whatever Node version you run, the API is there — native where Node ships it, and where Node doesn't, Nub fills the gap automatically. You write against the API; the version split is Nub's problem, not yours.
const now = Temporal.Now.plainDateTimeISO();
const route = new URLPattern({ pathname: "/u/:id" });
const ws = new WebSocket("wss://api.example.com");
const stream = new EventSource("https://api/stream");
const { DatabaseSync } = require("node:sqlite");Two mechanisms do the work, picked per API: Nub either preloads a JS polyfill (feature-detected, so it steps aside the moment the native global appears) or injects the --experimental-* flag that an older Node hides the API behind. Once Node stabilizes an API, it's native and Nub does nothing.
The minimum version column is the lowest Node where the API works under Nub.
| API | Minimum version | How |
|---|---|---|
Temporal | — | polyfill below Node 26, native above |
URLPattern | — | polyfill below Node 24, native above |
RegExp.escape | — | polyfill below Node 24, native above |
Error.isError | — | polyfill below Node 24, native above |
Promise.try | — | polyfill below Node 24, native above |
Float16Array | — | polyfill below Node 24, native above |
navigator.locks | — | polyfill below Node 24.5, native above |
reportError | — | polyfill |
vm.Module | — | flag-injected |
WebSocket | Node 20.10 | flag-injected below Node 22, native above |
EventSource | Node 20.18 | flag-injected below the native line, native above |
node:sqlite | Node 22.5 | flag-injected below Node 22.13, native above |
A dash means the API works across the full supported range, 18.19 and up. The three floored rows have a real cutoff, because the underlying Node mechanism doesn't exist below it:
- The
WebSocketglobal needs Node 20.10 — the flag Nub injects below the native line (Node 22) doesn't exist on older 20.x patches. - The
EventSourceglobal needs Node 20.18, the patch where Node added the flag on the 20 LTS line. - The
node:sqlitemodule needs Node 22.5, where Node first shipped it.
--node
Pass --node to run with zero augmentation — no load hook, no preload, no flag injection, no .env loading. Your code runs on plain Node, exactly as node <file> would. It still runs the project's pinned Node (from .node-version / .nvmrc, fetched if missing), which makes it the tool for differential debugging — does this reproduce on vanilla Node, on the exact version this project pins? A bare node script.js can't answer that, because it runs your shell's Node, not the project's.
nub --node script.js # the project's pinned Node, vanilla
node script.js # your shell's Node, unaugmented, unprovisionedIntroduction
Nub is an all-in-one Rust toolkit for Node.js. Run TypeScript files and scripts, install dependencies, and manage Node itself — on the Node you already have, with no lock-in.
TypeScript
How Nub runs the full TypeScript surface on stock Node — non-erasable syntax, resource-management downleveling, and source maps — none of which plain Node does.