← Blog
The Nub Team

Nub 0.10.0

Dependency build scripts and workspace filters follow pnpm 12, Nub writes pnpm lockfiles that pnpm accepts in more layouts, and util.debounce, util.throttle and the JSON.parse options proposal are backfilled.

Nub 0.10.0 brings dependency build scripts and --filter in line with pnpm 12, writes pnpm lockfiles that pnpm accepts in more layouts, and backfills util.debounce, util.throttle and the JSON.parse options proposal.

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.

Dependency build scripts

An install that meets a build script nobody has approved now fails, as under pnpm 12. It fails last, once node_modules is linked and the approved builds and the project's own scripts have run. Global installs and nubx stay non-strict.

$ nub install
dependencies:
+ core-js@3.40.0

ERR_NUB_IGNORED_BUILDS

  × Ignored build scripts: core-js@3.40.0
  help: Run `nub approve-builds` to pick which dependencies may run their
        build scripts, or set `strictDepBuilds=false` to install without them.
PRWhat changed
#1050Denials. nub approve-builds '!pkg' and --allow-build='!pkg' record a denial, and --allow-build=pkg refuses a package the project denies. An explicit false outranks dangerouslyAllowAllBuilds.
#1050Settings. dangerouslyAllowAllBuilds and strictDepBuilds are read from .npmrc, the environment and, in a pnpm project, pnpm-workspace.yaml.
#1050nub rebuild. Reports unapproved packages instead of passing over them. A filtered rebuild works in a shared-lockfile workspace, where it did nothing.
#1050Member approvals. nub rebuild, nub ignored-builds and nub approve-builds read the same approvals as nub install, workspace members' included.
#1045Git dependency approval. Approved by <name>@git+<repository> or the same key with #<commit>. A bare name, name@version or * does not approve one, and a denial installs it unbuilt.
#1045Git dependency builds. A hosted dependency such as rimraf@github:isaacs/rimraf#v6.1.3 is now built. It linked with no dist/. The build no longer needs cp, which stock Windows lacks.
#1052Project lifecycle stages. Project scripts follow pnpm 12's stage lists in every project: install adds preprepare and postprepare, remove runs the uninstall stages, and --prod skips the prepare family.

Workspace filters

Nub now uses pnpm 12's selector grammar and directory globs on every workspace command, including nub run (#1056).

nub run --filter ./packages build
nub run --filter './packages/*' build
PRWhat changed
#1064Empty selection. Does nothing and exits 0. install, remove and update print Scope: 0 of N workspace projects, and nub ci still cleans node_modules first, as pnpm's does.
#1056Workspace root. run, exec, add and test drop the root from a selection that names no project. Other commands keep it.
#1055Repeated filtered install. Reports up to date. It reinstalled and reran lifecycle scripts.
#1055recursiveInstall. Read from pnpm-workspace.yaml in a pnpm project.
#1064Flags before the verb. --filter-prod, -C and --dir work there.
#1064nub ls in a member. Lists that member.

Runtime APIs

JSON.parse() takes the options object from the Stage 2.7 proposal (#992). The freeze option deep-freezes every object and array in the result, and preferNullPrototype gives each object a null prototype.

const config = JSON.parse(text, { freeze: true });
Object.isFrozen(config.servers[0]); // true
Object.getPrototypeOf(config); // null

util.debounce() and util.throttle() are backfilled from Node 26.10 on older supported versions (#974).

import { debounce, throttle } from "node:util";

const save = debounce((value: string) => value, 100);
const send = throttle((value: string) => value, 2, 1000);
await Promise.all([save("draft"), send("event")]);

Both are typed in @nubjs/types.

Distribution

PRWhat changed
#571nub upgrade under mise. Detects a mise install and runs mise upgrade --bump nub. It took the npm channel, which planted a second global copy.
#982Release assets. Each stable release carries packslip.sigstore.json, a signed manifest of its archives, so mise can install Nub straight from the release.

Breaking changes

  • An unapproved dependency build script fails the install with ERR_NUB_IGNORED_BUILDS and exit 1. strictDepBuilds=false restores the warning (#1050)
  • A git dependency with a prepare script builds only when approved, and otherwise fails with ERR_NUB_GIT_DEP_PREPARE_NOT_ALLOWED (#1045)
  • --filter ./packages selects only a project whose directory is packages. Use ./packages/* or ./packages/** for the projects under it (#1056)
  • A --filter that selects no project exits 0, and --fail-if-no-match exits 1 (#1064)
  • nub rebuild <pkg> no longer approves the package it names (#1050)

Bug fixes

Lockfile compatibility

PRWhat changed
#1060Per-project pnpm lockfiles. Under shared-workspace-lockfile=false, a member's lockfile matches pnpm 10's bytes on the tested workspaces. pnpm's --frozen-lockfile rejected the old form.
#1062Frozen installs of those trees. nub install --frozen-lockfile passes on a tree Nub or pnpm 10 wrote, and a plain install keeps a member's locked versions. It failed on the first and re-resolved on the second.
#1058Rebuild in those trees. nub rebuild finds a member's packages in the root store. It built nothing and exited 0.
#1054pnpm's env document. pnpm-lock.yaml records a pinned pnpm in its leading document, so pnpm 11 and 12 accept Nub's lockfile under --frozen-lockfile, including after nub pm use pnpm.
#1079Local paths in pnpm-lock.yaml. link:./x is written link:x and file:./x is written file:x, and empty packages: and snapshots: are omitted. A package spelled with ./ relinks on its first install after upgrading.
#1004A file: dependency outside the workspace. A member's file: directory above the workspace root installs under nub ci and --frozen-lockfile. It failed with is not a directory.
#1088package-lock.json. An optional dependency of a dev dependency is flagged dev and optional. It was devOptional.
#1040Optional peers. Every resolved optional peer is linked next to its package, a plain install repairs an existing nub.lock, and bun.lock records peers as bun writes them.
#1057Catalog updates. nub update -r raises a ^ or ~ catalog entry to the version it resolved, and a catalog-only lockfile change is written instead of skipped.
#1039Overrides on catalog:. A range-keyed override matches a catalog: dependency by its reference, as under pnpm.

Package manager

PRWhat changed
#1036nub pack and nub publish. Never pack a file from outside the package. A symlinked or absolute main, or a symlinked README, put that file in the tarball.
#1036workspace: specs. Both commands resolve workspace: and catalog: specs and apply publishConfig. The specs shipped verbatim, which made the package uninstallable.
#1043nub update at a workspace root. Keeps members' locked versions. It re-resolved every member and bumped packages nobody named.
#1037patchedDependencies. Git renames, header-only deletions, empty new files and mode changes apply as under pnpm 11. A patched executable keeps its mode.
#1010nub patch-commit. Replaces the patch file with one consolidated patch instead of appending a section per round. nub patch resolves its target through the lockfile.
#1051Workspace .npmrc. A command run in a member reads the root's registry and auth. The member's file layers on top under pnpm 10, Yarn 1 and a Nub project, and is not read under pnpm 11, npm or Bun.
#953DNS. Hosts resolve through the OS with an in-process cache, so VPN-scoped DNS, /etc/resolver/* and Windows NRPT apply.
#1078Broken downloads. A tarball whose connection breaks off mid-body is downloaded again. It failed the install.
#1035Failed installs. An install that fails after a virtual-store mode change keeps node_modules. It removed the tree before resolving.
#1059--offline. Refuses an uncached full packument fetch, which went to the network.
#995file: dependencies on macOS. Two projects with a file: dependency at the same relative path get their own files. They shared whichever installed first, and a tarball rebuilt in place stayed stale.
#1080Overrides to a local copy. A registry package whose dependency is overridden to a file: path is ejected into the project. The link dangled and require failed with Cannot find module.
#1046Store locks on NFS. Prune, maintenance and GC lock files open readable, so shared locks work on Linux NFS. They failed with EBADF.
#1074Add-time refusals. --allow-similar-name and --allow-new-name each clear their own check, and every refusal names its flag and allowedUnpopularPackages.
#1074Aliases and bins. A dependency under its own name keeps its bin over an alias of the same package, so tsc stays with typescript when typescript5 is also installed.
#1042Remix 3. ^3.0.0, 3.1.0 and >=2 keep the shared store. Only a spec that may select a 3.0.0 prerelease gets a project-local store.
#983pnpm 12 through nub pm shim. The pnpm and pnpx shims run pnpm 12. They failed with SyntaxError: Unexpected identifier 'is'.
#1021Mismatched nub pm shim projects. Registry and account commands such as npm login pass through to the system manager instead of being refused.
#1034nub pm use nub. The Turborepo heads-up says which versions recognize Nub.
#984Peer placements. A package with peer-dependent copies reuses its file index and verified store read. Remapping across eight peer contexts went from 1.647 ms to 1.411 ms in a stage benchmark on Linux x86-64.

Runtime

PRWhat changed
#1067YAML and TOML imports. Values keep NaN, -0, dates and own __proto__ keys. A TOML date is a Date, and a TOML integer past 2^53 is a BigInt. It threw on import.
#1013UV_THREADPOOL_SIZE. Assigning it in process.env before the first pool use sizes the pool, as under Node. Nub's preload had already built the pool.
#1015Temporal. The polyfilled global has the spec's property attributes. It was a frozen module namespace.
#1061require() of a replaced package. @js-temporal/polyfill, urlpattern-polyfill and abort-controller load the installed package. They failed with ENOENT on Node 22.15+.
#796Decorated files. 400 decorated ESM files on Node 26.11 start in 0.18s, down from 0.99s, on macOS arm64 with a warm transpile cache.
#556Startup. Augmented startup no longer loads internal/file on Node 20+.
#1041Entry tsconfig check. The check follows the entry's real path and skips a dependency's own config. A workspace bin run through its node_modules link failed before starting.
#899Unreadable tsconfig. The diagnostic names the OS error. It read the same as a missing file.
#724Varlock. Nub stands down when Varlock already resolved the schema or is the program being launched. varlock run --path ./config exited 1.
#998Terminal jobs. A child that stops on its first terminal read resumes after the foreground handoff. It stayed stopped.
#1075, #1086Test and watch modes. --test=<value>, --watch=<value>, --watch_path and a watch flag in NODE_OPTIONS stay out of the children they start, which could recurse without end. nodeOptions in nub.jsonc refuses a watch flag.

Node discovery and provisioning

PRWhat changed
#1033latest and other aliases. Aliases and partial pins select releases only. A .nvmrc of latest failed with a 404 once nodejs.org listed a 27.0.0 alpha.
#1049Version-manager shims. The version behind a mise, asdf, nodenv or Volta shim is read on every launch. A cached version passed Node 26 flags to Node 24, which exited 9.
#1044, #1063, #1073Launch loops. A PATH node that hands the launch back to Nub, or links back to it, is skipped. Any chain of Nub processes stops at 64 with ERR_NUB_LAUNCH_LOOP.
#1047Windows runtime cache. Validates the cache directory rather than every ancestor. A broad grant on the user's profile made each command print warnings and run without the runtime.

The full release notes list every change in this release.

Get started

View repo