Part 4 · 2 chapters · ~20 min
Design-System Distribution
A design system consumed from source by a thousand call sites: the codemod written before the change, a rollout through bots and OWNERS with a lint that becomes an error on a date, a dashboard that is the roadmap, tokens as a cross-platform release, the metrics, and the anatomy of a codemod from find to report.
9
A design system with a thousand consumers
at head, the migration is the release
- Consumed from source at head (part 1): one version, every consumer on it, no pinning, no semver. A breaking change is a thousand call sites in one commit, or a compatibility period with both APIs and a migration. The Architecture course part 4 covered the registry-and-semver version at smaller scale.
- The codemod comes first: write it, dry-run it, read the conversion rate (94% mechanical, 60 manual), then design the rollout. A change whose codemod cannot be written needs a long compatibility period or should not happen.
- The rollout is part 2's machinery applied to a prop: both APIs work and the old warns; bot PRs per OWNERS directory with the report; the manual cases paired with a system-team engineer; a lint error for new code on the date; deletion at zero. Two weeks for a rename across a thousand sites; a year by memo.
- The dashboard is the roadmap: usage by team and prop, deprecated props remaining, overrides (CSS targeting the system's classes; local re-implementations), adoption of the latest patterns, accessibility findings per surface. The most overridden component is the one with the wrong API.
- Tokens and themes: one source compiled to web, iOS, Android and email; themes as token sets; a token change is a visual release across the company, through the train, with visual regression on the stories and sampled real screens.
- The metrics: percentage of UI from the system (instance counts from a usage scanner), release-to-full-adoption time, override trend, accessibility findings, and the team's migration share of time (a ratio that says whether the API is stable).
A DESIGN SYSTEM WITH A THOUSAND CONSUMERS
versioning, codemods, deprecation with a date, and the metrics that say it is working
swipe the figure sideways, or tap expand for full screen
1/6
the consumers
The consumers: a thousand call sites of Button across a hundred teams, in a monorepo (part 1) where the system is consumed from source at head: there is one version, the current one, and every consumer is on it. A breaking change is therefore a change to a thousand call sites in one commit, or a compatibility period with both APIs and a migration. There is no "consumers pin an old version"; the trunk is the version.
10
The anatomy of a codemod
code
// rename-prop codemod (jscodeshift): find Button from the design system, rename variant → appearance, map literal values, report the rest
import type { API, FileInfo, JSXAttribute } from 'jscodeshift'
const MAP: Record<string, string> = { primary: 'accent', secondary: 'default', danger: 'critical' }
export default function transform(file: FileInfo, api: API, options: { report: (f: string, line: number, why: string) => void }) {
const j = api.jscodeshift; const root = j(file.source); let changed = false
// 1. find: the local name bound to Button imported from the design system (handles `import { Button as DSButton }`)
const local = root.find(j.ImportDeclaration, { source: { value: '@org/design-system' } })
.find(j.ImportSpecifier, { imported: { name: 'Button' } }).nodes().map(n => n.local?.name ?? 'Button')
if (!local.length) return null // no design-system Button here: untouched
root.find(j.JSXOpeningElement).filter(p => local.includes((p.node.name as any).name)).forEach(p => {
const attr = p.node.attributes?.find(a => a.type === 'JSXAttribute' && a.name.name === 'variant') as JSXAttribute | undefined
if (!attr) return
const v = attr.value
// 2. transform: literals and ternaries of literals are mechanical; anything else is reported with a line, never guessed
if (v?.type === 'StringLiteral' && v.value in MAP) { attr.name.name = 'appearance'; v.value = MAP[v.value]; changed = true; return }
if (v?.type === 'JSXExpressionContainer' && v.expression.type === 'ConditionalExpression'
&& v.expression.consequent.type === 'StringLiteral' && v.expression.alternate.type === 'StringLiteral') {
attr.name.name = 'appearance'; v.expression.consequent.value = MAP[v.expression.consequent.value] ?? v.expression.consequent.value
v.expression.alternate.value = MAP[v.expression.alternate.value] ?? v.expression.alternate.value; changed = true; return
}
options.report(file.path, attr.loc?.start.line ?? 0, v?.type === 'JSXExpressionContainer' ? 'dynamic value' : 'unknown shape') // 3. report
})
return changed ? root.toSource({ quote: 'single' }) : null // recast preserves formatting: the diff is only the change
}
// run: jscodeshift -t rename-variant.ts $(code-search 'import .* from "@org/design-system"' --files) --dry → the conversion rate, before announcing
// then: the bot splits the diff by OWNERS directory, opens PRs with the report, runs tsc + affected tests, and tracks the dashboardfind, transform, verify, report
- Find is a resolution problem: a JSX element whose name resolves to Button imported from the design system (not a local Button; through aliases; through barrels only with resolution). Code search gives candidate files; the codemod confirms per node.
- Transform literals and ternaries of literals mechanically; never guess at expressions (a variable, a spread, a forwarded prop): mark and report them with a line. Print with formatting preserved so the diff is only the change.
- The long tail, known from the dry run: string literals 80%, ternaries 8%, variables bound to literals 4% (mechanical with a rule), wrappers 5% (their own codemod), spreads 2% and dynamic values 1% (a human). The codemod handles the first three and reports the rest.
- Verify: the type-check (the new prop's type catches wrong values), the exact affected tests (part 1), the visual regression suite; results in the bot's PR. A failing codemod PR is information: a transform bug or a site that needed a human, and the PR says which.
- Report: per file changed, unchanged, or skipped with a reason and a line; aggregated by owner with links. The report feeds the dashboard, is the to-do list, and is the evidence for the deprecation date (60 manual at two a day is six weeks). Skipped without a reason is the only unacceptable outcome.
- Reuse: the skeleton (resolution, formatting-preserving printing, verification, reporting) is shared; a library of transforms (rename a prop, change a default, move an import, wrap a call) makes a new migration one new function. Writing codemods is the framework and design-system teams' core skill and the most transferable one in this course: a team of thirty can keep the same library.
THE ANATOMY OF A CODEMOD
find, transform, verify, report: the AST transform that moves a company
swipe the figure sideways, or tap expand for full screen
1/6
find
Find: a pattern over the AST: JSXElement whose name resolves to Button imported from @org/design-system (not any Button: resolve the import), with a JSXAttribute named variant. Resolution matters: a local component also named Button, or a re-export through a barrel, must be handled or reported. The code-search index (part 1) gives the candidate files; the codemod confirms per node.