Nub now sizes libuv's threadpool to the cores of the machine on every augmented run, takes a prefix command in nub.jsonc, runs npm ci and npm install on its own engine behind an opt-in shim, and accepts more of the npm lockfiles and projects that npm ci accepts.
Important
Augmented runs on a machine with more than four cores now get a larger libuv threadpool. Nub sets UV_THREADPOOL_SIZE to the larger of four and the cores available to the process (a container gets its cgroup quota; Windows stops at eight). On Linux the threads beyond Node's four run at nice 10, so they yield under contention. Child processes start with Node's default of four. A UV_THREADPOOL_SIZE you set yourself is used as is, and --node / NODE_COMPAT keep Node's default. See Threadpool.
Runtime
Threadpool sized to the machine
Node hands async fs, dns.lookup, async zlib and crypto calls, and native addons such as bcrypt and sharp, to a pool of four threads whatever the core count. Nub now sets the size when it starts Node, on every launcher: the file run, nub run, nubx, nub watch and lifecycle scripts.
nub server.ts # pool sized to the cores, 16 on a 16-vCPU box
node server.ts # pool of 4
UV_THREADPOOL_SIZE=6 nub server.ts # your value, used as isThe preload removes Nub's own value from process.env once the pool exists, so a cluster worker or child_process spawn gets Node's default; a child that runs through nub is sized again. On Linux the threads beyond four are reniced to 10 after the pool is built, and on the compat tier (Node 18.19–22.14) a --require sidecar builds the pool so the demoted set is exact. A bcrypt route on a 16-vCPU box went from 18 to 66 requests per second under autocannon at 64 connections, mean of three rounds (benchmark); routes that use the pool lightly are within noise of plain Node. The Threadpool page documents the behavior. (#919)
A prefix command in nub.jsonc
A project can put a command in front of everything Nub runs for it: a file run, a package.json script, and nub watch.
A script is wrapped as a whole, so a script that starts no Node process still runs behind the prefix. The program resolves from the project's node_modules/.bin chain, then PATH; a path form anchors to the file that set it. A nested nub run, or a wrapper written in Node, does not wrap the same project again. The nubx, nub dlx and node shim entrypoints do not take it, and compat mode runs without it. The field is in the config reference and the JSON schema. (#915)
AsyncContextFrame on Node 22.9–23.x
Nub passes --experimental-async-context-frame on Node 22.9 through 23.x, the band where the flag exists and does not throw. Node 24 made that AsyncLocalStorage implementation the default; on the 22 line it roughly halves the cost of carrying a store across await, which tracing SDKs and request-context plugins pay on every request. (#912)
Memory-constrained launches
On Linux x64, a direct Node launch inside a cgroup memory budget starts the main isolate with a 16 MiB V8 semi-space on two measured releases: Node 22.23.2 in a 512 MiB–1 GiB budget and Node 24.20.0 at 512 MiB. The flag is hidden from process.execArgv, Workers keep their own resourceLimits, and the tuning stands down when Node options, preloads, NODE_OPTIONS, PnP, a prefix, or an environment loader own startup. Other releases, larger budgets, budgets below 512 MiB, watch mode, and compiled executables keep Node's defaults. (#917)
User loader hooks layered above Nub's
Running tsx script.ts under nub run, or a prepare script that does, failed with require is not defined in ES module scope. Nub's load results now carry responseURL, both tiers step aside when an outer resolve hook labels a .ts file with a bare commonjs/module format, and a require.extensions handler that is already registered stays in charge of its extension. (#920)
Package manager
npm installs on Nub's engine
One opt-in routes the two npm verbs an install pipeline runs through Nub's engine:
nub pm shim --route-installsWith the marker set, a bare npm ci in a project with a package-lock.json runs nub ci, and a bare npm install runs nub install, both from the same lockfile. Everything else reaches the real npm as typed, including an install that names a package or carries a flag Nub does not translate. Lifecycle scripts run as npm runs them, with NODE_ENV=production when dev dependencies are omitted and ignore-scripts honored from .npmrc and the environment. The engine also runs a file: directory dependency's lifecycle scripts in the link target on an npm-lockfile install, as npm does. The --no-route-installs flag turns the routing off. (#925, docs)
npm lockfile compatibility
Frozen-installing popular npm projects under Nub found refusals of lockfiles npm itself wrote.
| Area | What changed | PR |
|---|---|---|
| Workspace peers | A member's required peer is recorded on its importer, so the freshness check no longer reads every member with a peer as manifest adds <peer>
| #918 |
| Nested members | A member nested inside another member resolves through the parent member's own node_modules before the root
| #918 |
| Links | A link target with no version, as npm writes for a name-only local package, parses
| #918 |
| Integrity | A space-separated multi-digest SRI value verifies when any digest of the strongest algorithm matches, as ssri does | #918 |
| Peer placement | The peer each npm placement resolves to is recorded as a graph edge. A peer-only package reached solely through a transitive's peer edge was pruned and died at runtime with Cannot find package; a link inside node_modules pointing back at a registry package above it was taken for a local package and never fetched. A lockfile npm wrote for a project with an auto-installed peer now round-trips byte-identical.
| #923 |
| Manifest sections | A dependency declared in more than one section, which npm, pnpm and bun all accept, no longer fails a frozen install with ERR_NUB_OUTDATED_LOCKFILE; each declaration is compared against its own row
| #928 |
| Patches | A bun-style patchedDependencies key such as name@github:owner/repo#sha matches by resolved identity instead of failing ERR_NUB_PATCH_NON_SEMVER_RANGE; pnpm-origin declarations keep pnpm's semver-only grammar
| #929 |
Installs and lifecycle scripts
| Area | What changed | PR |
|---|---|---|
| tsconfig | The PM verbs install past a tsconfig whose extends target is a devDependency the install is about to provide; only a run that executes the program keeps the fatal gate
| #918 |
| Member scripts | A workspace member's lifecycle script gets every ancestor node_modules/.bin on PATH, out to the filesystem root, as npm's run-script does
| #918 |
| node-gyp | Every importer lifecycle script, root and member, gets the lazy node-gyp shim dependency builds already had, so a root preinstall that runs node-gyp install no longer exits 127
| #918 |
| Native addons | When the project's Node came from Nub's own store, Nub writes the header cache entry node-gyp would have downloaded from nodejs.org, so a from-source build works offline. An entry that already exists is never touched; Windows keeps the download because the official zip ships no headers. | #935 |
| Phantom detection | electron and vscode are injected by their host process and no install can supply them, so a reference to one is host-provided rather than a phantom. A package such as electron-log stays in the shared store.
| #939 |
Note
The phantom scanner version moved with #939, so the first install after upgrading re-scans cached verdicts and re-links a warm tree once.
CLI and documentation
- The
nub agent docscommand prints usage and the table of contents instead of the overview page;--listprints the contents alone and--pageselects a page (#930). - A docs page for the
@nubjs/extensionsdatabase, with ready-to-use configuration for pnpm and Yarn (5297d2f). - The blog post De-phantoming the npm ecosystem, a daily scan of 10,000 high-download packages published as that database (#926,
7319aa6,d077198,8375a03,11a435e,f231db0,b0a711b,afa009f). - A Threadpool runtime page, an
AsyncLocalStorageentry on the modern APIs page, and a memory-constrained launches section on the runtime overview (#919, #912, #917). - The config reference shows an absolute
nodeExecutablepath, with the Windows spelling (5da9a60). - The GitHub Action page shows
setup-nubbeside a keptactions/setup-nodestep (12c2854). - The Nub 0.9 release post (
56175e8), and a blog post's right-gutter table of contents now shows from thelgbreakpoint (0ede70a).
Testing & internals
- An npm-incumbent corpus: 28 popular npm projects frozen-installed under Nub in CI and checked against their own lockfile, which is where the #918 and #923 findings came from (#922,
ac4fa4f). - The cross-runtime scoring leaves a test the reference Node itself skips out of the node-relative denominator, for every runtime; the official build skips all 245
quic/tests. The homepage and README figures are regenerated: Nub passes 98.4% of the 4,690 tests Node 26.7 runs and passes (#931). - A Windows dispatch benchmark times a
nubcall fromcmd.exethrough npm'snub.cmdshim against the hardlinkednub.exe, with its first saved run (e5fa958,1b8e7ef). - A runtime benchmark family under
tests/bench/runtime/: threadpool, AsyncContextFrame, pool-bound routes (bcrypt, scrypt, pbkdf2, sharp, gzip, brotli, RSA keygen) and pool-priority beside busy co-tenants, with saved runs on 8, 16 and 22 vCPU (9134538,96f7a14,a33bc4c,b058f75,746c47c,e88ce4f,ab890a2,d4d9e8f,92c348e,250372c,d3d8a38,78a0225). - A chart generator for the benchmark figures, with its margin and legend rules (
c05a354,056fa95,a71d9e3,2e18646,0e10604,5a8a8b5,9654c1b). - The release workflow holds
publish-npmopen until every one of the ten packages answers a registry read, so the Docker and install jobs no longer race a package the registry has not served yet (3f87d09). - Pull-request CI is opt-in behind the
cilabel, andci-watchrequires theCI gateaggregate before a green verdict (5c0b233,291af85). - The docs bake key is renamed
__NUB_DOCS_KEYso it counts as internal plumbing under the brand-boundary test (#921). - The governed
rustcwrapper keeps stdin, so a build-script feature probe fails on stable as it should (fa34f36). - The download-stats snapshot folds paginated release pages before parsing (
c25bc82,2b9a448). - Local Lat search is wired into the agent workflows (
fd626cc). - Cold-start research notes: the entropy change landed in Node as nodejs/node#65796, and
--use-system-cais not free on a populated macOS keychain (88a0167,d3bf766).
What's Changed
- runtime: enable AsyncContextFrame for AsyncLocalStorage on Node 22.9–23.x by @colinhacks in #912
- config: a prefix field that wraps file runs, scripts, and watch by @colinhacks in #915
- build: rename the docs bake key to __NUB_DOCS_KEY by @colinhacks in #921
- pm: accept the npm lockfiles and projects that npm ci accepts by @colinhacks in #918
- runtime: let a user hook layered above nub's keep ownership of its files by @colinhacks in #920
- tests: an npm-incumbent corpus, frozen-installed under nub and checked against its lockfile by @colinhacks in #922
- pm: record the peer each npm lockfile placement resolves to as a graph edge by @colinhacks in #923
- pm: accept a dependency declared in more than one manifest section by @colinhacks in #928
- pm: run npm ci and npm install on nub's engine behind
nub pm shim --route-installsby @colinhacks in #925 - pm: accept a bun-style source identity as a patchedDependencies key by @colinhacks in #929
- agent: show concise docs help and table of contents by @colinhacks in #930
- tests: leave the tests the reference node skips out of the node-relative denominator by @colinhacks in #931
- De-phantoming the npm ecosystem by @colinhacks in #926
- runtime: size libuv's threadpool to the cores on augmented runs by @colinhacks in #919
- compile: precompile ESM and reuse published extractions by @colinhacks in #932
- pm: fill node-gyp's header cache from the provisioned Node by @colinhacks in #935
- Tune the main nursery for memory-constrained Node launches by @colinhacks in #917
- phantom: treat host-provided modules as declared by @colinhacks in #939
Full Changelog: v0.9.0...v0.9.1
{ "prefix": "dotenvx run --" // split like a shell; the array form takes exact arguments }