v0.3.0
Undeclared (phantom) dependencies now resolve out of the box. nub's isolated install layout used to reject a package's undeclared require/import of a transitive it didn't list — the strict-but-surprising behavior behind a run of real-world breakages. nub now detects those cases and materializes just the needed closure on install, so the affected projects work without a config change.
Important
Phantom-eject is on for every install — no opt-in and no config change. Existing warm installs re-link once on the next install to converge to the new layout; there is no manual migration step.
Highlights
- Phantom dependencies just work — undeclared transitive
require/importresolves out of the box, no config change - Unified nubx — one runner for a file, a script, a local CLI, or a registry package, with a consent-gated fetch that never runs remote code silently
- Framework support — Vite, Parcel, Next.js, Nuxt 4, and more scaffold-and-run out of the box
- tsx and ts-node on Node 22 LTS — a script that shells out to
tsxno longer crashes on Node 22.15–24.11.0 - Node auto-provisioning —
nubruns on a machine with no Node installed - Cleaner install progress — a smooth spinner and the name of each package as it's fetched replace the bar that looked stuck during linking
- The lockfile is now nub.lock — migrates automatically, and
nub cinever rewrites a checked-in one
Phantom dependencies just work
- Before:
@inkjs/uiimportingreact, or@crawlee/basicrequiring@apify/datastructures, failed withERR_MODULE_NOT_FOUNDunder the isolated layout even though the package installed fine. - Now: a per-version scanner detects the undeclared use and materializes the ancestor closure that makes it reachable, matching what pnpm's private-hoist fallback does by default. (#302, #319, #321, #328; closes #280)
The detector replaces the previous curated static list — it self-maintains across package versions, and a detection improvement re-scans already-cached content instead of going stale. To bypass the virtual store entirely, install.materialization: disk materializes every dependency to disk.
Unified nubx
One command now runs whatever a name points to. A bare nubx <name> resolves through four tiers and runs the first match:
- File —
nubx build.tsruns the file, the same asnub build.ts. - Script —
nubx testruns thepackage.jsonscript. - Local bin —
nubx viteruns the CLI already innode_modules/.bin. - Registry —
nubx create-vite my-appfetches and runs a package that isn't installed.
Scripts beat bins and a file beats both — the same precedence as nub run. A local match never touches the network.
The registry tier is consent-gated, so nubx never runs remote code silently. On a full local miss in an interactive terminal, nubx prompts before the first fetch of a spec and remembers the answer, so a consented tool doesn't re-prompt; in CI or any non-interactive context it fails closed. -y/--yes is the explicit opt-in for CI and scripts.
nub dlx (short alias nub x) splits off as the explicit downloader — invoking it is the consent, so it fetches without a prompt and runs in CI. nub exec stays the local-bin-only runner. (#224, #275)
Framework support
Scaffold-and-run verified out of the box (install, dev, build, serve) for Vite, Astro, SvelteKit, SolidStart, Qwik, Next.js, React Router 7 / Remix, Angular, Nuxt 4, and Parcel.
- Vite dev/build serve correctly under the symlinked layout. (#318, closes #315)
- Parcel builds under the global virtual store out of the box —
@parcel/coreno longer splits across two store copies, so the worker farm starts cleanly. (#338; harness:tests/parcel-gvs/) - Expo / React-Native installs resolve Babel correctly: an auto-installed wildcard peer now binds the already-resolved major instead of the registry-highest, so react-native no longer gets pulled onto an incompatible Babel. (#327)
- Ambient
@types/*resolve under the global virtual store via a pnpm-parity hidden hoist tree. (#293, closes #286)
Runtime
- tsx and ts-node run under nub on Node 22.15–24.11.0 — a script that shells out to
tsx(for examplenub run dev) previously crashed on that Node range and now runs. (#340) - Import Text —
import contents from "./file.txt" with { type: "text" }on any extension. (#284) process.versions.nubreports the running nub version. (#253)- Electron no longer crashes on startup — nub stopped auto-injecting
--experimental-shadow-realm. (#254, closes #246) Workermirrors more ofnode:worker_threads, and an inboundmessageevent'stargetisself. (#256, #264)- A
NODE_ENVset inside a.envfile is ignored. (#267)
Node provisioning
- nub auto-provisions Node when a project has no pin and no
nodeon PATH, sonubworks on a machine with no Node installed. (#296, closes #294; #308 covers transientnubx/bin-exec, closes #303) nub node shim/nub node unshiminstall or remove a persistent globalnodeshim. (#297)
Lockfile
nub's own lockfile is now nub.lock (previously lock.yaml). An existing lock.yaml keeps working — nub reads it — and migrates the next time the lockfile actually changes: on a dependency add, remove, or update, nub writes nub.lock and removes the old lock.yaml in the same diff. A routine install with nothing to change never rewrites or renames the lockfile, and nub ci never touches a checked-in one. (#274)
Install progress
Install progress no longer looks stuck during linking. The old progress bar held its fill at the end of fetching for the whole linking phase, which could read as hung on a slow or network filesystem; the redesigned view drops the bar for a spinner and a fixed-width phase label. (#300, refs #288)
Three follow-ups refine it:
- The spinner now animates smoothly. (#337)
- The fetch phase drops its ETA, which printed misleading sub-second estimates on the tail of a few large native binaries. (#339)
- The progress line now names the package being fetched, updating as the install moves through them — e.g.
⠏ fetching 42/465 pkgs · react, thentypescript, thenwebpack. (#343)
Package manager
| Change | PR |
|---|---|
nub pm pin locks a project to an exact nub version |
#276 |
| Verify dependency freshness before run/exec/file/nubx | #278 |
nub ci produces a project-local, COPY-safe node_modules |
#261 |
prune --prod keeps hoisted production transitives |
#250 |
Read .npmrc / verifyDepsBeforeRun correctly under a pnpm-11 incumbent |
#301, #289 |
Virgin install writes a devEngines caret range and honors packageManager: nub@x.y.z |
#266, #265 |
Global settings file moved to nub.jsonc |
#291 |
Strip a UTF-8 BOM before parsing package.json |
#269 |
run / exec fixes
node_modules/.binis always on the script PATH, even when the directory doesn't exist at spawn. (#283, closes #281)- Files with a dot in the name load correctly. (#245, closes #240 — thanks to
@ctorres-lynxlabs) - Args after the entry point pass through verbatim on the
nodehijack path. (#248) - The cache dir falls back to
%LOCALAPPDATA%on Windows. (#268)
What's Changed
- Modify extension check in resolve.rs by @ctorres-lynxlabs in #245
- test(pnpm-conformance): green the nightly on known divergences, red only on new regressions by @colinhacks in #247
- fix(node-hijack): pass through args after the entry point verbatim by @colinhacks in #248
- test(pnpm-conformance): stop the seam-swap fork bomb on local runs by @colinhacks in #249
- fix(pm): keep hoisted prod transitives on prune --prod by @colinhacks in #250
- test(pnpm-conformance): rebaseline allowlist at pnpm 11.3.0 by @colinhacks in #251
- feat(runtime): publish process.versions.nub self-identification marker by @colinhacks in #253
- fix(node): stop auto-injecting --experimental-shadow-realm (Electron crash #246) by @colinhacks in #254
- feat(pm): stamp packageManager: nub@ on a virgin install by @colinhacks in #255
- cli: suggest
nub execfor an unknown bareword binary by @colinhacks in #259 - feat(worker): additive node-mirroring on the global Worker polyfill by @colinhacks in #256
- pm(ci): make
nub ciproduce a project-local, COPY-safe node_modules by @colinhacks in #261 - fix(exec): set npm_config_user_agent on the bin-launch path by @colinhacks in #260
- pm: rename nub's own lockfile to package.lock by @colinhacks in #257
- fix(worker): inbound message event.target is self, not null by @colinhacks in #264
- feat(pm): virgin install writes a devEngines caret range, not an exact packageManager pin by @colinhacks in #266
- fix(env): ignore NODE_ENV set in a .env file by @colinhacks in #267
- fix(node): fall back to %LOCALAPPDATA% for the cache dir on Windows by @colinhacks in #268
- fix(pm): strip UTF-8 BOM before parsing package.json by @colinhacks in #269
- feat(nubx): unified runner with a consent-gated registry tier by @colinhacks in #224
- fix(spawn): inject preload --require via NODE_OPTIONS only on the file-run path by @colinhacks in #271
- revert: restore lock.yaml lockfile name (undo #257) by @colinhacks in #273
- feat(nubx): interactive dlx consent prompt + exec.implicit-dlx kill-switch by @colinhacks in #275
- feat(pm): honor packageManager: nub@x.y.z by provision + delegate (self-shim) by @colinhacks in #265
- feat(pm): add
nub pm pinto lock a project to an exact nub version by @colinhacks in #276 - fix(pm): default trustPolicyIgnoreAfter to a 14-day window under nub by @colinhacks in #277
- feat(run): verify dependency freshness before run/exec/file/nubx (#252) by @colinhacks in #278
- fix(pm): auto-persist minimumReleaseAgeExclude on loose-mode fallback (#262) by @colinhacks in #279
- feat(runtime): Import Text —
import x from "./f" with { type: "text" }on any extension by @colinhacks in #284 - fix(run): prepend node_modules/.bin to script PATH unconditionally by @colinhacks in #283
- config: move nub's global settings file from nub.toml to nub.jsonc by @colinhacks in #291
- fix(pm): make hoist default GVS-aware — hidden hoist tree wherever GVS does not engage by @colinhacks in #293
- test(ci): fix standing red — import-text 18.19/Windows gates + aube-bats devEngines skips by @colinhacks in #295
- feat(node): auto-provision Node when there is no pin and no node on PATH by @colinhacks in #296
- feat(node):
nub node shim/unshim— persistent global node shim by @colinhacks in #297 - fix(pm): clear rejection for
nub pm updateon a bun-pinned project by @colinhacks in #298 - cli: rename --shell-emulator to --posix-shell by @colinhacks in #299
- pm: read project/user .npmrc through pnpm 11's key allowlist under a pnpm-11 incumbent by @colinhacks in #301
- feat(install): bun/uv-style linking progress — spinner + live file count by @colinhacks in #300
- feat(nub-phantom): phantom-dependency detector + top-N ecosystem scanner by @colinhacks in #302
- feat(pm): per-package force-materialization for subpath-adapter phantoms under GVS by @colinhacks in #304
- feat(pm): force-materialize next-themes/@react-pdf so ambient @types resolve under GVS by @colinhacks in #305
- cli: remove --posix-shell run flag by @colinhacks in #306
- pm: drop lib0 from the force-materialize seed (proven false-inclusion) by @colinhacks in #307
- fix(dlx): resolve nub-provisioned node for transient bin-exec on a node-less machine by @colinhacks in #308
- pm: drop 4 storybook-family entries from the force-materialize seed by @colinhacks in #309
- pm: force-materialize @prisma/client to stop shared-store codegen corruption by @colinhacks in #310
- pm: add react-native to the default GVS-off list by @colinhacks in #311
- fix(linker): keep force-materialized package's store copy so store-resident dependents don't orphan by @colinhacks in #312
- feat(run): warn on undeclared (phantom) dependencies before a run by @colinhacks in #314
- perf(linker): fire whole-dir clonefile for scoped packages on macOS by @colinhacks in #316
- Vite symlink-GVS serving compat (#315) by @colinhacks in #318
- feat(pm): selective-subtree ancestor-closure force-materialization (behind NUB_DYNAMIC_PHANTOM_EJECT) by @colinhacks in #319
- feat(phantom): wire dynamic per-version scanner into #319's force-materialize closure by @colinhacks in #320
- feat(phantom): default-on dynamic phantom eject; purge static materialize/GVS lists by @colinhacks in #321
- Phantom detector: remove dead extraction ladder, dedup eval tool, close two precision gaps by @colinhacks in #322
- test(framework-matrix): fix driver.sh dev-server leak; add fresh-build acceptance runner by @colinhacks in #323
- docs(install): drop Nuxt from the whole-install project-local-store list by @colinhacks in #325
- refactor(pm): rename forceMaterializePackages to diskMaterializePackages by @colinhacks in #324
- refactor(install): make illegal GVS/hoist layout states unrepresentable by @colinhacks in #326
- fix(phantom): key sidecars by scanner version so a detection improvement re-scans cached content by @colinhacks in #328
- fix(install): stop force-materializing vite ≥ 8.1 and its closure by @colinhacks in #329
- fix(install): restrict phantom-eject safe-skip reachability to depth-1 deps by @colinhacks in #330
- fix(resolver): auto-installed peers bind a resolved major, not the registry highest by @colinhacks in #327
- fix(ci-watch): never hang on a nameless/stuck-pending ghost check by @colinhacks in #331
- fix(merge-cascade): never hang on a #327 ghost check; gate on the required set by @colinhacks in #332
- fix(phantom): scan extensionless main/exports entries instead of dropping them by @colinhacks in #333
- fix(pm): read verifyDepsBeforeRun from pnpm-workspace.yaml under pnpm 11 by @colinhacks in #289
- tests(bench): fix install harness for current CLI + npm 11 by @colinhacks in #334
- Make phantom-eject unconditional for users; keep an internal test seam by @colinhacks in #335
- pm: rename nub's own lockfile to nub.lock by @colinhacks in #274
- fix(progress): animate the install spinner fast + smooth by @colinhacks in #337
- site: switch install benchmark to a TanStack Start fixture by @colinhacks in #336
- fix(gvs): host-filter the prewarm graph so the shared store doesn't over-split @parcel/core by @colinhacks in #338
- fix(install): drop the fetch-phase ETA from the nub TTY progress line by @colinhacks in #339
- fix(runtime): async-tier registration when composing with a foreign loader on Node 22.15–24.11.0 by @colinhacks in #340
- feat(install): churn the currently-fetching package name in the TTY progress line by @colinhacks in #343
New Contributors
- @ctorres-lynxlabs made their first contribution in #245
Full Changelog: v0.2.10...v0.3.0