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
  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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 dashboard
find, transform, verify, report
  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.