Skip to content

fix(pnpm): link workspace packages by name+version to unblock release flow#3243

Merged
misama-ct merged 3 commits into
mainfrom
fix/pnpm-link-workspace-packages
May 18, 2026
Merged

fix(pnpm): link workspace packages by name+version to unblock release flow#3243
misama-ct merged 3 commits into
mainfrom
fix/pnpm-link-workspace-packages

Conversation

@misama-ct
Copy link
Copy Markdown
Contributor

@misama-ct misama-ct commented May 18, 2026

TL;DR

Hotfix for the broken Release workflow on main after #3239 was merged. Three commits:

  1. .npmrc — adds link-workspace-packages=true + prefer-workspace-packages=true so 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 into link:../packages/* entries).
  2. vercel.json — runs pnpm build before storybook build so workspace dist/ 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-built dist/.
  3. storybook/.storybook/main.ts — anchors the stories glob to a literal src/ 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 npm step:

ERR_PNPM_NO_MATCHING_VERSION  No matching version found for @commercetools-uikit/hooks@20.6.0 while fetching it from https://registry.npmjs.org/
The latest release of @commercetools-uikit/hooks is "20.5.0".

Root cause (release flow)

Workspace-linking behavior difference between yarn 3 and pnpm 10:

  • Internal deps in this repo are concrete version specifiers (e.g. "@commercetools-uikit/hooks": "20.5.0"), not workspace:^.
  • Yarn 3 always links workspaces by name+version match → local workspace satisfies the dep, no registry call.
  • pnpm 10 defaults link-workspace-packages to false → 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.0 tarball have equivalent runtime behavior), but it breaks the release flow:

  1. changesets/action's version step (pnpm changeset:version-and-format) bumps every workspace AND every cross-workspace dep specifier from 20.5.020.6.0.
  2. The action then runs pnpm install to refresh pnpm-lock.yaml.
  3. pnpm tries to fetch @commercetools-uikit/hooks@20.6.0 from 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 canarypnpm changeset publish --tag canary, with no install in between. The real-release path goes through changesets/action, which always reinstalls after version. 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.18

vercel.json ran pnpm install --frozen-lockfile then storybook build directly, with no intermediate pnpm build. Pre-hotfix this worked because pnpm installed registry tarballs of every internal @commercetools-uikit/* package — those tarballs ship pre-built dist/, so storybook's bundler resolved imports into node_modules/.pnpm/.../dist/.... Post-hotfix the same imports resolve through workspace symlinks to packages/<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 the vercel.json build command. CI was unaffected because .github/workflows/main.yml already 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:

packages/components/buttons/primary-button/node_modules/@commercetools-uikit/
  accessible-button -> ../../../accessible-button
  design-system     -> ../../../../../../design-system
  spacings-inline   -> ../../../../spacings/spacings-inline
  text              -> ../../../../text
  utils             -> ../../../../../utils

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 after pnpm build was added.

Fixed in commit 3 by anchoring the glob to a literal src/ segment so it can only match packages/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 0
  • pnpm --filter @commercetools-local/storybook run build → ✓ in ~11s (pre-fix: hung indefinitely under workspace linking)
  • Repro of the failing release scenario: bump packages/hooks version to 20.6.0 AND design-system's @commercetools-uikit/hooks specifier to 20.6.0, then run pnpm install --ignore-scripts.
    • Pre-fix: fails with ERR_PNPM_NO_MATCHING_VERSION fetching from npm.
    • Post-fix: succeeds; node_modules/@commercetools-uikit/hooks symlinks to ../../packages/hooks; lockfile records version: 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

  • The longer-term/idiomatic fix is to convert every internal @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.
  • Storybook 8 → 9 upgrade (full structural fix for the symlink-traversal bug): tracked in refactor(FEC-935): tighten pnpm hoisting + Storybook 9 upgrade #3242 (FEC-935).

Test plan

  • CI on this PR passes (Main workflow end-to-end)
  • Vercel preview build completes (storybook deploy renders)
  • After merge to main, the Release workflow on the merge commit successfully opens / updates the Version Packages PR (Version Packages #3236) instead of erroring at install

… 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.
@misama-ct misama-ct requested a review from a team as a code owner May 18, 2026 07:09
@vercel
Copy link
Copy Markdown

vercel Bot commented May 18, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
ui-kit Error Error May 18, 2026 8:15am

Request Review

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 18, 2026

⚠️ No Changeset found

Latest commit: b084268

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

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.
@misama-ct misama-ct merged commit 21b58fe into main May 18, 2026
9 checks passed
@misama-ct misama-ct deleted the fix/pnpm-link-workspace-packages branch May 18, 2026 07:44
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.
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.
@ByronDWall ByronDWall mentioned this pull request May 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants