Skip to content

refactor(FEC-938): ui-kit post-pnpm tooling polish (workspace:^, bundlewatch, catalogs)#3245

Merged
ByronDWall merged 12 commits into
mainfrom
FEC-938-ui-kit-post-pnpm-tooling-polish-catalogs-bundlewatch-bundlesize
May 18, 2026
Merged

refactor(FEC-938): ui-kit post-pnpm tooling polish (workspace:^, bundlewatch, catalogs)#3245
ByronDWall merged 12 commits into
mainfrom
FEC-938-ui-kit-post-pnpm-tooling-polish-catalogs-bundlewatch-bundlesize

Conversation

@misama-ct
Copy link
Copy Markdown
Contributor

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

Summary

Follow-up to FEC-924 (Yarn 3 → pnpm migration). Closes the post-migration tooling polish in one PR.

FEC-938

What changed

1. Internal deps switched to workspace:^

Rewrote 706 internal @commercetools-uikit/* and @commercetools-frontend/ui-kit specifiers from concrete versions to workspace:^ across 97 workspaces. The .npmrc workaround from PR #3243 (link-workspace-packages=true / prefer-workspace-packages=true) is no longer needed and was dropped. Lockfile collapses ~5500 lines as duplicated per-internal-package tarball trees become workspace links.

2. Bundle-size script renamed

Only bundlewatch is installed; the bundlesize script name was a vestige. Renamed across package.json, the config file (bundlesize.config.jsonbundlewatch.config.json), and the CI workflow.

3. External deps consolidated under pnpm-workspace.yaml

pnpm-workspace.yaml is now the single source of truth for every external dep version — 160 install pins + 8 peer-compatibility ranges. The earlier audit's "version drift" cases (react / react-intl / react-router-dom / react-dom / moment) turned out to be install-pin vs peer-range section differences, not real drift; the two catalog axes (install pin vs peer range) cleanly model both.

4. Catalogs organized by toolchain role

Install pins are split across 7 named catalogs + a default catalog for singletons. Grouping rule: deps that must (or strongly want to) move together share a catalog.

Catalog Entries Scope
react 24 React framework + React-bound libraries
build 27 compile/bundle/style toolchain (babel, vite, svgr, emotion, postcss, ts)
test 20 jest, testing library, percy, puppeteer, jsdom
rich-text 16 slate + remark/unified/vfile (rich-text editor stack)
repo 12 monorepo plumbing & release tooling (changesets, manypkg, husky, …)
lint 8 code-quality enforcers (eslint, stylelint, prettier, typescript-eslint)
storybook 6 storybook dev environment (must move in lockstep)
catalog: 47 leaf utilities / singletons with no cohort partner
peer 8 consumer compatibility ranges for public packages

Entries within each named catalog are grouped by sub-cohort (e.g. react splits into core / @types / React Router / i18n / React-bound utilities) with # header comments and blank-line separators — visual aid for maintainers adding deps. The default catalog stays alphabetical since its members are singletons by definition.

5. Validator enforces all of the above

scripts/check-workspace-constraints.js runs in postinstall and CI. In addition to the prior per-package metadata checks it now enforces:

  • Every dep in the default catalog: must be consumed via catalog: in dependencies / devDependencies.
  • Every dep in a named catalog catalogs.<name> must be consumed via catalog:<name>.
  • Every dep in catalogs.peer must be consumed via catalog:peer in peerDependencies.
  • Literal versions on any cataloged dep are an error.
  • Drift: an uncataloged external dep used at two or more distinct specifiers across workspaces is an error — the fix is to add it to a catalog so the version is centrally controlled. Install (deps + devDeps) and peer drift are checked separately.

The YAML parser tolerates blank lines and # comment-only lines anywhere inside a catalog block, so the sub-cohort layout doesn't trip enforcement.

6. Changeset

Patch-level changeset on @commercetools-frontend/ui-kit so the next release exercises the production flow with workspace:^ in place. The fixed group propagates the bump.

Why this resolves PR #3236

manypkg check in postinstall was failing on the Version Packages PR with 5 specifier-drift errors on the root ui-kit workspace. changeset version bumps workspace versions and cross-workspace specifiers in public workspaces, but the root is private: true and was skipped — leaving its 5 internal deps pinned at the prior release. With workspace:^, the root auto-tracks each workspace's version, so the bump no longer creates drift. PR #3243's .npmrc workaround only covered install-time resolution; manypkg's static check needed the structural fix.

Test plan

  • CI green: install / typecheck / lint / test / publint / constraints / bundlewatch
  • Percy: subpixel anti-aliasing diff noted on first run, confirmed non-issue
  • Changesets canary publishes successfully when merged
  • Reviewer sanity-checks pnpm-workspace.yaml (single source of truth) and scripts/check-workspace-constraints.js (default + 7 named + peer enforcement + drift detection)

Out of scope (follow-ups)

  • Publint warnings (FEC-936)

Bonus points

Two tidy-ups landed on top of the main work:

Centralized pnpm config in pnpm-workspace.yaml

pnpm.overrides (37 Dependabot/security pins), pnpm.onlyBuiltDependencies (postinstall allow-list), and pnpm.patchedDependencies (one patches/ entry) moved from root package.jsonpnpm-workspace.yaml, alongside the catalogs. All entries ported verbatim including yarn-style package@range keys (path-to-regexp@^1.7.0, picomatch@^2.2.1, etc.). pnpm install reports "Lockfile is up to date" — pnpm 10 honors all three fields from pnpm-workspace.yaml with a byte-identical resolution graph.

Cyclic workspace dependencies eliminated

pnpm install was emitting two cyclic workspace dependencies warnings. Both resolved without any structural restructuring.

SCC A (design-system, hooks, presets/ui-kit, presets/fields, date-field, calendar-utils) traced to a single back-edge: @commercetools-uikit/hooks declared @commercetools-frontend/ui-kit as a devDependency solely for Spacings.Stack + PrimaryButton scaffolding in two spec files. Swapped for native <button aria-label=...> (the specs test hooks, not UI Kit components) and dropped the devDep.

SCC B (select-inputselect-utils, originally surfaced as checkbox-inputselect-utils) had two contributing edges: checkbox-input → select-utils was a phantom dep with no source/test reference, and select-utils → select-input was a single type-only import (TSelectInputProps['appearance'], a 3-value string literal). Dropped the phantom dep and inlined the literal — select-utils no longer depends on select-input at all.

pnpm install no longer emits a WARN There are cyclic workspace dependencies line.

misama-ct added 2 commits May 18, 2026 11:14
…rc 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.
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.
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 18, 2026

🦋 Changeset detected

Latest commit: 473aa67

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 98 packages
Name Type
@commercetools-frontend/ui-kit Patch
@commercetools-uikit/design-system Patch
@commercetools-uikit/calendar-time-utils Patch
@commercetools-uikit/calendar-utils Patch
@commercetools-uikit/hooks Patch
@commercetools-uikit/i18n Patch
@commercetools-uikit/localized-utils Patch
@commercetools-uikit/utils Patch
@commercetools-uikit/accessible-hidden Patch
@commercetools-uikit/avatar Patch
@commercetools-uikit/card Patch
@commercetools-uikit/collapsible-motion Patch
@commercetools-uikit/collapsible-panel Patch
@commercetools-uikit/collapsible Patch
@commercetools-uikit/constraints Patch
@commercetools-uikit/data-table-manager Patch
@commercetools-uikit/data-table Patch
@commercetools-uikit/field-errors Patch
@commercetools-uikit/field-label Patch
@commercetools-uikit/field-warnings Patch
@commercetools-uikit/filters Patch
@commercetools-uikit/grid Patch
@commercetools-uikit/icons Patch
@commercetools-uikit/label Patch
@commercetools-uikit/link Patch
@commercetools-uikit/loading-spinner Patch
@commercetools-uikit/messages Patch
@commercetools-uikit/notifications Patch
@commercetools-uikit/pagination Patch
@commercetools-uikit/primary-action-dropdown Patch
@commercetools-uikit/progress-bar Patch
@commercetools-uikit/quick-filters Patch
@commercetools-uikit/stamp Patch
@commercetools-uikit/tag Patch
@commercetools-uikit/text Patch
@commercetools-uikit/tooltip Patch
@commercetools-uikit/view-switcher Patch
@commercetools-uikit/accessible-button Patch
@commercetools-uikit/flat-button Patch
@commercetools-uikit/icon-button Patch
@commercetools-uikit/link-button Patch
@commercetools-uikit/primary-button Patch
@commercetools-uikit/secondary-button Patch
@commercetools-uikit/secondary-icon-button Patch
@commercetools-uikit/dropdown-menu Patch
@commercetools-uikit/async-creatable-select-field Patch
@commercetools-uikit/async-select-field Patch
@commercetools-uikit/creatable-select-field Patch
@commercetools-uikit/date-field Patch
@commercetools-uikit/date-range-field Patch
@commercetools-uikit/date-time-field Patch
@commercetools-uikit/localized-multiline-text-field Patch
@commercetools-uikit/localized-text-field Patch
@commercetools-uikit/money-field Patch
@commercetools-uikit/multiline-text-field Patch
@commercetools-uikit/number-field Patch
@commercetools-uikit/password-field Patch
@commercetools-uikit/radio-field Patch
@commercetools-uikit/search-select-field Patch
@commercetools-uikit/select-field Patch
@commercetools-uikit/text-field Patch
@commercetools-uikit/time-field Patch
@commercetools-uikit/async-creatable-select-input Patch
@commercetools-uikit/async-select-input Patch
@commercetools-uikit/checkbox-input Patch
@commercetools-uikit/creatable-select-input Patch
@commercetools-uikit/date-input Patch
@commercetools-uikit/date-range-input Patch
@commercetools-uikit/date-time-input Patch
@commercetools-uikit/input-utils Patch
@commercetools-uikit/localized-money-input Patch
@commercetools-uikit/localized-multiline-text-input Patch
@commercetools-uikit/localized-rich-text-input Patch
@commercetools-uikit/localized-text-input Patch
@commercetools-uikit/money-input Patch
@commercetools-uikit/multiline-text-input Patch
@commercetools-uikit/number-input Patch
@commercetools-uikit/password-input Patch
@commercetools-uikit/radio-input Patch
@commercetools-uikit/rich-text-input Patch
@commercetools-uikit/rich-text-utils Patch
@commercetools-uikit/search-select-input Patch
@commercetools-uikit/search-text-input Patch
@commercetools-uikit/select-input Patch
@commercetools-uikit/select-utils Patch
@commercetools-uikit/selectable-search-input Patch
@commercetools-uikit/text-input Patch
@commercetools-uikit/time-input Patch
@commercetools-uikit/toggle-input Patch
@commercetools-uikit/spacings-inline Patch
@commercetools-uikit/spacings-inset-squish Patch
@commercetools-uikit/spacings-inset Patch
@commercetools-uikit/spacings-stack Patch
@commercetools-uikit/buttons Patch
@commercetools-uikit/fields Patch
@commercetools-uikit/inputs Patch
@commercetools-uikit/spacings Patch
visual-testing-app Patch

Not sure what this means? Click here to learn what changesets are.

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

@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 Ready Ready Preview, Comment May 18, 2026 1:14pm

Request Review

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.
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.
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.
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.
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.
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.
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.
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.
@misama-ct misama-ct marked this pull request as ready for review May 18, 2026 12:10
@misama-ct misama-ct requested a review from a team as a code owner May 18, 2026 12:10
@misama-ct misama-ct requested review from ByronDWall and tdeekens May 18, 2026 12:12
@misama-ct
Copy link
Copy Markdown
Contributor Author

[preview_deployment]

@ct-changesets
Copy link
Copy Markdown
Contributor

ct-changesets Bot commented May 18, 2026

Release workflow succeeded ✅\nSee details: Workflow Run

@misama-ct
Copy link
Copy Markdown
Contributor Author

misama-ct commented May 18, 2026

Release workflow succeeded ✅\nSee details: Workflow Run

YASS

My work here is done. 💅 💃

@misama-ct misama-ct self-assigned this May 18, 2026
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.
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.
@misama-ct misama-ct requested review from a team and removed request for a team May 18, 2026 13:27
Copy link
Copy Markdown
Contributor

@tdeekens tdeekens left a comment

Choose a reason for hiding this comment

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

Good stuff!

Comment thread pnpm-workspace.yaml
- packages/components/spacings/*
- presets/*
- visual-testing-app

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@misama-ct what are your thoughts on implementing a catalog in all our repos? To me it seems worth it, if for no other reason than it gives us a single file in which to change package versions instead of having to change versions in each lib/app's package.json

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, that would be the next logical step. The catalogs drastically simplify maintenance concerns. I initially wanted to do the mc first, as it would have the best bangs-for-bucks ratio, but I think starting with the satellites makes more sense. The catalogs can also be used as upgrade-cohorts for dependabot with minimal additional configuration.

@ByronDWall ByronDWall merged commit a4aa0f6 into main May 18, 2026
9 checks passed
@ByronDWall ByronDWall deleted the FEC-938-ui-kit-post-pnpm-tooling-polish-catalogs-bundlewatch-bundlesize branch May 18, 2026 18:15
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.

4 participants