Migrate from Yarn to pnpm and bump to Node 26 - #289
Merged
Conversation
Switch all three packages (api, web, web/serve) from Yarn classic to pnpm 11.5.2, and bump Node to 26.3.0 everywhere. - Replace yarn.lock with pnpm-lock.yaml; consolidate pnpm settings into per-package pnpm-workspace.yaml (nodeLinker: hoisted). - Supply-chain hardening: minimumReleaseAge (7d), blockExoticSubdeps, and an explicit allowBuilds allowlist for install scripts. - No corepack (dropped in Node 26): pnpm/action-setup in CI, npm i -g in Docker. - Dockerfiles: Node 26, pnpm install; production api images install --prod (pnpm ignores NODE_ENV) so devDependencies are omitted. - CI: pnpm caching keyed on the lockfiles. - Remove all yarn references; switch READMEs to pnpm; document the optional enableGlobalVirtualStore worktree opt-in.
devDependencies are only needed for host-side tests/lint (the dev compose container runs node --watch, no dev-only tooling), so drop the NODE_ENV-gated conditional and just install --prod in all three api images.
Bump to actions/checkout@v6 and actions/setup-node@v6, and SHA-pin pnpm/action-setup to v6.0.5, per https://pnpm.io/continuous-integration. Keep the exact pnpm version pin (11.5.2, matching packageManager), explicit --frozen-lockfile, and the cache-dependency-path our subdir layout needs.
pnpm v11 writes a ~200MB index/metadata cache to /root/.cache/pnpm (separate from the content store at /root/.local/share/pnpm/store). Only the store was cache-mounted, so the cache got baked into every image as dead weight. Mounting it too drops the api image 832MB -> 556MB.
Build deps in a deps stage; the final stage copies only node_modules + source and runs node src/index.js directly, so pnpm/npm and their caches are no longer in the runtime image (556MB -> 512MB). Switch the docker-compose dev command to scripts/start-dev (node --watch) so the dev container no longer needs pnpm either.
Move npm install -g pnpm from the base stage into the build stage so the production stage (FROM base, runs node serve/static.js) no longer inherits pnpm or its npm cache. The dev target still has pnpm. Web prod 411MB -> 370MB.
…ted hoisted The committed nodeLinker: hoisted overrides a globally-set enableGlobalVirtualStore, so 'pnpm config set --global ...' has no effect. Document GVS as not-enabled (conflicts with hoisted) instead of a working opt-in.
andrewplummer
approved these changes
Jun 9, 2026
| "license": "MIT", | ||
| "engines": { | ||
| "node": ">=20" | ||
| "node": ">=24" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Migrates all three packages (
services/api,services/web,services/web/serve) from Yarn classic (1.22.22) to pnpm 11.5.2, and bumps Node to 26.3.0 everywhere it's pinned.Why Node 26 (and not the current LTS, 24)?
npm i -g pnpmin Docker,pnpm/action-setupin CI). Staying on the current LTS (Node 24, which still ships corepack) would just mean redoing that bootstrap once we inevitably move to 26 and corepack disappears.What changed
yarn.lock→pnpm-lock.yaml(3 files). pnpm settings live in a per-packagepnpm-workspace.yamlwithnodeLinker: hoisted(Yarn-like flat layout, Docker/CI-safe).minimumReleaseAge: 10080— refuse versions published < 7 days ago.blockExoticSubdeps: true— block git/tarball transitive deps.allowBuilds:— explicit allowlist for the only deps permitted to run install scripts (pnpm blocks the rest by default).pnpm/action-setup; Docker usesnpm i -g pnpm@11.5.2.--prod(pnpm ignoresNODE_ENV) so devDependencies are omitted; web build setsCI=trueto keep the nestedserveinstall frozen./root/.cache/pnpm) that was baking ~200MB into every image. api 832MB → 512MB, web (prod) 411MB → 370MB.pnpm/action-setup,checkout@v6/setup-node@v6).enableGlobalVirtualStoreis described in the root README but left disabled: it conflicts with the committednodeLinker: hoistedthat keeps Docker images self-contained (and the committed setting wins over global config, so it can't be toggled on per-machine).resolutions→ pnpmoverrides).Verification
pnpm install --frozen-lockfilepasses for all three packages.mongodb-memory-serverbuilds correctly); webpnpm buildsucceeds; both lints pass.