[WOODPECKER-4406] Migrate package manager from npm to pnpm#4738
Conversation
- Replace npm with pnpm across all GitHub Actions workflows - Add pnpm/action-setup step to all CI jobs - Switch cache key from package-lock.json to pnpm-lock.yaml - Add pnpm-workspace.yaml and pnpm-lock.yaml - Remove package-lock.json - Update package.json: packageManager field, engines, overrides → pnpm.overrides - Update npm run → pnpm run in scripts - Fix Jest transformIgnorePatterns to handle .pnpm virtual store - Remove workspaces field (moved to pnpm-workspace.yaml)
|
Visit https://backpack.github.io/storybook-prs/4738 to see this build running in a browser. |
There was a problem hiding this comment.
Pull request overview
Note
Copilot couldn't run its full agentic review because no GitHub Actions runner was available. Make sure your repository has a runner available to run Copilot's review, or add a copilot-setup-steps.yml file specifying one with the runs-on attribute. See the docs for more details.
Migrates the monorepo’s package manager from npm to pnpm, updating workspace config, scripts, and CI workflows to use pnpm and the new lockfile.
Changes:
- Introduces pnpm workspace + lockfile configuration and updates root
package.jsonto require pnpm. - Replaces
npm ci/npm run …usage across GitHub Actions with pnpm equivalents and updates cache keys. - Updates repo docs/templates and tooling configs (
.npmrc,.gitignore) for pnpm.
Reviewed changes
Copilot reviewed 15 out of 17 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
pnpm-workspace.yaml |
Adds pnpm workspace globs replacing package.json workspaces. |
package.json |
Declares pnpm as package manager, updates scripts, Jest config, and moves overrides to pnpm.overrides. |
AGENTS.md |
Updates agent/developer instructions to use pnpm commands. |
.specify/templates/tasks-template.md |
Updates validation steps from npm to pnpm. |
.specify/templates/plan-template.md |
Updates documented package manager requirement to pnpm. |
.specify/memory/constitution.md |
Updates supported tooling list from npm to pnpm. |
.npmrc |
Adds pnpm-related install behavior settings. |
.gitignore |
Ignores pnpm debug logs and package-lock.json. |
.github/workflows/sync-figma-variables.yml |
Switches install + token sync steps to pnpm. |
.github/workflows/release.yml |
Switches caching/install/build steps to pnpm and pnpm lockfile hashing. |
.github/workflows/pr.yml |
Switches caching/install/build steps to pnpm and pnpm lockfile hashing. |
.github/workflows/main.yml |
Switches caching/install/build steps to pnpm and pnpm lockfile hashing. |
.github/workflows/dev-release.yml |
Switches install/build and cache naming/keys to pnpm. |
.github/workflows/backpack-adoption-guard-release.yml |
Switches install step to pnpm. |
.github/workflows/_build.yml |
Updates build/test pipelines to pnpm, including React 19 override step. |
.github/actions/figma-token-sync-pr/action.yml |
Switches token build step to pnpm. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
Visit https://backpack.github.io/storybook-prs/4738 to see this build running in a browser. |
Each job now uses setup-node cache: 'pnpm' + pnpm install --frozen-lockfile instead of manually caching node_modules/. This fixes esbuild-not-found failures caused by pnpm's isolated linker creating per-package node_modules that weren't included in the cached path. Also removes the stale CACHE_NAME env var and fixes the React19 job which was missing pnpm setup and still referenced package-lock.json in its cache key.
|
Visit https://backpack.github.io/storybook-prs/4738 to see this build running in a browser. |
|
Visit https://backpack.github.io/storybook-prs/4738 to see this build running in a browser. |
pnpm's isolated linker does not hoist transitive dependencies, so phantom deps that worked under npm are no longer accessible. Explicitly declare stylelint and @types/lodash to restore the binaries and type declarations that were previously available via hoisting.
…host - Add bpk-storybook-utils (workspace:*) and @jest/globals as devDependencies in backpack-web so pnpm creates proper symlinks; fixes import/no-unresolved ESLint errors caused by pnpm isolation removing npm hoisting - Fix backpack-storybook-host deps from bare '*' to 'workspace:*' for explicit workspace protocol
|
Visit https://backpack.github.io/storybook-prs/4738 to see this build running in a browser. |
pnpm isolation does not hoist @types/node from other workspace packages, causing TS2688 when tsconfig.lib.json references the 'node' type library.
|
Visit https://backpack.github.io/storybook-prs/4738 to see this build running in a browser. |
pnpm stores packages in node_modules/.pnpm/ virtual store. Jest resolves modules to the physical path (.pnpm/...) rather than the symlink path. The old pattern only matched node_modules/@Skyscanner, but Jest saw node_modules/.pnpm first and skipped transformation, causing ESM files (base.es6.js) to fail with 'SyntaxError: Unexpected token export'. Adding \.pnpm to the negative lookahead lets Jest pass through to the inner node_modules/@Skyscanner match.
pnpm isolation does not hoist @types/node from transitive deps. tsconfig.lib.json explicitly references 'node' in compilerOptions.types, so the consuming package must declare it directly.
pnpm isolation does not hoist transitive deps; scripts/jest/normalizeUseIdSerializer.js requires pretty-format directly, and scripts/react-19/transforms/strip-proptypes.test.js requires jscodeshift directly. Both must be declared explicitly.
|
Visit https://backpack.github.io/storybook-prs/4738 to see this build running in a browser. |
|
Visit https://backpack.github.io/storybook-prs/4738 to see this build running in a browser. |
This reverts commit 2acf428.
|
Visit https://backpack.github.io/storybook-prs/4738 to see this build running in a browser. |
| "@skyscanner/bpk-svgs/dist/svgs/^.+\\.svg$": "<rootDir>/scripts/stubs/fileStub.js", | ||
| "^react($|/.+)": "<rootDir>/node_modules/react$1" | ||
| "^react($|/.+)": "<rootDir>/node_modules/react$1", | ||
| "^react-dom($|/.+)": "<rootDir>/node_modules/react-dom$1" |
There was a problem hiding this comment.
Two react-dom instances exist at runtime: the root is upgraded to 19.2.5 by pnpm add -w, but @ark-ui/react and @zag-js/react still load their lockfile-locked peer instance react-dom@18.3.1. When react-dom@18 initialises, moduleNameMapper redirects its require('react') to react@19, whose restructured internals no longer expose ReactCurrentDispatcher — crash.
Fixed by adding "^react-dom($|/.+)": "<rootDir>/node_modules/react-dom$1" to moduleNameMapper, so all require('react-dom') calls — including those from the @18 peer instances — are redirected to the root react-dom@19.2.5, eliminating the version mismatch.

|
Visit https://backpack.github.io/storybook-prs/4738 to see this build running in a browser. |
|
Visit https://backpack.github.io/storybook-prs/4738 to see this build running in a browser. |
| shell: bash -l {0} | ||
|
|
||
| env: | ||
| CACHE_NAME: node-modules-cache-v2 |
There was a problem hiding this comment.
Replace the mannual cache to the PNPM cache in actions/setup-node
|
|
||
| - name: Override React to 19.2.5 | ||
| run: npm install --no-save react@19.2.5 react-dom@19.2.5 @types/react@19.0.14 @types/react-dom@19.0.6 @types/prop-types | ||
| run: pnpm_config_lockfile=false pnpm add -w react@19.2.5 react-dom@19.2.5 @types/react@19.0.14 @types/react-dom@19.0.6 @types/prop-types @types/react-transition-group@4.4.12 |
There was a problem hiding this comment.
Declare the @types/react-transition-group@4.4.12 to fix below type error:
Reason: Because npm uses flat hoisting, npm install @types/react@19 overwrites the single shared node_modules/@types/react/ that every package resolves to, so @types/react-transition-group automatically picks up the new version. With pnpm, @types/react-transition-group@4.4.11 declares @types/react as a dependency, so pnpm installs a private isolated copy (18.3.1) inside .pnpm/ that the root override can never touch — causing a type mismatch. 4.4.12 fixed this by moving @types/react to peerDependencies, so pnpm no longer installs a private copy and resolves directly to the root 19.0.14.
npm (flat hoisting)
─────────────────────────────────────────────────────
node_modules/
└── @types/react/ ← 19.0.14 (overwritten, single copy)
▲
│ both resolve to the same copy ✅
│
@types/react-transition-group ──────────────────────┘
pnpm + 4.4.11 (@types/react as "dependencies")
─────────────────────────────────────────────────────
node_modules/
├── @types/react/ ← 19.0.14 (root, overridden)
└── .pnpm/
└── @types+react-transition-group@4.4.11/
└── node_modules/
└── @types/react/ ← 18.3.1 (private copy, untouched)
▲
│ resolves to OLD copy → type mismatch ❌
│
@types/react-transition-group ──────────────────────┘
pnpm + 4.4.12 (@types/react as "peerDependencies")
─────────────────────────────────────────────────────
node_modules/
├── @types/react/ ← 19.0.14 (root)
└── .pnpm/
└── @types+react-transition-group@4.4.12/
└── node_modules/
└── @types/react/ ← (no private copy, peer resolved from root)
▲
│ resolves to root copy → types aligned ✅
│
@types/react-transition-group ──────────────────────┘
|
Visit https://backpack.github.io/storybook-prs/4738 to see this build running in a browser. |
|
Visit https://backpack.github.io/storybook-prs/4738 to see this build running in a browser. |
| **Solution:** For new components, this is expected on first run: | ||
| ```bash | ||
| npm run jest -- packages/backpack-web/src/bpk-component-[name] -u | ||
| ppnpm run jest -- packages/backpack-web/src/bpk-component-[name] -u |
There was a problem hiding this comment.
There's a typo with a double "p".
|
|
||
| - name: Override React to 19.2.5 | ||
| run: npm install --no-save react@19.2.5 react-dom@19.2.5 @types/react@19.0.14 @types/react-dom@19.0.6 @types/prop-types | ||
| run: pnpm_config_lockfile=false pnpm add -w react@19.2.5 react-dom@19.2.5 @types/react@19.0.14 @types/react-dom@19.0.6 @types/prop-types @types/react-transition-group@4.4.12 |
There was a problem hiding this comment.
pnpm_config_lockfile=false is not a format pnpm recognises — only npm_config_lockfile=false (npm-compat) or PNPM_CONFIG_LOCKFILE=false (pnpm uppercase prefix) work. This prefix will be silently ignored, so pnpm add will still write the lockfile.
Suggest:
run: PNPM_CONFIG_LOCKFILE=false pnpm add -w react@19.2.5 ...There was a problem hiding this comment.
Thanks, Kerrie. I checked the official document:
Environment variables whose names start with pnpm_config_ (or PNPM_CONFIG_) are loaded into configuration. These override settings from pnpm-workspace.yaml but not CLI arguments.
| "pnpm": { | ||
| "overrides": { | ||
| "braces": "3.0.3" | ||
| }, | ||
| "neverBuiltDependencies": ["node-sass"] |
There was a problem hiding this comment.
engines.pnpm allows >=9.15.9, but package.json#pnpm.overrides and neverBuiltDependencies are only read by pnpm 9 — pnpm 10+ moved these settings to pnpm-workspace.yaml and ignores the package.json#pnpm field with a warning.
This means on pnpm 10/11:
overrides.braces: 3.0.3(security patch) is silently droppedneverBuiltDependencies: ["node-sass"]is ignored, triggering native compilation and likely breaking install
Suggest moving these to pnpm-workspace.yaml:
overrides:
braces: "3.0.3"
neverBuiltDependencies:
- node-sassOr constrain engines to pnpm 9.x if pnpm 10+ is not intended to be supported.
There was a problem hiding this comment.
Thanks, Kerrie. The packageManager is set to pnpm@9.15.9 now:
https://github.com/Skyscanner/backpack/pull/4738/changes#diff-7ae45ad102eab3b6d7e7896acd08c427a9b25b346470d7bc6507b6481575d519.
And pnpm-workspace.yaml is supported by pnpm 11: https://pnpm.io/package_json.
So I think we can move the configs to the pnpm-workspace.yaml when we migrate to the pnpm 11
|
Visit https://backpack.github.io/storybook-prs/4738 to see this build running in a browser. |
|
Visit https://backpack.github.io/storybook-prs/4738 to see this build running in a browser. |
|
Visit https://backpack.github.io/storybook-prs/4738 to see this build running in a browser. |
https://skyscanner.atlassian.net/browse/WOODPECKER-4406
What changed
This PR migrates the Backpack monorepo package manager from npm to pnpm.
It was generated by the skill, then reviewed and refined by a person.
Why
pnpm provides faster installs, stricter dependency isolation (phantom dependency prevention), and better monorepo workspace support — aligning Backpack's toolchain with the direction set in monorepo
Checklist
README.mdchanges needed