npm to pnpm migration
⚠️ Deprecated
This migration has been completed. The steps below are kept for historical reference only — new engineers do not need to run them.
For ongoing pnpm usage notes (gotchas, CI examples, autocompletion, etc.), see the Further info section at the bottom.
Original migration steps
These steps were required when the team migrated from npm to pnpm.
-
Update local scripts that call
npm— replacenpmwithpnpmfor any scripts targeting:gf-app-tnggf-sourcerstech-docs
-
Install pnpm globally
npm i -g pnpm@10.23.0 -
(macOS only) Prepare puppeteer for crawlerService
Install Chromium:
brew install chromium --no-quarantineAdd to
~/.zshrc:echo "\nexport PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true" >> ~/.zshrc
echo "\nexport PUPPETEER_EXECUTABLE_PATH=`which chromium`" >> ~/.zshrc
source ~/.zshrc -
Pull
mainforgf-app-tng,gf-sourcers, andtech-docs. -
Reinstall dependencies at the root of each project:
rm -rf `find . -type d -name node_modules` && pnpm i -
Report any problems to Ian.
Further info
Reference notes for working with pnpm day-to-day. Still relevant.
Migrating a new repository to pnpm
-
Run
pnpm importat the project root to migratepackage-lock.json. -
Delete the old npm artefacts:
rm -rf package-lock.json node_modules -
Enforce pnpm in
package.json:{
"devEngines": {
"packageManager": {
"name": "pnpm",
"onFail": "error"
}
},
"engines": {
"node": ">=20.0",
"pnpm": ">=10.0.0"
}
} -
Install:
pnpm i -
Review (pre/post)install scripts.
-
Configure security:
minimumReleaseAge: 10080 # 7 days -
Replace
npmusages in package scripts, shell scripts, and CI/CD actions.
Building dependencies incrementally
To build a package along with its dependencies (direct and transitive), use selective filtering:
pnpm --filter crawler-service... run build
Hoisted dependencies
npm lets you accidentally use packages not declared in package.json if they happen to be in node_modules. pnpm is stricter — undeclared dependencies fail.
Find undeclared usages with:
pnpx npm-check
Install scripts blocked by default
pnpm blocks pre/postinstall scripts by default for security.
⚠️ This is a safety feature to prevent malicious scripts from running.
To allow specific packages, use onlyBuiltDependencies. When you install a package that needs build scripts, pnpm will guide you through pnpm approve-builds, which updates pnpm-workspace.yaml.
Flat vs nested node_modules
pnpm doesn't create a flat node_modules. This breaks:
- Legacy build tools that
require()undeclared dependencies (e.g. a webpack plugin assumingreactis hoisted). - Older monorepo tooling that doesn't resolve pnpm's nested layout.
- Packages that import undeclared deps (e.g. importing
lodashwithout declaring it — works under npm only because of accidental hoisting).
Option 1 — fully flatten (nuclear)
pnpm install --shamefully-hoist
Loses pnpm's isolation benefits.
Option 2 — middle ground
Add to .npmrc / .pnpmfile.cjs:
node-linker=hoisted
Or in pnpm-workspace.yaml:
nodeLinker: hoisted
Hoists as much as possible while keeping symlinks and the pnpm store.
Installing all monorepo deps
pnpm --recursive --filter '*' install
⚠️ Per the pnpm install docs, this is now redundant inside a workspace — pnpm install already installs all projects' deps. Disable with recursive-install=false if needed.
CI installs
Example workflow
name: pnpm Example Workflow
on:
push:
jobs:
build:
runs-on: ubuntu-22.04
strategy:
matrix:
node-version: [20]
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
Pinning installs
The pnpm equivalent of npm ci:
pnpm i --frozen-lockfile # all deps
pnpm i --frozen-lockfile --prod # dependencies only
pnpm i --frozen-lockfile --dev # devDependencies only
Security: minimum release age
Block packages released in the last 24 hours:
minimumReleaseAge: 1440 # minutes — 10080 = 7 days
Tab autocompletion
OhMyZsh:
pnpm completion zsh >| ~/completion-for-pnpm.zsh
echo 'source ~/completion-for-pnpm.zsh' >> ~/.zshrc
Bash:
pnpm completion bash > ~/completion-for-pnpm.bash
echo 'source ~/completion-for-pnpm.bash' >> ~/.bashrc
Fish:
pnpm completion fish > ~/.config/fish/completions/pnpm.fish
Recursive command behaviour
By default, pnpm runs commands recursively if no matching script exists in the current folder.
For example, running pnpm run test at the root with no root test script will run test in every workspace package.
Likewise, pnpm i at the root installs all workspace deps; at a sub-project, only that project's deps.
Control this with:
-r,--recursive— run in all projects--filter <package>— run in specific packages
# Run tests only in gf-app-backend
pnpm --filter gf-app-backend test
npx equivalent
The repo enforces pnpm as the package manager, so npx is locked down inside it (still fine elsewhere on your machine).
Use pnpx as a drop-in replacement.
⚠️
package.jsonscripts should call deps directly. Usingnpx/pnpxwithout a pinned version is unpredictable.{
"scripts": {
"lint": "npx eslint", // ❌ may use a later version than installed
"lint": "eslint" // ✅ uses the declared version
},
"devDependencies": {
"eslint": "8.7.1"
}
}
Deduplicating dependencies
pnpm dedupe # remove older lockfile entries when a newer version can be used
pnpm dedupe --check # exit non-zero if dedupe would change anything (no install, no edit)