Cra to vite
Skill Siddharth00/agent-revamp-skills/skills/04-migrate/frontend/cra-to-vite
Production-grade migration skills for AI coding agents. Revamp any product, module, or feature from one stack to another.
npx -y skills add Siddharth00/agent-revamp-skills --skill cra-to-viteAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Migrates a Create React App project to Vite 5.x + Vitest using a strangler-fig approach: both build tools run in parallel until Vite is proven equivalent, then CRA is removed.
SKILL.md
25.3 KB, as published. Nobody here has run it
1. Purpose
This skill replaces a Create React App setup (react-scripts, Webpack 5) with Vite 5.x and
Vitest using a strangler-fig approach: Vite is introduced alongside the existing CRA toolchain,
both build pipelines run in CI simultaneously, and traffic is switched via a feature flag once
Vite output is proven equivalent. A frontend engineer or build-systems engineer runs this skill
once per application — it is not file-by-file; it operates at the toolchain level. The migration
is non-trivial because CRA abstracts Webpack configuration that Vite does not replicate
automatically: environment variable prefixes change (REACT_APP_ → VITE_), absolute import
resolution must be re-expressed as Vite aliases, CSS Modules defaults differ in edge cases, and
the Jest test suite must be ported to Vitest, whose API is compatible but whose module mocking
semantics diverge in specific patterns. The rollback path keeps react-scripts in package.json
and the original CI build script intact throughout the transition; Vite is only promoted to
primary when all equivalence gates pass in staging.
2. Trigger Conditions
Use when:
- The application's build tooling is
react-scripts(CRA), confirmed by"react-scripts"inpackage.jsonscripts.build. - The Phase 3 preparation is complete: the repo has a staging environment, a CI pipeline that can run two build scripts in parallel, and a feature flag system that can toggle which build artifact is served.
- The full Jest suite passes on the current commit (
npm test -- --watchAll=falseexits 0). - The application has no CRA
ejecthistory —package.jsonmust not contain a"webpack"direct dependency or aconfig/webpack.config.jsfile. Ejected CRAs require a different skill. - A Lighthouse baseline for the staging URL has been recorded (needed for the regression gate).
Do NOT use when:
- The application has been ejected from CRA (
config/webpack.config.jsexists) — the Webpack config must be ported manually before this skill applies. - The application uses
react-app-rewiredorcraco— custom Webpack overrides need to be inventoried and mapped to Vite plugin equivalents before running this skill. - The Jest suite is currently failing — do not migrate the test runner on top of a broken suite.
- The team is mid-way through a TypeScript migration (
skills/04-migrate/frontend/js-to-typescript/) — finish the TS migration first; mixed.js/.tsfiles in CRA behave differently under Vite's default config. - There is no staging environment — the Lighthouse regression gate cannot be validated without one.
3. Inputs
Required:
| Input | Type | Description |
|---|---|---|
repo_root | file-path | Absolute path to the repository root (where package.json lives). All commands run from here unless noted. |
app_name | string | Value of name in package.json. Used to name output artifacts and identify the correct package.json when the repo is a monorepo. |
staging_url | string | Full URL of the staging environment (e.g. https://staging.example.com). Used to run Lighthouse before and after the Vite build is wired in. |
lighthouse_baseline | file-path | Path to a Lighthouse JSON report captured before this skill runs. Generate with npx lighthouse <staging_url> --output json --output-path <path>. If absent, halt and ask the user to generate it. |
feature_flag_key | string | The feature flag key (in the team's flag system) that controls which build artifact CI serves to staging. The flag must exist and default to false (CRA artifact) before this skill runs. |
Optional:
| Input | Type | Default | Description |
|---|---|---|---|
vite_port | integer | 3001 | Local dev port for the Vite dev server. CRA defaults to 3000; use a different port so both can run simultaneously during parallel validation. |
lighthouse_budget_points | integer | 5 | Maximum allowed Lighthouse score regression (Performance category) before the equivalence gate fails. Raise to 10 only with explicit sign-off; >10 is the hard rollback trigger. |
css_modules_local_ident | string | [local]_[hash:5] | CSS Modules generateScopedName pattern for Vite. CRA uses [name]__[local]--[hash:base64:5]; set this to match if server-rendered class names are hardcoded anywhere. |
vitest_setup_file | file-path | src/setupTests.ts | Path to the test setup file to pass to Vitest's setupFiles. Maps directly from CRA's setupTests convention. |
4. Steps
-
Read
<repo_root>/package.json. Confirmscripts.buildcontainsreact-scripts build. Record the current versions ofreact-scripts,react, andreact-domascra_react_version. Confirm noconfig/webpack.config.jsexists — if it does, STOP: "This project has been ejected from CRA. This skill does not apply." -
Read every file matching
<repo_root>/src/**/*.{js,ts,jsx,tsx}and grep forprocess.env.REACT_APP_. Collect each unique variable name (e.g.REACT_APP_API_URL,REACT_APP_FEATURE_X). Write the full list tooutput/cra-to-vite-env-map-<timestamp>.jsonas an array of{ "old": "REACT_APP_FOO", "new": "VITE_FOO" }objects.- Also grep for
process.env.NODE_ENV— these do not need renaming but must be confirmed to work under Vite (Vite exposesimport.meta.env.MODE, notprocess.env.NODE_ENV). Flag each occurrence for Step 10.
- Also grep for
-
Read
<repo_root>/tsconfig.json(orjsconfig.json). Extract every path alias undercompilerOptions.paths(e.g."@components/*": ["src/components/*"]). Write the alias list tooutput/cra-to-vite-alias-map-<timestamp>.jsonas{ "find": "@components", "replacement": "/src/components" }objects. These becomeresolve.aliasentries invite.config.ts. -
→ Hand off to
code-archaeologist(see Section 5. Agent Handoffs) to inventory CSS Modules usage patterns. Wait foroutput/cra-to-vite-css-inventory-<timestamp>.mdbefore continuing. -
Install Vite and its dependencies without removing
react-scripts. Run from<repo_root>:npm install --save-dev \ vite@^5 \ @vitejs/plugin-react@^4 \ vitest@^1 \ @vitest/ui@^1 \ @testing-library/jest-dom@^6 \ jsdom@^24- If this fails: check for peer dependency conflicts with
react@<cra_react_version>. The most common conflict is@testing-library/reactversion pinned by CRA. Resolve by adding--legacy-peer-depsand log the override in the migration log.
- If this fails: check for peer dependency conflicts with
-
Create
<repo_root>/vite.config.tswith the following content, substituting the alias list from Step 3 and the port fromvite_port:import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import { resolve } from 'path'; // Aliases derived from tsconfig.json compilerOptions.paths — keep in sync. // Generated by skills/04-migrate/frontend/cra-to-vite on <timestamp>. const aliases = [ // <<INSERT alias objects from output/cra-to-vite-alias-map-<timestamp>.json>> // Example: { find: '@components', replacement: resolve(__dirname, 'src/components') } ]; export default defineConfig(({ mode }) => ({ plugins: [react()], resolve: { alias: aliases, }, server: { port: <vite_port>, // CRA uses 3000; keep Vite on a different port during parallel run. open: false, // Don't auto-open browser — CI environments will fail. strictPort: true, // Fail fast if the port is taken rather than silently rebinding. }, build: { outDir: 'build-vite', // Deliberately NOT 'build' — CRA still owns 'build/' during transition. sourcemap: true, rollupOptions: { output: { // Stable chunk names for diffing against CRA output. entryFileNames: 'static/js/[name].[hash].js', chunkFileNames: 'static/js/[name].[hash].js', assetFileNames: 'static/media/[name].[hash][extname]', }, }, }, css: { modules: { // CRA default: [name]__[local]--[hash:base64:5] // Adjust generateScopedName to match if class names are referenced in tests or snapshots. generateScopedName: '<css_modules_local_ident>', }, }, test: { globals: true, // Allows `describe`, `it`, `expect` without imports — matches Jest API. environment: 'jsdom', setupFiles: ['<vitest_setup_file>'], css: true, // Process CSS Modules in tests — CRA's Jest config does this via identity-obj-proxy. coverage: { provider: 'v8', reporter: ['text', 'lcov'], }, }, // Vite does not inject process.env. Replace REACT_APP_ vars and NODE_ENV shims. define: { // <<INSERT: one entry per REACT_APP_ variable found in Step 2, using import.meta.env>> // Example: 'process.env.REACT_APP_API_URL': 'import.meta.env.VITE_API_URL', // NODE_ENV shim — remove once all process.env.NODE_ENV references are updated to import.meta.env.MODE. 'process.env.NODE_ENV': JSON.stringify(mode === 'production' ? 'production' : 'development'), }, }));- After writing the file, run
npx tsc --noEmit -p tsconfig.jsonto confirm the config parses without errors. If it fails, check that"moduleResolution": "bundler"or"node"is set in tsconfig and thatvite/clienttypes are available.
- After writing the file, run
-
Create
<repo_root>/index.htmlin the repo root (Vite requires the HTML entry point at root, notpublic/index.html). Copypublic/index.html, then:- Remove the
%PUBLIC_URL%prefix from all asset paths (Vite serves from root;%PUBLIC_URL%is a CRA-ism). - Add
<script type="module" src="/src/index.tsx"></script>(or.jsx/.js— match the actual entry file) before</body>. - Confirm
<link rel="icon">and<link rel="manifest">still reference valid paths underpublic/. - If this fails: check that
public/index.htmlexists and that the entry file issrc/index.tsx(or equivalent). Log the actual entry file path used.
- Remove the
-
Add Vite scripts to
package.jsonwithout removing the CRA scripts. The CRA scripts remain untouched until Section 9 gates pass:{ "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test --watchAll=false", "start:vite": "vite --port <vite_port>", "build:vite": "vite build", "test:vite": "vitest run", "preview:vite":"vite preview --port <vite_port>" } } -
Rename all environment variables in source files: for each entry in
output/cra-to-vite-env-map-<timestamp>.json, replaceprocess.env.<OLD>withimport.meta.env.<NEW>across all files in<repo_root>/src/. Write each replacement to the migration log (file path, line number, old → new).- Also update any
.env,.env.development,.env.production,.env.testfiles: rename eachREACT_APP_FOO=valueline toVITE_FOO=value. - Flag any
process.env.NODE_ENVoccurrences found in Step 2 with an inline comment:/* TODO(cra-to-vite): replace with import.meta.env.MODE */. Do not replace them automatically — MODE and NODE_ENV have slightly different value sets.
- Also update any
-
Address CSS Modules incompatibilities surfaced in the inventory from Step 4:
- If the inventory reports any
:local()or:global()pseudo-selectors: confirm Vite's CSS Modules implementation handles them identically. Log any divergence. - If any test file imports a
.module.cssand asserts on class name strings (e.g.expect(el.className).toBe('Button__root--abc12')): update the expected string to matchcss_modules_local_identpattern, or convert the assertion totoHaveClasswhich is class-name-format-agnostic.
- If the inventory reports any
-
Port the Jest test configuration to Vitest. Read
<repo_root>/package.jsonjestkey (orjest.config.jsif present). For each Jest configuration field, apply the Vitest equivalent:moduleNameMapper→resolve.aliasinvite.config.ts(already handled for path aliases in Step 6); CSS module mappers (identity-obj-proxy) are replaced bycss: truein the Vitest config.transform→ handled by Vite's plugin pipeline; remove unless there is a non-standard transform.setupFilesAfterFramework/setupFiles→ already mapped tovitest_setup_fileinvite.config.ts.testEnvironment: 'jsdom'→ already set invite.config.ts.globalsmock patterns usingjest.mock(...): audit for patterns that usejest.fn()orjest.spyOn()— these work unchanged in Vitest withglobals: true. Patterns that usejest.resetModules()orjest.isolateModules()requirevi.resetModules()/vi.isolateModules()— find and replace.- Write a diff of every Jest→Vitest config change to the migration log.
-
Run the Vitest suite:
npx vitest runfrom<repo_root>. Record full output and exit code asvitest_result.- If exit code is non-zero: read the failure output. For each failing test:
- If failure is
vi.fn is not a functionor similar: the test uses a Jest global not shimmed — addimport { vi } from 'vitest'at the top of that test file. - If failure is a CSS class name assertion mismatch: update the assertion per Step 10 guidance.
- If failure is a module resolution error (
Cannot find module '@/...'): the alias from Step 3 is missing or malformed invite.config.ts— re-read the alias map and fix. - Fix the minimum required to make the suite pass. Do not refactor test logic.
- If failure is
- Do not proceed until
vitest_resultexit code is 0.
- If exit code is non-zero: read the failure output. For each failing test:
-
Run the Vite build:
npm run build:vitefrom<repo_root>. Confirm it exits 0 and writes artifacts tobuild-vite/.- If this fails: read the Rollup error. Common causes:
require is not defined— a CommonJS dependency is not pre-bundled; add it tooptimizeDeps.includeinvite.config.ts.- Dynamic
require()calls in source — replace withawait import(). - Missing
index.htmlentry reference — re-check Step 7.
- Run
npm run build(CRA) in the same CI step to produce the baselinebuild/artifact. Both must succeed before Step 14.
- If this fails: read the Rollup error. Common causes:
-
Compare bundle output sizes. Run from
<repo_root>:du -sh build/ build-vite/ find build/static/js -name '*.js' | xargs wc -c | sort -n find build-vite/static/js -name '*.js' | xargs wc -c | sort -nWrite the comparison to the migration log. A Vite bundle that is more than 20% larger than the CRA bundle (after gzip) warrants investigation before cut-over — log it as a warning, not a hard failure, unless the team has set a size budget.
-
Deploy the Vite build artifact (
build-vite/) to staging behind the feature flag (feature_flag_key = true). Run Lighthouse against<staging_url>with the Vite artifact serving:npx lighthouse <staging_url> --output json --output-path output/cra-to-vite-lighthouse-post-<timestamp>.jsonCompare the
categories.performance.scorevalue against the baseline in<lighthouse_baseline>. Compute the delta as(post_score - baseline_score) * 100(Lighthouse scores are 0–1).- If delta < −
lighthouse_budget_points: log a warning and continue — this is a soft gate. - If delta < −10: this is the hard rollback trigger. Set
feature_flag_key = false, write the regression to the migration log, STOP: "Lighthouse performance regressed <N> points (threshold: 10). Vite artifact rolled back. Investigate bundle size and code splitting before retrying."
- If delta < −
-
Write all outputs declared in Section 7. Run every Equivalence Test in Section 6 and record results in
output/cra-to-vite-equiv-<timestamp>.md. Evaluate every item in Section 9 Done Criteria; report pass/fail inline, then print the final verdict.
5. Agent Handoffs
code-archaeologist
- File:
agents/code-archaeologist.md - Triggered by: Step 4
- Prompt template:
TASK: Inventory all CSS Modules usage in the repository. For each .module.css or .module.scss file found in SCOPE, report: - File path and number of class names defined - Whether any class names are referenced as string literals in test files (e.g. expect(el.className).toBe('...')) — list file path and line number - Whether :local() or :global() pseudo-selectors are used anywhere - Whether the file is imported in a .tsx/.jsx file that also has Snapshot tests (class name changes will break snapshots) Separately, list every file that imports 'identity-obj-proxy' — these are Jest CSS mock configurations that need to be removed under Vitest. REPO_ROOT: <repo_root> SCOPE: <repo_root>/src OUTPUT_FILE: output/cra-to-vite-css-inventory-<timestamp>.md FORMAT: markdown
6. Equivalence Tests
<!-- Tests are run in Step 16. Results written to output/cra-to-vite-equiv-<timestamp>.md. "CRA baseline" refers to the build artifact in build/ and the Jest result from the pre-migration test run. -->| Test Name | Input | Expected Output | Tool |
|---|---|---|---|
vitest-suite | npx vitest run from <repo_root> (Step 12 result) | Exit code 0. Passed/failed/skipped counts are identical to the pre-migration Jest run (npm test -- --watchAll=false). A count divergence is a fail even if exit code is 0. | Bash — captured in Step 12 as vitest_result. |
vite-build-exits-0 | npm run build:vite from <repo_root> (Step 13 result) | Exit code 0; build-vite/index.html exists and references at least one JS chunk in build-vite/static/js/. | Bash — captured in Step 13. |
entry-points-present | grep -r '<script' build-vite/index.html and grep -r '<link rel="stylesheet"' build-vite/index.html | At least one <script type="module"> and one <link rel="stylesheet"> present — Vite bundle is wired into the HTML entry point. | Bash |
env-vars-renamed | grep -rn 'REACT_APP_' <repo_root>/src/ | No matches. All REACT_APP_ references replaced with VITE_ equivalents. | Bash |
no-process-env-react-app | grep -rn 'process\.env\.REACT_APP_' <repo_root>/src/ | No matches. All usages migrated to import.meta.env.VITE_*. | Bash |
lighthouse-perf | Lighthouse JSON at output/cra-to-vite-lighthouse-post-<timestamp>.json, categories.performance.score field | Score does not regress more than <lighthouse_budget_points> points vs. <lighthouse_baseline>. Compute as (post − baseline) × 100; must be ≥ −<lighthouse_budget_points>. | Bash: node -e "const b=require('<lighthouse_baseline>').categories.performance.score; const p=require('output/...').categories.performance.score; const d=(p-b)*100; process.exit(d < -<lighthouse_budget_points> ? 1 : 0)" |
no-cra-artifacts-in-vite-output | grep -r 'react-scripts' build-vite/ | No matches — the Vite build output must not reference CRA tooling. | Bash |
7. Outputs
| Artifact | Path Pattern | Format | Description |
|---|---|---|---|
| Env var rename map | output/cra-to-vite-env-map-<timestamp>.json | json | Array of {old, new} pairs mapping REACT_APP_* → VITE_*; consumed in Step 9 and used as evidence in env-vars-renamed equivalence test. |
| Alias map | output/cra-to-vite-alias-map-<timestamp>.json | json | Vite resolve.alias objects derived from tsconfig paths; consumed in Step 6 when writing vite.config.ts. |
| CSS inventory | output/cra-to-vite-css-inventory-<timestamp>.md | markdown | Produced by code-archaeologist; lists CSS Modules files, class name string assertions in tests, and identity-obj-proxy usage. Consumed in Step 10. |
| Migration log | output/cra-to-vite-log-<timestamp>.md | markdown | Chronological record of every change made (env renames, alias additions, Jest→Vitest config diffs, bundle size comparison), manual-review flags, and the final assumptions list and confidence level. Consumed by /validate and the PR reviewer. |
| Lighthouse post report | output/cra-to-vite-lighthouse-post-<timestamp>.json | json | Raw Lighthouse JSON captured in Step 15 after deploying the Vite artifact to staging. Performance score diff is computed against <lighthouse_baseline>. |
| Equivalence test results | output/cra-to-vite-equiv-<timestamp>.md | markdown | Pass/fail verdict for every row in Section 6 with raw command output attached. Required by Section 9 Done Criteria. |
8. References
references/strangler-fig-pattern.md— the parallel-run + feature flag mechanism used in Steps 8 and 15 is the traffic-shifting pattern described here.references/migration-anti-patterns.md— "Migrating Without a Seam" (§2): the feature flag is the seam; confirm it exists before starting. "Skipping Equivalence Validation" (§3): the Lighthouse gate is non-negotiable.references/stack-compatibility-matrix.md— "Framework Compatibility: CRA → Vite" row (add once assessed).skills/03-prepare/— CI parallelism, feature flag provisioning, and staging environment setup must be complete before this skill runs.skills/04-migrate/frontend/js-to-typescript/— complete the TypeScript migration before this skill if the repo has mixed.js/.tsfiles; Vite's default config handles.tsxcleanly but mixed-module CRA projects have edge cases.skills/05-validate/— run/validateafter cut-over to confirm the Vite artifact behaves identically to CRA in production traffic.https://vitejs.dev/guide/migration.html— official Vite migration notes; consult for CommonJS interop andoptimizeDepsconfiguration when Step 13 fails.https://vitest.dev/guide/migration.html— official Jest→Vitest migration guide; thevi.*API mapping table is the authoritative reference for Step 11.
9. Done Criteria
<!-- Claude evaluates each item and reports pass/fail before declaring this skill complete. Any unchecked item means the skill is NOT complete. Skill-specific gates (1–10) are above the mandatory universal gates (11–15). -->-
react-scriptsis absent frompackage.jsondependenciesanddevDependencies—grep '"react-scripts"' <repo_root>/package.jsonreturns no matches. (This gate applies only after the feature flag has been flipped to Vite permanently and the CRA scripts have been removed in the cleanup commit.) -
scripts.buildinpackage.jsonis"vite build"—grep '"build":' <repo_root>/package.jsonshowsvite build, notreact-scripts build. - CI runs only
vite build; thebuild:vitescript alias and thereact-scripts buildstep have been removed from the CI pipeline config — read the CI config file (Makefile/.github/workflows/*.yml/Jenkinsfile) and confirm. - All
REACT_APP_prefixes are gone fromsrc/—grep -rn 'REACT_APP_' <repo_root>/src/returns no matches. Also confirmed byenv-vars-renamedandno-process-env-react-appequivalence tests. - No CRA config files remain:
config/directory does not exist;react-app-env.d.ts(if present) has been replaced with/// <reference types="vite/client" />insrc/vite-env.d.ts— verify with a glob check. -
vitest-suiteequivalence test recorded as pass — Vitest test counts match the pre-migration Jest baseline exactly. -
vite-build-exits-0equivalence test recorded as pass — build artifact exists atbuild-vite/index.html. -
entry-points-presentequivalence test recorded as pass — HTML entry has<script type="module">and a stylesheet link. -
lighthouse-perfequivalence test recorded as pass — Lighthouse Performance score does not regress more than<lighthouse_budget_points>points. -
no-cra-artifacts-in-vite-outputequivalence test recorded as pass —grep -r 'react-scripts' build-vite/returns no matches. - All output files listed in Section 7 exist at their declared paths — verify each with a file read.
- Every equivalence test in Section 6 has a recorded result in
output/cra-to-vite-equiv-<timestamp>.md— no test name is missing from the results file. - No equivalence test in Section 6 is recorded as fail — grep the results file for
fail; zero matches required. - The migration log includes a confidence level (High / Medium / Low) — grep
output/cra-to-vite-log-<timestamp>.mdforConfidence:. - The migration log includes a numbered assumptions list — grep
output/cra-to-vite-log-<timestamp>.mdforAssumptions:.