-
Notifications
You must be signed in to change notification settings - Fork 1.6k
RFC: pnpm migration
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:
-
Package manager (pnpm) —
minimumReleaseAgeinpnpm-workspace.yamlblocks installing versions < 1 day old -
Renovate —
minimumReleaseAge: "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.
| 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 |
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 installdoesn't block the new version
- 1440 minutes (pnpm
minimumReleaseAgeinpnpm-workspace.yaml) -
"1 day"(RenovateminimumReleaseAge)
- Change
"packageManager"to"pnpm@10.x.x"(latest stable) - Remove
"workspaces"field (moves topnpm-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.
node-linker=hoistedPlus any repo-specific settings (evaluate if legacy-peer-deps=true etc. are still needed).
- Delete old lockfile (
package-lock.jsonoryarn.lock) - Delete website/ lockfile if present (website/ becomes a workspace)
- Run
pnpm installto generatepnpm-lock.yaml
- Add
pnpm/action-setup@v4step (before setup-node) - Change
actions/setup-nodecache tocache: 'pnpm' - Replace
npm ci/yarn install --immutable→pnpm install --frozen-lockfile - Replace
npm run X/yarn X→pnpm X - Replace
npx X→pnpm exec Xorpnpm dlx X - Remove Yarn-specific env vars / corepack setup if present
- Replace
npm run X→pnpm Xin script definitions - Replace
npx→pnpm exec
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.
- From npm: nothing extra (just the lockfile)
-
From Yarn:
.yarnrc.yml,.yarn/directory, removeyarnfromignoreDepsin renovate.json
-
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
Root stays uv for Python deps. Only the website/ subdirectory migrates:
-
website/package.json— change"packageManager"to"pnpm@10.x.x" -
website/.npmrc— create withnode-linker=hoisted -
website/pnpm-workspace.yaml— create with release age settings - Delete
website/yarn.lockandwebsite/.yarnrc.yml - Generate
website/pnpm-lock.yamlviacd website && pnpm install - Update CI workflows that build the website: replace yarn → pnpm
- Update
renovate.json— addinternalChecksFilter: "strict"and whitelist packageRule (these already haveminimumReleaseAge: "1 day")
| 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"
}
]
}-
With
node-linker=hoisted: Still a flatnode_moduleslayout, keeping dependency debugging straightforward. pnpm hard-links from a global store, saving disk space.
-
package-lock.json/yarn.lock→pnpm-lock.yaml(YAML, easier diffs)
- pnpm uses a global content-addressable store with hard-links. Faster installs, especially in CI with caching.
-
"workspaces"inpackage.json→pnpm-workspace.yaml. Internal deps use"workspace:*"protocol.
-
minimumReleaseAge: 1440+minimumReleaseAgeExcludeinpnpm-workspace.yaml - Full exclusion support (unlike npm which has none)
-
npm ci/yarn install --immutable→pnpm install --frozen-lockfile - Add
pnpm/action-setup@v4beforeactions/setup-nodewithcache: 'pnpm'
-
npm install/yarn→pnpm install -
npm run X/yarn X→pnpm X -
npx X→pnpm exec X/pnpm dlx X -
corepack enablestill works
-
Simple repos (no website, straightforward migration): apify-shared-js, actor-scraper, got-scraping, apify-storage-local-js, camoufox-js, impit, fingerprint-suite
-
Repos with website/ (pnpm migration + workspace restructure): apify-client-js, apify-sdk-js, apify-cli, apify-docs, apify-sdk-python, apify-client-python
-
crawlee (largest — Yarn 4 monorepo with 20 packages): Last, after gaining confidence from smaller migrations
All PRs opened as drafts.
- Run
pnpm installandpnpm buildlocally 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)