fix(pnpm): link workspace packages by name+version to unblock release flow#3243
Merged
Conversation
… flow Set `link-workspace-packages=true` and `prefer-workspace-packages=true` in `.npmrc` to restore yarn 3's default workspace-resolution behavior. The pnpm migration (#3239) left internal `@commercetools-uikit/*` deps as concrete version specifiers ("20.5.0") rather than `workspace:^`. Under yarn 3 those resolved to the local workspace; pnpm 10 defaults `link-workspace-packages` to false and therefore treated them as registry specifiers, fetching tarballs from npm. That difference is invisible during normal CI/dev (the published version matches the workspace version, so an `npm pack`-equivalent copy is good enough), but it breaks the changesets release flow on push-to-main: 1. `changesets/action`'s version step bumps every cross-workspace dep specifier from `20.5.0` to the next unpublished version (e.g. `20.6.0`). 2. The action then runs `pnpm install` to refresh the lockfile. 3. pnpm tries to fetch `@commercetools-uikit/hooks@20.6.0` from npm, which doesn't exist yet (it's the very version this release is about to publish), and fails with `ERR_PNPM_NO_MATCHING_VERSION`. With workspace linking enabled, pnpm resolves the bumped specifier from the local workspace package (whose version was bumped in the same step), no registry round-trip required. Lockfile regenerated; `link:../packages/*` entries replace 600+ redundant per-internal-package tarball trees, hence the large net-deletion in `pnpm-lock.yaml`. Verified locally by reproducing the failure scenario: bump `packages/hooks` to `20.6.0` and `design-system`'s `@commercetools-uikit/hooks` specifier to `20.6.0`, then run `pnpm install`. Pre-fix: fails fetching 20.6.0 from npm. Post-fix: succeeds, hooks resolves to `link:../packages/hooks`. The pre-merge migration validation didn't catch this because only the canary publish path was exercised (`pnpm changeset version --snapshot` followed by `pnpm changeset publish`, with no install in between). The real-release path goes through `changesets/action`, which always re-installs after `version` — that path was never tested end-to-end before merge.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
The Vercel preview pipeline ran `pnpm install --frozen-lockfile` then `storybook build` directly, with no intermediate `pnpm build` step. Under the previous (broken) workspace-linking behavior this worked by accident: pnpm fetched registry tarballs of every internal `@commercetools-uikit/*` package, and those tarballs ship pre-built `dist/`. Storybook's bundler resolved imports straight into `node_modules/.pnpm/.../dist/...`. With `link-workspace-packages=true` (this branch), the same imports now resolve through workspace symlinks to `packages/<pkg>/dist/`, which doesn't exist on a fresh runner (`dist/` is a build output, not committed). Storybook stalls trying to resolve missing files. CI was unaffected because `.github/workflows/main.yml` already runs `pnpm build` before any downstream step — Vercel just lacked parity. Build workspace packages explicitly before the storybook build, matching CI. Adds ~1–2 minutes to preview deploys.
…links With `link-workspace-packages=true` (this branch), every workspace package's nested `node_modules/@commercetools-uikit/*` is now a symlink back to another workspace package (e.g. `packages/components/buttons/primary-button/node_modules/@commercetools-uikit/accessible-button` points at `packages/components/buttons/accessible-button`). Storybook 8's stories glob `'../../packages/components/**/*.stories.tsx'` uses `**` and follows symlinks, so the same story file is reachable through many paths: once via its canonical location and again through every consumer's nested node_modules. Storybook chokes on the duplicate story ids (or hangs traversing the cyclic graph), which manifested as the Vercel preview build stalling after the `@storybook/core v8.6.18` banner. Anchor the glob to a literal `src/` segment so it can only match `packages/components/<pkg>/src/...` (and the two-level `<group>/<pkg>/src/...` variant for fields, inputs, etc.) — never the nested `<pkg>/node_modules/.../src/...` symlinked paths. Verified locally: `pnpm --filter @commercetools-local/storybook run build` completes in 11s (pre-fix: hangs indefinitely). The structural fix is the Storybook 8 → 9 upgrade tracked in FEC-935; this is the minimal change that keeps the storybook preview working under the new workspace-linking posture in the meantime.
tdeekens
approved these changes
May 18, 2026
misama-ct
added a commit
that referenced
this pull request
May 18, 2026
The four FEC-935 commits were rebased onto post-hotfix main (PR #3243), which added `link-workspace-packages=true` + `prefer-workspace-packages=true` to .npmrc. The rebase took --theirs for the lockfile during the first conflict; this regeneration reconciles the lockfile with the merged .npmrc state (strict pnpm hoisting + workspace linking). Net: ~700 internal cross-deps now resolve through workspace symlinks (`version: link:`) instead of registry tarballs, matching what the hotfix established on main. Note: the workspace-linking .npmrc settings remain as a short-term unblock. The canonical fix is to convert internal `@commercetools-uikit/*` cross-deps from concrete `"20.5.0"` specifiers to `"workspace:^"`, deferred to FEC-938 (post-pnpm tooling polish) since it fits naturally alongside pnpm catalogs adoption and is unrelated to FEC-935's hoisting scope.
misama-ct
added a commit
that referenced
this pull request
May 18, 2026
* refactor(FEC-935): tighten pnpm hoisting (WIP)
Drop shamefully-hoist=true in favour of declared-deps-only. Adds an ESLint
override for docs snippets (per-package decision: snippets show consumer
perspective, not the documenting workspace's deps), hoists @storybook/*
via public-hoist-pattern, and declares the previously-phantom transitives
across every affected workspace (lodash, react-router-dom, react-select,
history, prop-types, @emotion/styled, popper.js, react-is, jsdom, shelljs,
@storybook/addon-docs, plus workspace-internal deps).
Two follow-up blockers — see PR description:
- storybook build picks up @storybook/react's bundled template stories
(Button.jsx/Page.jsx) under strict pnpm; structural Storybook 8 issue,
fixed in Storybook 9.
- ~40-50 typecheck residuals not yet audited.
Acceptance criteria from FEC-935 not yet met; PR opens as draft.
* refactor(FEC-935): upgrade Storybook 8.6.18 → 9.1.20
Upgrades the storybook workspace to the latest 9.x line via
`pnpm dlx storybook@9.1.20 upgrade`, plus targeted manual cleanups.
Bundles the SB9 migration into FEC-935 so the branch lands the way
the strict-pnpm posture should look long-term.
Why bundled with FEC-935:
Under strict pnpm the storybook build was tripping on Storybook 8's
`@storybook/react/template/cli/js/{Button,Page}.jsx` template files,
which the workspace stories glob accidentally matched. Storybook 9's
flatter dependency graph removes the offending `template/cli/`
directory entirely, so the FEC-935 .npmrc no longer needs to lean on
a broad `@storybook/*` carve-out for that specific reason.
What changed:
- `storybook/package.json`: drop packages consolidated into core
(`@storybook/addon-essentials`, `addon-interactions`,
`addon-storysource`, `blocks`, `manager-api`, `react`, `test`,
`theming`) and drop unused ones (`addon-themes`, `addon-onboarding`).
Remaining `@storybook/*` deps and core `storybook` pinned at 9.1.20.
- 92 `*.stories.tsx`: `from '@storybook/react'` → `'@storybook/react-vite'`.
- 80 `*.readme.mdx`: `from '@storybook/blocks'` → `'@storybook/addon-docs/blocks'`.
- `storybook/.storybook/manager.ts`: `@storybook/manager-api` → `storybook/manager-api`.
- `storybook/.storybook/theme.ts`: `@storybook/theming/create` → `storybook/theming/create`.
- `storybook/src/decorators/*-decorator.tsx`: `Decorator` type → `@storybook/react-vite`.
- `storybook/.storybook/main.ts`: addons array trimmed (essentials/interactions/
storysource merged into core), framework name wrapped in `getAbsolutePath`.
- Stories glob anchored to a literal `src/` segment so it can't descend into
the nested `<pkg>/node_modules/@commercetools-uikit/*` symlinks pnpm leaves
for workspace deps (was tripping the indexer's duplicate-id check).
.npmrc:
The branch's broad `public-hoist-pattern[]=@storybook/*` carve-out stays —
the namespace hoist is justified independent of the v8 template bug:
leaf packages import Storybook-namespaced types from a framework-user
perspective (matching the ESLint `**/docs/**` override), and declaring
~15 `@storybook/*` packages on 80+ leaf workspaces would be pure noise.
The carve-out is still ~1000x narrower than `shamefully-hoist=true`.
Validation:
- pnpm install: clean under strict pnpm (29 added, 55 removed — SB9's
"less than half the size of SB8" promise paying off)
- pnpm build: green
- pnpm test: 1402 passing, 0 failing
- pnpm lint: 1308 passing, 0 failing (after dropping an orphan
`eslint-plugin-storybook` import the automigration left at root)
- pnpm lint:publint: green (warnings only)
- pnpm --filter storybook build: green (was the original FEC-935 blocker)
- pnpm typecheck: 273 errors — exact match with d5e4725 baseline;
SB9 upgrade introduces zero new typecheck errors. Residuals are
pre-existing tech debt (114 implicit-any in CSF2 StoryFn callbacks
+ emotion styled template literals, 70 unused @ts-expect-error
directives, 89 type mismatches in data-table-manager and similar).
* fix(FEC-935): resolve typecheck regressions from strict pnpm
Two issues surfaced once shamefully-hoist=true was dropped:
- tsconfig.json: flip preserveSymlinks to false. Under strict pnpm,
every dep is a symlink (node_modules/.pnpm/<pkg>/node_modules/<pkg>/).
preserveSymlinks=true makes TypeScript resolve type imports from the
symlink location instead of the real path, breaking peer-dep subpath
imports like @storybook/react -> 'storybook/internal/types'.
AnnotatedStoryFn/StoryFn/Meta/Decorator degraded to any-ish shapes,
cascading to 272 errors across stories and data-table-manager
(TS7006 implicit-any in StoryFn callbacks, TS2578 newly-unused
@ts-expect-error suppressions, TS2339/TS2559 type mismatches). The
flag predated pnpm and was a no-op under yarn classic flat hoisting.
- packages/components/tooltip: declare @types/react-is (transitively
visible under shamefully-hoist, invisible under strict pnpm). Also
bump accessible-button's @types/react-is to 19.2.0 for manypkg
compliance.
pnpm typecheck: 273 -> 0 errors.
* fix(FEC-935): resolve visualroute phantom-dep regressions
visual-testing-app's vite build surfaced more strict-pnpm phantom-dep
gaps in visualroute fixtures (was previously masked by typecheck
failure). Same FEC-935 pattern as the rest of the migration:
shamefully-hoist flattened everything to root, strict pnpm requires
each workspace to declare what it imports.
**Self-references in visualroute files (Option A: relative imports)**
Four visualroute fixtures imported their own workspace by package
name (e.g. `from '@commercetools-uikit/pagination'` inside
pagination/src/). Under strict pnpm, pnpm doesn't create a self-symlink
into a workspace's own node_modules, so the name was unresolvable.
Rewritten to relative imports — matches the established pattern in
this repo (story files, decorators, etc. all use relative imports for
in-workspace files). Concerns: introducing a self-devDep is an
unusual pattern not used anywhere else in the codebase and pnpm
explicitly warns about cyclic deps in their workspaces guide.
Files: pagination, card, grid, icons (4 imports — main + 3 subpath
entries), design-system.
**Cross-workspace imports (Option B: declare devDeps)**
Visualroute fixtures importing other workspaces by package name
(consumer's-perspective style) now declare those workspaces as
devDependencies — matches FEC-935's broader "declare what you use"
philosophy.
- card: +@commercetools-uikit/text
- icons: +@commercetools-uikit/text, +@commercetools-uikit/spacings
- stamp: +@commercetools-uikit/icons
- grid: +@commercetools-uikit/design-system
- text: +@emotion/styled
- progress-bar: +@emotion/styled
**Router phantom deps (Option B)**
Seven visualroute fixtures use react-router(-dom) for declarative
URL routing within the Percy test app. All seven workspaces now
declare it as a devDep pinned to 5.3.4 (matching visual-testing-app).
- rich-text-input: +react-router
- design-system: +react-router
- icons, primary-action-dropdown, async-creatable-select-field,
async-select-field, select-input, localized-rich-text-input:
+react-router-dom
**Root devDep additions**
Two test/percy helpers (suite.jsx, spec.jsx, local-theme-provider.jsx)
and one orphan visualroute fixture (packages/components/spacings/
spacings.visualroute.jsx — sits at a path with no owning workspace,
since `spacings/` has 4 sub-workspaces but no parent package.json)
import packages that need to be resolvable from the repo root.
- @commercetools-uikit/design-system, hooks, i18n, utils
- @emotion/styled, prop-types, lodash
pnpm install / build / test / lint / lint:publint / storybook build /
visual-testing-app build / typecheck — all green.
* chore: regenerate pnpm-lock.yaml after rebase onto main
The four FEC-935 commits were rebased onto post-hotfix main (PR #3243),
which added `link-workspace-packages=true` + `prefer-workspace-packages=true`
to .npmrc. The rebase took --theirs for the lockfile during the first
conflict; this regeneration reconciles the lockfile with the merged .npmrc
state (strict pnpm hoisting + workspace linking).
Net: ~700 internal cross-deps now resolve through workspace symlinks
(`version: link:`) instead of registry tarballs, matching what the hotfix
established on main.
Note: the workspace-linking .npmrc settings remain as a short-term unblock.
The canonical fix is to convert internal `@commercetools-uikit/*` cross-deps
from concrete `"20.5.0"` specifiers to `"workspace:^"`, deferred to FEC-938
(post-pnpm tooling polish) since it fits naturally alongside pnpm catalogs
adoption and is unrelated to FEC-935's hoisting scope.
Merged
4 tasks
ByronDWall
pushed a commit
that referenced
this pull request
May 18, 2026
…lewatch, catalogs) (#3245) * refactor(FEC-938): migrate internal deps to workspace:^ and drop .npmrc workaround The FEC-924 pnpm migration left 706 internal @commercetools-uikit/* and @commercetools-frontend/ui-kit dep specifiers as concrete versions (e.g. "20.5.0"). Under yarn 3 those resolved to the local workspace; pnpm 10 defaults link-workspace-packages to false and treated them as registry specifiers. PR #3243 added link-workspace-packages=true and prefer-workspace-packages=true in .npmrc as a stop-gap so the release flow wouldn't try to fetch unpublished versions from npm. Replace concrete specifiers with workspace:^ across 97 package.json files (706 rewrites; 4 specifiers were already workspace:^). With the protocol explicit at the specifier level the .npmrc workaround is no longer needed and is removed. Two external @commercetools-frontend/* deps (babel-preset-mc-app, eslint-config-mc-app from merchant-center-application-kit) are preserved as concrete versions. The lockfile collapses ~5500 lines as duplicated per-internal-package tarball trees are replaced with workspace links. Verified locally: install/build/typecheck/lint/test/publint all green; check-workspace-constraints passes; the PR #3243 release-flow replay (bump packages/hooks to 20.6.0, pnpm install --offline) succeeds without a registry round-trip. * chore(FEC-938): rename bundlesize script and config to bundlewatch Naming hygiene. Only `bundlewatch` is actually installed (the `bundlesize` npm package is not a dependency); the previous `bundlesize` script name was a vestige from before this repo switched to bundlewatch. Rename script, config file, and CI invocation so the tool's name appears consistently across package.json, the workflow, and the on-disk config. - `bundlesize` -> `bundlewatch` (root package.json scripts) - `bundlesize.config.json` -> `bundlewatch.config.json` (git mv) - `pnpm bundlesize` -> `pnpm bundlewatch` (.github/workflows/main.yml) No functional change; same tool, same thresholds, same CI gate. Verified `pnpm bundlewatch` exits 0 with PASS locally. * feat(FEC-938): adopt pnpm catalogs for shared external deps Introduce a default catalog in pnpm-workspace.yaml for 10 external deps that previously had identical versions duplicated across many workspace package.json files. Versions are the exact specs the workspaces declared before adoption, so the catalog rewrite is a no-op for resolution. @babel/runtime ^7.20.13 (97 workspaces) @babel/runtime-corejs3 ^7.20.13 (97 workspaces) @emotion/react ^11.10.5 (90 workspaces) @emotion/styled ^11.10.5 (84 workspaces) prop-types ^15.8.1 (34 workspaces) lodash 4.18.1 (36 workspaces) react-select 5.10.2 (17 workspaces) downshift 9.0.10 ( 7 workspaces) react-is 19.2.0 ( 6 workspaces) history 4.10.1 ( 5 workspaces) 473 workspace specifiers rewritten to "catalog:" across 101 package.json files. Version-drift deps (react, react-intl, react-router-dom, react-dom, moment) are intentionally not in the catalog yet — they need a normalisation pass first, tracked as a follow-up. Extend scripts/check-workspace-constraints.js with a second pass that enforces every dep listed in catalog: is consumed only via the catalog: reference; literal versions on a cataloged dep are an error. The new pass accumulates cross-workspace dep usage during the existing per-workspace loop. A minimal in-script YAML parser reads the catalog keys to avoid pulling in a YAML dependency for a root-only validation script. Also fixes a small miss from the FEC-938 workspace:^ migration: packages/hooks/package.json had two internal @commercetools-uikit/utils and @commercetools-frontend/ui-kit specifiers left as concrete versions after the prior commit (the migration script's edit was reverted by a git checkout during the release-flow replay before that commit landed). Migrated to workspace:^ here. Verified locally: install / typecheck / build / lint / test / publint all green; constraints validator passes and rejects a deliberate "@babel/runtime": "^7.0.0" violation on packages/hooks; bundlewatch PASS. * chore(FEC-938): add changeset for tooling polish Mark FEC-938 with a patch-level changeset on @commercetools-frontend/ui-kit so the next release exercises the production release flow with the workspace:^ migration in place. The `fixed` group in .changeset/config.json propagates the bump to all @commercetools-uikit/* and @commercetools-frontend/* packages; the actual release version stays at minor (driven by the pnpm-migration changeset already in the queue). This also explicitly documents that the change resolves the manypkg/release-flow drift that has been blocking the Version Packages PR. * chore(FEC-938): tighten changeset prose Drop the release-flow narrative from the consumer-facing changelog entry (that detail lives in the PR description). Keep the entry to what a consumer reading the changelog needs: scope (internal-only), reassurance (no consumer-facing changes), and the three concrete things that changed. * feat(FEC-938): consolidate all external deps under pnpm catalogs Make pnpm-workspace.yaml the single source of truth for every external dep version used anywhere in the workspace. Previously only 10 high-duplication deps were cataloged; per-workspace package.json files still owned the version for the other ~150 deps, so understanding "what version of X are we on" required grepping across ~100 files. Two catalogs reflect the two semantically different intents: catalog: install pins for dependencies + devDependencies catalogs.peer: consumer compatibility ranges for peerDependencies 160 deps in the default catalog; 8 in the peer catalog. Specs are verbatim copies of what the workspaces declared before, so adoption is a no-op for resolution. Sweep: 582 specifier rewrites across 100 package.json files (415 to `catalog:`, 167 to `catalog:peer`). The earlier audit's "version drift" (react / react-intl / react-router-dom / react-dom / moment with N versions each) turned out to be install-vs-peer section difference, not real drift — every dep has exactly one spec across deps + devDeps, and exactly one spec across peerDeps. The peer catalog cleanly captures the looser ranges that were previously inline. scripts/check-workspace-constraints.js extended: - Catalog reader now also reads named catalogs (`catalogs.peer`). - Pass-2 usage accumulator split into install (deps + devDeps) and peer (peerDependencies) — semantically distinct rules. - New rule: peerDeps on a name listed in `catalogs.peer` must use `catalog:peer`; literal versions are an error. Verified: install / typecheck / build / lint / test / publint / bundlewatch all green; constraints validator passes and rejects deliberate `lodash: "4.18.1"` (default catalog rule) and `react: "19.x"` in peerDependencies (peer catalog rule) violations on packages/hooks. * refactor(FEC-938): split default catalog into named cohorts Reorganize pnpm-workspace.yaml's single 160-entry default catalog into seven named catalogs grouped by role in the toolchain — react, storybook, test, build, lint, repo, rich-text — leaving 47 leaf utilities and singletons in the default `catalog:`. The `catalogs.peer:` block is unchanged. Workspace package.json files now reference deps via `catalog:<name>` for grouped deps and `catalog:` for default-only deps. Same resolved versions; lockfile delta is purely reorganization of the catalogs block. Validator extended to enforce each named catalog using the existing `enforceCatalog()` helper, and the YAML parser fixed to handle blank lines between named catalogs. * refactor(FEC-938): group catalog entries by sub-cohort Reorganize each named catalog in pnpm-workspace.yaml so entries are ordered by sub-cohort (e.g. React core / @types / React Router / i18n / React-bound utilities within `catalogs.react`) instead of alphabetically, with `# header` comments and blank lines as visual separators. The default catalog stays alphabetical — its members are singletons. Patch the YAML parser in check-workspace-constraints.js to tolerate blank lines and `#`-prefixed comment-only lines anywhere inside a catalog block, so the sub-cohort layout doesn't trip up enforcement. No semantic change to catalog contents; lockfile untouched. * fix(FEC-938): restore react peer dep on @commercetools-uikit/hooks Commit 438fafb (`consolidate all external deps under pnpm catalogs`) dropped `react` from `packages/hooks` peerDependencies during the bulk rewrite, leaving only `react-dom`. preconstruct's build step then errored on the eight hooks source files that import `react` directly (`"react" is imported ... but the package is not specified in dependencies or peerDependencies`). Re-add `react: "catalog:peer"` to align with what main has and what the hooks sources require. * feat(FEC-938): add drift-detection rule to workspace validator An uncataloged external dep used at two or more distinct specifiers across workspaces is now an error. The catalog system prevents drift on cataloged deps; this rule guards against new deps slipping in uncataloged at inconsistent versions. Install (deps + devDeps) and peer drift are scanned separately because their cataloged keysets differ. * refactor(FEC-938): centralize pnpm config in pnpm-workspace.yaml Relocates pnpm.overrides, pnpm.onlyBuiltDependencies, and pnpm.patchedDependencies from root package.json to pnpm-workspace.yaml so workspace-wide dependency policy lives alongside the catalogs in one file. All overrides ported verbatim (including yarn-style package@range keys). pnpm install reports "Lockfile is up to date" — byte- identical resolution graph confirms pnpm 10 honors all three fields from pnpm-workspace.yaml. * refactor(FEC-938): break cyclic workspace dependencies pnpm install was warning about two cyclic workspace SCCs. Both resolved without any structural restructuring. SCC A (design-system, hooks, presets/ui-kit, presets/fields, date-field, calendar-utils) was caused by a single back-edge: @commercetools-uikit/hooks declared @commercetools-frontend/ui-kit as a devDependency solely for Spacings.Stack + PrimaryButton in two spec files. Replaced with native <button aria-label=...> scaffolding (the specs test hooks, not UI Kit components) and dropped the devDep. SCC B (select-input ↔ select-utils, originally surfaced as checkbox-input ↔ select-utils) had two contributing edges. One was a phantom: checkbox-input declared @commercetools-uikit/ select-utils as a runtime dep without importing anything from it. The other was a type-only import in select-utils referencing TSelectInputProps['appearance'] — a 3-value string literal that now lives inline, so select-utils no longer depends on select-input at all.
Merged
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.
TL;DR
Hotfix for the broken Release workflow on
mainafter #3239 was merged. Three commits:.npmrc— addslink-workspace-packages=true+prefer-workspace-packages=trueso pnpm resolves internal@commercetools-uikit/*deps from the local workspace (matching yarn 3's default), instead of trying to fetch unpublished versions from npm during the release flow. Lockfile regenerated as a side-effect (~2.8k net deletion as 600+ redundant per-internal-package tarball trees collapse intolink:../packages/*entries).vercel.json— runspnpm buildbeforestorybook buildso workspacedist/exists when storybook resolves imports. Matches what CI has always done; pre-hotfix this only worked on Vercel by accident because the registry-fetched tarballs shipped pre-builtdist/.storybook/.storybook/main.ts— anchors the stories glob to a literalsrc/segment so it can't descend into workspace symlinks and re-discover the same stories under multiple paths. Storybook 8's**-glob otherwise chokes on duplicate story ids / cyclic traversal.No published-package change. No source code change to any shipped package. CI/tooling only.
What broke
Release workflow on the merge commit of #3239 (run 26017897664) failed at the
Create Release Pull Request or Publish to npmstep:Root cause (release flow)
Workspace-linking behavior difference between yarn 3 and pnpm 10:
"@commercetools-uikit/hooks": "20.5.0"), notworkspace:^.link-workspace-packagestofalse→ treats"20.5.0"as a registry semver and fetches the tarball from npm.This is invisible during normal CI/dev (the workspace and the published
20.5.0tarball have equivalent runtime behavior), but it breaks the release flow:changesets/action's version step (pnpm changeset:version-and-format) bumps every workspace AND every cross-workspace dep specifier from20.5.0→20.6.0.pnpm installto refreshpnpm-lock.yaml.@commercetools-uikit/hooks@20.6.0from npm. That version doesn't exist yet — it's exactly what this release was about to publish. Install fails. Chicken-and-egg.Why pre-merge validation didn't catch it
Pre-merge validation exercised the canary publish path only:
pnpm changeset version --snapshot canary→pnpm changeset publish --tag canary, with no install in between. The real-release path goes throughchangesets/action, which always reinstalls afterversion. That path was never run end-to-end before merge.Knock-on issues that fell out of the fix
Enabling workspace linking flushes out two further regressions that were also masked by the prior registry-fetch behavior. Both are fixed in this PR.
Vercel previews stalled at
@storybook/core v8.6.18vercel.jsonranpnpm install --frozen-lockfilethenstorybook builddirectly, with no intermediatepnpm build. Pre-hotfix this worked because pnpm installed registry tarballs of every internal@commercetools-uikit/*package — those tarballs ship pre-builtdist/, so storybook's bundler resolved imports intonode_modules/.pnpm/.../dist/.... Post-hotfix the same imports resolve through workspace symlinks topackages/<pkg>/dist/, which doesn't exist on a fresh runner (build output, not committed). Storybook stalls on the missing files.Fixed in commit 2 by adding
pnpm build &&to thevercel.jsonbuild command. CI was unaffected because.github/workflows/main.ymlalready builds packages before any downstream step — Vercel just lacked parity.Storybook 8 stories glob followed workspace symlinks
After enabling workspace linking, every workspace's nested
node_modules/@commercetools-uikit/*is a symlink back to another workspace package:Storybook 8's glob
'../../packages/components/**/*.stories.@(js|jsx|mjs|ts|tsx)'uses**and follows symlinks, so the same story file is reachable through many paths (its canonical location plus every consumer's nested node_modules). Storybook either chokes on duplicate story ids or hangs traversing the cyclic graph — same symptom as above, manifested in the Vercel build afterpnpm buildwas added.Fixed in commit 3 by anchoring the glob to a literal
src/segment so it can only matchpackages/components/<pkg>/src/...(plus the two-level<group>/<pkg>/src/...variant for fields, inputs, etc.). The structural fix is the Storybook 8 → 9 upgrade tracked separately in #3242 (FEC-935); this is the minimal short-term workaround.Verification
pnpm install --frozen-lockfile→ ✓ (rerun against the regenerated lockfile)node scripts/check-workspace-constraints.js→ ✓pnpm build→ ✓pnpm lint:publint→ exit 0pnpm --filter @commercetools-local/storybook run build→ ✓ in ~11s (pre-fix: hung indefinitely under workspace linking)packages/hooksversion to20.6.0ANDdesign-system's@commercetools-uikit/hooksspecifier to20.6.0, then runpnpm install --ignore-scripts.ERR_PNPM_NO_MATCHING_VERSIONfetching from npm.node_modules/@commercetools-uikit/hookssymlinks to../../packages/hooks; lockfile recordsversion: link:../packages/hooks.Side benefit (local dev)
This also fixes a silent local-dev regression introduced by #3239: under the previous setting, edits to
packages/hooks(or any other internal package) were NOT picked up by sibling workspaces — they were using registry-fetched tarballs of the last published version. Post-fix, internal edits propagate via the workspace symlink, like they did under yarn.Out of scope
@commercetools-uikit/*cross-dep from a concrete"20.5.0"specifier to"workspace:^". That's ~100 packages and a much larger change; this hotfix unblocks releases now without touching package.json files.Test plan
Main workflowend-to-end)main, theReleaseworkflow on the merge commit successfully opens / updates theVersion PackagesPR (Version Packages #3236) instead of erroring at install