Jump to content
Main menu
Main menu
move to sidebar
hide
Navigation
Main page
Recent changes
Random page
freem
Search
Search
Appearance
Create account
Log in
Personal tools
Create account
Log in
Pages for logged out editors
learn more
Contributions
Talk
Editing
Openai/695d28c7-94c8-8006-836b-96dbe559b5eb
Add languages
Page
Discussion
English
Read
Edit
Edit source
View history
Tools
Tools
move to sidebar
hide
Actions
Read
Edit
Edit source
View history
General
What links here
Related changes
Special pages
Page information
Appearance
move to sidebar
hide
Warning:
You are not logged in. Your IP address will be publicly visible if you make any edits. If you
log in
or
create an account
, your edits will be attributed to your username, along with other benefits.
Anti-spam check. Do
not
fill this in!
{{DISPLAYTITLE:Monorepo UI Component Versioning}} ''This conversation was exported from ChatGPT''<ref>{{cite web|title=Monorepo UI Component Versioning|url=https://chatgpt.com/share/695d28c7-94c8-8006-836b-96dbe559b5eb|publisher=ChatGPT|access-date=2026-01-06}}</ref> == gpt-5-2-thinking == === User: This is a question from Software Engineering Stack Exchange: === This is a question from Software Engineering Stack Exchange: ’m working on a very large frontend project organized as a single monorepository. We have a components/ directory that contains 30+ shared UI components. These components are used across a large number of features, and many independent teams work on different features in parallel. The core problem: Any change to a shared component can potentially cause regressions across the entire product, which makes QA extremely expensive and slow. Because of this, teams are afraid to modify shared components, even when improvements or fixes are needed. We must keep a single repository (splitting shared components into a separate repo is considered a bad option due to the number of teams and coordination overhead). Different teams should be able to work independently without blocking each other. Changes to shared components must not trigger full-product regression testing by default. The solution should scale to dozens of components and teams over time. I’m looking for architectural patterns or proven approaches to organizing shared UI components in a large monorepo. I stand with versioning of components (can have Button, Button.v2 Button.v3 and etc), what are the pros and cons I might encounter? I’m especially interested in real-world approaches used in large-scale frontend systems, not just theoretical ideas. If helpful, the frontend stack is React + TypeScript, but I’m more interested in architectural patterns than framework-specific solutions. Any guidance, references, or examples would be greatly appreciated. === Assistant: You can make this scale in a monorepo, but you usually need two things at the same time: === You can make this scale in a monorepo, but you usually need two things at the same time: # A real “package boundary” for the shared UI library (or libraries) so consumers do not silently pick up breaking changes. # A CI strategy that only tests what is affected, plus strong component-level automated checks, so you stop paying “full product QA” for every change. Below are patterns that actually work in large React + TS monorepos. ==== What it is: Keep one repo, but model components/ as one or more workspace packages, published to an internal registry (or at least versioned and consumed like packages). Use semantic versioning and a release tool (Changesets is a common choice in TS monorepos). changesets-docs.vercel.app<ref>{{cite web|title=changesets-docs.vercel.app|url=https://changesets-docs.vercel.app/|publisher=changesets-docs.vercel.app|access-date=2026-01-06}}</ref> ==== Key property: A feature team only gets a component change when they choose to bump the dependency. ===== - Teams work independently: Component changes do not immediately affect every consumer. Consumers update on their timeline. ===== * No default full regression: Your default is “component tests + affected consumer tests,” not “entire product QA.” * Scales to many teams: Versioning + changelog + deprecation policy becomes the coordination mechanism. ===== - packages/ui (design system components) ===== * optionally packages/ui-tokens, packages/ui-icons, packages/ui-utils * apps/features depend on @org/ui with a pinned semver range * updates happen via automated “dependency bump” PRs (Renovate-style workflow) ===== - Blast radius control (biggest win) ===== * Clear contracts: public API surface becomes explicit * Natural deprecation/migration: old versions can live while teams migrate ===== - You are signing up to maintain versions (release notes, deprecations, support window) ===== * Local dev ergonomics can be trickier if you are not using workspace tooling well * Potential for version skew: different parts of the product on different UI versions (you mitigate this with “supported versions” policy and scheduled migrations) ==== Even with package boundaries, you still want to avoid massive CI runs. ==== Tools like Nx compute “affected projects” (changed projects plus dependents) so CI only runs the relevant builds/tests. Nx<ref>{{cite web|title=Nx|url=https://nx.dev/docs/features/ci-features/affected|publisher=nx.dev|access-date=2026-01-06}}</ref> In Bazel ecosystems, the same idea exists via dependency queries and “diff-to-affected-targets” tooling (for example, bazel-diff). GitHub<ref>{{cite web|title=GitHub|url=https://github.com/Tinder/bazel-diff|publisher=github.com|access-date=2026-01-06}}</ref> Reality check: if you change a core Button that everything depends on, “affected” might still be huge. That is not a tooling failure, it is telling you the truth about coupling. The fix is usually more boundaries (split packages) and/or more backward compatibility. ==== If the fear is “we will break unknown screens,” you need fast, trustworthy signals closer to the component library: ==== * Storybook as the contract surface * Visual regression on stories (Chromatic, Percy, etc.). Chromatic integrates with Storybook and adds PR checks for UI changes. chromatic.com<ref>{{cite web|title=chromatic.com|url=https://www.chromatic.com/storybook|publisher=chromatic.com|access-date=2026-01-06}}</ref> * Component unit tests (Testing Library), accessibility checks (axe), and type-level API tests (for TS public props) This does not eliminate all integration risk, but it dramatically reduces “manual QA for every tweak.” ==== In big orgs, the technical setup fails without governance: ==== * Component owners/maintainers (not necessarily gatekeepers, but accountable) * Backward-compatibility rule by default * Breaking-change RFC and a defined support window * Deprecation workflow: warn in console/dev builds, codemods, migration guide * “No breaking changes on Friday” style operational rules if needed This is how you keep velocity without making the shared library radioactive. === This is a real pattern, but it has sharp edges. === ===== - Zero-risk rollout for consumers: old stays old, new opts in ===== * Great for big redesigns where compatibility would be messy * Good when migration needs to be gradual across many teams ===== - Permanent maintenance tax: bug fixes, a11y fixes, and behavior changes now have to be duplicated or backported across variants ===== * Inconsistent product UX: you end up with mixed button styles/behaviors across the app * Harder to remove old versions: teams stop prioritizing migrations because “it still works” * API fragmentation: docs, examples, and developer intuition get worse over time ===== Instead of Button.v2 as a naming convention, prefer one of these: ===== * Package major versions (@org/ui@1 vs @org/ui@2) with semver and a support window (cleaner contract) * New component name when semantics change (PrimaryButton/Button2 is not great, but sometimes Button vs ActionButton is legitimately clearer) * Adapter layer: keep Button as a thin wrapper over ButtonNext during migration ==== If I had to pick one approach that matches your constraints: ==== # Make shared UI a workspace package (or a few packages) with semantic versioning (Changesets is a common workflow). changesets-docs.vercel.app<ref>{{cite web|title=changesets-docs.vercel.app|url=https://changesets-docs.vercel.app/|publisher=changesets-docs.vercel.app|access-date=2026-01-06}}</ref> # Consumers pin versions and update via automated bump PRs. # Require Storybook stories for every component and run visual regression on PRs. Storybook<ref>{{cite web|title=Storybook|url=https://storybook.js.org/docs/writing-tests/visual-testing|publisher=storybook.js.org|access-date=2026-01-06}}</ref> # Use affected-only CI so the default pipeline is fast. Nx<ref>{{cite web|title=Nx|url=https://nx.dev/docs/features/ci-features/affected|publisher=nx.dev|access-date=2026-01-06}}</ref> # Establish a deprecation and support policy (for example: “we support the last 2 major UI versions; older majors get security/a11y fixes only, then are removed on a schedule”). That combination gives you independence, reduces QA cost, and scales socially. If you want, I can sketch a concrete repo layout + CI pipeline outline (GitHub Actions style) for this setup, including how teams consume a pinned UI version while still developing locally against workspace packages. === User: Is the actual problem that they're using a monorepo? === Is the actual problem that they're using a monorepo? === Assistant: Not really. === Not really. A monorepo can make it easier to accidentally ship breaking UI changes everywhere at once, but the underlying problem is usually: * No real boundary between “library” and “consumers.” If every app/feature imports shared components as “just another folder,” any change is instantly live everywhere. * No release/compatibility contract. Without semantic versioning (even inside a monorepo), there’s no “opt in” moment for consumers. That’s what forces “test the whole world” every time. * Insufficient automated signals at the component layer. If you don’t have strong Storybook coverage, visual regression, a11y checks, and interaction tests, the only way to be confident is broad manual regression. * Coupling is too high. A Button used by everything should require a lot of confidence to change. If it’s not designed for backward compatibility, every tweak becomes a product-wide risk. A multirepo can hide this temporarily (because updates don’t propagate automatically), but it doesn’t fix the root issue. It just forces a boundary by making dependency updates explicit. You can get the same benefits in a monorepo by treating shared components as versioned packages (workspace package + semver) and adopting an “affected-only” CI strategy plus strong component-level regression testing. So: the monorepo isn’t the problem; the lack of boundary + contracts + tests + upgrade mechanics is. === User: Do your suggested changes fix those problems, or do they need to step back and actually make enforceable contracts? === Do your suggested changes fix those problems, or do they need to step back and actually make enforceable contracts? === Assistant: My suggestions only work if they create enforceable contracts. === My suggestions only work if they create enforceable contracts. The tools (workspace packages, semver, “affected” CI, Storybook/visual tests) are just the mechanics that make a contract real. If they only “reorganize folders” inside the monorepo, nothing is fixed. ==== At minimum, it has to be true that: ==== # There is a defined public API surface * Only exported components/types are allowed for consumers. '' Consumers cannot reach into internal files (components/Button/internal/''). * Styling tokens, CSS variables, DOM structure expectations are either part of the contract or explicitly not. # Breaking changes are detectable and gated * A change that would break consumers requires an explicit “major” bump. * PR checks can fail if someone introduces a breaking change without bumping appropriately. # Consumers can opt in * They don’t automatically get breaking changes “because main changed.” * Upgrading is an explicit dependency bump (even inside a monorepo). # There’s a support/deprecation policy * You can’t keep Button.v1 forever. You need a window: “2 majors supported” or similar. ==== ### ==== Yes — this is the core contract mechanism. It makes upgrades explicit and creates a place to put “this is breaking” vs “this is safe.” Without it, you’ll always have fear-driven QA. ===== Helpful, but it’s not the contract. It just prevents wasting time when changes are localized. If you change a widely-used component, “affected” will still be big, and that’s correct. ===== ===== This is how you validate the contract cheaply. It reduces the need for manual full-product QA for non-breaking changes. But it doesn’t replace versioning. ===== So the real answer is: they need contracts, and the suggested architecture is how you enforce them. ==== If they want the shortest path to “teams stop being afraid”: ==== # Turn <code>components/</code> into a package (@org/ui) with strict exports * Enforce with lint rules like “no deep imports” and TS path restrictions. * Treat anything not exported as private. # Adopt semver releases inside the monorepo * Every component change comes with a patch/minor/major classification. * Consumers update by bumping the package version (can be automated PRs). # Define “what is breaking” Examples that are usually breaking: * removing/renaming props * changing default behavior in a user-visible way * changing DOM in a way tests or CSS selectors rely on * token changes that alter appearance beyond tolerance # Add fast regression at the component boundary * Visual snapshots on every story (PR-gated) * a11y checks * interaction tests for key components # Set a deprecation window * “We support the last 2 majors.” * Old majors get only critical fixes (security/a11y), then get removed on a schedule. ==== If their “shared components” are actually a grab-bag of UI + business rules + feature-specific behavior, contracts will keep failing. In that case, they need to split the library (still in the monorepo) into: ==== * pure primitives (Button, Input, Modal) * composed design-system patterns (FormField, DataTable wrapper) * feature-level components (should live with the feature) That’s also contract work: deciding what’s truly shared. If you want, I can write what a minimal contract doc looks like (“UI library compatibility policy”) plus the CI gates that enforce it. === User: Show me that. === Show me that. === Assistant: Below is a “minimal but enforceable” contract doc you can drop into the repo e.g., packages/ui/COMPATIBILITY.md, follow… === Below is a “minimal but enforceable” contract doc you can drop into the repo (e.g., packages/ui/COMPATIBILITY.md), followed by concrete CI gates (GitHub Actions + a couple config snippets) that actually enforce it. ==== UI Library Compatibility Policy (Contract) ==== <syntaxhighlight lang="md"># UI Library Compatibility Policy (Contract) Applies to: @org/ui (and any sibling packages like @org/ui-tokens) Goal: Component teams can ship improvements without forcing full-product regression testing, while consumer teams can upgrade on their schedule with predictable risk. ==== 1) What is Public API vs Private ==== ===== Public API ===== Only these are considered supported and stable: * Package entrypoints exported from: * <code>@org/ui</code> (root) '' <code>@org/ui/<subpath></code> ''only if explicitly listed in <code>package.json#exports</code>* * Types re-exported from public entrypoints. * CSS variables / design tokens explicitly documented in @org/ui-tokens. * Storybook stories are the canonical behavioral + visual examples for public components. ===== Private API (NO consumer dependencies) ===== Consumers must NOT import: * deep paths like <code>@org/ui/src/...</code> or <code>@org/ui/components/...</code> * any file not reachable via <code>package.json#exports</code> * internal DOM structure (e.g., <code>.kebab > div:nth-child(2)</code> selectors) * internal class names (unless explicitly documented as stable) * test ids unless explicitly documented as stable (prefer role/label-based selectors) Breaking this rule means the consumer owns the breakage risk. ==== 2) Semantic Versioning Rules ==== We use SemVer for @org/ui: ===== Patch (x.y.Z) ===== * bug fixes that do not change intended behavior * performance improvements * internal refactors * docs/story fixes * a11y improvements that do not change visual layout materially ===== Minor (x.Y.z) ===== * new components * new optional props with safe defaults * new variants/slots that do not change existing default rendering * additive token additions ===== Major (X.y.z) ===== Any change that can reasonably break consumers or change user-visible behavior by default: * removing or renaming exports, components, props, events * changing default prop values in a user-visible way * changing DOM in a way that breaks common integration (e.g., removes an element frequently targeted by consumer CSS) unless explicitly declared private * significant visual change outside defined tolerances * token changes that alter appearance beyond tolerances ==== 3) Definition of “Breaking” (Practical) ==== A change is breaking if it causes any of these: * TypeScript compile errors for consumers * runtime errors for consumers * layout/visual regressions outside approved tolerances * behavioral regressions (click, focus, keyboard nav, validation) * a11y regressions (color contrast, missing labels, keyboard trap, etc.) If in doubt, treat as breaking (major) or provide a compatibility path. ==== 4) Compatibility Paths (Preferred over Majors) ==== Before a major, prefer: * keep old prop, introduce new prop; deprecate old prop * support both old and new values for a release window * adapter/wrapper exports (temporary) to keep old behavior * codemod + migration guide ==== 5) Deprecation Policy ==== * Deprecated APIs remain supported for at least: * 2 minor releases OR 60 days (whichever is longer) within the same major. * Deprecated APIs must include: * JSDoc <code>@deprecated</code> with the replacement * a migration note in CHANGELOG * We support at most the last 2 majors at any time. * Older majors receive only critical security/a11y fixes, then are removed on schedule. ==== 6) Consumer Upgrade Policy ==== * Consumers pin versions (no auto-floating to new majors). * Upgrades happen via explicit dependency bump PRs. * Any component library major upgrade requires consumer-side validation. * Patch/minor upgrades should be safe by default; validation can be localized. ==== 7) Required Evidence for Changes ==== Every change to public components must include at least one: * updated Storybook story (new/changed behavior) * unit/integration test (Testing Library / Playwright component test) * visual regression approval (if visuals changed) ==== 8) Ownership ==== Each component has an owner group responsible for: * reviewing breaking changes * maintaining deprecations/migrations * ensuring test coverage is adequate </syntaxhighlight> ==== Enforced boundaries (the part that makes this real) ==== ===== 1) Hard export boundary via package.json#exports ===== In packages/ui/package.json: <syntaxhighlight lang="json">{ "name": "@org/ui", "version": "1.12.0", "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts", "exports": { ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" }, "./button": { "types": "./dist/button.d.ts", "default": "./dist/button.js" } } } </syntaxhighlight> This is the key: if it’s not in exports, it’s not importable (or at least you can make it fail in modern Node/bundlers). ===== 2) Ban deep imports with ESLint ===== In the consumer apps’ ESLint config: <syntaxhighlight lang="js">module.exports = { rules: { "no-restricted-imports": [ "error", { "patterns": [ { "group": ["@org/ui/''/''", "@org/ui/src/''", "@org/ui/dist/''"], "message": "Deep imports from @org/ui are forbidden. Import only from public entrypoints defined in @org/ui package exports." } ] } ] } }; </syntaxhighlight> (Adjust patterns to your repo conventions.) ===== 3) Require a Changeset for any UI public change ===== If you use Changesets, you can enforce “no changes to packages/ui without a changeset” in CI. ==== CI gates: what to run on every PR ==== ===== The checks you want ===== # Lint + typecheck (affected scope if you’re using Nx/Turborepo; otherwise run for ui + touched apps) # Unit/integration tests for @org/ui # Build <code>@org/ui</code> (so the export boundary is real) # Public API diff check (type-level breaking changes) # Storybook build + visual regression (Chromatic/Percy) # Changeset presence check for UI changes ===== GitHub Actions PR workflow (example) ===== <syntaxhighlight lang="yaml">name: PR Checks on: pull_request: jobs: ui_contract: runs-on: ubuntu-latest steps: * uses: actions/checkout@v4 * uses: pnpm/action-setup@v4 with: version: 9 * uses: actions/setup-node@v4 with: node-version: 20 cache: "pnpm" * run: pnpm install --frozen-lockfile # 1) Enforce no deep imports via lint * name: Lint run: pnpm -r lint # 2) Typecheck (catch breaking prop/type changes early) * name: Typecheck run: pnpm -r typecheck # 3) UI tests * name: UI tests run: pnpm --filter @org/ui test # 4) Build UI (ensures exports/types are generated and valid) * name: Build UI run: pnpm --filter @org/ui build # 5) Require a changeset if UI changed * name: Require changeset for UI changes run: pnpm changeset status --since origin/${{ github.base_ref }} # Optional: add a small script that fails if packages/ui changed but no changeset exists. # 6) Public API report/diff (example using API Extractor) * name: API report (detect breaking API) run: pnpm --filter @org/ui api:check # 7) Storybook + visual regression (Chromatic) * name: Chromatic uses: chromaui/action@v11 with: projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} workingDir: packages/ui exitOnceUploaded: true </syntaxhighlight> ==== Public API diff gate (TypeScript contract enforcement) ==== If you want a strong “did we break the public API?” check, a common approach is Microsoft’s API Extractor (it produces an API report you can diff). In packages/ui: ===== package.json scripts ===== <syntaxhighlight lang="json">{ "scripts": { "build": "tsc -p tsconfig.build.json", "api:check": "api-extractor run --local --verbose" }, "devDependencies": { "@microsoft/api-extractor": "^7.0.0" } } </syntaxhighlight> ===== api-extractor.json (minimal) ===== <syntaxhighlight lang="json">{ "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", "mainEntryPointFilePath": "<projectFolder>/dist/index.d.ts", "apiReport": { "enabled": true, "reportFileName": "ui.api.md" }, "docModel": { "enabled": false }, "dtsRollup": { "enabled": false } } </syntaxhighlight> Commit the generated API report (e.g., packages/ui/etc/ui.api.md). Now PRs that change the public API produce a diff that reviewers can treat as “the contract changed.” ==== What this buys them (and what it doesn’t) ==== * If they implement exports boundary + no-deep-import lint + semver/changesets + API diff + component-level visual regression, they’ve created an enforceable contract. * They will still sometimes have “large affected surface area” for foundational components, but now: - Breaking changes become explicit and opt-in (major bumps). - Non-breaking changes have strong automated confidence so you don’t default to full manual regression. If you want, I can add the “fail PR if packages/ui/** changed but no changeset exists” script (it’s ~20 lines in Node) and/or show the “two majors supported” policy operationalized (how to remove old majors without drama). == References == <references /> [[Category:ChatGPT Shares]] [[Category:openai]]
Summary:
Please note that all contributions to freem are considered to be released under the Creative Commons Attribution-ShareAlike 4.0 (see
Freem:Copyrights
for details). If you do not want your writing to be edited mercilessly and redistributed at will, then do not submit it here.
You are also promising us that you wrote this yourself, or copied it from a public domain or similar free resource.
Do not submit copyrighted work without permission!
Cancel
Editing help
(opens in new window)
Templates used on this page:
Template:Cite web
(
edit
)
Module:Citation/CS1
(
edit
)
Module:Citation/CS1/COinS
(
edit
)
Module:Citation/CS1/Configuration
(
edit
)
Module:Citation/CS1/Date validation
(
edit
)
Module:Citation/CS1/Identifiers
(
edit
)
Module:Citation/CS1/Utilities
(
edit
)
Module:Citation/CS1/Whitelist
(
edit
)
Module:Citation/CS1/styles.css
(
edit
)