Design System Component Deprecation: Usage Inventory, Codemod Proof, and Phased Removal
Adding `@deprecated` does not mean consumers have migrated. This article uses a versioned lifecycle registry, static and runtime usage closure, idempotent Codemods, and visual and accessibility gates to make legacy component removal measurable and reversible.
Key Points
- Topic Focus: React、TypeScript、开源
- Suitable for: indie hackers, full-stack engineers, AI application developers
- Core value: quickly understand background, practical paths, common pitfalls, and engineering advice
The design system marks LegacyModal as @deprecated, and searches in the main repository already return zero results. Yet after the major release, some micro-frontends still load it through wrapper components, aliased imports, or old lockfiles. In another class of incident, a Codemod changes Button kind="danger" to the new prop and type checking passes, but keyboard focus and the accessible name during loading are lost. Design Systems and Design Tokens explains tokens, component layering, and general versioning strategies. This article focuses specifically on governing the complete lifecycle of a concrete component API, from its deprecation declaration to its physical removal.
SemVer requires incompatible changes to public APIs to increment the major version, but a major release does not migrate consumers for you. npm's deprecate command attaches a warning to a version; it does not withdraw that version, and it should not be used to mark an entire package that remains usable. Component-level lifecycles require a machine-readable contract finer-grained than package versions, connecting source code, build artifacts, and the actual consumer inventory.
Establish componentLifecycle.v1
The design system repository publishes an immutable designSystem.componentLifecycle.v1. Each entry uses a stable componentId rather than an export name that may change. It records the package, symbol and export path, ACTIVE/DEPRECATED/BLOCKED/REMOVED state, initial deprecation version, earliest removal version and date, replacement component, prop mappings, behavioral differences, Codemod digest, owner, support matrix, and approved exceptions. The registry, package tarball, type declarations, and documentation build all use the same release digest.
componentId: overlay.legacy-modal
state: DEPRECATED
deprecatedIn: 8.3.0
removalNotBefore: 10.0.0
replacement: overlay.dialog
codemod: sha256:09c...
exceptionsExpireAt: 2026-10-01
State may advance only in one direction. An accidental deprecation is corrected through a special event that restores ACTIVE in a new registry generation; historical records are never rewritten. When a reader encounters an unknown state, a missing replacement, or an expired exception, it returns UNKNOWN and routes the case for human review rather than counting it as migrated. A component becomes a removal candidate only after both its removal date and version conditions are satisfied, preventing release schedule changes from shortening the migration window.
Freeze the Managed Universe Before Computing the Consumption Closure
First publish an immutable consumerScope.v1: enumerate every managed repository ID, default branch, workspace, package registry, published micro-frontend directory, explicit exclusion, and owner, then compute scopeRoot over the canonicalized universe. A missing repository does not mean "zero consumption"; it means the scope changed and requires renewed approval. Scan source code, Storybook, tests, MDX, generated templates, and dependency graphs for published packages. Resolve ESM/CJS imports, re-exports, aliases, JSX member expressions, and wrappers. Mark dynamic loading as UNKNOWN when it cannot be proven. Every snapshot pins the Git tree, lockfile, parser, and registry generation, and all results together form snapshotRoot. Planning Changes with a Repository Dependency Graph provides candidate edges at the package level; this process must also identify the exact components and props involved.
The Codemod uses ASTs and type information to produce a plan before writing changes by cohort. It must preserve comments, keys, refs, and event order, while props that cannot be determined because of JSX spreads go onto a manual-review list. When run twice consecutively, the second run must produce an empty diff. A reverse adapter or Git revert must restore the old usage. jscodeshift is suitable for executing bulk transformations, but the matcher, parser, versions, and input digests must all be pinned. AST Codemod Refactoring Invariants defines constraints for general transformations; this migration adds component-specific semantic acceptance criteria.
Passing type checks is only the first layer. Every approved scenario runs the old and new components side by side, comparing user events and state transitions, focus entry and restoration, ARIA roles/names/states, keyboard interactions, RTL and multi-theme screenshots, responsive layouts, and bundle changes. Screenshots must use pinned fonts and browsers, with masks declared for dynamic regions. Differences above the threshold may be explained only by human approval with an expiration date. If the replacement component intentionally changes behavior, the migration decision must be listed in the registry instead of being hidden by widening screenshot tolerances.
Deliberately Attack Paths That "Do Not Appear in Search"
Failure fixtures cover barrel re-exports, renamed imports, memo/forwardRef, higher-order components, JSX spreads, dynamic maps, MDX, legacy micro-frontends, two lockfiles, interrupted Codemods, and simultaneous loading of old and new CSS. Also remove a repository from the scope, swap prop defaults, delete aria-describedby, and make a consumer install an older dependency version. The gates must detect scope drift, unknown consumption, partial migration, behavioral differences, and dependency rollback. After entering BLOCKED, every managed consumer build uses the latest snapshot as its baseline. Any reference to either the new or old component where prohibited, legacy dependency, or new exception fails immediately; it is not enough merely to prevent the design system repository from adding another export.
Before entering BLOCKED, require all of the following: managed snapshots no more than 24 hours old; 100% usage classification; a zero diff from the second Codemod run; 100% interaction and accessibility assertions; zero unapproved visual differences and zero UNKNOWN results; zero legacy calls across two consecutive releases; and at least a 98% successful automated migration rate across 50 representative repositories. Removal proof must bind the approved scopeRoot, repository-wide snapshotRoot, registry generation, and digest of the package tarball awaiting release. Any change to a repository, lockfile, registry, or tarball invalidates the proof and triggers a rescan. After REMOVED, use the legacy fixture again to prove that the build fails explicitly rather than resolving to the wrong export.
Roll out releases by repository cohort: begin with warnings only, then provide a compatibility adapter, block legacy usage in new code, and only then remove the export. If the error rate rises, a focus regression occurs even once, unknown consumption reappears, or the automated migration failure rate exceeds 2%, point the registry alias back to the previous generation, restore the old export and matching styles, and pause the cohort. Do not unpublish released packages. Compatibility Gates for Dependency Upgrades and Lockfiles ensures that consumer lockfile rollbacks remain consistent with artifacts, while Accessible React Components provides the baseline for focus and semantic assertions. Deprecation then becomes an observable migration contract rather than a warning that everyone eventually stops reading.
Related
Related Content
Article
Self-Hosting Open-Source LLMs: Getting Started with Ollama, vLLM, and llama.cpp
Technical trends, engineering practices, and an independent developer's perspective on LLMs and open source.
Article
Is Next.js App Router a Good Fit for a Technical Community?
Technical trends, engineering practices, and independent development perspectives centered on Next.js and React.
Article
SEO Design for a Content Community with Next.js
Technical trends, engineering practices, and an indie developer's perspective on Next.js and TypeScript.
Article
Learning Technical Community Product Design from dev.to
Technical trends, engineering practices, and an independent developer's perspective on independent development and open source.