Skip to content

PATTERN Cited by 1 source

Gradual transpiler-driven language migration

Pattern

Migrate an entire codebase from source language A to target language B without blocking feature development or taking a one-way-door rewrite risk, by building a transpiler that mechanically produces B from A, and flipping source-of-truth in a gated, reversible sequence:

  1. Build transpiler. Consumes A's IR, emits readable B. Goal: A → B passes A's entire test suite and produces functionally equivalent output.

  2. Dual-check-in. Developers write A. Transpiler generates B on every commit. Both are checked into the same repository, so reviewers can see exactly what B will look like. Original A → machine-code pipeline stays live; B is generated-but-unused.

  3. Shift consumers to B's output. Cut the production build to use A → B → machine-code instead of A → machine-code. Developers still write A; they just ship bytes built through B. Gate behind a feature flag / percentage rollout. Reverse per-consumer on regressions.

  4. Flip source of truth. Pick a quiet moment (Friday night, PR-free window), cut off the auto-generation process, delete A from the repository, make B the source developers modify. One-way door — but by this point every line of B has been test-passed, source-map-debugged, and production-traffic-verified.

Steps 2–3 resemble patterns/shadow-migration but the "shadow" is build output, not a reconciled data-pipeline output. Step 4's irreversibility is intentional — leaving A in place re-introduces two source-of-truth risk, which compounds developer-confusion cost.

What makes it possible

  • Owning the source-language compiler. Figma could freely modify Skew to make transpilation easier (e.g. tighten IR shapes the transpiler needed to emit). Migrating from a third-party language forecloses this and is measurably harder.
  • Language-semantic parity work is scoped, not open-ended. Three classes of divergence: runtime perf differences in the target language's idioms (Figma: JS array destructuring is slow), optimizations the source compiler did that the target doesn't (Figma: devirtualization), evaluation-order semantics (Figma: TS requires declaration before use at module top level, Skew doesn't). Each is a targeted transpiler patch; they surface as the transpiler matures.
  • concepts/source-map-composition. Browser breakpoints in A must resolve through the composed A → B → bundle map. Non-optional — without it, developers hit invisible-regression-class debugger bugs they attribute to themselves, and migration velocity silently tanks.

What goes wrong without it

Alternatives to this pattern all fail in characteristic ways at large codebase scale:

Alternative Failure mode
Manual file-by-file rewrite Interrupts feature dev for months-to-years; no safety net against subtle semantic drift; no way to roll back a "done" file
Big-bang rewrite on a branch Merge hell; branch diverges faster than rewrite progresses; rewrite team loses ground-truth feedback from the shipping product
Write all new code in B, leave A Two source-of-truth problem; tooling (linters, test runners, debuggers) splits; onboarding worse than before
Runtime interop layer Neither language's ecosystem works cleanly on the other's code; performance unpredictable; "temporary" becomes permanent

Case study: Figma Skew → TypeScript (2024)

  • Skew: compile-to-JS language Figma cultivated for its prototype viewer / mobile client (~2014–2024). Own compiler, static types, devirtualization + other optimizations; generated minified JS.
  • TypeScript: industry standard, massive ecosystem.
  • Built a Skew-to-TypeScript transpiler whose backend consumed Skew's IR (same IR the Skew-to-JS backend used) and emitted readable TS.
  • Checked transpiled TS into Git alongside Skew for reviewer visibility.
  • Cut production bundle to build from the generated TS. Caught a devirtualization divergence via a Smart Animate breakage, rolled back the gate, patched, re-rolled. (Differentiates this from a one-way rewrite: rollback was cheap.)
  • Three transpiler-patch classes surfaced (JS array-destructuring replacement → +25% per-frame latency; devirtualization-safety audit via logging every call site; initialization-order correctness).
  • One build-system difference required a bundler-side answer: Skew's if BUILD == "TEST" compile-time specialization has no TypeScript equivalent, so Figma used esbuild's defines + dead code elimination to strip the non-matching branch after type-check (cost: slightly larger bundle, test-only names always present).
  • Final cutover on a Friday night: cut off auto-generation, deleted Skew, made TS the source of truth.

(Source: sources/2024-05-03-figma-typescript-migration)

Contrast with sibling patterns

  • patterns/pilot-component-language-migration — answers "should we migrate?" via a small-scoped pilot that measures real productivity
  • perf. This pattern answers "how do we migrate an entire codebase once we've decided to?"
  • patterns/shadow-migration — same two-engines-in-parallel topology, different reconciliation bar (data-pipeline output for shadow-migration; build-output test-suite + production-traffic behaviour for this pattern).
  • patterns/weighted-sum-strategy-migration — same "feature-flag gate + reversible per-consumer" rollout shape but for runtime routing, not compilation.

Seen in

Last updated · 200 distilled / 1,178 read