← Blog
The Nub Team

Nub v0.7 — nub.jsonc, Varlock support, Promise.allKeyed, and more

A typed project config file, schema-driven environments through Varlock, and every Stage 3 library surface no supported Node ships.

Nub v0.7 is now released with some major new features.

A project can carry a typed nub.jsonc at its root instead of repeating flags on every script. Environment loading hands over to Varlock when a project declares an @env-spec schema. And every Stage 3+ library surface that no supported Node ships is now available under Nub, feature-detected so a native implementation always wins.

Nub is an all-in-one toolkit for Node.js written in Rust. The nub command is flag-for-flag compatible with node, while adding full support for TypeScript, JSX, tsconfig.json, .env loading, and modern Web and ECMAScript APIs. It also includes a fast script runner (nub run), package runner (nubx), Node version manager (nub node), and pnpm-compatible package manager.

nub.jsonc

The node CLI configures everything through flags, so a real project ends up repeating a long prefix on every package.json script. Node shipped a config file of its own in v22.16, but it stays behind --experimental-default-config-file. Nub reads one typed file at the project root, covering the runtime, installs, and temporary package runs:

nub.jsonc
{
  "$schema": "https://nubjs.com/schema/latest.json",
  "preload": ["./instrumentation.ts"],
  "conditions": ["development"],
  "loader": { ".graphql": "text" },
  "install": {
    "linker": { "strategy": "global-virtual-store", "eject": ["electron"] },
    "minimumReleaseAge": "3d"
  }
}

It is JSON with comments and trailing commas, and Nub finds the nearest one by walking up from the working directory. Settings resolve most-specific-first: a command-line option, then an environment variable, then the project file, then a global ~/.config/nub/nub.jsonc, then the built-in default.

The two files take the same fields and are validated differently on purpose. The project file is checked in and shared, so an unknown key stops the command. The global file applies to every project on your machine, so an unrecognized section is ignored rather than taking the rest of your defaults down with it — a key left behind by a newer Nub never blocks someone else's project.

Read and write any field with nub config, which validates against the same rules that read the file and rewrites it in place:

nub config set install.linker hoisted
nub config set preload '["./setup.ts"]'
nub config set --global envFile false   # personal default, everywhere
nub config path

Comments, blank lines, and key order survive an edit, so a hand-annotated file stays readable. Keys that are not fields of this file keep their existing meaning and still read and write .npmrc. A JSON Schema ships at https://nubjs.com/schema/latest.json for editor completion, the config reference lists every field, and a dedicated post walks through what each field buys you.

Varlock support

An @env-spec schema — conventionally .env.schema — describes an environment as types, validation, which variables are secret, and where their values come from:

.env.schema
# @required @type=port
PORT=3000

# @required @type=enum(development, staging, production)
APP_ENV=development

# @required @sensitive @type=url
DATABASE_URL=

Varlock implements that format. Install it, and Nub hands the environment over rather than loading .env* itself:

nub add -D varlock
nub server.ts

Nub does not resolve the schema, inject its values, or redact anything. It runs Varlock in front of Node, so type generation, providers, validation, and secret redaction all happen on Varlock's own terms. A schema is a graph rather than a file list, since it picks its own environment selector and can pull values from providers, so only Varlock knows how to resolve it and there is no second interpretation to drift out of sync.

The hand-over covers a file run, nub run, nub watch, nubx, and lifecycle scripts, and --node turns it off with the rest of Nub's augmentation. In a workspace, Nub looks for the schema in the project root and then the workspace root, so a member without one uses the root's.

Nub uses whichever Varlock the project or your PATH provides, and vendors none of it. Because dotenv-extended has claimed the same filename since 2016 for an incompatible format, Nub only stands down when the file is actually @env-spec and no rival tool is a declared dependency. The Varlock page covers the rest.

Stage 3 APIs

Every Stage 3+ library surface that no supported Node ships is now available under Nub. Each is feature-detected before it is defined, so a native implementation always wins and nothing native is ever replaced.

Promise.allKeyed()

The keyed combinators take an object and return a null-prototype object with the same keys. allKeyed rejects on the first rejection; allSettledKeyed records every outcome.

const { account, invoices } = await Promise.allKeyed({
  account: getAccount(accountId),
  invoices: listInvoices(accountId),
});

const outcomes = await Promise.allSettledKeyed({
  profile: getProfile(account.id),
  avatar: getAvatar(account.id),
});

Iterator.zip()

The joint-iteration methods advance several iterables together. zip() yields arrays; zipKeyed() preserves the keys of an input object.

const pairs = Iterator.zip([ids, names]).toArray();

for (const row of Iterator.zipKeyed({ id: ids, name: names })) {
  console.log(row.id, row.name);
}

Iterator.prototype.chunks()

These helpers batch an iterator, produce overlapping windows, search for a value, or join values into a string.

const batches = Iterator.from(records).chunks(100).toArray();
const movingTriples = Iterator.from(values).windows(3).toArray();
const hasDraft = Iterator.from(statuses).includes("draft");
const label = Iterator.from(tags).join(", ");

Math.sumPrecise()

Summing an array with Math.sumPrecise() reduces floating-point error and avoids an overflow caused only by an unfortunate addition order.

const total = Math.sumPrecise([1, 1e100, 1, -1e100]);
console.log(total); // 2

Symbol.metadata

Decorators use Symbol.metadata as the key for class metadata.

const metadata = Example[Symbol.metadata];

Atomics.pause()

Inside a short spin-wait loop, Atomics.pause() hints to the processor that the thread is waiting.

while (Atomics.load(state, 0) === 0) {
  Atomics.pause();
}

Surfaces Node ships only in newer majors

These exist natively in a recent Node. Nub fills them in below that version and steps aside above it:

Native fromNub fills in onSurfaces
Node 2118.19–20Object.groupBy, ArrayBuffer.prototype.transfer
Node 2218.19–21Promise.withResolvers, the seven Set methods, Array.fromAsync, the iterator helpers
Node 2618.19–25Map.getOrInsert, Iterator.concat

The @nubjs/types package carries declarations for the surfaces no @types/node or TypeScript lib describes. The modern APIs reference lists every surface, how it is delivered, and the version Nub steps aside.

Supply-chain gates

Two gates now run when you add a package by name. A name first registered within minimumPackageAge — 30 days by default — is challenged, and so is a name close enough to a popular package to look like a typosquat.

Both prompt in an interactive terminal and refuse without one, and both exempt anything already in the lockfile or listed in allowedUnpopularPackages. Names the popularity corpus itself lists are never treated as typosquats, --allow-low-downloads clears both for a single invocation, and neither applies to nub install, nub ci, or nubx.

The release-age cooling window now decides correctly when a registry serves no per-version publish times, where it previously fell open and installed. The undeterminable case gets its own error, ERR_NUB_RELEASE_AGE_MISSING_TIME with exit 28, instead of a message listing years-old versions and suggesting a setting that could not have helped. Two per-invocation flags set the window for one command, on install, ci, the engine verbs, and nubx:

nub install --minimum-release-age 3d --minimum-release-age-exclude '@company/*'

A bare number is minutes, matching pnpm, and 0 disables the gate for that run.

Distribution

Each platform package shipped two byte-identical copies of the 45 MB binary, one per command name. They now ship one, with the command carried in the environment, which halves both the unpacked package and the release archive. Six of the eight platform packages were over the 80 MiB unpacked limit npmmirror enforces, which is why that mirror stopped syncing Nub at 0.0.31.

On Windows, Nub installs a real nub.exe alongside npm's generated shims, so cmd.exe runs the binary directly instead of booting Node to spawn it. The shims npm owns are left in place, and shells other than cmd.exe are unaffected.

Note

Because that executable is not a file npm created, npm uninstall -g @nubjs/nub leaves it on PATH, and cmd.exe keeps answering nub after the package is gone. Delete it from npm's global bin directory by hand.

Breaking changes

Five changes affect existing projects on upgrade.

  • Adding a package now challenges unfamiliar names. A public npm name first registered in the last 30 days prompts in a terminal and fails without one, and a name closely resembling a popular package is challenged the same way. Lockfile entries are exempt; pass --allow-low-downloads for one invocation, or set minimumPackageAge=0 in .npmrc.
  • The release-age gate now fails closed. A registry that serves no publish times silently disabled the 24-hour cooling window. Nub now falls back to the package's last-modified time and refuses when even that cannot establish an age, so a registry without publish metadata blocks where it previously installed. Ways through: minimumReleaseAgeExclude, minimumReleaseAge=0, or the new per-invocation flag.
  • The allowBuilds field is no longer read from .npmrc. Set it in pnpm-workspace.yaml or package.json instead. A project that kept its build allowlist in .npmrc will see those dependencies need approval again.
  • A bunfig.toml [install].linker value is no longer read. Nub cannot reproduce Bun's linker modes from that setting, so a Bun-owned project gets Nub's default layout. Choose a layout explicitly with node-linker in .npmrc, or with install.linker in nub.jsonc once the project is Nub's own. Yarn's nodeLinker was already unread and is unchanged.
  • Scripts now receive the node-options field from .npmrc, matching npm and pnpm. A project that set it for other tools will see those options reach its scripts for the first time.

The first install after upgrading re-bootstraps two caches once, because their on-disk paths were renamed. Nothing is re-downloaded.

Bug fixes

Package manager

The largest fix in this release is a data-loss defect in the hoisted layout, and build output already lost that way is not restored by upgrading — reinstall to rebuild it.

PRFix
#616Under the hoisted layout, every relink wiped and refilled each placed package, restoring published tarball contents only, so an unrelated nub add deleted whatever a postinstall had produced. A package directory is now reused when the previous link ran to completion and its contents are unchanged.
#609Auditing queried the advisory endpoint using the alias name for any npm:-aliased package, so those packages came back clean no matter how many advisories they had.
#596Binaries in node_modules/.bin were symlinked by absolute path, so a moved or copied node_modules arrived with dangling links.
#597Under the hoisted layout, binaries were linked only for the root package's direct dependencies, so a lifecycle script calling a hoisted transitive's binary exited 127.
#598A link that failed partway left a half-written package in the store, which the next run counted as complete, reporting "Already up to date" over an unusable tree.
#595, #613, #618On Windows, nub add and nub remove failed with os error 183, and nub install failed with Access is denied in any project where npm or Yarn had already written node_modules.
#601, #608On macOS, native binaries materialized out of the store kept com.apple.quarantine, so Gatekeeper refused to load them.
#604An optionalDependencies entry whose version matched nothing failed the entire install; npm and pnpm skip it.
#646, #648A registry serving an unexpected shape for a cosmetic packument field aborted the install, naming a package chosen at random by whichever concurrent fetch lost the race. Those fields now degrade; integrity and shasum stay strict.
#666Approved builds bootstrapped node-gyp before every build, so an unreachable registry aborted the install even with a warm store and nothing invoking node-gyp.
#640Inside a container with a cgroup v1 PID limit, the limit was not detected and the thread pool was sized as if unrestricted, aborting the install.

Runtime

PRFix
#599An extensionless bare package subpath such as import "pkg/sub" raised ERR_MODULE_NOT_FOUND where TypeScript and require() both resolve it. Subpaths are now probed, and a dependency that declares exports is never probed.
#599On Node 18.19–22.14, Nub's TypeScript handlers reordered Node's own resolution inside dependencies, so require("pkg/sub") could return a dependency's unshipped TypeScript source instead of the file Node resolves.
#673, #651A preload entry could silently disable Nub's augmentation in forked processes, because a consumer that re-parses NODE_OPTIONS keeps only the last token of a repeated flag. Entries now load through one generated module.
#573On Node 18.19–20.5, nub watch failed to start at all in any project with a .env file, because it passed --env-file to a Node that predates the flag.
#620On macOS and Linux, nub watch left its node --watch supervisor running after the parent died. Existing strays must still be killed by hand.
#555Running against a tree installed for a different Node major failed with a raw ERR_DLOPEN_FAILED from inside a dependency. Nub records the engine at install time and reinstalls once when it changes.
#587Importing .yaml, .toml, .json5, .jsonc, or .txt through require() compiled the document as JavaScript and returned an empty object instead of parsing it.
#649The launcher's fast path never engaged under pnpm 11, whose shim template the detector could not parse, so every call paid a Node startup it was meant to skip.

The full release notes list every change in this release.

Get started

Or paste this "Get Started" prompt into an agent. It will install Nub and explain how it can be used in your project. (It won't make any changes without permission.)