Skip to main content
Samrat Dutta Roy
Back to selected work
[05]·Design System Infrastructure & Metaprogramming

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

In Active Migration

Domain

Systems & Architecture

Deployment

Design System Migration
TypeScriptSVG SpritesAST CodemodsjscodeshiftSVGONext.jsReactWeb Performance

Key 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:

  1. 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.
  2. 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.
  3. 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:

Architectural Blueprint · System Topology
[ 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 ]
Monorepo Architecture Flow

Toolchain Command Orchestration

bash
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-* classNames

03 — 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

tsx
// 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:

FamilyComponent PrefixSymbol ID SchemeScope
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 VectorSource Code PatternResolution Strategy
Named Component Importsimport { 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.svg files 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.svg so 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 PatternTransformed JSX OutputResolution 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 ReportBailout: Dynamic interpolation
<span className="icon-x">Non-empty</span>Untouched — Flagged in ReportBailout: Non-self-closing container
className="icon-a icon-b"Untouched — Flagged in ReportBailout: Conflicting multi-class glyph

Safeguards & Audit Reporting

  1. Dry-Run by Default: Running pnpm icon:codemod-jsx calculates and displays exact diffs without touching disk files. The --write flag must be explicitly passed to apply changes.
  2. 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.
  3. 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

ComponentTechnical ImplementationArchitectural Rationale
SVG ExtractionStandard Icomoon ZIP parserOff-the-shelf extraction; no need to build bespoke ZIP parsers.
Component FactoryCustom React <use> wrapperStandardizes props (className, size, aria-hidden) across all storefronts.
Usage ScannerMonorepo static AST/regex walkerCustom-built to handle multi-package monorepo boundaries and legacy prop fallback rules.
Codemod EngineTargeted line/regex parser with AST safety bailoutsChosen 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.