Skip to content

RFC: pnpm migration

Martin Adámek edited this page Apr 1, 2026 · 1 revision

Plan: Enable Minimum Release Age + Migrate All Repos to pnpm

Context

Supply chain attacks via compromised npm packages are a growing threat. Minimum release age adds a 1-day buffer between when a package version is published and when it can be installed. We enable this at two layers:

  1. Package manager (pnpm)minimumReleaseAge in pnpm-workspace.yaml blocks installing versions < 1 day old
  2. RenovateminimumReleaseAge: "1 day" delays PR creation until the version has aged

Internal packages (@apify/*, @crawlee/*) are whitelisted at both layers for immediate updates.

All PRs will be opened as drafts.


All Repos — Migrating to pnpm

Repo Current PM (root) Current PM (website/) Notes
apify-docs npm N/A (theme is local workspace) npm → pnpm; 13 CI workflows
apify-sdk-js npm npm (own lockfile) npm → pnpm; website becomes workspace
apify-client-js npm npm (own lockfile) npm → pnpm; website becomes workspace
apify-cli Yarn 4 Yarn 4 (own lockfile) Yarn → pnpm; website becomes workspace
apify-sdk-python uv (Python) Yarn 4 (own lockfile) Website only → pnpm
apify-client-python uv (Python) Yarn 4 (own lockfile) Website only → pnpm
crawlee Yarn 4 N/A Yarn → pnpm; 20-package monorepo
apify-shared-js npm N/A npm → pnpm
fingerprint-suite npm N/A npm → pnpm
actor-scraper npm N/A npm → pnpm
impit Yarn 4 (node subpkg) N/A Yarn → pnpm (node subpkg)
camoufox-js Yarn 4 N/A Yarn → pnpm
got-scraping npm N/A npm → pnpm; no Renovate config (create new)
apify-storage-local-js npm N/A npm → pnpm; no Renovate config, no lockfile

Renovate Whitelist (applies to ALL repos)

Every repo's renovate.json gets this packageRule:

{
  "matchPackageNames": ["@apify/*", "@crawlee/*"],
  "minimumReleaseAge": "0 days"
}

Both layers need the whitelist:

  • Renovate — so PRs are created immediately
  • pnpm — so pnpm install doesn't block the new version

Threshold: 1 day

  • 1440 minutes (pnpm minimumReleaseAge in pnpm-workspace.yaml)
  • "1 day" (Renovate minimumReleaseAge)

Common Migration Steps (all JS repos)

1. package.json

  • Change "packageManager" to "pnpm@10.x.x" (latest stable)
  • Remove "workspaces" field (moves to pnpm-workspace.yaml)

2. Create pnpm-workspace.yaml

packages:
  - <workspace-dirs>          # only if monorepo

minimumReleaseAge: 1440
minimumReleaseAgeExclude:
  - "@apify/*"
  - "@crawlee/*"

For non-monorepo projects, packages is omitted but the file is still needed for release age settings.

3. .npmrc

node-linker=hoisted

Plus any repo-specific settings (evaluate if legacy-peer-deps=true etc. are still needed).

4. Lockfile swap

  • Delete old lockfile (package-lock.json or yarn.lock)
  • Delete website/ lockfile if present (website/ becomes a workspace)
  • Run pnpm install to generate pnpm-lock.yaml

5. CI workflows (.github/workflows/*.yaml)

  • Add pnpm/action-setup@v4 step (before setup-node)
  • Change actions/setup-node cache to cache: 'pnpm'
  • Replace npm ci / yarn install --immutablepnpm install --frozen-lockfile
  • Replace npm run X / yarn Xpnpm X
  • Replace npx Xpnpm exec X or pnpm dlx X
  • Remove Yarn-specific env vars / corepack setup if present

6. Scripts in package.json

  • Replace npm run Xpnpm X in script definitions
  • Replace npxpnpm exec

7. renovate.json

Add (or update existing):

"minimumReleaseAge": "1 day",
"internalChecksFilter": "strict"

Add to packageRules:

{
  "matchPackageNames": ["@apify/*", "@crawlee/*"],
  "minimumReleaseAge": "0 days"
}

Update constraints: remove npm/yarn constraints, add pnpm if needed.

8. Delete old PM files

  • From npm: nothing extra (just the lockfile)
  • From Yarn: .yarnrc.yml, .yarn/ directory, remove yarn from ignoreDeps in renovate.json

9. Update documentation

  • CONTRIBUTING.md — replace npm/yarn commands with pnpm equivalents
  • README.md — update setup/install instructions
  • CLAUDE.md / AGENTS.md — update if present and references npm/yarn commands

Python Repos (sdk-python, client-python) — Website Only

Root stays uv for Python deps. Only the website/ subdirectory migrates:

  1. website/package.json — change "packageManager" to "pnpm@10.x.x"
  2. website/.npmrc — create with node-linker=hoisted
  3. website/pnpm-workspace.yaml — create with release age settings
  4. Delete website/yarn.lock and website/.yarnrc.yml
  5. Generate website/pnpm-lock.yaml via cd website && pnpm install
  6. Update CI workflows that build the website: replace yarn → pnpm
  7. Update renovate.json — add internalChecksFilter: "strict" and whitelist packageRule (these already have minimumReleaseAge: "1 day")

Per-Repo Specifics

Repo Workspaces Key details
apify-docs ["apify-docs-theme"] 13 CI workflows; many npm run in scripts
apify-sdk-js ["website"] website/ becomes workspace
apify-client-js ["website"] website/ becomes workspace
apify-cli ["website"] From Yarn 4; extra Renovate rules for eslint/inquirer
crawlee ["packages/*", "website"] 20 packages; Turbo + Lerna; already has min age (update whitelist)
apify-shared-js none Simple single-package repo
fingerprint-suite TBD (check if monorepo) npm → pnpm
actor-scraper TBD npm → pnpm
impit none (Rust + node subpkg) impit-node/ uses Yarn → pnpm
camoufox-js none Yarn → pnpm
got-scraping none No Renovate config — create full standard template
apify-storage-local-js none No Renovate config, no lockfile — create both
apify-sdk-python N/A Website only; root stays uv
apify-client-python N/A Website only; root stays uv

For repos that need a NEW Renovate config (got-scraping, apify-storage-local-js):

{
  "extends": ["config:base", ":semanticCommitTypeAll(chore)"],
  "pinVersions": false,
  "separateMajorMinor": false,
  "dependencyDashboard": false,
  "semanticCommits": "enabled",
  "schedule": ["every weekday"],
  "minimumReleaseAge": "1 day",
  "internalChecksFilter": "strict",
  "lockFileMaintenance": {
    "enabled": true,
    "schedule": ["before 2am"],
    "automerge": true,
    "automergeType": "branch"
  },
  "packageRules": [
    {
      "matchUpdateTypes": ["patch", "minor"],
      "matchCurrentVersion": "!/^0/",
      "groupName": "patch/minor dependencies",
      "groupSlug": "all-non-major",
      "automerge": true,
      "automergeType": "branch"
    },
    {
      "matchPackageNames": ["@apify/*", "@crawlee/*"],
      "minimumReleaseAge": "0 days"
    }
  ]
}

How Things Differ After pnpm Migration

Dependency resolution

  • With node-linker=hoisted: Still a flat node_modules layout, keeping dependency debugging straightforward. pnpm hard-links from a global store, saving disk space.

Lockfile

  • package-lock.json / yarn.lockpnpm-lock.yaml (YAML, easier diffs)

Install speed

  • pnpm uses a global content-addressable store with hard-links. Faster installs, especially in CI with caching.

Workspaces

  • "workspaces" in package.jsonpnpm-workspace.yaml. Internal deps use "workspace:*" protocol.

Security

  • minimumReleaseAge: 1440 + minimumReleaseAgeExclude in pnpm-workspace.yaml
  • Full exclusion support (unlike npm which has none)

CI

  • npm ci / yarn install --immutablepnpm install --frozen-lockfile
  • Add pnpm/action-setup@v4 before actions/setup-node with cache: 'pnpm'

Developer workflow

  • npm install / yarnpnpm install
  • npm run X / yarn Xpnpm X
  • npx Xpnpm exec X / pnpm dlx X
  • corepack enable still works

Execution Order

  1. Simple repos (no website, straightforward migration): apify-shared-js, actor-scraper, got-scraping, apify-storage-local-js, camoufox-js, impit, fingerprint-suite

  2. Repos with website/ (pnpm migration + workspace restructure): apify-client-js, apify-sdk-js, apify-cli, apify-docs, apify-sdk-python, apify-client-python

  3. crawlee (largest — Yarn 4 monorepo with 20 packages): Last, after gaining confidence from smaller migrations

All PRs opened as drafts.


Verification

  • Run pnpm install and pnpm build locally for each migrated repo
  • Validate Renovate configs with npx renovate-config-validator
  • Verify CI passes on the draft PR
  • For crawlee: run full test suite (pnpm test)