Skip to content

[PF-1993] Migrate from Yarn 1 to pnpm 10 - #4920

Merged
denieler merged 21 commits into
masterfrom
bill-migrate-to-pnpm
May 7, 2026
Merged

[PF-1993] Migrate from Yarn 1 to pnpm 10#4920
denieler merged 21 commits into
masterfrom
bill-migrate-to-pnpm

Conversation

@denieler

@denieler denieler commented May 4, 2026

Copy link
Copy Markdown
Collaborator

Summary

Migrates picasso's package manager from Yarn 1 to pnpm 10. This unblocks @toptal/davinci-syntax@24 (which requires pnpm) and aligns picasso with the rest of Toptal's frontend tooling.

  • Lockfile converted via pnpm import to preserve every resolved version from yarn.lock (verified by byte-identical reproduction and per-package sampling).
  • All 90 workspace packages declared as root devDependencies (concrete versions per syncpack) so .storybook/* and tailwind.config.js resolve them — yarn 1 auto-hoisted via the workspaces field, pnpm doesn't.
  • tsconfig project references regenerated for the new workspace graph.
  • All CI workflows + composite actions migrated (yarn-install action removed; local pnpm-install composite action handles corepack + pnpm-store cache + --frozen-lockfile).
  • Dockerfile switched to corepack + pnpm install --frozen-lockfile.

Override changes vs old resolutions

  • ✅ Restored @storybook/core-server>ip@toptal/davinci-ip@2.0.3 (mitigates GHSA-2p57-rm9w-gvfp; the alias had been dropped in the WIP).
  • 🗑️ Dropped js-yaml ^3.13.1 global override — it was forcing every consumer to v3 and downgrading lerna, semantic-release, jsdom, cosmiconfig, etc. that yarn.lock had on v4.

Other fixes

  • .eslintrc.js extends path → require.resolve(...) (no longer depends on linker mode).
  • bin/build.js tsc path → require.resolve('typescript/bin/tsc').
  • Alpha publish action: npx davinci-enginepnpm exec davinci-engine (deterministic resolution against the just-installed binary).

Storybook

Intentionally not upgraded — staying on Storybook 6 + the existing @storybook/react ^6.5.15 override.

Test plan

Local validation passed:

  • pnpm install --frozen-lockfile
  • pnpm lint (0 errors)
  • pnpm typecheck
  • pnpm build:package (90 projects)
  • pnpm test:unit:ci
  • pnpm build:storybook resolution clean (manager bundle built; preview bundle OOM'd locally — bundle size; CI runners have sufficient memory)

Watch in CI:

  • CI workflow (lint, typecheck, unit, circularity, syncpack)
  • davinci-integration-tests.yml — Cypress canary against the built artifacts
  • visual-testing.yml — Happo storybook smoke
  • Docker build

denieler and others added 2 commits April 30, 2026 11:09
- Delete yarn.lock; pnpm-lock.yaml is now derived from yarn.lock via
  pnpm import (resolved versions preserved across all root and
  transitive deps).
- Add 90 workspace packages as devDependencies of root so .storybook/*
  and tailwind.config.js can resolve them under pnpm (yarn 1 auto-
  hoisted via the workspaces field; pnpm requires explicit
  declaration). Refresh tsconfig references for the resulting
  workspace graph.
- Restore @storybook/core-server>ip alias to @toptal/davinci-ip@2.0.3
  to mitigate GHSA-2p57-rm9w-gvfp (the override was lost in the
  resolutions -> pnpm.overrides translation).
- Drop js-yaml ^3.13.1 override that was forcing every consumer to
  v3 and downgrading lerna, semantic-release, jsdom, and others that
  yarn.lock had on v4.
- Replace './node_modules/...' hardcoded ESLint extends path and the
  ../node_modules/.bin/tsc path in bin/build.js with require.resolve
  so resolution doesn't depend on the linker mode.
- Switch the alpha publish action from npx to pnpm exec for
  deterministic resolution against the locally installed binary.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@denieler
denieler requested a review from a team as a code owner May 4, 2026 15:34
@changeset-bot

changeset-bot Bot commented May 4, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: ca57120

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

denieler and others added 2 commits May 4, 2026 20:19
The pnpm config (node-linker, auto-install-peers, link-workspace-packages,
resolve-peers-from-workspace-root) was on disk but gitignored, so CI
checked out without it and silently fell back to pnpm's default
node-linker=isolated. That broke type resolution for @types/react,
@types/node, and @types/lodash from picasso-provider/picasso-shared
because optional-peer types weren't being hoisted to root node_modules.

The token placeholder uses ${NPM_TOKEN}, no secret is baked in.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Since .npmrc is now committed to the repo, the two COPY steps already
place the correct file in /app. The printf was overwriting it with an
auth-only version that dropped node-linker=hoisted and the rest of the
pnpm config — any pnpm install inside the runtime image would have
silently switched to the isolated linker.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@denieler denieler self-assigned this May 4, 2026
@denieler denieler changed the title Migrate from Yarn 1 to pnpm 10 [PF-1993] Migrate from Yarn 1 to pnpm 10 May 4, 2026
denieler and others added 8 commits May 4, 2026 20:43
GitHub's dependency-graph compare API returns 502 when diffing
yarn.lock <-> pnpm-lock.yaml across the migration boundary. The action
just relays the upstream error (tracked in
actions/dependency-review-action#398). The check resumes working for
every PR once master holds pnpm-lock.yaml.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The picasso storybook bundle hits the default ~4 GiB heap on
ubuntu-latest and aborts with FATAL ERROR: Allocation failed - JavaScript
heap out of memory. The Storybook 6 webpack5 builder loads every
package's stories into a single bundle and exceeds the default cap.

Apply NODE_OPTIONS via the existing cross-env wrapper so it applies to
both build:storybook (used by ci.yaml deploy + release.yml) and
happo:storybook (visual-testing.yml + ci.yaml happo job).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Post Cache built packages step was hanging indefinitely. Under
pnpm with link-workspace-packages=true, every workspace package gets
internal symlinks like packages/<a>/node_modules/@toptal/<b> ->
packages/<b>. actions/cache@v4 uses @actions/glob which follows
symlinks for **, so packages/**/dist-package enumerated forever
through the symlink loops.

Replace with explicit non-** patterns that match the two real
workspace tiers. Also drop the stray }} that was on the cache key.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
pnpm's link-workspace-packages=true creates symlinks like
packages/picasso/node_modules/@toptal/picasso-provider ->
packages/picasso-provider, which itself contains a symlink back to
packages/picasso. Webpack's _resolveContextTimestamp walks each entry's
symlinks recursively without dedup, so the cycle blows the local
hashes array past 2^32-1 and Array.push throws
"RangeError: Invalid array length" at FileSystemInfo.js:3593.

The fix is to mark the relevant trees as managedPaths so _readContext
short-circuits via fromManagedItem before reaching fromSymlink. Webpack
matches managedPaths against the path being read; the symlink targets
are workspace package directories (packages/<x>), not node_modules
paths, so the node_modules regex alone wasn't enough. Add the workspace
package tiers as plain string entries (more specific first so
packages/base/<Comp> wins over packages/<x>).

Verified locally: pnpm test:integration runs all 57 specs, 289/291
passing (2 skipped, no failures).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GitHub-hosted ubuntu runners have 16 GB physical RAM. The previous
8 GB cap was hitting FATAL ERROR: Reached heap limit on the preview
bundle for picasso's 90-package design system. 12 GB leaves ~4 GB for
thread-loader workers and the runner's own overhead.

Doesn't reduce wall-time (compile speed is unchanged); just stops the
OOM. If wall-time is still painful, next step is bigger runners.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Source-map generation on the 90-package preview bundle costs ~2-3 GB
peak heap and 20-30% wall time. Output goes to gh-pages and happo where
maps aren't useful — strip them on production builds, keep
cheap-module-source-map for `pnpm start:storybook`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Without snapshot.managedPaths, webpack's _resolveContextTimestamp
recurses through pnpm's circular workspace symlinks
(packages/<x>/node_modules/@toptal/<y> -> packages/<y>) and the
storybook build silently hangs after "Compiling preview..", with no
output until the runner times out. Cypress had the same issue (fixed in
cypress.config.mjs). Apply the same managedPaths shape here:

  - regex covering any */node_modules/* path
  - workspace package directories themselves (where the symlinks point)
    so _readContext short-circuits via fromManagedItem before reaching
    fromSymlink. More specific path first so packages/base/<Comp>
    matches before packages/<x>.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors the fix used in staff-portal's pnpm migration
(toptal/staff-portal patches/webpack+5.95.0.patch). Webpack 5.98.0's
FileSystemInfo._resolveContextTimestamp /
_resolveContextHash / _getContextTimestampAndHash recurse through
entry.symlinks without dedup. Under pnpm's hoisted linker that creates
circular workspace symlinks
(packages/<x>/node_modules/@toptal/<y> -> packages/<y>), the recursion
either hangs the build for an hour ("Compiling preview..") or grows the
hashes array past 2^32-1 and throws RangeError: Invalid array length.

Patch adds a processedSymlinks Set at each of the three call sites and
skips already-visited targets, breaking the cycle.

Picasso pins webpack to 5.98.0 via pnpm.overrides; the staff-portal
patch context lines line up exactly at 3598/3713/3890.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread bin/add-missing-workspace-deps.mjs Outdated
Comment thread docs/migration-references/pnpm-migration-plan-for-claude-code.md Outdated
Comment thread docs/migration-references/yarn-to-pnpm-migration-tutorial.md Outdated
The migration helper bin/add-missing-workspace-deps.mjs treated
*.example.tsx as a "test/story" file and added every import in those
files to the package's devDependencies. But the *.example.tsx files
are storybook code-sample templates and the __testfixtures__/ files
are codemod fixtures — neither is real consumer code, so the imports
shouldn't drive package metadata.

This commit prunes 111 such bogus devDependencies (workspace packages
only) across 76 packages. Build still passes for all 90 projects.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread MIGRATION_PLAN.md Outdated
Comment thread patches/webpack@5.98.0.patch Outdated
denieler and others added 6 commits May 5, 2026 11:48
The migration helper bin/add-missing-workspace-deps.mjs added a number
of cross-workspace deps to individual package.json files based on
imports it found in source. Many of those imports were in
*.example.tsx (storybook code-sample templates) or __testfixtures__/
(codemod input/output fixtures) — neither of which is real consumer
code, so they shouldn't drive the package's published metadata.

Rather than try to filter the right deps out, restore the
dependencies / devDependencies / peerDependencies blocks of every
workspace package to the master version. Other fields (scripts,
version, etc.) keep the migration changes. Refresh tsconfig project
references for the new (smaller) workspace dep graph.

pnpm install + build:package both still succeed for all 90 projects.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The rollback commit (60f65eb) updated package.json files but I
forgot to stage the regenerated pnpm-lock.yaml; CI's frozen install
saw the mismatch (manifest "*" vs lockfile "5.0.2" for
@toptal/picasso-provider in packages/picasso) and aborted with
ERR_PNPM_OUTDATED_LOCKFILE.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@@ -1,8 +1,22 @@
import React from 'react'
import { render, waitFor } from '@toptal/picasso-test-utils'
import { render as baseRender, waitFor } from '@testing-library/react'

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to remove circularity test-utils -> provider -> shared -> test-utils

@github-actions

github-actions Bot commented May 6, 2026

Copy link
Copy Markdown
Contributor

📖 Storybook Preview

🚀 Your Storybook preview is ready: View Storybook

📍 Preview URL: https://toptal.github.io/picasso/prs/4920/

This preview is updated automatically when you push changes to this PR.

github-actions Bot added a commit that referenced this pull request May 6, 2026
@github-actions

github-actions Bot commented May 6, 2026

Copy link
Copy Markdown
Contributor

🎉 Alpha packages are ready!

PR: #4920
Triggered by: @denieler
Workflow run: 25442874175

Installation commands:
yarn add @toptal/picasso@54.1.6-alpha-bill-migrate-to-pnpm-7ee2baf8a.0
yarn add @toptal/picasso-utils@4.0.1-alpha-bill-migrate-to-pnpm-7ee2baf8a.0
yarn add @toptal/picasso-charts@59.0.7-alpha-bill-migrate-to-pnpm-7ee2baf8a.0
yarn add @toptal/picasso-tailwind-merge@2.0.5-alpha-bill-migrate-to-pnpm-7ee2baf8a.0
yarn add @toptal/picasso-pictograms@5.5.2-alpha-bill-migrate-to-pnpm-7ee2baf8a.0
yarn add @toptal/picasso-notification@5.0.1-alpha-bill-migrate-to-pnpm-7ee2baf8a.0
yarn add @toptal/picasso-provider@5.0.3-alpha-bill-migrate-to-pnpm-7ee2baf8a.0
yarn add @toptal/picasso-test-utils@2.0.1-alpha-bill-migrate-to-pnpm-7ee2baf8a.0
yarn add @toptal/picasso-application-update-notification@2.0.45-alpha-bill-migrate-to-pnpm-7ee2baf8a.0
yarn add @topkit/analytics-charts@56.0.11-alpha-bill-migrate-to-pnpm-7ee2baf8a.0
yarn add @toptal/picasso-forms@74.0.1-alpha-bill-migrate-to-pnpm-7ee2baf8a.0
yarn add @toptal/picasso-query-builder@8.0.36-alpha-bill-migrate-to-pnpm-7ee2baf8a.0
yarn add @toptal/picasso-rich-text-editor@18.0.1-alpha-bill-migrate-to-pnpm-7ee2baf8a.0
yarn add @toptal/picasso-codemod@6.0.1-alpha-bill-migrate-to-pnpm-7ee2baf8a.0

These alpha packages were built from the latest commit in this PR.

@vedrani vedrani left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great 💯

Just a couple of things to check. Claude is throwing couple of concerns about docker prune and caching.

Comment thread Dockerfile
# syntax=docker/dockerfile:1
FROM node:18-alpine
ARG NODE_VERSION=22.20.0
ARG PNPM_VERSION=10.32.1

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can move this to package.json packageManager to have one single point of truth for version
https://github.com/toptal/staff-portal/blob/master/package.json#L153

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addressed in #4939

Comment thread Dockerfile
COPY --chown=node:node packages/base/Utils/package.json ./packages/base/Utils/package.json
# Prune to manifests so dependency installation stays cache-friendly.
RUN find packages -mindepth 2 -maxdepth 2 \! -name "package.json" -print | xargs rm -rf
RUN find packages/base -mindepth 2 -maxdepth 2 \! -name "package.json" -print | xargs rm -rf

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After the first RUN, packages/base/ is empty, so the subsequent pnpm install --frozen-lockfile cannot resolve any of the 70+ base workspace packages and the image build will fail.

Or it's fine because you copied them?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addressed in #4939

Comment thread Dockerfile
# Enables layer caching: copy lockfile + workspace manifests first,
# install, then copy the rest of the source.
COPY --chown=node:node package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc lerna.json ./
COPY --chown=node:node packages packages

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new flow is COPY packages packages → RUN find … rm -rf → RUN pnpm install. The first COPY layer's hash depends on every file under packages/, so the install layer invalidates on every code change.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addressed in #4939

Comment thread package.json Outdated
"micromatch": "^4.0.8",
"nx": "21.5.1",
"@nx/js": "21.5.1",
"@types/react": "17"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not 18?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vedrani we still use react 17 in some nested packages, so I used the "minimal compatible version"

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vedrani it seems this is the blocker to migrate to types from v18:

The build fails because packages/base/Utils/src/utils/forward-ref.ts:33-38 uses the older React 17 forwardRef signature. React 18 types tighten forwardRef's argument to ForwardRefRenderFunction<T, PropsWithoutRef<P>>. This is a real reason to keep 17 — unless we fix the helper. Let me try a targeted cast.

looks legit to me, but probably easy to fix also 🤔

@github-actions

github-actions Bot commented May 7, 2026

Copy link
Copy Markdown
Contributor

📖 Storybook Preview

🚀 Your Storybook preview is ready: View Storybook

📍 Preview URL: https://toptal.github.io/picasso/prs/4920/

This preview is updated automatically when you push changes to this PR.

github-actions Bot added a commit that referenced this pull request May 7, 2026
@denieler
denieler merged commit d8c0329 into master May 7, 2026
14 checks passed
@denieler
denieler deleted the bill-migrate-to-pnpm branch May 7, 2026 08:48
@github-actions

github-actions Bot commented May 7, 2026

Copy link
Copy Markdown
Contributor

🗑️ Storybook preview cleaned up

The preview deployment has been automatically removed since this PR was closed.

github-actions Bot added a commit that referenced this pull request May 7, 2026
@denieler denieler added the claude label May 7, 2026
dulishkovych added a commit that referenced this pull request May 14, 2026
`nx release publish --tag=canary` has been failing on every PR
dispatched against the post-pnpm-migration master because Nx
detects a four-edge circular dependency in the task graph:

  picasso-provider ─→ picasso-notification
                          │
                          ▼
  picasso-test-utils ←── picasso-tailwind-merge
        │
        └─ (prod-dep) ─→ picasso-provider  (closes the loop)

This cycle has existed structurally in `package.json` declarations
for a long time. Before PR #4920 ("PF-1993 Migrate from Yarn 1 to
pnpm 10") it was suppressed by an `nx.implicitDependencies`
negation hint inside `picasso-provider/package.json`:

  "nx": { "implicitDependencies": ["!@toptal/picasso-test-utils"] }

PR #4920 also removed picasso-provider's explicit devDep on
picasso-test-utils, and treated the nx hint as dead code from that
removal — but the hint was load-bearing for the larger cycle that
runs *through* notification and tailwind-merge, not just provider's
own outgoing edge. Removing the hint exposed the four-edge cycle to
Nx for the first time since the migration.

This commit reinstates an equivalent hint at the safest edge of the
cycle: the devDependency from picasso-tailwind-merge to
picasso-test-utils. Picking this edge has minimal risk because:

* It's a devDep, not a production dep — real consumers of
  tailwind-merge never receive test-utils transitively.
* tailwind-merge's own tests still get test-utils via `node_modules`
  at install time; only Nx's task-graph view is affected.
* The three remaining edges (all production deps) still encode the
  correct build/publish order: provider → notification →
  tailwind-merge.

No source code change, no public API change, no lockfile change.
Local `pnpm install`, `pnpm tsc:all`, `pnpm lint`, `pnpm test:unit`
all behave identically before and after — those don't consult Nx's
project graph.

Follow-up for a separate ticket: convert picasso-test-utils' regular
dep on picasso-provider to a peer dep, which would break the cycle
structurally rather than via an Nx hint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
dulishkovych added a commit that referenced this pull request May 14, 2026
`nx release publish --tag=canary` has been failing on every PR
dispatched against the post-pnpm-migration master because Nx
detects a four-edge circular dependency in the task graph:

  picasso-provider ─→ picasso-notification
                          │
                          ▼
  picasso-test-utils ←── picasso-tailwind-merge
        │
        └─ (prod-dep) ─→ picasso-provider  (closes the loop)

This cycle has existed structurally in `package.json` declarations
for a long time. Before PR #4920 ("PF-1993 Migrate from Yarn 1 to
pnpm 10") it was suppressed by an `nx.implicitDependencies`
negation hint inside `picasso-provider/package.json`:

  "nx": { "implicitDependencies": ["!@toptal/picasso-test-utils"] }

PR #4920 also removed picasso-provider's explicit devDep on
picasso-test-utils, and treated the nx hint as dead code from that
removal — but the hint was load-bearing for the larger cycle that
runs *through* notification and tailwind-merge, not just provider's
own outgoing edge. Removing the hint exposed the four-edge cycle to
Nx for the first time since the migration.

This commit reinstates an equivalent hint at the safest edge of the
cycle: the devDependency from picasso-tailwind-merge to
picasso-test-utils. Picking this edge has minimal risk because:

* It's a devDep, not a production dep — real consumers of
  tailwind-merge never receive test-utils transitively.
* tailwind-merge's own tests still get test-utils via `node_modules`
  at install time; only Nx's task-graph view is affected.
* The three remaining edges (all production deps) still encode the
  correct build/publish order: provider → notification →
  tailwind-merge.

No source code change, no public API change, no lockfile change.
Local `pnpm install`, `pnpm tsc:all`, `pnpm lint`, `pnpm test:unit`
all behave identically before and after — those don't consult Nx's
project graph.

Follow-up for a separate ticket: convert picasso-test-utils' regular
dep on picasso-provider to a peer dep, which would break the cycle
structurally rather than via an Nx hint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants