Skip to content

fix(build): stop dts emit leaking @commercetools-frontend/ui-kit into published types#3254

Merged
valoriecarli merged 2 commits into
mainfrom
fix/dts-emit-meta-package-leak
Jun 11, 2026
Merged

fix(build): stop dts emit leaking @commercetools-frontend/ui-kit into published types#3254
valoriecarli merged 2 commits into
mainfrom
fix/dts-emit-meta-package-leak

Conversation

@misama-ct

@misama-ct misama-ct commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Summary

The published .d.ts of three compound components — @commercetools-uikit/constraints, @commercetools-uikit/radio-input, and @commercetools-uikit/view-switcher — leak a reference to the aggregate preset package:

// before (20.6.0 / 20.6.1)
Group: { (props: import("@commercetools-frontend/ui-kit").TGroupProps): JSX.Element;  }

Strict-TS consumers that install only the granular @commercetools-uikit/* packages don't have @commercetools-frontend/ui-kit, so the reference resolves to any, contextual handler typing is lost, and noImplicitAny flags it as TS7006 — on what looks like a routine minor bump. This was discovered downstream in commercetools/identity#757 (RadioInput.Group onChange handlers).

Root cause

Not an API change — source is byte-identical and tsc alone emits the correct relative specifier. With preserveSymlinks: false (set in FEC-935 so typecheck resolves @storybook/* subpaths), TypeScript's declaration emit follows pnpm's symlinks, discovers that the leaf type is also reachable through the node_modules package @commercetools-frontend/ui-kit (which re-exports it), and prefers the bare node_modules specifier over the relative path — a deliberate, long-standing TypeScript behavior.

That package is only present in node_modules because it became a root devDependency (added in FEC-938 so the .visualroute/bundlespec fixtures resolve under strict pnpm). Verified by toggling the single variable — with the symlink present the emit leaks; with it removed (everything else held constant, including preserveSymlinks: false) the emit is correct:

condition emitted specifier
meta-package symlink present (current main) import("@commercetools-frontend/ui-kit").TGroupProps
meta-package symlink hidden during emit import("./radio-group.js").TGroupProps

Fix (minimally invasive)

Belt and suspenders:

  • The beltscripts/build.sh hides the meta-package symlink for the preconstruct build step only (preconstruct doesn't need it — the fixtures aren't part of the dts program), then restores it (trap-guarded so it's restored even if the build fails). No source, component API, or preserveSymlinks change; the .visualroute/bundlespec fixtures are untouched.
  • The suspenders — after emit, the build asserts that no granular package's published declarations reference the aggregate preset by its bare specifier, and fails the build if one does. This future-proofs the workaround against being dropped, preconstruct changing its resolution, or a new compound component reintroducing the pattern. The check uses find (not a ** glob) so it covers packages at every nesting depth.

This is the smallest change that produces correct published types, and it's the right trade-off for a legacy codebase: it touches one build script and leaves every TypeScript config, dependency, and source file exactly as it is.

Alternatives considered (and why not)

paths cannot fix this — TypeScript's paths changes module resolution, never declaration emit. The two structural options were both heavier and were rejected:

  • Flip preserveSymlinks: true (globally, or via a dedicated emit tsconfig). It does fix the specifier, but it reintroduces the full FEC-935 typecheck breakage (~270 errors, including in published component source, not just stories), so it breaks pnpm typecheck and the IDE. Decoupling it for emit-only isn't clean either: preconstruct requires a custom-named tsconfig to exist in every package directory (it doesn't walk up to the root), and VS Code can't be pointed at a non-default tsconfig — so the editor would still surface the ~270 errors.
  • Remove the root @commercetools-frontend/ui-kit devDependency and repoint the 71 .visualroute/bundlespec fixtures off the aggregate onto granular packages. This is the most structurally "correct" fix (the symlink would never exist), but it's 71 mechanical edits plus a Percy visual-regression re-run — disproportionate to a one-line build-step problem.

The chosen fix implements the same underlying strategy as the structural option — keep the aggregate package out of the leaf packages' declaration-emit graph — but does it at build time with no churn.

Known limitation

A bare pnpm preconstruct build run outside scripts/build.sh would still leak, and the regression guard only runs as part of scripts/build.sh. The release path goes through pnpm build, so published artifacts are both fixed and guarded.

Verification

Full pnpm build run on this branch:

  • ✅ build green; meta-package symlink restored afterward
  • ✅ leaked specifiers across the whole published cohort: 0 (was 3)
  • ✅ the 3 packages now emit relative specifiers:
    • radio-inputimport("./radio-group.js").TGroupProps, import("./radio-option.js").TOptionProps
    • constraintsimport("./horizontal/index.js").THorizontalProps
    • view-switcherimport("./view-switcher.js").TViewSwitcherProps
  • ✅ regression guard tested both ways: passes on the fixed dist, and fails the build (naming the offending file) when a leak is injected

Release

Patch changeset included. The cohort is a fixed changeset group, so the whole 20.6.x line republishes together — every package is rebuilt with the fixed build, so the fix propagates cohort-wide.

Refs: commercetools/identity#757

@misama-ct misama-ct requested a review from a team as a code owner June 10, 2026 12:28
@changeset-bot

changeset-bot Bot commented Jun 10, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 7f3ccdf

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

This PR includes changesets to release 97 packages
Name Type
@commercetools-uikit/constraints Patch
@commercetools-uikit/radio-input Patch
@commercetools-uikit/view-switcher Patch
@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/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/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/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

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

vercel Bot commented Jun 10, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
ui-kit Ready Ready Preview, Comment Jun 11, 2026 1:48pm

Request Review

… published types

The compound components (Constraints, RadioInput, ViewSwitcher) emitted
import("@commercetools-frontend/ui-kit").T...Props in their published .d.ts.
Strict consumers installing only the granular @commercetools-uikit/* packages
don't have the aggregate preset, so the reference resolved to any (TS7006).

Root cause: @commercetools-frontend/ui-kit is a root devDependency (so the
.visualroute/bundlespec fixtures resolve under strict pnpm), which makes the
bare specifier resolvable during preconstruct's declaration emit, and TS prefers
it over the in-package relative path. The build now hides that symlink for the
preconstruct build step only (trap-guarded restore), forcing the correct
relative specifier. No component API or source changed.

Refs: commercetools/identity#757
Add a post-emit regression guard to scripts/build.sh that fails the build
if any granular package's published declarations reference the aggregate
@commercetools-frontend/ui-kit preset by its bare specifier. Future-proofs
the FEC-938 declaration-emit workaround against being dropped, preconstruct
changing resolution, or a new compound component reintroducing the pattern.

Also reword the changeset to be consumer-facing.
@valoriecarli valoriecarli force-pushed the fix/dts-emit-meta-package-leak branch from e3479a7 to 7f3ccdf Compare June 11, 2026 20:07
@valoriecarli valoriecarli merged commit 3a050ee into main Jun 11, 2026
7 checks passed
@valoriecarli valoriecarli deleted the fix/dts-emit-meta-package-leak branch June 11, 2026 20:13
@ct-changesets ct-changesets Bot mentioned this pull request Jun 11, 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.

3 participants