cra-vite-migrate
v0.2.0
Published
Codemod tool for migrating create-react-app codebases to Vite + React 19
Maintainers
Readme
cra-vite-migrate
cra-vite-migrate — a migration orchestrator that takes a react-scripts /
Create React App (CRA) codebase ~80% of the way to a Vite + React 19 target
stack, then hands off a precise, verified burndown list for the judgment-heavy
remainder.
It is not a one-shot magic rewriter. The product is "80% migrated + a ranked, verified burndown list." The CLI only applies deterministic transforms; non-deterministic fixes are sliced into briefs that a human or agent must satisfy against the same gates.
What it does
- Audit a repo and score its migration difficulty (easy/medium/hard/blocked).
- Run idempotent, re-runnable phases, each owning a narrow set of files.
- Checkpoint with git so every phase has a clean rollback (
--dry-runuses a throwaway worktree). - Verify after each phase — install,
tsc, biome, unit tests, Vite build — with structured error parsing. - Emit a resumable manifest (
.cra-vite-migrate/manifest.json) and an agent/human burndown loop for the residual 20%.
Requirements
- Node.js
>= 20.0.0 - The target repo must be a git working tree (checkpoints/rollback rely on
git) with a
package.jsonat its root. - The target repo's own
install,typecheck, andbuildcommands must work — gates shell out to the workspace's real toolchain.
Install
npm install
npm run build # emits ./dist (tsc)Run locally without building:
npm run dev <command> [args] # = tsx src/cli/index.tsUsage
cra-vite-migrate <command> [args]Core workflow
# 1. Score and inventory one or more CRA repos
cra-vite-migrate audit path/to/repo # styled report
cra-vite-migrate audit ./repos/* --json \
--out reports/audit.json # machine-readable
# 2. Execute migration phases
cra-vite-migrate run path/to/repo # all phases
cra-vite-migrate run path/to/repo --phase 05-cjs-to-esm
cra-vite-migrate run path/to/repo --from 02-vite-scaffold --to 11-vitest
cra-vite-migrate run path/to/repo --continue # resume from first non-terminal phase
cra-vite-migrate run path/to-repo --dry-run # detached worktree, no commits
# 3. Re-run verification gates
cra-vite-migrate verify path/to/repo --gate typecheck
cra-vite-migrate verify path/to/repo --all # install, typecheck, biome, unit, build, e2ePhases that finish red but are marked recoverable are recorded as
needs_burndown rather than failing the run; the summary prints them.
Batch operations
# Audit many repos into an inventory (easiest-first)
cra-vite-migrate batch audit ./repos/* --out inventory.json
# Migrate across the inventory
cra-vite-migrate batch run \
--from-audit inventory.json \
--strategy easiest-first \
--max-parallel 3 \
--stop-on-fail \
--out batch-report.mdCommit message prefixes
By default every phase commits with migrate(<phaseId>): apply phase. Many
remotes enforce their own subject policy (JIRA ticket prefixes, Conventional
Commits types, signing trailers) and reject pushes that don't match. Pass a
prefix header and it's prepended to each phase commit, joined with exactly one
space (a trailing space in the value is collapsed, so JIRA-1234 and
JIRA-1234 produce the same subject).
# Single repo: one prefix for every phase in the run
cra-vite-migrate run path/to/repo --commit-prefix "JIRA-1234"
# → "JIRA-1234 migrate(02-vite-scaffold): apply phase"For batch migrations, supply a JSON map keyed by the absolute repoRoot
values that appear in inventory.json. Repos absent from the map get no prefix.
cra-vite-migrate batch run \
--from-audit inventory.json \
--commit-prefixes prefixes.json// prefixes.json — keys must match inventory.json `repoRoot` (absolute paths)
{
"/abs/path/repo-a": "JIRA-1234",
"/abs/path/repo-b": "PROJ-456"
}The map is resolved once at batch start and threaded to each repo's run
invocation. Keys are absolute paths because that's how the inventory already
identifies repos (see AuditReport.repoRoot); the file is an ops artifact
that travels with inventory.json and is machine-specific by design.
Agent-assisted burndown
When a gate stays red, slice its errors into model-sized briefs and either review them by hand or drive an automated loop:
# Emit one burndown brief (prints, or writes to --out)
cra-vite-migrate agent next path/to/repo \
--gate tsc --max-files 5 --max-errors 25 --out task.md
# Drive iterative burndown against a gate
cra-vite-migrate agent loop path/to/repo \
--gate tsc --max-iterations 10 --provider local-commandProviders: local-command (default), noop. The loop only accepts diffs that
pass the gate; the summary reports accepted / stuck / remaining.
Migration phases
Executed in this order (governed by PHASE_ORDER in src/core/phase.ts):
| ID | Phase |
| ---------------------------- | ----------------------------------------- |
| 00-preflight | preflight checks |
| 01-package-upgrades | bump deps to React-19-compatible versions |
| 02-vite-scaffold | add Vite config + entry |
| 03-craco-webpack-translate | translate craco/webpack overrides |
| 04-env-migration | process.env.* → import.meta.env.* |
| 05-cjs-to-esm | CommonJS → ESM |
| 06-reactdom-root | ReactDOM.render → createRoot |
| 07-react19-codemods | React 19 codemods |
| 08-jsx-extension-migration | .js→.jsx / .tsx where needed |
| 09-mui-codemods | Material UI React-19 codemods |
| 10-biome | scaffold Biome |
| 11-vitest | Jest → Vitest |
| 12-playwright | scaffold Playwright |
| 13-typescript-strict | tighten TS config |
| 14-zod-boundaries | Zod boundaries |
| 15-lefthook-cleanup | Lefthook pre-push gate + CRA cleanup |
| 16-final-report | emit final report |
Each phase implements detect / isSatisfied / run, so phases are skipped
when they don't apply and are safe to re-run.
State
Progress is persisted in <repo>/.cra-vite-migrate/manifest.json (a
Zod-validated manifest of phase records, audit snapshot, and burndown items).
Delete it to start fresh.
Command status
Implemented: audit, run, verify, batch audit, batch run, agent next,
agent loop.
Stubbed (print "not yet implemented"): plan, status, rollback.
Development
npm run build # tsc → ./dist
npm run typecheck # tsc --noEmit
npm test # vitest run
npm run test:watch # vitestPre-push (Lefthook): typecheck, lint, unit (test:run) must be green.
License
MIT
