Claude Code Workflow for Flow-to-TypeScript Migration

CameronCat Intermediate 3d ago 400 views 6 likes 3 min read

Tossing a thousand files at an LLM and asking for a conversion isn't a migration strategy—it's a gamble. When dealing with a legacy codebase, specifically one with over 1,100 files trapped in Flow (circa 2019), the primary bottleneck isn't the model's intelligence, but the context window and the lack of a verification loop. If you try to migrate file-by-file in isolation, the AI will hallucinate types because it can't see the dependencies in other files, leading to a cascade of broken imports.

To solve this, we built a custom AI migration engine that treated the codebase as a graph rather than a folder of text files. Here is the breakdown of the AI workflow and the architectural decisions that made it work.

The Pre-Migration Infrastructure

Before touching a single line of code with an LLM, we had to stabilize the environment. We switched the build system from Webpack to Vite + esbuild. This was non-negotiable because it allowed .js and .ts files to coexist seamlessly. Without this, you can't maintain a "green" build while migrating incrementally.

Next, we performed a deep static analysis of the codebase to create two maps:

  • Dependency Graph: Identifying who imports whom and flagging every circular dependency.
  • Type-Ownership Map: Tracking which file defines a type and which files consume it.

We used this data to group files into "atomic units." If File A defines a type used by File B, they migrate together. We then sorted these groups leaf-first, ensuring that dependencies were converted before the components that relied on them. For massive files (some over 4,000 lines), we manually split them into smaller domain modules first, because one-shotting a monolith is a recipe for truncation and errors.

The AI Orchestration Pipeline

We didn't use a single prompt; we built a state machine using LangGraph JS. This allowed us to separate the roles of planning, execution, and validation.

 Planner ──► Implementer ──► Reviewer ──► ✅ commit
 (next group (Claude writes (tsc + tests)
 from plan) the TS) │
 ▲ │ errors?
 │ ▼
 └──────── retry (feed errors back in) ◄──┘
 │
 stuck after 3 tries?
 ▼
 🧠 Architect (bigger model)
 → retry smarter / reorder / flag a human

The logic follows a strict "Reviewer" pattern:

  • Implementer: Claude Sonnet handles the bulk of the work (about 95% of files) because it is fast and efficient.
  • Reviewer: A combination of tsc (TypeScript Compiler) and existing test suites. If the build fails, the exact error message is fed back to the Implementer for a retry.
  • Architect: If a file fails three times, we escalate to Claude Opus. The heavier model analyzes why the conversion is failing and decides if the plan needs to be reordered or if a human needs to step in.

Strict Prompt Engineering Constraints

To prevent the AI from taking the "easy way out," we imposed hard constraints in the system prompts. We explicitly forbade the use of any and the as unknown as X casting trick, which often hides bugs. The model was forced to trace prop types from actual parent component usage and import legitimate library types rather than inventing shapes.

Crucially, the LLM was restricted to editing files only. It had no shell or git access; all verification and commits happened in the Reviewer layer, ensuring the AI couldn't "fake" a successful migration.

  • Orchestration: LangGraph JS
  • LLM Engine: Claude Code CLI (Sonnet for implementation, Opus for architecture)
  • Verification: tsc + Vite/esbuild
softwareengineering
A more systematic set of tool reviews lives in these AI tool field notes, with plenty of directly applicable cases.

All Replies (3)

J
JordanSurfer Intermediate 3d ago
I found that running a custom lint pass after the migration caught a few hallucinated types.
0 Reply
R
Riley97 Advanced 3d ago
tried a bulk convert once and spent a week fixing the mess. way better to go incremental.
0 Reply
K
KaiDev Expert 3d ago
Does this actually handle the complex union types without just slapping 'any' on everything?
0 Reply

Write a Reply

Markdown supported