Node.js Package Management & Supply-Chain
Parent: JavaScript and Node.js · researched 2026-06-02T20:15:41.745Z· 13 sources · 6 concepts · skill nodejs-package-management-supply-chain
This reference is the consumer side of the npm ecosystem: how you *install,
Overview
- This reference is the consumer side of the npm ecosystem: how you *install, [source]
- resolve, lock, and secure* the dependencies a Node.js project pulls in. It covers the [source]
- four mainstream package managers (npm, pnpm, Yarn Berry, bun), the lockfile + [source]
- reproducible-install contract, workspaces/monorepos, semver resolution and override [source]
- mechanics, npm scripts/lifecycle, and the supply-chain hardening surface (audit, [source]
- provenance, install-script defenses, PM pinning). [source]
- It deliberately stops at three boundaries owned by sibling references: [source]
- nodejs-module-resolution - owns how Node finds a module at runtime [source]
- (require/CJS, ESM_RESOLVE, the exports/imports fields, conditional exports). [source]
- This file gets the bytes onto disk; that file resolves a specifier against them. [source]
- devops-containers-cicd (library packaging) - owns publishing a library: [source]
- npm publish, files/exports for distribution, ESM↔CJS dual builds, [source]
- semantic-release. This file consumes the registry; that one ships to it. (Provenance [source]
- here is covered only as a consumer verification + the --provenance publish flag.) [source]
- nodejs-typescript-and-runtime-features - owns native TS loading, SEA, and the [source]
- full node --run runtime story. This file references node --run only for its [source]
- lifecycle-script behavior. [source]
- The mental model: **manifest (package.json) declares intent → resolver picks concrete [source]
- versions → lockfile freezes them → installer materializes node_modules (or a PnP map) → [source]
- lifecycle scripts run → audit/provenance/pinning guard the supply chain.** [source]
1. The package managers — install models that differ at the layout layer
- All four read package.json, but they materialize dependencies very differently: [source]
- npm - the default. Builds a flat, hoisted node_modules: transitive deps are [source]
- pulled up to the top level when versions allow. Simple and maximally compatible, but [source]
- hoisting exposes phantom dependencies - your code can require a package you never [source]
- declared (because a transitive dep hoisted it), and the import silently breaks the day [source]
- that transitive dep drops it. [source]
- pnpm - a content-addressable global store (~/.pnpm-store/~/.local/share/pnpm): [source]
- every file of every package version is stored once and hard-linked into projects, so [source]
- N projects sharing a version cost ~one copy on disk. node_modules is **symlinked and [source]
- isolated**: only packages you actually declared are reachable at the top level (the rest [source]
- live under node_modules/.pnpm/), which eliminates phantom deps by construction. [source]
- Fastest on large/monorepo installs. [source]
- Yarn (Berry, v2+) with Plug'n'Play (PnP) - eliminates node_modules entirely. [source]
- Resolution lives in a generated .pnp.cjs loader that maps every package to its [source]
- location inside zipped caches; Node loads it via a runtime hook. Strict ("semantic [source]
- erroring" on undeclared deps), fast, low storage, and supports zero-installs [source]
- (commit the cache + .pnp.cjs). Fallback to a classic layout via [source]
- nodeLinker: node-modules in .yarnrc.yml for tools that can't speak PnP (e.g. some [source]
- React Native setups). [source]
- bun install - Bun's installer is a drop-in for npm install that uses a **global [source]
- cache (~/.bun/install/cache/) with hardlinks / copy-on-write**, parallel downloads, [source]
- and platform-tuned syscalls (hardlink backend on Linux). Materializes a normal [source]
- node_modules; markedly faster than npm on cold and warm installs. [source]
2. Lockfiles — the reproducibility contract
- A lockfile pins the entire resolved tree (exact versions + resolved URLs + [source]
- integrity hashes, typically SHA-512 / SRI) so a second install reproduces the first [source]
- bit-for-bit. Each PM has its own: [source]
- Always commit the lockfile for apps (libraries usually publish without dictating [source]
- consumers' trees). It is the single source of truth for what actually ran in CI/prod. [source]
- npm ci vs npm install: npm install reconciles - if a range in [source]
- package.json no longer matches the lock, it re-resolves and rewrites the lock. [source]
- npm ci is strict and reproducible: it requires an existing lock that matches [source]
- package.json, wipes node_modules, installs exactly the locked versions, and [source]
- errors on any mismatch (it never edits the lock). Use npm ci in CI/CD and [source]
- post-clone. The frozen equivalents: yarn install --frozen-lockfile (Classic) / [source]
- --immutable (Berry), pnpm install --frozen-lockfile, bun install --frozen-lockfile. [source]
3. Workspaces / monorepos
- A workspace is a repo of multiple packages sharing one install + one lock, with local [source]
- packages linked to each other instead of being fetched from the registry. [source]
- Declaration: npm and Yarn use a "workspaces": [...] array in the root [source]
- package.json; pnpm uses a dedicated pnpm-workspace.yaml (packages: globs). [source]
- The workspace: protocol (originated in pnpm; supported by Yarn Berry and npm 7+): [source]
- a dependency written "pkg-a": "workspace:*" (or workspace:^) must resolve to the [source]
- local workspace package, never the registry. On publish, the PM rewrites workspace:* [source]
- to the concrete version so external consumers get a normal range. [source]
- Hoisting vs isolation: npm/Yarn-classic hoist shared deps to the root (re-exposing [source]
- phantom-dep risk across the monorepo); pnpm keeps each package isolated by default. [source]
- Running scripts across packages: npm --workspace=<name> / --workspaces; Yarn [source]
- yarn workspace <name> <cmd> / yarn workspaces foreach; pnpm --filter <name> and [source]
- -r (recursive). pnpm/Yarn order execution by the dependency graph (build a [source]
- package's local deps first); this topological awareness is a major monorepo win. [source]
4. Dependency resolution & semver
- Ranges in package.json are semver (MAJOR.MINOR.PATCH); the resolver picks the [source]
- highest published version satisfying every constraint, then dedupes shared transitive [source]
- Range operators: ^1.2.3 allows the leftmost-non-zero to stay fixed → `>=1.2.3 [source]
- <2.0.0 (npm/Yarn/pnpm default on add); ~1.2.3 allows only patch bumps → >=1.2.3 [source]
- <1.3.0. Beware ^0.x: ^0.2.3 means >=0.2.3 <0.3.0` (minor is treated as breaking [source]
- while major is 0). Also 1.2.x, *, >=, ||, hyphen ranges. [source]
- peerDependencies - "the host must provide this" (e.g. a React plugin peers [source]
- react). Auto-install behavior differs per PM - state it precisely: [source]
- npm 7+: auto-installs missing peers by default (npm ≤6 only warned). [source]
- pnpm 8+: auto-install-peers defaults to true (it was false in v7). [source]
- Yarn Berry: does not auto-install peers - it warns and you add them yourself. [source]
- On a conflicting peer requirement, PMs decline to auto-install and warn. [source]
- optionalDependencies - install failure is non-fatal (used for platform-specific [source]
- native binaries); guard usage with try/catch since they may be absent. [source]
- Overriding a transitive version (force a patched nested dep) - the field name [source]
- differs per PM: npm "overrides", Yarn "resolutions", pnpm "pnpm.overrides". [source]
- The primary lever for fast supply-chain remediation when a deep dep is vulnerable but its [source]
- parent hasn't bumped. [source]
- engines - declares supported node/PM versions; advisory by default, enforced with [source]
- engine-strict (npm) / engineStrict. Pair with the packageManager field (concept 6). [source]
5. npm scripts & lifecycle
- "scripts" in package.json defines named commands run via npm run <name>. npm [source]
- auto-wraps any script with hooks: running <name> executes pre<name> → <name> → [source]
- post<name> in sequence (e.g. prebuild/build/postbuild). [source]
- Install lifecycle: preinstall → install → postinstall run during [source]
- npm install. postinstall is the most-abused hook - it's how a malicious dependency [source]
- gets arbitrary code execution on npm install (see concept 6). [source]
- prepare runs on local install (no package args) and before npm pack/npm publish; [source]
- npm guidance is to use prepare for build steps, not install/preinstall - the only [source]
- legitimate use of install/preinstall is native compilation that must happen on the [source]
- Security note - node --run skips pre/post hooks. Node's built-in node --run <script> [source]
- executes only the named script and, by **intentional design, does NOT run pre/post [source]
- lifecycle scripts** (and ignores NODE_OPTIONS). That makes it faster and more predictable, [source]
- but means a predeploy/postdeploy you rely on will silently not run. For the full [source]
- node --run runtime semantics see nodejs-builtin-modules-modern (or [source]
- nodejs-typescript-and-runtime-features). [source]
6. Supply-chain security
- The dependency graph is the largest untrusted attack surface in a Node app. The defenses: [source]
- npm audit cross-checks the installed tree against the advisory DB; npm audit fix [source]
- remediates within ranges, --force may bump majors (review the diff). **`npm audit [source]
- signatures` verifies registry signatures and provenance attestations** - tying audit [source]
- to concept-6 provenance. [source]
- Provenance & trusted publishing (GA 2023, via Sigstore): publishing with [source]
- npm publish --provenance from a supported CI (GitHub Actions, GitLab) creates a [source]
- cryptographically signed, publicly logged (Rekor transparency log) link between the [source]
- published tarball, the source commit, and the build. Trusted publishing uses [source]
- short-lived CI OIDC tokens instead of long-lived npm tokens, so a leaked token can't [source]
- publish. As a consumer, you verify with npm audit signatures / the package's [source]
- provenance badge. (Authoring/publishing detail lives in devops-containers-cicd.) [source]
- Install-scripts attack surface: a compromised package's postinstall runs on [source]
- npm install. --ignore-scripts (or npm config set ignore-scripts true) blocks all [source]
- lifecycle scripts - OWASP calls it the single most effective mitigation; re-enable per-package [source]
- only for deps that genuinely need native compilation. --omit=dev / --omit=optional [source]
- trims the prod install surface (replaces the old --production/--no-optional). [source]
- Dependency confusion (a.k.a. substitution): if a build pulls from a private and the [source]
- public registry, an attacker publishes a public package with your internal name at a [source]
- higher version, and the resolver grabs the malicious one. Mitigate by scoping [source]
- internal packages (@org/...) and pinning that scope's registry in .npmrc, and never [source]
- letting a public range win over an internal name. [source]
- Lockfile injection: a malicious PR edits the lockfile to point a name at a different [source]
- tarball/URL while leaving package.json innocent-looking. Mitigate by **reviewing lockfile [source]
- diffs**, using npm ci/frozen installs (which honor the lock's integrity hash), and [source]
- version/cooldown policies (rejecting versions published in the last N days blunts [source]
- fast-moving compromise windows). [source]
- Pinning the package manager - corepack + the packageManager field: [source]
- "packageManager": "[email protected]+sha224.<hash>" (name@version, optional but recommended [source]
- hash). Corepack (shipped with Node) reads it and transparently runs that exact PM [source]
- version, so every contributor and CI uses the same tool - closing a reproducibility/trust [source]
- gap the lockfile alone can't (the lock pins deps, not the resolver). npm, pnpm, yarn [source]
- are the permitted values; explicit pinning beats COREPACK_ENABLE_AUTO_PIN. [source]
Practical patterns
- CI/CD always uses the frozen install - npm ci / pnpm i --frozen-lockfile / [source]
- yarn install --immutable / bun install --frozen-lockfile. It's faster (skips [source]
- re-resolution), reproducible, and fails loudly on lock drift. [source]
- Pin the PM with packageManager + corepack so "works on my machine" can't be a [source]
- resolver-version difference. Commit the field; let corepack enforce it. [source]
- Default to --ignore-scripts org-wide, then allowlist the handful of deps that need [source]
- native builds. Combine with --omit=dev/--omit=optional for prod images. [source]
- Remediate deep CVEs with the override field (overrides/resolutions/pnpm.overrides) [source]
- to force a patched transitive version while you wait for the parent to bump. [source]
- Reach for pnpm on monorepos - graph-ordered --filter -r runs and the isolated store [source]
- are the biggest practical wins; reach for Yarn PnP for strictness + zero-installs, and bun [source]
- install for raw speed. [source]
- Verify provenance on critical deps (npm audit signatures) and prefer packages [source]
- published with provenance/trusted publishing. [source]
Anti-patterns
- Not committing the lockfile (or .gitignore-ing it) - you forfeit reproducibility and [source]
- can't audit what actually shipped. [source]
- npm install in CI instead of npm ci - lets a stale/mismatched lock silently [source]
- re-resolve and rewrite, defeating the whole point of locking. [source]
- Relying on a phantom dependency - importing a package you never declared because it [source]
- hoisted; it breaks the day a transitive dep drops it. (pnpm/PnP make this an immediate [source]
- error - a feature, not a nuisance.) [source]
- Running npm install untrusted with scripts enabled - a single postinstall is RCE. [source]
- Audit new deps and default to --ignore-scripts. [source]
- audit fix --force without reading the diff - it can pull a breaking major and quietly [source]
- Mixing package managers in one repo (an npm install over a pnpm project) - produces a [source]
- conflicting/second lockfile and an inconsistent tree. Pin one PM via packageManager. [source]
- **Wide-open * / latest ranges or ^0.x blindness** - surrenders control over what [source]
- resolves and widens the compromise window. [source]
Troubleshooting
- npm ci fails: "lock file's ... does not satisfy ... in package.json" → the lock is [source]
- out of sync; run npm install locally to reconcile and commit the updated lock, then [source]
- npm ci passes. Never hand-edit the lock to fix this. [source]
- ERESOLVE peer-dependency conflict (npm 7+) → a peer range can't be satisfied across [source]
- the tree. Fix the real version, or use overrides; --legacy-peer-deps suppresses the [source]
- check (last resort - it ships a tree npm considers invalid). [source]
- "Cannot find module X" after switching to pnpm/Yarn PnP → X was a phantom dependency; [source]
- declare it in package.json (PnP: or add via packageExtensions). This is the strict [source]
- layout doing its job. [source]
- Integrity / EINTEGRITY checksum mismatch → the downloaded tarball's hash ≠ the lock's [source]
- integrity. Clear the cache (npm cache clean --force), confirm the registry, and treat an [source]
- unexplained mismatch as a possible tampering/lockfile-injection signal. [source]
- Different results local vs CI → almost always a PM-version drift (pin via corepack) [source]
- or npm install vs npm ci. Lock the resolver, not just the deps. [source]
- postinstall/build step not running under node --run → expected: node --run skips [source]
- pre/post hooks by design. Use npm run (or call the script explicitly) when you need the [source]
References
- npm Docs - npm ci (reproducible install, strict lock match): https://docs.npmjs.com/cli/v11/commands/npm-ci/ [source]
- npm Docs - package-locks (package-lock.json, integrity/SRI): https://docs.npmjs.com/cli/v6/configuring-npm/package-locks/ [source]
- npm Docs - scripts (lifecycle, pre/post hooks, prepare vs install): https://docs.npmjs.com/cli/v11/using-npm/scripts [source]
- npm Docs - semver / version ranges (^, ~): https://docs.npmjs.com/cli/v6/using-npm/semver/ [source]
- npm Docs - Generating provenance statements & Trusted publishers: https://docs.npmjs.com/generating-provenance-statements/ ; https://docs.npmjs.com/trusted-publishers/ [source]
- pnpm - Motivation, symlinked node_modules, store (content-addressable + hard links): https://pnpm.io/motivation ; https://pnpm.io/symlinked-node-modules-structure [source]
- pnpm - Workspaces & the workspace: protocol: https://pnpm.io/workspaces ; Settings (auto-install-peers, overrides): https://pnpm.io/settings [source]
- Yarn - Plug'n'Play (.pnp.cjs, strict deps, nodeLinker fallback): https://yarnpkg.com/features/pnp ; Workspaces (workspace: protocol): https://yarnpkg.com/features/workspaces [source]
- Yarn (Classic) - dependency versions / resolutions: https://classic.yarnpkg.com/lang/en/docs/dependency-versions/ [source]
- Bun - bun install (global cache, hardlink backend) & text lockfile: https://bun.com/docs/pm/cli/install ; https://bun.com/blog/bun-lock-text-lockfile [source]
- Node.js - --run (skips pre/post lifecycle scripts; ignores NODE_OPTIONS): https://nodejs.org/api/cli.html#--run [source]
- Node.js - Corepack & the packageManager field (PM pinning): https://nodejs.org/api/corepack.html ; https://github.com/nodejs/corepack [source]
- GitHub Blog - Introducing npm package provenance (Sigstore): https://github.blog/security/supply-chain-security/introducing-npm-package-provenance/ ; Sigstore Blog - provenance GA: https://blog.sigstore.dev/npm-provenance-ga/ [source]
- OWASP / Snyk - dependency confusion & --ignore-scripts mitigations: https://snyk.io/blog/detect-prevent-dependency-confusion-attacks-npm-supply-chain-security/ ; https://github.com/lirantal/npm-security-best-practices [source]
Children
- The package managers — npm / pnpm / Yarn Berry (PnP) / bun install models and node_modules layout (frontier)
- Lockfiles and the reproducible-install contract (npm ci vs install, integrity hashes) (frontier)
- Workspaces / monorepos and the workspace: protocol (frontier)
- Dependency resolution & semver (ranges, dedupe, overrides, peerDependencies, optionalDependencies, engines) (frontier)
- npm scripts & lifecycle (pre/post hooks, install hooks, node --run skip) (frontier)
- Supply-chain security (npm audit, provenance/sigstore, install-scripts defense, dependency confusion, lockfile injection, corepack pinning) (frontier)
Frontier under this node: Dependency resolution & semver (ranges, dedupe, overrides, peerDependencies, optionalDependencies, engines), Lockfiles and the reproducible-install contract (npm ci vs install, integrity hashes), Supply-chain security (npm audit, provenance/sigstore, install-scripts defense, dependency confusion, lockfile injection, corepack pinning), The package managers — npm / pnpm / Yarn Berry (PnP) / bun install models and node_modules layout, Workspaces / monorepos and the workspace: protocol, npm scripts & lifecycle (pre/post hooks, install hooks, node --run skip)