refactor(FEC-938): ui-kit post-pnpm tooling polish (workspace:^, bundlewatch, catalogs)#3245
Conversation
…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 detectedLatest commit: 473aa67 The changes in this PR will be included in the next version bump. This PR includes changesets to release 98 packages
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 |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
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.
|
[preview_deployment] |
|
Release workflow succeeded ✅\nSee details: Workflow Run |
My work here is done. 💅 💃 |
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.
| - packages/components/spacings/* | ||
| - presets/* | ||
| - visual-testing-app | ||
|
|
There was a problem hiding this comment.
@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
There was a problem hiding this comment.
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.

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-kitspecifiers from concrete versions toworkspace:^across 97 workspaces. The.npmrcworkaround 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
bundlewatchis installed; thebundlesizescript name was a vestige. Renamed acrosspackage.json, the config file (bundlesize.config.json→bundlewatch.config.json), and the CI workflow.3. External deps consolidated under
pnpm-workspace.yamlpnpm-workspace.yamlis 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.
reactbuildtestrich-textrepolintstorybookcatalog:peerEntries within each named catalog are grouped by sub-cohort (e.g.
reactsplits 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.jsruns inpostinstalland CI. In addition to the prior per-package metadata checks it now enforces:catalog:must be consumed viacatalog:independencies/devDependencies.catalogs.<name>must be consumed viacatalog:<name>.catalogs.peermust be consumed viacatalog:peerinpeerDependencies.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-kitso the next release exercises the production flow withworkspace:^in place. Thefixedgroup propagates the bump.Why this resolves PR #3236
manypkg checkinpostinstallwas failing on the Version Packages PR with 5 specifier-drift errors on the rootui-kitworkspace.changeset versionbumps workspace versions and cross-workspace specifiers in public workspaces, but the root isprivate: trueand was skipped — leaving its 5 internal deps pinned at the prior release. Withworkspace:^, the root auto-tracks each workspace'sversion, so the bump no longer creates drift. PR #3243's.npmrcworkaround only covered install-time resolution; manypkg's static check needed the structural fix.Test plan
pnpm-workspace.yaml(single source of truth) andscripts/check-workspace-constraints.js(default + 7 named + peer enforcement + drift detection)Out of scope (follow-ups)
Bonus points
Two tidy-ups landed on top of the main work:
Centralized pnpm config in
pnpm-workspace.yamlpnpm.overrides(37 Dependabot/security pins),pnpm.onlyBuiltDependencies(postinstall allow-list), andpnpm.patchedDependencies(onepatches/entry) moved from rootpackage.json→pnpm-workspace.yaml, alongside the catalogs. All entries ported verbatim including yarn-stylepackage@rangekeys (path-to-regexp@^1.7.0,picomatch@^2.2.1, etc.).pnpm installreports "Lockfile is up to date" — pnpm 10 honors all three fields frompnpm-workspace.yamlwith a byte-identical resolution graph.Cyclic workspace dependencies eliminated
pnpm installwas emitting twocyclic workspace dependencieswarnings. 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/hooksdeclared@commercetools-frontend/ui-kitas adevDependencysolely forSpacings.Stack+PrimaryButtonscaffolding 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-input↔select-utils, originally surfaced ascheckbox-input↔select-utils) had two contributing edges:checkbox-input → select-utilswas a phantom dep with no source/test reference, andselect-utils → select-inputwas a single type-only import (TSelectInputProps['appearance'], a 3-value string literal). Dropped the phantom dep and inlined the literal —select-utilsno longer depends onselect-inputat all.pnpm installno longer emits aWARN There are cyclic workspace dependenciesline.