github nubjs/nub v0.7.0

3 hours ago

Nub 0.7.0 adds nub.jsonc, a typed configuration file for the runtime, installs, and temporary package runs; hands environment loading to Varlock when a project has an @env-spec schema; and fixes a hoisted-linker defect that deleted build output.

Important

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. Both gates are new in this release. 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, including under the default minimumReleaseAgeStrict=true. 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 will block 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 one explicitly with install.linker in nub.jsonc or node-linker in .npmrc. Yarn's nodeLinker is unaffected.
  • 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.

Project configuration

A project can now carry a nub.jsonc at its root — one typed file for the runtime, installs, and dlx, in JSON with comments and trailing commas. Nub finds the nearest one by walking up from the working directory.

{
  "$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"
  }
}

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.

Read and write any field with nub config, which validates against the same rules that read the file and rewrites it in place. Comments, blank lines, and key order survive an edit. Keys that are not fields of this file keep their existing meaning and still read and write .npmrc.

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

The full field list is in the config reference. A JSON Schema ships at https://nubjs.com/schema/latest.json for editor completion. (#587)

Schema-driven environments

An @env-spec schema — conventionally .env.schema — describes an environment as types, validation, which variables are secret, and where their values come from. 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 values, or redact anything. It runs Varlock in front of Node, so type generation, providers, validation, and secret redaction happen on Varlock's own terms. Explicit envFile in nub.jsonc or --env-file still wins, and --node turns the whole path off with the rest of Nub's augmentation.

The hand-over covers a file run, nub run, nub watch, nubx, and lifecycle scripts. 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. 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. When Varlock is not installed, Nub keeps loading .env* and warns once; when it is declared in package.json but not installed, the run stops rather than starting with no values at all. (#659)

Package manager

Fix PR
Under 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, such as a downloaded binary or a compiled addon, and the delta filter then skipped rebuilding it. A package directory is now reused when the previous link ran to completion and its contents are unchanged. Only projects that opted into the hoisted layout were affected. Build output already lost this way is not restored by upgrading — reinstall to rebuild it. #616
Auditing 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. The same root cause made dedupe --check report a phantom removal on every run against an unchanged lockfile, and left outdated and approve-builds unable to see aliased dependencies. #609
Binaries in node_modules/.bin were symlinked by absolute path, so a moved or copied node_modules arrived with dangling links, and one project's path could be written into the shared store and repoint another project's binary. #596
Under 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. #597
A 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. Every materialize is now staged and renamed into place. #598
On 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. A workspace could wedge permanently where a single-package project recovered. #595, #613, #618
On macOS, native binaries materialized out of the store kept com.apple.quarantine, so Gatekeeper refused to load them. Quarantine is now cleared on materialize, on side-effects-cache restores and misses, on Nub's own bundled addon, and on self-upgrade. #601, #608
An optionalDependencies entry whose version matched nothing failed the entire install; npm and pnpm skip it. #604
A registry serving an unexpected shape for a cosmetic packument field — dist.unpackedSize, modified, peerDependenciesMeta.optional, hasInstallScript — 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. #646, #648
A project declaring Nub as its package manager could not install when a second lockfile was present, and the error's suggested remedy was already applied. #647
A bundled metadata entry carried validators for a document it only partially contained, so a revalidation restored the truncated copy as authoritative and a version the registry serves failed to resolve. #612
Approved builds bootstrapped node-gyp before every build, so an unreachable registry aborted the install even with a warm store and nothing invoking node-gyp. It is now bootstrapped on demand. #666
Inside 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. #640
Nub wrote engine-branded directory and marker names into caches and project trees. These are renamed, which is what re-bootstraps two caches once on upgrade. #600

The engine sync brings five smaller changes to package-manager behavior (#621):

  • Updating to the latest version now writes back to catalog: entries, which previously could not be bumped.
  • A non-deprecated version outranks a deprecated one within the same range, in every direction resolution scans.
  • Auditing falls back to an audit.level setting when --audit-level is absent, and gains audit.ignore for advisory IDs.
  • The update.ignoreDeps field replaces updateConfig.ignoreDependencies, which is now deprecated.
  • Setting cacheDir relocates the global virtual store as well as cached metadata, and warns when the two land on different filesystems, which would otherwise degrade every install to a per-file copy.

Supply-chain gates

The cooling window now decides correctly when a registry serves no per-version publish times, and the undeterminable case gets its own error — ERR_NUB_RELEASE_AGE_MISSING_TIME, exit 28 — instead of a message listing years-old versions and suggesting a setting that could not have helped. The verbs that resolve without the full metadata cache — add, update, dedupe, audit — now fetch publish times too, which is what keeps them working under the stricter gate. (#602, #607, #622, closes #581)

Note

Setting minimumReleaseAgeStrict=false does not give you a smaller window. It makes a version whose age cannot be established count as clearing the gate, so the newest version matching your range installs with no age checked at all.

Two per-invocation flags set the window for one command, on install, ci, the engine verbs, and nubx (#645). A bare number is minutes, matching pnpm; 0 disables the gate for that run.

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

Two gates now run when adding 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, both exempt anything already in the lockfile or listed in allowedUnpopularPackages, and --allow-low-downloads clears them for a single invocation. Names the popularity corpus itself lists are never treated as typosquats. Neither gate applies to nub install, nub ci, or nubx. (#621, #658)

Runtime

Every Stage 3+ library surface that no supported Node ships is now available under Nub: Promise.allKeyed and Promise.allSettledKeyed, Iterator.zip and Iterator.zipKeyed, Iterator.prototype.chunks/windows/includes/join, Math.sumPrecise, Symbol.metadata, and Atomics.pause. Surfaces Node ships only in newer majors are filled in below their native version — Promise.withResolvers, the seven Set methods, Array.fromAsync and the iterator helpers below 22; Object.groupBy and ArrayBuffer.prototype.transfer below 21; Map.getOrInsert and Iterator.concat below 26. Nothing native is ever replaced. @nubjs/types carries declarations for the surfaces no @types/node or TypeScript lib describes. (#584)

Fix PR
An extensionless bare package subpath — import "pkg/sub" — raised ERR_MODULE_NOT_FOUND where TypeScript and require() both resolve it. Subpaths are now probed for .js, .json, .ts, and .tsx; a dependency that declares exports is never probed. #599, closes #562
On 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. #599
A 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 are now loaded through one generated module, which also fixes a .cjs entry running before Nub's hooks and running twice. A bare specifier resolves from the working directory, so a workspace member's copy wins where it should. #673, #651
On 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. Below 20.6 the values are injected directly instead, which does not live-reload on edit. A NODE_ENV set in .env no longer leaks into watched processes. #573
On macOS and Linux, nub watch left its node --watch supervisor running after the parent died, so closed terminals and killed sessions accumulated processes holding a file watch indefinitely. Existing strays must still be killed by hand. #620
Running 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. #555
Importing .yaml, .toml, .json5, .jsonc, or .txt through require() compiled the document as JavaScript and returned an empty object instead of parsing it. YAML alias expansion is now bounded. #587

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. (#665)

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. (#671)

Note

Because nub.exe 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. An upgrade run with --ignore-scripts also leaves the previous version's copy in place.

The 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. (#649)

Documentation

Deployment guides for Vercel, Cloudflare, Railway, Render, AWS Lambda, and Google Cloud Run. Cloudflare added Nub to the Workers Builds image on 2026-07-30, so build commands there call nub with no setup step. (#639, #641)

A cooling-window reference was added to the install page, and the trust-policy exclusion example was corrected — the bracketed form it previously showed was silently ignored. (#607)

Internal

  • The vendored package-manager engine syncs to upstream 1.35.0 (#621).
  • Analysis gates run under the fast profile, matching the development loop, and CI gates can run on ephemeral cloud builders (#589, #592, #594).
  • Build scripts resolve their manifest directory at run time, so a shared target directory cannot serve one worktree's files to another (#586, #614).
  • A configuration cache entry read within the file's own modification-time tick is no longer trusted (#663).
  • Test fixes for a watcher-registration race and a mock registry connection (#617, #619).

Thanks

@afonsojramos reported the release-age gate failing open (#581) and wrote the metadata fetch that fixes it, carried into #607. Bugs in this release were also reported by @cusxio, @dvictorjhg, @iamnafets, @MrLoh, @crypto-scythe, @PuerNya, @shekharsikku, and @NarHakobyan.


What's Changed

  • fix(watch): gate auto-discovered --env-file on Node 20.6, and stop a .env NODE_ENV leaking by @colinhacks in #573
  • fix(run): repair a nub-installed tree built for a different Node major by @colinhacks in #555
  • build: bring the prebuilt addon into new worktrees by @colinhacks in #586
  • feat(runtime): polyfill every Stage 3+ library surface missing under nub by @colinhacks in #584
  • fix(install): link bins of hoisted transitive deps by @colinhacks in #597
  • fix(linker): drop com.apple.quarantine from materialized native binaries by @colinhacks in #601
  • fix(resolver): skip optional deps whose version pick finds no match by @colinhacks in #604
  • fix(linker): reclaim a per-project virtual store when relinking on Windows by @colinhacks in #595
  • fix(resolver): fail closed when publish time can't prove a version's age by @colinhacks in #602
  • fix(linker): write relative .bin symlinks so node_modules relocates by @colinhacks in #596
  • fix(pm): stop nub writing aube-branded filesystem paths by @colinhacks in #600
  • fix(linker): stage every materialize so a failed link cannot poison the store by @colinhacks in #598
  • fix(resolver): fetch publish times on the paths that resolve without the full-packument cache by @colinhacks in #607
  • fix(install): clear quarantine on side-effects-cache restores and self-upgrades by @colinhacks in #608
  • fix(lockfile): drop the redundant npm-alias target left by the pnpm reader by @colinhacks in #609
  • fix(resolver): drop primer validators on the abbreviated packument cache by @colinhacks in #612
  • build: run the analysis gates under --profile fast, matching the dev loop by @colinhacks in #592
  • fix(linker): reclaim an incumbent node_modules tree on Windows by @colinhacks in #613
  • scripts: run the CI gates on ephemeral GCE spot VMs by @colinhacks in #594
  • fix(watch): stop nub watch leaking an immortal node --watch supervisor by @colinhacks in #620
  • build: seed isolated target dirs from the shared cache; content-key the buckets by @colinhacks in #589
  • fix(linker): clear a stale workspace store entry recursively by @colinhacks in #618
  • test(watch): close the watcher-registration race in the env reload waits by @colinhacks in #619
  • fix(linker): stop wiping unchanged packages on every hoisted relink by @colinhacks in #616
  • aube: sync upstream v1.35.0 by @colinhacks in #621
  • fix(resolve): probe extensions for bare package subpaths by @colinhacks in #599
  • test(init): close the mock registry connection gracefully by @colinhacks in #617
  • fix(resolver): give an undeterminable publish age its own error by @colinhacks in #622
  • fix(build): resolve CARGO_MANIFEST_DIR at run time, not compile time by @colinhacks in #614
  • site: add Cloudflare and Vercel deployment pages by @colinhacks in #639
  • site: add deployment provider guides by @colinhacks in #641
  • feat: project nub.jsonc — typed runtime, install, and dlx configuration by @colinhacks in #587
  • fix(resolver): tolerate a non-integer dist.unpackedSize by @colinhacks in #646
  • fix(registry): attribute packument decode failures; three more cosmetic fields stop aborting installs by @colinhacks in #648
  • fix(lockfile): a declaration resolves ambiguity, whatever it names by @colinhacks in #647
  • fix(runtime): a .cjs preload must not outrun nub's hooks or run twice by @colinhacks in #651
  • pm: add per-invocation minimumReleaseAge CLI flags by @colinhacks in #645
  • fix(add): stop the similar-name gate refusing popular packages by @colinhacks in #658
  • fix(core): never trust a config-cache entry read inside the file's own mtime tick by @colinhacks in #663
  • fix(launcher): self-heal never fired under pnpm 11 by @colinhacks in #649
  • feat(dist): ship one binary per platform package, carry the verb in __NUB_ARGV0 by @colinhacks in #665
  • fix(registry): write the packument cache with serde_json, not sonic-rs by @colinhacks in #668
  • fix(install): bootstrap node-gyp lazily, not before every build fan-out by @colinhacks in #666
  • env: stand down from .env loading when a .env.schema owns the environment by @colinhacks in #659
  • fix(install): detect cgroup-v1 PID limits inside containers; repair remote-build by @colinhacks in #640
  • feat(launcher): drop a real nub.exe beside npm's shims on Windows (add-only) by @colinhacks in #671
  • fix(preload): one NODE_OPTIONS token per flag name, and resolve bare preload entries from the CWD by @colinhacks in #673

Full Changelog: v0.6.0...v0.7.0

Don't miss a new nub release

NewReleases is sending notifications on new releases.