Skip to content

v0.3.0

Choose a tag to compare

@github-actions github-actions released this 07 Jul 15:33

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

  • Before: @inkjs/ui importing react, or @crawlee/basic requiring @apify/datastructures, failed with ERR_MODULE_NOT_FOUND under 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:

  • Filenubx build.ts runs the file, the same as nub build.ts.
  • Scriptnubx test runs the package.json script.
  • Local binnubx vite runs the CLI already in node_modules/.bin.
  • Registrynubx create-vite my-app fetches 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/core no 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 example nub run dev) previously crashed on that Node range and now runs. (#340)
  • Import Textimport contents from "./file.txt" with { type: "text" } on any extension. (#284)
  • process.versions.nub reports the running nub version. (#253)
  • Electron no longer crashes on startup — nub stopped auto-injecting --experimental-shadow-realm. (#254, closes #246)
  • Worker mirrors more of node:worker_threads, and an inbound message event's target is self. (#256, #264)
  • A NODE_ENV set inside a .env file is ignored. (#267)

Node provisioning

  • nub auto-provisions Node when a project has no pin and no node on PATH, so nub works on a machine with no Node installed. (#296, closes #294; #308 covers transient nubx/bin-exec, closes #303)
  • nub node shim / nub node unshim install or remove a persistent global node shim. (#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, then typescript, then webpack. (#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/.bin is 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 node hijack 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 exec for 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 ci produce 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 pin to 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 update on 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

Full Changelog: v0.2.10...v0.3.0