12k
All articles

Introducing Nub, an All-in-One Node.js Toolkit

Nub is a Rust Node.js toolkit that runs TypeScript, scripts, installs and Node versions on stock Node while preserving lockfiles and safety checks.

OpenReplay Team
OpenReplay Team
Introducing Nub, an All-in-One Node.js Toolkit

Nub is a Rust command-line toolkit for Node.js. It transpiles TypeScript, dispatches package.json scripts, installs dependencies and provisions Node versions, then hands execution to the stock node binary your project already pins. It augments Node rather than replacing it, which is the whole difference between it and Bun or Deno.

Most teams who weighed Bun and Deno against Node never got past the first question: you do not swap the runtime under a production service because the developer experience is nicer. Nub takes the other route, calling itself a Rust toolkit that leaves your Node, your lockfile and your package manager where they are. Here is what that buys you, and what it costs to try.

Key Takeaways

  • Nub is a Rust CLI that layers TypeScript execution, script dispatch, package installation and Node version management on top of the stock node binary, so there is no new runtime to qualify.
  • Node’s own TypeScript support only deletes annotations and turns away anything needing generated code, such as enums, parameter properties or a namespace holding runtime code; Nub’s loader compiles those forms instead.
  • Nub’s installer is pnpm-shaped and reads and writes existing npm, pnpm and bun lockfiles in place, with yarn lockfiles read-only.
  • The install defenses need no configuration: dependency build scripts stay blocked until you approve them, every fresh resolve is checked against OSV, and a 24-hour release-age gate keeps brand-new versions out.
  • There are no Nub-specific APIs and no Nub lockfile, and nub.jsonc is optional, so removing Nub returns the project to plain Node.

What Is Nub, and What Is It Not?

Nub is not a fourth runtime. It is a single binary that sits in front of Node, does the work that currently requires tsx, nvm, npx and a package manager, and then execs real Node. Its homepage puts the mechanism plainly: oxc compiles your files in memory from inside a native addon, and the stock node binary executes what comes out. There is no separate runtime underneath, and the file runner takes the same flags node does.

Nothing about your deployment target changes. The V8 version, the C++ ABI your native modules were built against, the process surface your instrumentation hooks, all of it is whatever Node you were already shipping. The augmented path requires Node 18.19 or newer (Node 18 LTS), on macOS, Linux and Windows, each on both x64 and arm64.

The project is early. The npm package @nubjs/nub is published under MIT and still pre-1.0, on the 0.9.x line as of the latest release, with new versions landing frequently.

How Does Nub Run TypeScript With No Build Step?

Node’s own TypeScript support strips types rather than compiling them. Annotations are replaced with whitespace, and anything that would need JavaScript generated for it is turned away. Node’s documentation lists the cases: enums, namespaces that hold runtime code, parameter properties and import = aliases all raise ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX; decorators fail to parse; and because Node never opens tsconfig.json, paths aliases are not applied. A fuller transform mode used to sit behind --experimental-transform-types, but Node removed that flag in version 26, so erasure is now the only built-in path.

Those exclusions are exactly the syntax a NestJS or TypeORM codebase is made of. Take a file using an enum, a parameter property and an extensionless relative import:

// invoice.ts
import { Model } from "./base"

enum Status { Draft, Sent, Paid }

export class Invoice extends Model {
  constructor(public status: Status = Status.Draft) {
    super()
  }
}

Under plain node invoice.ts the enum and the parameter property are non-erasable and the import has no extension. Under nub invoice.ts the same file runs untouched. Nub hands every file to its native addon to compile, which is why the enum, the parameter property and the bare import all work. It also walks your tsconfig.json and any config that file extends, then passes the paths aliases to Node’s own resolver through a module.registerHooks() resolve hook.

Decorators are supported in one flavour only. The launch post covers legacy experimentalDecorators, the form NestJS, TypeORM and Angular are written against, with emitDecoratorMetadata alongside it. Stage 3 decorators, which TypeScript 5 uses by default, are rejected, because the transform is still an open gap in oxc. The runner does emit inline source maps, so stack traces point at your source rather than at generated output. That last detail is not cosmetic: transpiled TypeScript that loses its source maps produces traces against code nobody wrote, which is a recurring source of wasted triage time.

Which Commands Does Nub Replace?

Nub’s single binary covers work currently split across a shelf of tools. The documented replacement mapping is direct:

Nub commandStands in for
nub <file>node, tsx, ts-node, dotenv-cli
nub run <script>npm run, pnpm run, yarn run
nubxnpx, pnpm dlx, pnpm exec, yarn dlx
nub installnpm, pnpm, yarn
nub watchnodemon, node --watch, tsx watch
nub nodenvm, fnm, n, volta
nub pmcorepack

That table is not the full surface. The README also covers nubr, a single command that will run a file, a package.json script or a bin from node_modules/.bin, trying them in that order. It ships on its own as @nubjs/runner for projects that cannot install a binary.

The important property is that these are independent. Adopting the file runner does not oblige you to adopt the installer, and swapping a dev script from tsx watch src/server.ts to nub watch src/server.ts leaves package.json a normal npm-compatible manifest. The project reports its own benchmarks for the speed claims: 24× faster than pnpm run for script dispatch, 19× faster than npx for bin execution, and 18× faster than pnpm install. The README’s paired timings put script dispatch at 14.7 ms against 329.9 ms for npm, and a warm frozen install at 171 ms against 3193 ms for pnpm, both measured on macOS. A second install benchmark, run with hyperfine on an ubuntu-latest runner against a 1,168-package tree, reports 346 ms for Nub and 3453 ms for pnpm.

The Package Manager: pnpm-Shaped and Lockfile-Preserving

Nub’s installer does not introduce a lockfile format. It works out which package manager the project already uses, from package.json#packageManager or from whichever lockfile it finds, then runs in compat-mode and honours that tool’s config files and environment variables. The CLI itself is pnpm-shaped, so nub install, nub add -E -D react, nub remove, nub update and nub ci behave the way the muscle memory expects.

On lockfiles specifically: npm, pnpm and bun lockfiles are read and written in place, and yarn lockfiles are read-only. Nothing is converted, and no second lockfile appears in the diff. For a team on pnpm, that is the question that decides whether this is evaluable at all.

Node Version Resolution Without nvm

nub node resolves the Node version a project expects and provisions it on demand. The version comes from .node-version, .nvmrc or package.json#engines, and a missing version is downloaded and cached automatically, with explicit verbs available too: nub node install 26, nub node ls, nub node pin 26 and nub node uninstall 22. It does this without shell hooks and without rewriting your PATH, which is the part of nvm that tends to break in CI and in non-interactive shells.

Supply-Chain Defaults, and the Absence of Lock-In

Nub’s install-time defenses are on with no configuration. Four of them are documented. A dependency’s build scripts do not run until you approve that package. Every fresh resolve is checked against OSV for known-malicious versions. A version that has lost the publishing trust evidence an earlier release carried is refused outright. And minimumReleaseAge defaults to 24 hours, the same window pnpm uses, so a version published minutes ago cannot reach your tree. The launch post adds that a transitive dependency resolving to a git+, file: or raw-tarball URL is refused rather than silently fetched. If you have already worked through a defense posture against npm supply-chain attacks, this is that checklist as the default rather than as a .npmrc you maintain.

The reversibility claim is the other half. Nub adds no APIs to import, writes no lockfile of its own, and treats nub.jsonc as optional configuration rather than a requirement. Uninstall the binary and the project runs on plain Node with the tooling it had before, because the source never referenced Nub in the first place.

Who Should Try Nub, and Who Should Not?

Try it if you are running TypeScript through tsx or ts-node, keeping nvm around for version pinning, and would rather not spend a quarter qualifying a new runtime to get out of that. Start with the file runner on one service, leave the installer alone, and see whether the enum-and-decorator class of build-step friction disappears. Skip it, for now, if you need a pinned, boring toolchain for a regulated release process, because a pre-1.0 project shipping releases days apart is not that yet. The cost of finding out is npm install -g @nubjs/nub and one command against a file you already have.

FAQs

How do I run a file through Nub with no augmentation at all?

Use compatibility mode: pass --node for a single invocation, or set NODE_COMPAT to 1, true or yes to cover the whole process tree. In that mode Nub applies nothing at all, so there is no load hook, no preload, no flag injection and no .env loading. It still works out which Node the project pins and installs it if needed, so your code runs vanilla on the right version. That makes it useful for telling a Nub bug apart from a Node one.

Which platforms and Node versions does Nub support?

Nub ships prebuilt Rust binaries for Linux, macOS and Windows, on x64 and arm64, and pulls in the matching N-API addon for your platform at install time. Augmented modes need Node 18.19 or newer, because that is where the loader-hook API behind the transpile-on-import path first appears. On anything older, an augmented command stops with an error that names the floor and points you at compatibility mode.

Why does an install fail with ERR_NUB_ALLOW_BUILDS_RENAMED?

Nub 0.9.0 renamed the top-level build allowlist in package.json from allowBuilds to allowScripts, matching the slot npm 12 reads. A project still carrying a root allowBuilds map is refused with that error rather than warned, so the fix is renaming the key. A pnpm allowBuilds is a different setting and is left alone, whether it lives in pnpm-workspace.yaml or under package.json#pnpm.

Can I use nubx without switching package managers?

Yes. nubx finds a locally installed CLI in node_modules/.bin whatever put it there, so it works on a project installed by npm, pnpm, yarn or bun without migrating anything. It accepts pnpm exec's flags under the same names, and nub dlx mirrors pnpm dlx down to shell mode, so command lines you already have carry over.

DevTools for the frontend

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.