The CLI you type is pnpm-shaped; the files Nub reads and writes are npm's. When npm is the incumbent — a packageManager: "npm@…" field or an existing package-lock.json — Nub reads and writes npm's own package-lock.json in its native format, honors npm's config cascade, and introduces no pnpm-lock.yaml. Everything below is gated on that.

Configuration

FeaturenpmnubNotes
dependencies / devDependenciesSupportedSupported
optionalDependenciesSupportedSupported
peerDependencies / peerDependenciesMetaSupportedSupported
overridesSupportedSupported
engines / os / cpu / libcSupportedSupported
packageManagerSupportedSupported
bundleDependenciesSupportedSupported
workspacesSupportedSupported
workspace rangesSupportedSupported
workspace selectorsSupportedPartially supported. No git-since ([ref]) selector.No git-since ([ref]) selector.
project + user .npmrcSupportedSupported
global + builtin .npmrcSupportedSupported
npm_config_*SupportedSupported
NPM_CONFIG_* / NPM_TOKENSupportedSupported
package-lock.json v1 / v2 / v3SupportedSupported. v1 (npm 5/6) read; rewritten as v3 on the first mutating install.v1 (npm 5/6) read; rewritten as v3 on the first mutating install.
npm-shrinkwrap.jsonSupportedSupported. Includes pre-2017 (nested, no lockfileVersion).Includes pre-2017 (nested, no lockfileVersion).

package-lock.json

Nub reads lockfileVersion 2 and 3, resolves against it, and writes it back in npm's exact format. On a no-op install the writer reproduces npm's output byte-for-byte — key ordering, the devOptional collapse, peerDependenciesMeta.optional, and platform fields all preserved — so the file doesn't churn and npm ci accepts it unchanged.

// package-lock.json from `nub install` — accepted by `npm ci` byte-for-byte
{
  "name": "app",
  "version": "1.0.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "app",
      "version": "1.0.0",
      "dependencies": { "is-odd": "3.0.1" }
    },
    "node_modules/is-odd": {
      "version": "3.0.1",
      "resolved": "https://registry.npmjs.org/is-odd/-/is-odd-3.0.1.tgz",
      "integrity": "sha512-CQpnWPrD…CBaXgWMA==",
      "license": "MIT",
      "dependencies": { "is-number": "^6.0.0" },
      "engines": { "node": ">=4" }
    }
  }
}

A no-op rewrite keeps the incoming lockfileVersion — a v2 lock stays v2. The first mutating operation (nub add, nub remove) rewrites it as v3 (npm's default since npm 9), so the version field is an expected diff. An npm-shrinkwrap.json is read and preserved as-is.

Older formats are read too. A lockfileVersion: 1 lock (npm 5/6) and a pre-2017 npm-shrinkwrap.json — a nested dependencies tree with no lockfileVersion field at all — both resolve and install: Nub lifts the legacy nested tree into the same install-path scheme it uses for v2/v3. Registry dependencies install fully. The first mutating operation (nub add, nub remove) rewrites the lock as v3, the same in-place upgrade npm 7+ performs.

Two legacy edges remain out of scope. Git and file: dependencies, which a v1 lock encodes in the version field rather than resolved, are not yet read. And a fully-hoisted pre-npm-5 shrinkwrap that records no requires edges cannot place every transitive — Nub installs what it can and warns about the rest (WARN_NUB_LOCKFILE_LEGACY_INCOMPLETE_GRAPH). For either, re-lock once under npm 7+, then nub install.

.npmrc

When npm is the incumbent, Nub reads npm's .npmrc cascade:

builtin   npm's builtin npmrc
global    $PREFIX/etc/npmrc, or NPM_CONFIG_GLOBALCONFIG
user      ~/.npmrc, or NPM_CONFIG_USERCONFIG
project   ./.npmrc

Everything at those scopes works — default and scoped registries, auth tokens, TLS, proxies, and install-shaping keys like engine-strict and save-exact. The npm_config_* env vars drive install behavior too. Precedence matches npm, highest first: CLI flags, then npm_config_*, then project, user, global, and builtin .npmrc.

Custom CAs

Corporate or self-signed certificate authorities are configured through the standard npm TLS keys, top-level or scoped to one registry:

# .npmrc
cafile=./corp-ca.pem                          # PEM bundle, all registries
//registry.example.com/:cafile=./corp-ca.pem  # per-registry override
# inline PEM (repeat ca[]= to stack)
ca="-----BEGIN CERTIFICATE-----..."
# disable verification (user/global scope only)
strict-ssl=false

The certificates are added to the trust store of the install client. A committed project .npmrc cannot turn off strict-ssl — only the user or global scope can.

Config

When npm is the incumbent, Nub honors npm's graph-shaping fields:

  • overrides — npm's own version-pin field (npm 8.3+), applied during resolution and written into the lockfile.
  • workspaces — the neutral membership array, plus npm's --workspace / --workspaces flags.
  • engines / os / cpu / libc — platform fields enforced during resolution.
// package.json
{
  "overrides": { "is-number": "7.0.0" },
  "workspaces": ["packages/*"]
}

npm ignores the Yarn-style top-level resolutions, so Nub drops it with a warning under an npm incumbent. Use overrides.

Install behavior

The default node_modules layout is isolated — direct dependencies at the top level, transitive packages in a per-project virtual store, and phantom (undeclared) dependencies that fail instead of resolving by accident. This is stricter than npm's flat tree, so a project that relies on undeclared transitives opts back in with one line in .npmrc:

node-linker=hoisted

When an undeclared package fails to resolve at runtime, Nub's error names it and points at this opt-out.

Lifecycle scripts run under Nub's deny-by-default trust posture plus a gated default-trust floor (see the default-trust floor), not npm's run-everything default. Curated, age-vetted packages build automatically; everything else is skipped with WARN_NUB_IGNORED_BUILD_SCRIPTS until nub approve-builds. Platform-gated optionals (e.g. fsevents) resolve and round-trip into the lock.

Gaps

  • Legacy git / file: deps in a v1 lock (source encoded in version, not resolved) are not yet read — re-lock under npm 7+ first.
  • A v1 or v2 lock becomes v3 on the first mutating install — no-op installs preserve the incoming version.
  • No drop-in npm CLI grammar. npm-specific subcommand spellings and flags are not emulated; the CLI is pnpm-shaped.