Nub mirrors pnpm most closely: the CLI surface, the isolated node_modules layout, and the lockfile. When pnpm is the incumbent — a packageManager: "pnpm@…" field or an existing pnpm-lock.yaml — Nub reads and writes pnpm's own pnpm-lock.yaml in its native format, and honors pnpm's config. Everything below is gated on that.

Commands

FeaturepnpmnubNotes
install / i / ciSupportedSupported
add / remove / updateSupportedSupported
importSupportedSupported
dedupe / pruneSupportedSupported
rebuild / fetchSupportedSupported
link / unlinkSupportedSupported
patch / patch-commit / patch-removeSupportedSupported
approve-builds / ignored-buildsSupportedSupported
dlx / createSupportedSupported
list / why / outdatedSupportedSupported
audit / licenses / peersSupportedSupported
publish / pack / versionSupportedSupported
store / configSupportedSupported
recursive meta-verbSupportedSupported
deploySupportedNot supported. Not wired; pnpm deploy stays the path for now.Not wired; pnpm deploy stays the path for now.

Configuration

FeaturepnpmnubNotes
dependencies / devDependenciesSupportedSupported
optionalDependenciesSupportedSupported
peerDependencies / peerDependenciesMetaSupportedSupported
dependenciesMeta.injectedSupportedSupported
pnpm.overridesSupportedSupported
pnpm.packageExtensionsSupportedSupported
pnpm.patchedDependenciesSupportedSupported
resolutionsSupportedSupported
onlyBuiltDependencies / neverBuiltDependenciesSupportedSupported
packageManager / enginesSupportedSupported
workspaces / pnpm-workspace.yamlSupportedSupported. Pnpm 11+ layout and resolution settings.Pnpm 11+ layout and resolution settings.
catalog: / catalogs:SupportedSupported
workspace: protocolSupportedSupported
namedRegistriesSupportedSupported. Alias-to-registry map; built-in gh: alias for GitHub Packages.Alias-to-registry map; built-in gh: alias for GitHub Packages.
workspace selectorsSupportedPartially supported. No git-since ([ref]) selector.No git-since ([ref]) selector.
.npmrcSupportedSupported
.pnpmfile.cjs / .pnpmfile.mjsSupportedSupported
npm_config_*SupportedSupported
pnpm_config_*SupportedSupported
pnpm-lock.yaml v9SupportedSupported. v6/v5.4 declined — re-lock under pnpm 9+.v6/v5.4 declined — re-lock under pnpm 9+.
nodeLinkerSupportedPartially supported. From pnpm-workspace.yaml on pnpm 11+, or .npmrc on earlier majors. No PnP.From pnpm-workspace.yaml on pnpm 11+, or .npmrc on earlier majors. No PnP.

Lockfile

Nub reads and writes pnpm lockfile v9 (lockfileVersion: '9.0') — what pnpm 9+ emits. A lockfile Nub writes installs cleanly under pnpm:

$ nub install
dependencies:
+ is-odd@3.0.1

$ grep lockfileVersion pnpm-lock.yaml
lockfileVersion: '9.0'

$ pnpm install --frozen-lockfile
Lockfile is up to date, resolution step is skipped

The round-trip holds both directions, for single packages and for workspaces with catalog: and workspace: entries (verified against pnpm 10.15.1).

Older formats — v6 (pnpm 8) and v5.4 (pnpm 7) — keep root dependencies under a top-level dependencies: map instead of v9's importers:. Nub declines them up front and leaves your node_modules and lockfile untouched:

# captured: nub 0.0.44 on a lockfileVersion: '6.0' lockfile
$ nub install
Error: ERR_NUB_LOCKFILE_UNSUPPORTED_FORMAT

  × pnpm-lock.yaml is lockfileVersion 6.0 (pnpm 8); nub reads v9 (pnpm 9+).
  help: Re-lock under pnpm 9+ (`pnpm install`), then `nub install`.

The refusal names the detected version (v5.4 reads (pnpm 7)). Migrate by running a one-time pnpm install under pnpm 9+ to upgrade the lockfile to v9, then nub install.

Config

When pnpm is the incumbent, Nub honors pnpm's configuration surface:

  • pnpm-workspace.yamlpackages: globs and the catalog: / catalogs: maps.
  • namedRegistries — an alias-to-registry-URL map from pnpm-workspace.yaml or the global pnpm config.yaml; a dependency spec prefixed <alias>: (e.g. "@work/constants": "work:1.x.x") resolves from that registry, the built-in gh: alias points at GitHub Packages, and auth rides existing //host/:_authToken= entries in .npmrc.
  • pnpm.overrides — version pins applied during resolution, written into the lockfile's overrides: block.
  • pnpm.packageExtensions — extra dependency/peer metadata merged onto resolved packages, hashed into the lockfile's packageExtensionsChecksum.
  • pnpm.patchedDependencies — patch files applied to dependency contents during linking, wired to nub patch / patch-commit / patch-remove.
  • pnpm.onlyBuiltDependencies / pnpm.neverBuiltDependencies / pnpm.allowBuilds — feed Nub's lifecycle-script policy.
  • .pnpmfile.cjs / .pnpmfile.mjs — the readPackage, preResolution, and afterAllResolved hooks run. Dependency-map edits from readPackage apply; package-map edits from afterAllResolved apply. New importers/packages, identity rewrites, updateConfig hooks, and config-dependency pnpmfiles are not supported.
# pnpm-workspace.yaml
packages:
  - 'packages/*'
catalog:
  lodash.merge: 4.6.2
namedRegistries:
  work: https://npm.example.com/
// package.json
{
  "pnpm": { "overrides": { "is-number": "7.0.0" } }
}

overrides, packageExtensions, and patchedDependencies are read from either home pnpm accepts — the pnpm.* namespace in package.json or the matching key in pnpm-workspace.yaml. The pnpm resolver prefers pnpm.overrides; top-level resolutions is still honored (pnpm supports it for Yarn compatibility), but top-level overrides is npm/Bun's field and is ignored under pnpm.

These fields are honored because the project is pnpm-owned — a packageManager: "pnpm@…" declaration or an existing pnpm-lock.yaml. Adopting Nub into an existing pnpm repo keeps it pnpm-owned, so all three apply unchanged. Only switching the project to Nub's own identity (nub pm use nub) migrates them to the neutral overrides / patchedDependencies package.json fields.

Under a non-pnpm incumbent — npm, Yarn, Bun, or a Nub-identity project — none of this is read. For npm, Yarn, and Bun incumbents, a stray .pnpmfile.cjs is ignored with a warning rather than applied silently:

$ nub install
nub: `.pnpmfile.cjs` ignored — this project uses npm, which doesn't
     apply pnpmfile hooks. Remove it, name it explicitly with
     `--pnpmfile`, or switch to pnpm (`nub pm use pnpm`).

Under Nub identity, pnpm-named files are outside the supported config surface and default .pnpmfile.cjs / .pnpmfile.mjs files are ignored silently. An explicit --pnpmfile <path> always loads.

Install behavior

The default node_modules layout is isolated — pnpm's symlink-into-a-virtual-store scheme. The --node-linker hoisted flag gives the flat npm-style layout.

Nub mirrors the incumbent major: pnpm 11 and later read nodeLinker and the hoisting keys from pnpm-workspace.yaml, while pnpm 10 and below read their .npmrc spellings. The CLI flag works under every major.

There is no pnp linker. A pnp request from the active pnpm config source or the --node-linker pnp flag is refused rather than silently downgraded:

$ nub install
  × node-linker=pnp is not supported by nub; use `isolated` (default) or   # ❌
  │ `hoisted`

Lifecycle scripts for dependencies are skipped by default, exactly as pnpm 10 does. A package runs install scripts only when allowlisted via pnpm.onlyBuiltDependencies (or pnpm.allowBuilds), or when Nub's gated default-trust floor vouches for it (see the default-trust floor); nub approve-builds adds an entry and builds the approved packages in the same invocation. pnpm.neverBuiltDependencies is a denylist that wins over any allow and over the floor.

Unsupported

A few pnpm surfaces produce honest unsupported-command errors rather than silent fallbacks:

sbom        # CycloneDX/SPDX bill of materials — not yet wired
deploy      # not yet wired; the operation stays manual

The node-linker=pnp install mode is also declined; see the table above.