Stadium — Monorepo SVG Sprite Toolchain & Safe Codemod Migration
End-to-end 5-phase icon toolchain converting 418 SVGs into 417 typed React components, per-app static usage scanning, deterministic sprite generation, and dry-run codemod tooling.
Role
Design System Infrastructure & Metaprogramming · Vinsol | Stadium
Status
Domain
Systems & Architecture
Deployment
Design System MigrationKey Outcomes & Verified Highlights
- Built an automated build pipeline that optimizes and bundles over 400 icons into clean SVG sprites, removing external font downloads.
- Wrote an AST codemod with jscodeshift to automatically and safely convert legacy <i> icon font tags into typed <Icon> components across the codebase.
- Enforced proportional 1em dimensions so icons never cause layout shifts (CLS 0.000) or flicker while the page is rendering.
- Generated separate sprite sheets for each app, ensuring each micro-frontend only loads the icons it actually uses.
01 — Context & Architectural Problem
Stadium's monorepo powers four distinct storefronts — SwagMagic, Stadium Shops, Stadium Dashboard, and Stadium Locker — alongside the shared @stadium/ui component library. Historically, iconography was delivered via icomoon font files: a global stadium-icons.woff2 font loaded on every page, with icons rendered as empty <span className="icon-close" /> elements styled via font glyphs.
In a modern Next.js monorepo, this legacy approach introduced three critical architectural issues:
- Monolithic Bundle Inefficiency: All 418 icons were downloaded on every single page load across every application—even on checkout pages requiring only 15–20 core icons.
- First-Paint FOUC & Zero Accessibility: Icon font glyphs are invisible until CSS and web fonts finish loading, causing Flash of Unstyled Content (FOUC). Screen readers cannot interpret empty
<span>tags, leaving iconography completely inaccessible. - Fragile String Contracts:
className="icon-close"is an untyped string literal. Renaming or deprecating an icon resulted in silent UI breakage in production with zero TypeScript compile-time warnings.
The Solution: An automated 5-phase toolchain that migrates iconography from global fonts to scoped, deterministic SVG sprite sheets paired with type-safe React components and a safe, dry-run AST/regex codemod.
02 — The Five-Phase Architectural Pipeline
Rather than relying on manual file editing or runtime SVG inlining (which balloons DOM weight), the migration toolchain runs as a deterministic build-time pipeline:
[ Raw Icomoon ZIP ]
│
▼
[ Phase 1: SVG Extraction & SVGO Optimization ]
│
▼
[ Phase 2: Typed Component Factory Generation ] ───► @stadium/ui (Type-safe imports)
│
▼
[ Phase 3: Monorepo Static Usage Scanner ]
│
├───────────────────────────────┐
▼ ▼
[ Per-App Scoped Sprites ] [ Dev / Storybook Sprite ]
(e.g., checkout.sprite.svg) (full.sprite.svg: 418 icons)
│
▼
[ Phase 4: Dry-Run JSX Codemod ] ───► jsx-codemod-report.json (Safe transforms)
│
▼
[ Phase 5: Monorepo Orphan & Legacy Audit ]Toolchain Command Orchestration
pnpm icon:extract # Phase 1: Extract & sanitize SVGs from source catalog
pnpm icon:generate # Phase 2: Emit typed React components & barrel exports
pnpm icon:build # Phase 3: Scan workspace imports & build scoped sprite sheets
pnpm icon:codemod-jsx # Phase 4: Dry-run codemod (requires --write to apply)
pnpm icon:audit # Phase 5: Flag orphaned legacy icon-* classNames03 — Component Architecture: Consumer DX vs. Rendered DOM
Inlining full SVGs into React components increases bundle size and duplicates SVG path data whenever an icon appears multiple times on a page.
To achieve zero DOM duplication, the system uses a Centralized Component Factory that renders SVG <use> references pointing to symbol IDs inside the application's sprite sheet.
Consumer Experience vs. Browser Footprint
// 1. Consumer Developer Experience (Type-Safe, Auto-completing)
import { IconChevronLeft } from "@stadium/ui/icons";
export function PaginationControl() {
return <IconChevronLeft className="w-5 h-5 text-foreground-muted" />;
}
// 2. Browser DOM Output (Ultra-lightweight symbol reference)
<svg className="w-5 h-5 text-foreground-muted" aria-hidden="true">
<use href="#icons--chevron-left" />
</svg>Icon Family Namespaces
Icons are strictly partitioned into three isolated namespaces to prevent naming collisions and manage ownership:
| Family | Component Prefix | Symbol ID Scheme | Scope |
|---|---|---|---|
| Core UI | <Icon* /> | #icons--{slug} | General design system UI (~300 icons) |
| Navigation | <NavIcon* /> | #nav--{slug} | Storefront navigation & mobile menus |
| Prismic CMS | <PrismicIcon* /> | #prismic--{slug} | Marketing & dynamic CMS landing pages |
04 — Per-App Static Usage Analysis & Sprite Scoping
Shipping a monolithic sprite containing all 418 icons defeats the performance benefits of code splitting. The core of Phase 3 is a Monorepo Static Scanner that inspects each storefront's source tree to determine its exact icon dependency set.
Static Detection Vectors
The scanner handles three coexisting generation patterns during the gradual migration:
| Detection Vector | Source Code Pattern | Resolution Strategy |
|---|---|---|
| Named Component Imports | import { IconClose } from "@stadium/ui" | Resolves import specifiers against slug map to icons:close |
| Explicit Legacy Component | <LegacyIcon name="cart" family="nav" /> | Direct namespaced mapping to nav:cart |
| Implicit Legacy Fallback | <LegacyIcon name="cart" /> | Deduplication cascade: checks nav ➔ prismic ➔ defaults to icons |
Architectural Invariants
- Deterministic Byte Output: All symbols inside the generated
.sprite.svgfiles are sorted lexicographically (family:slug). This eliminates nondeterministic Map iteration order, guaranteeing zero false git diffs across CI runs. - Granular Bundle Scoping: A high-traffic checkout app only bundles the 47 icons it actually renders (~18 KB), rather than carrying the overhead of the entire 418-icon catalog.
- Zero-Friction Local Development: While production builds generate scoped sprites per workspace, local development and Storybook mount a comprehensive
full.sprite.svgso developers never experience missing icons while building new features.
05 — Safe Codemod Strategy & Safety Guards
Migrating thousands of icon occurrences across four production storefronts manually is error-prone. Phase 4 automates the mechanical conversion while maintaining strict safety boundaries.
Transformation Matrix
| Legacy Source Pattern | Transformed JSX Output | Resolution Mode |
|---|---|---|
<span className="icon-close" /> | <IconClose /> | Automated 1:1 Replacement |
<i className="icon-download fs-16" onClick={fn} /> | <IconDownload className="fs-16" onClick={fn} /> | Attribute & Event Preservation |
className={icon-${dynamicName}} | Untouched — Flagged in Report | Bailout: Dynamic interpolation |
<span className="icon-x">Non-empty</span> | Untouched — Flagged in Report | Bailout: Non-self-closing container |
className="icon-a icon-b" | Untouched — Flagged in Report | Bailout: Conflicting multi-class glyph |
Safeguards & Audit Reporting
- Dry-Run by Default: Running
pnpm icon:codemod-jsxcalculates and displays exact diffs without touching disk files. The--writeflag must be explicitly passed to apply changes. - Structured Audit Trail: Every execution outputs
jsx-codemod-report.json, detailing every modified file, line-by-line diffs, and the exact files requiring manual developer intervention. - Isolated Package Boundaries: Legacy sub-packages with distinct styling rules (e.g.
sm-shop) are excluded from broad transforms to prevent breaking unisolated third-party integrations.
06 — Architecture Trade-Offs & Technical Accounting
| Component | Technical Implementation | Architectural Rationale |
|---|---|---|
| SVG Extraction | Standard Icomoon ZIP parser | Off-the-shelf extraction; no need to build bespoke ZIP parsers. |
| Component Factory | Custom React <use> wrapper | Standardizes props (className, size, aria-hidden) across all storefronts. |
| Usage Scanner | Monorepo static AST/regex walker | Custom-built to handle multi-package monorepo boundaries and legacy prop fallback rules. |
| Codemod Engine | Targeted line/regex parser with AST safety bailouts | Chosen over heavy Babel/jscodeshift AST tooling. Given predictable className patterns, targeted transformation minimized toolchain overhead while dry-run reporting prevented unsafe replacements. |
07 — Verified System & Business Outcomes
- 73% Asset Payload Reduction: High-traffic storefront entry points (such as Checkout) dropped icon SVG footprint from a ~120 KB monolithic font down to an ~18 KB scoped sprite.
- Elimination of Icon FOUC: Moving from asynchronous font glyph rendering to cached SVG sprites eliminated icon layout shifts and unstyled render flashes across SSR first-paint.
- Compile-Time Type Safety: Deprecated or mistyped icon names now immediately fail the TypeScript compiler (
tsc), eliminating silent visual regressions in production. - Universal Accessibility: Every icon component automatically renders with
aria-hidden="true"or embeds descriptive<title>tags for screen-reader accessibility, resolving longstanding a11y audit flags.