npm
Nub speaks npm's on-disk formats — the version-2 and version-3 lockfile round-trips byte-for-byte, npm workspaces and overrides are honored, and npm config is read across the builtin, global, user, and project scopes. The CLI is pnpm-shaped; the files are npm's.
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
| Feature | npm | nub | Notes |
|---|---|---|---|
dependencies / devDependencies | Supported | Supported | |
optionalDependencies | Supported | Supported | |
peerDependencies / peerDependenciesMeta | Supported | Supported | |
overrides | Supported | Supported | |
engines / os / cpu / libc | Supported | Supported | |
packageManager | Supported | Supported | |
bundleDependencies | Supported | Supported | |
workspaces | Supported | Supported | |
| workspace ranges | Supported | Supported | |
| workspace selectors | Supported | Partially supported. No git-since ([ref]) selector.No git-since ([ref]) selector. | No git-since ([ref]) selector. |
project + user .npmrc | Supported | Supported | |
global + builtin .npmrc | Supported | Supported | |
npm_config_* | Supported | Supported | |
NPM_CONFIG_* / NPM_TOKEN | Supported | Supported | |
package-lock.json v1 / v2 / v3 | Supported | Supported. 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. | v1 (npm 5/6) read; rewritten as v3 on the first mutating install. |
npm-shrinkwrap.json | Supported | Supported. Includes pre-2017 (nested, no lockfileVersion).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 ./.npmrcEverything 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=falseThe 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/--workspacesflags.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=hoistedWhen 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 inversion, notresolved) 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
npmCLI grammar. npm-specific subcommand spellings and flags are not emulated; the CLI is pnpm-shaped.
pnpm
Nub mirrors pnpm most closely — native version-9 lockfile read and write, the full dependency-verb surface, workspace selectors, an isolated dependency tree, and pnpm-owned config, all gated on pnpm being the incumbent.
Bun
When Bun is the incumbent, Nub installs against it — Bun's text lockfile round-trips byte-for-byte, its trusted-dependencies list gates build scripts, and overrides, resolutions, patches, and catalogs all resolve Bun's way.