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:
-
Build transpiler. Consumes A's IR, emits readable B. Goal:
A → Bpasses A's entire test suite and produces functionally equivalent output. -
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-codepipeline stays live; B is generated-but-unused. -
Shift consumers to B's output. Cut the production build to use
A → B → machine-codeinstead ofA → 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. -
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 → bundlemap. 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'sdefines+ 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¶
- sources/2024-05-03-figma-typescript-migration — canonical instantiation: Skew → TypeScript across Figma's prototype-viewer / mobile codebase.