npm pnpm yarn Packages Lockfiles Supply Chain and Monorepos

Reading time
11 min read
Word count
2080 words
Diagram count
0 diagrams

Source: Victor Bona's Obsidian Compendium snapshot, Knowledge base/nodejs-v8-runtime-engineering/07 npm pnpm yarn Packages Lockfiles Supply Chain and Monorepos.md.

Purpose: Provide a production field guide to npm, pnpm, Yarn, package metadata, lockfiles, install modes, scripts, audit signals, supply-chain controls, and monorepo workflows for Node.js V8 Runtime Engineering.

07 npm pnpm yarn Packages Lockfiles Supply Chain and Monorepos

Related: Node.js V8 Runtime Engineering, 05 Node.js Core Architecture Bootstrapping Bindings and Native Boundaries, 06 Modules CommonJS ESM Resolution Package Exports and TypeScript Interop

Mental model

The package manager is part of the runtime. It decides which files exist on disk, which lifecycle scripts run, how workspace packages are linked, whether undeclared dependencies are visible, and whether CI installs exactly the graph that developers tested.

Treat package management as a production subsystem with these contracts:

ContractFile or commandProduction consequence
dependency intentpackage.jsonwhat the project says it needs
dependency realizationlockfilewhat CI and deploys should install
package manager versionpackageManager plus Corepack or pinned toolwhich resolver and lockfile semantics apply
workspace graphworkspaces, pnpm-workspace.yaml, Yarn confighow local packages link
install policynpm ci, pnpm install --frozen-lockfile, yarn install --immutablewhether drift is allowed
lifecycle scriptsscripts, package install scriptscode that may execute during install
supply-chain policyaudit, provenance, registry config, reviewhow package risk is managed

Package metadata

package.json is both manifest and API surface.

FieldUseFootgun
namepackage identityrenames break imports and workspace protocol references
versionpublish identityprivate apps still use it in diagnostics
privateprevents accidental publishnot a security boundary for local scripts
typedefault module format for .jschanges runtime semantics across a whole package
mainlegacy entry pointcan disagree with exports
exportspublic runtime subpathsblocks deep imports when added
importsprivate package import mapsonly applies inside the package
filespublish allowlistmissing dist files break consumers
binexecutable entry pointsmust point at shipped files with a shebang
scriptstask interface and lifecycle hooksinstall scripts can execute third-party code
dependenciesruntime dependenciesbloats production and attack surface if overused
devDependenciesbuild and test dependenciesneeded in CI build stages
peerDependencieshost-provided compatibility contractunresolved peers cause runtime mismatch
optionalDependenciesbest-effort platform featuresfailure may be silent or platform-specific
enginessupported Node and package manager versionsnot always enforced unless configured
packageManagerexpected tool and versionignored unless the environment honors it

Lockfiles

A lockfile records a concrete dependency graph. It is not just an optimization. It is the reproducibility boundary between "the package range allows this" and "this exact artifact was installed".

ToolLockfilePrimary clean install behavior
npmpackage-lock.jsonnpm ci requires an existing lockfile and fails if it does not match package.json
pnpmpnpm-lock.yamlpnpm install --frozen-lockfile refuses lockfile changes
Yarnyarn.lockmodern Yarn commonly uses yarn install --immutable for CI immutability

npm's lockfile is automatically generated when npm modifies node_modules or package.json. npm documents it as the exact tree that lets later installs produce identical dependency trees. For applications, commit it. For libraries, commit it for development reproducibility, but remember published consumers resolve their own graph unless you publish a shrinkwrap or bundled artifacts.

npm

npm is the default package manager distributed with Node.js. The operational distinction is npm install versus npm ci.

CommandUseBehavior
npm installlocal dependency changesmay update package-lock.json
npm ciCI and deploy buildsremoves existing node_modules, installs from lockfile, fails on manifest mismatch
npm auditvulnerability report and remediation planningqueries registry advisory data and reports known vulnerabilities
npm run <script>project task executionruns scripts from package.json
npm workspaces featuresmonorepo managementauto-symlinks configured workspaces during install

npm ci field rules:

  • requires package-lock.json or npm-shrinkwrap.json;
  • fails instead of updating lockfile when manifest and lockfile disagree;
  • removes existing node_modules before installing;
  • installs whole projects, not individual dependencies;
  • should use the same flags that shaped the lockfile, such as peer or install strategy options.

Example CI:

steps:
  - run: npm ci
  - run: npm run lint
  - run: npm test
  - run: npm run build

pnpm

pnpm uses a content-addressed store and a strict dependency layout by default. That strictness exposes undeclared dependency bugs that npm's flatter layout can hide.

pnpm conceptProduction meaning
content-addressed storepackage files are reused efficiently
virtual storeproject node_modules links into pnpm-managed structure
strict dependency accesspackages should only access declared dependencies
pnpm-workspace.yamlrequired root marker for pnpm workspaces
workspace: protocolensures local workspace package resolution where intended
--frozen-lockfileCI immutability, default true in many CI environments when lockfile exists

Example workspace:

packages:
  - apps/*
  - packages/*

Example dependency:

{
  "dependencies": {
    "@acme/config": "workspace:*"
  }
}

pnpm footguns:

  • A package that imports undeclared dependencies may work under npm and fail under pnpm.
  • Hoisting settings can hide dependency declaration errors.
  • file: dependencies can make frozen installs fail if local targets changed.
  • Shared lockfiles require discipline: a small package change can update a large graph.
  • CI must use the same pnpm major as developers.

Yarn

Yarn has two major operational worlds: Yarn Classic 1.x and Yarn Modern. Modern Yarn supports workspaces deeply and can use Plug'n'Play, node_modules, or other linker strategies depending on configuration.

Yarn areaProduction meaning
workspaceslocal packages are linked as part of one project
Plug'n'Playdependency access is resolved through Yarn metadata rather than conventional node_modules
constraintsmonorepo policy checks in modern Yarn
immutable installsCI should reject lockfile drift
zero-installscache artifacts may be committed by policy

Yarn Plug'n'Play changes assumptions:

  • code that scans node_modules can fail;
  • packages with undeclared dependencies fail more reliably;
  • tools may need PnP SDK integration;
  • native packages and postinstall behavior need explicit verification.

Use Yarn Modern intentionally. Do not mix Yarn Classic install habits with a modern .yarnrc.yml project.

Workspace comparison

FeaturenpmpnpmYarn
Root workspace declarationpackage.json workspacespnpm-workspace.yamlpackage.json workspaces
Local linkingauto symlink during installworkspace linking with strict layoutworkspace linking, PnP or linker dependent
CI immutable installnpm cipnpm install --frozen-lockfileyarn install --immutable
Undeclared dependency detectionweaker under flat hoistingstrong by defaultstrong with PnP
LockfileJSONYAMLYarn lock format
Best fitdefault ecosystem compatibilitystrict monorepos and disk efficiencypolicy-heavy monorepos and PnP workflows

Scripts

npm scripts and compatible package-manager scripts are the task interface of most Node projects. npm documents that npm run <name> runs commands from the scripts object, and matching pre<name> and post<name> scripts run around the named script.

{
  "scripts": {
    "prebuild": "node ./scripts/check-env.mjs",
    "build": "tsc -p tsconfig.json",
    "postbuild": "node ./scripts/check-dist.mjs",
    "test": "node --test",
    "lint": "eslint ."
  }
}

Script guidance:

PracticeReason
keep scripts deterministicCI should run the same task every time
avoid hidden network calls in build scriptsreproducibility and supply-chain control
prefer node ./script.mjs over shell-heavy one-linerscross-platform behavior
pass args after --avoids npm parsing intended script flags
document required environment variablesprevents silent local-only success
keep lifecycle scripts auditableinstall can execute code before tests run

Footgun: ignore-scripts can disable dependency lifecycle scripts during install, but commands explicitly meant to run a script still run the requested script. Know whether your CI disables install scripts and which native packages depend on them.

Supply-chain risk model

Supply-chain risk in Node.js is not one risk. It is a bundle:

RiskExampleControl
vulnerable packageadvisory in transitive dependencyaudit, update, patch, replace
malicious publishmaintainer account compromisedlockfile review, provenance, allowlists
install script executionpackage runs code on installdisable scripts where possible, review exceptions
dependency confusionprivate package name resolved from public registryscoped registry config, private registry policy
typosquattingsimilar package namereview new direct dependencies
protestware or behavior changepackage changes runtime behaviorpin lockfiles, review updates
abandoned packageno security fixesreplace or vendor with owner review
native addon binaryprebuild runs with process privilegesverify source, signatures, build provenance
broad permissionsbuild has publish or cloud credsleast-privilege CI tokens

Audit signals

npm audit submits dependency information to the configured registry and reports known vulnerabilities with severity and remediation. Audit output is an input to triage, not an automatic patch policy.

Triage table:

Audit resultProduction action
critical reachable runtime pathpatch immediately or remove package
high in server request pathpatch quickly and add regression coverage
vulnerability in dev-only toolassess CI exposure and build artifact exposure
no fix availablereduce reachability, patch-package, fork, replace, or isolate
breaking fixevaluate exploitability and schedule upgrade
noisy transitive advisoryidentify parent package and update the parent

Avoid blind audit fix --force in production branches. It can change major versions and rewrite a large graph without enough behavioral review.

Lockfile review

Review lockfile changes like code:

  • Did a direct dependency change?
  • Did the registry or resolved URL change?
  • Did integrity metadata change unexpectedly?
  • Did package count explode?
  • Did a native package enter the graph?
  • Did install scripts enter the graph?
  • Did a workspace dependency resolve to the registry instead of local workspace?
  • Did the package manager version change the lockfile format?

Minimal lockfile review commands:

git diff -- package.json package-lock.json pnpm-lock.yaml yarn.lock
npm audit --audit-level=high
npm ls --all

For pnpm:

pnpm install --frozen-lockfile
pnpm list --recursive --depth 10

For Yarn:

yarn install --immutable
yarn workspaces list

Monorepo production patterns

Good workspace contract

repo/
  package.json
  pnpm-workspace.yaml
  apps/
    api/
      package.json
  packages/
    config/
      package.json
    logger/
      package.json

Root package.json:

{
  "private": true,
  "packageManager": "[email protected]",
  "scripts": {
    "build": "pnpm -r build",
    "test": "pnpm -r test",
    "lint": "pnpm -r lint"
  },
  "devDependencies": {
    "typescript": "5.9.0"
  }
}

Package dependency:

{
  "name": "@acme/api",
  "dependencies": {
    "@acme/config": "workspace:*",
    "@acme/logger": "workspace:*"
  }
}

Monorepo rules

  • Every package declares what it imports.
  • Cross-package imports use package names, not relative paths into siblings.
  • Internal packages still define exports.
  • Build order follows dependency order.
  • CI can run filtered tasks, but graph validation and lockfile validation stay global.
  • Release tooling verifies that published packages contain built files and declarations.
  • Root dev dependencies are for root tooling, not hidden runtime dependencies.

Package manager drift

Symptoms:

SymptomLikely drift
CI lockfile changes after installpackage manager version differs
local tests pass, Docker failslifecycle scripts, native build, or OS dependencies differ
pnpm fails, npm passesundeclared dependency or hoisting assumption
Yarn PnP fails, node_modules passestool assumes filesystem package layout
package imports registry version instead of workspacemissing workspace: or workspace declaration

Controls:

  • Set packageManager in root package.json.
  • Enable Corepack or install the expected package manager version explicitly.
  • Use clean installs in CI.
  • Reject lockfile changes in validation jobs.
  • Keep one package manager per repository unless migration is active.

Troubleshooting

npm ci fails with lockfile mismatch

Meaning: package.json and the lockfile disagree. npm ci is doing its job.

Fix:

  1. Run the package manager locally with the intended version.
  2. Commit the resulting manifest and lockfile together.
  3. Re-run npm ci from a clean checkout.
  4. Check whether install flags changed the lockfile shape.

Package works locally but not in CI

Likely causes:

  • undeclared dependency exists at root locally;
  • stale node_modules;
  • different package manager major;
  • install scripts disabled in CI;
  • native prebuild unavailable on CI architecture;
  • environment variable present locally only.

Fix:

  1. Delete node_modules.
  2. Install from lockfile only.
  3. Run with CI environment variables.
  4. Check direct dependency declarations.
  5. Reproduce in the production image.

Workspace package resolves from registry

Likely causes:

  • package not included in workspace patterns;
  • name mismatch in child package;
  • range does not match local version;
  • missing workspace: protocol where strict local resolution is required.

Fix:

  1. List workspaces with the package manager.
  2. Confirm child package.json name.
  3. Use workspace:* or an exact workspace protocol policy.
  4. Reinstall and inspect lockfile resolution.

Native package fails during install

Likely causes:

  • missing Python, compiler, make, or platform headers;
  • no prebuild for current Node ABI, libc, OS, or CPU;
  • install scripts disabled;
  • package tried to download a binary but network is blocked.

Fix:

  1. Prefer packages with N-API prebuilds.
  2. Build in a dedicated build stage.
  3. Cache package-manager store and native artifacts intentionally.
  4. Verify runtime shared libraries in the final image.
  5. Avoid compiling in production startup.

Production install policies

EnvironmentPolicy
developer laptopnormal install allowed, lockfile changes reviewed
CI validationimmutable install, tests, lint, build
Docker buildimmutable install, deterministic package manager version
production runtime imageno package install at startup
emergency patchlockfile diff reviewed, direct dependency reason recorded
monorepo releasepacked artifacts tested before publish

Golden path commands

npm:

npm ci
npm run lint
npm test
npm run build
npm audit --audit-level=high

pnpm:

pnpm install --frozen-lockfile
pnpm -r lint
pnpm -r test
pnpm -r build

Yarn:

yarn install --immutable
yarn workspaces foreach -A run lint
yarn workspaces foreach -A run test
yarn workspaces foreach -A run build

Field rules

  • Commit the lockfile for applications and monorepos.
  • Pin the package manager version.
  • Use immutable installs in CI.
  • Never install dependencies during production startup.
  • Review lifecycle scripts for new packages.
  • Treat native packages as privileged code.
  • Do not mix lockfiles from multiple package managers as a normal state.
  • Keep dependency updates small enough to review.
  • Prefer workspace protocol for internal packages when supported.
  • Audit is a signal, not a substitute for reachability analysis.
  • Validate package contents with a pack or publish dry run.
  • Make dependency policy boring and automatic.

Official docs checked