npm, pnpm and Yarn: Node.js Package Managers in the Build
How npm, pnpm and Yarn install, lock and run Node.js projects, which files they need, how to scan dependencies, and a complete GitHub Actions pipeline.
Posted
DevOps pipeline loop motif with the title Node Package Managers: npm, pnpm, Yarn
npm, pnpm and Yarn: Node.js Package Managers in the Build
In the JavaScript world the package manager (npm, pnpm, Yarn, Bun) is the entry point of every build: it installs dependencies from a lockfile, runs the scripts defined in package.json and publishes packages. The actual compile/bundle step is a separate tool, covered in JavaScript Bundlers.
Semgrep, CodeQL, eslint-plugin-security, SonarQube Community Build
SAST (commercial)
Checkmarx One, Veracode, Snyk Code, SonarQube Server
SCA
npm audit, pnpm audit, OSV-Scanner, Trivy, Snyk Open Source, Socket, Mend, Black Duck
Frameworks
Express, NestJS, Fastify, Next.js
Test and coverage
Jest, Vitest, Node test runner (node --test), Istanbul / c8
Build output
npm tarball (.tgz), node_modules + app for container images
CI cache
~/.npm, pnpm store, Yarn cache
How the build works
In CI, each stage of the generic build pipeline maps to a npm (or pnpm / Yarn) command, plugin or scanner. Press play, or step through it:
Source + lockfile: A commit triggers CI. package.json and exactly one committed lockfile (package-lock.json, pnpm-lock.yaml or yarn.lock) define the dependency graph.
Install from the lockfile: npm ci (pnpm install --frozen-lockfile, yarn install --immutable) installs exactly the lockfile and fails if it disagrees with package.json.
Lint and format: npm run lint runs ESLint from a package.json script; Prettier or Biome check formatting.
Build: npm run build runs the project's build script, for example tsc or a bundler.
Test and coverage: npm test runs Jest, Vitest or node --test; add --coverage for Istanbul / c8 coverage.
Security scans in parallel: semgrep scan --config p/nodejs checks our code, npm audit --audit-level=high --omit=dev checks production dependencies, and Gitleaks scans for secrets.
Quality gate: Lint, tests, coverage and the audit are required checks. One failure stops the pipeline before anything is packed.
Fail the build (fail): A failed check stops the pipeline at the gate, so nothing is packaged, signed or published.
Package (pass): npm pack builds the .tgz tarball exactly as the registry would receive it.
SBOM and provenance (pass): npx @cyclonedx/cyclonedx-npm writes sbom.json, and npm publish --provenance adds signed build provenance.
Publish (pass): npm publish --provenance --access public uploads to npmjs.com or a private registry; consumers verify with npm audit signatures.
Install from the lockfile in CI.npm ci, pnpm install --frozen-lockfile and yarn install --immutable fail if package.json and the lockfile disagree.
Layouts differ. npm and Yarn classic hoist packages into a flat node_modules; pnpm uses a content-addressable store with symlinks (strict, no phantom dependencies); Yarn Berry can use Plug’n’Play with no node_modules at all.
Install scripts run arbitrary code.preinstall / postinstall scripts of dependencies run on your build agent, which is the main supply-chain risk in this ecosystem.
{"name":"@example/api","version":"1.0.0","type":"module","packageManager":"pnpm@10.4.0","engines":{"node":">=22"},"files":["dist"],"scripts":{"build":"tsc -p tsconfig.json","lint":"eslint .","test":"vitest run --coverage","start":"node dist/index.js"},"dependencies":{"express":"^5.0.0"},"devDependencies":{"@vitest/coverage-v8":"^3.0.0","eslint":"^9.0.0","typescript":"^5.6.0","vitest":"^3.0.0"}}
1
2
3
4
# .npmrc — private registry for one scope, token from the environment
@example:registry=https://artifactory.example.com/artifactory/api/npm/npm-virtual///artifactory.example.com/artifactory/api/npm/npm-virtual/:_authToken=${NPM_TOKEN}engine-strict=true
name:node-cion:[push,pull_request]permissions:contents:readjobs:build:runs-on:ubuntu-lateststeps:-uses:actions/checkout@v4-uses:actions/setup-node@v4with:node-version-file:.nvmrccache:npm# use pnpm/action-setup first for pnpm-run:npm ci-run:npm run lint-run:npm test-run:npm audit --audit-level=high --omit=dev-run:npm run build-run:npx @cyclonedx/cyclonedx-npm --output-file sbom.json-uses:actions/upload-artifact@v4with:name:distpath:|dist/sbom.jsonpublish:if:startsWith(github.ref, 'refs/tags/v')needs:buildruns-on:ubuntu-latestpermissions:contents:readid-token:write# required for --provenancesteps:-uses:actions/checkout@v4-uses:actions/setup-node@v4with:node-version-file:.nvmrcregistry-url:https://registry.npmjs.org-run:npm ci && npm run build-run:npm publish --provenance --access publicenv:NODE_AUTH_TOKEN:${{ secrets.NPM_TOKEN }}
Best practices
Commit exactly one lockfile and install with the frozen/immutable flag in CI.
Pin the package manager with the packageManager field and the Node.js version with .nvmrc.
Treat install scripts as untrusted. Consider ignore-scripts=true in CI (and allow-list the packages that need them), and use pnpm’s onlyBuiltDependencies / minimumReleaseAge settings.
Proxy the registry through Artifactory or Nexus and route scoped packages to the private registry to prevent dependency confusion.
Audit production dependencies with --omit=dev as a gate, and keep a scheduled full audit.
Publish with provenance and verify it with npm audit signatures.
Automate updates with Dependabot or Renovate, grouped so PRs stay reviewable.