Create React AppからViteへ移行する — Claude Codeでビルドとテストを通す手順
CRAのreact-scriptsを外してViteへ移すとき、REACT_APP_からVITE_への置換とJestの扱いが山場です。ビルドとテストが通ることを完了条件にしてClaude Codeに進めさせる手順をまとめます。
Create React App(CRA)で作ったアプリをViteへ移すとき、作業の大半は機械的な置換です。ただし置換の取りこぼしは、ビルドが通っても画面で初めて気づく形で出ます。Claude Codeに任せるなら、先に「npm run buildとnpm testが両方通ったら終わり」と決めてから始めるのが安全です。
この記事では、react-scriptsを外す順序、環境変数のプレフィックス置換、Jestをどうするかの3点を、Claude Codeへの指示と検証コマンド付きで追います。
CRAを移す先はViteでよいのか
React公式は2025年2月にCRAの非推奨を発表しました。理由は、アクティブなメンテナがいないことです。CRAはメンテナンスモードで動き続け、React 19対応版も公開されています。新規アプリでcreate-react-appを入れると、非推奨の警告が表示されます。
移行先は2系統に分かれます。
CRAからの移行先は2系統
フレームワークへ
ルーティング、データ取得、コード分割まで統合されたものを選ぶ道です。Reactチームが推奨するのはこちらで、クライアントだけのSPAとして移す手引きも各フレームワークが用意しています。
ビルドツールへ
設定を自分で持つ道です。目安は「ルーティングが必要なアプリなら、フレームワークの恩恵が大きい」です。
この記事はビルドツール側、Viteへの移行を扱います。ルーティングを自前で足す予定がある場合は、先にフレームワーク案も比べてください。ルーターの型生成まで含めた例はReact Router v7のtypegenの記事にあります。
作業の前に完了条件をCLAUDE.mdへ書く
Claude Codeのベストプラクティスは、Claudeに自分の作業を確かめる手段を渡すことを勧めています。確かめ方の強さは4段階です。
- プロンプトで「テストが通るまで続けて」と頼む
/goalで終了条件を設定し、別のモデルに毎ターン判定させるStopフックで、チェックが通るまでターンを終えさせない/code-reviewで、会話を知らないサブエージェントにdiffを見せる
CRAの移行は、確かめる対象が最初から決まっています。ビルドとテストです。まずプロジェクトのCLAUDE.mdに書いておきます。
## CRAからViteへの移行(作業中のみ)
- 完了条件: `npm run build` と `npm test` が両方 exit 0
- 検証のたびにコマンドと出力の末尾を貼ること(「通りました」だけの報告は不可)
- `REACT_APP_` と `process.env` が src/ に残っていないこと
(確認: `grep -rn "REACT_APP_\|process\.env" src public index.html`)
- 依存の追加・削除は package.json の差分を見せてから実行CLAUDE.mdにはビルドとテストのコマンドを書くのが想定用途です。移行が終わったら、この節は消して構いません。
長めの作業で終了条件を強制したいときは、/goalを使います。
/goal npm run build と npm test が exit 0 で、src/ に REACT_APP_ が残っていない/goalは、Claudeが進捗の要約だけを返してターンを終えてしまう長時間のバックグラウンド実行向けとして説明されています。対話で見張れる移行なら、プロンプトとCLAUDE.mdで足ります。
権限はnpm runだけ先に許可する
移行中はnpm run buildとnpm testを何十回も回します。確認ダイアログで止まらないよう、.claude/settings.jsonで許可しておきます。
{
"permissions": {
"allow": [
"Bash(npm run *)",
"Bash(npm test *)"
]
}
}Bash(npm run *)はnpm run buildやnpm run test --watchに一致し、npm installには一致しません。依存の入れ替えは確認が入る側に残るので、package.jsonの差分を見てから承認できます。許可ルールの詳細はpermissionsコマンドの記事で扱っています。
Planモードで「CRAに頼っていた箇所」を洗い出す
最初の1手は編集ではなく棚卸しです。Planモードなら、Claudeはファイルを読んで計画を出し、承認するまで編集しません。
claude --permission-mode planセッション中にShift+Tabを押して切り替えることもできます。棚卸しでは次のように頼みます。
react-scripts に依存している箇所をすべて列挙して。
package.json の scripts、public/index.html の %PUBLIC_URL%、
REACT_APP_ で始まる環境変数(.env* と src/ の両方)、
package.json の jest 設定や setupTests、CI とデプロイ設定の build ディレクトリ名、
package.json の "proxy" と src/setupProxy.js、
SVG の ReactComponent import、tsconfig / jsconfig の baseUrl による絶対 import。
まだ編集せず、移行手順の案だけ出して。CRAが暗黙に面倒を見ていた領域は、おおよそ次の7つです。
react-scriptsが肩代わりしていたもの
ビルドと開発サーバー
react-scripts startとreact-scripts build。ViteではCLIのviteとvite buildに置き換わります。HTMLの雛形
public/index.htmlと%PUBLIC_URL%。Viteではindex.htmlがプロジェクト直下に移ります。環境変数
REACT_APP_プレフィックスとprocess.env。ViteはVITE_とimport.meta.envです。テスト実行
react-scripts testが束ねていたJest。Viteには付属しません。開発サーバーのプロキシ
package.jsonの"proxy"とsrc/setupProxy.js。Viteではvite.configのserver.proxyに書き直します。SVGのコンポーネント化
import { ReactComponent as Logo } from './logo.svg'。Viteの標準では動きません。絶対import
baseUrlでsrcを起点にしたimport。Viteではresolve.aliasが要ります。
Planモードの出力に上の領域がすべて入っているかを見てから承認します。抜けがあれば、そのまま追加で聞きます。
移行の手順
CRAからViteへの移行の流れ
- 1
移行前の状態を記録する
npm run buildとnpm testを実行し、結果をそのまま残します。移行前から落ちているテストがあると、移行後の失敗と区別がつかなくなります。 - 2
依存を入れ替える
react-scriptsを外し、viteと@vitejs/plugin-reactを開発依存に足します。 - 3
index.htmlをプロジェクト直下へ移す
public/index.htmlを直下に移し、エントリをtype="module"のscriptで指定します。 - 4
環境変数を置換する
REACT_APP_をVITE_に、process.envをimport.meta.envに変えます。 - 5
プロキシ・SVG・絶対importを置き換える
開発サーバーのプロキシは
server.proxyへ、SVGのコンポーネントimportと絶対importは専用の設定へ移します。 - 6
テストランナーを決めて通す
Vitestへ移すかJestを残すかを選び、テストを再び緑にします。
- 7
build出力先とCIを合わせる
出力先ディレクトリの違いを、デプロイ設定側に反映します。
手順2: 依存を入れ替える
Viteはnpm create vite@latestでひな形を作れますが、既存アプリの移行では手動インストールが近道です。Viteのガイドも、プロジェクトにviteを開発依存で入れる方法を案内しています。ReactのJSX変換とFast Refreshには、公式が管理する@vitejs/plugin-reactを使います。
npm uninstall react-scripts
npm install -D vite @vitejs/plugin-reactvite.config.jsは最小構成で始めます。
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
})package.jsonのscriptsは次のとおりです。
{
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
}
}Viteの対応Node.jsは20.19以上または22.12以上です。CIやDockerイメージのNodeが古いと、依存を入れ替えた時点で動かなくなります。この確認はClaudeに頼む前に、自分の環境で済ませておくと切り分けが楽です。
手順3: index.htmlを直下へ移す
CRAではpublic/の中にあったindex.htmlが、Viteではプロジェクトルートの入口になります。Viteはindex.htmlをソースコードの一部として扱い、<script type="module" src="...">が指すファイルをたどります。
<!-- before: public/index.html -->
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<!-- after: ./index.html -->
<link rel="icon" href="/favicon.ico" />
<script type="module" src="/src/index.jsx"></script>Viteはindex.html内のURLを自動で書き換えるので、%PUBLIC_URL%の置換用プレースホルダは要りません。サブパスで配信する場合は、Viteのbase設定で基準パスを指定します。
JSXを含むファイルの拡張子にも注意します。Viteのドキュメントが「そのままサポートする」と書いているのは.jsxと.tsxです。.jsにJSXを書いている既存コードがあるなら、.jsxへのリネームをClaudeに頼みます。importパスの更新も含めて一括で任せられる作業ですが、リネーム後にnpm run buildを必ず通させます。
プロキシ・SVG・絶対importの置き換え
環境変数ほど目立ちませんが、開発サーバーの挙動とimportの書き方もCRAの機能に乗っていた部分です。ビルドは通っても、開発サーバーで画面が崩れる形で出ます。
CRAはpackage.jsonの"proxy"フィールドかsrc/setupProxy.jsで、開発時のAPIリクエストを別サーバーへ転送していました。Viteではvite.configのserver.proxyに、パスの接頭辞をキーにして書きます。
export default defineConfig({
plugins: [react()],
server: {
proxy: {
'/api': 'http://localhost:4000',
},
},
})server.proxyは開発サーバー専用の設定で、本番の振り分けは別に用意します。開発サーバーのポートも変わります。CRAは3000番が既定でしたが、Viteのserver.portの既定は5173です。ポートが埋め込まれたCORS許可設定や、認証プロバイダーのリダイレクトURLは書き換え対象です。
SVGは、CRAのimport { ReactComponent as Logo } from './logo.svg'がViteでは使えません。ViteでSVGをimportするとファイルのURLが返るためです。コンポーネントとして使い続けるなら、vite-plugin-svgrを入れてpluginsにsvgr()を足し、import Logo from './logo.svg?react'と書き換えます。
jsconfig.jsonやtsconfig.jsonのbaseUrlでsrcを起点にしていたimportは、Viteのresolve.aliasに同じ対応を書きます。エイリアスの値には絶対パスを使います。相対パスはそのまま使われ、ファイルシステム上のパスには解決されません。
import path from 'node:path'
export default defineConfig({
plugins: [react()],
resolve: {
alias: { '@': path.resolve(__dirname, 'src') },
},
})エイリアスの名前を変える場合は、import文も@/components/Buttonのように書き換えます。この置換は、完了条件のnpm run buildで取りこぼしを検出できます。
環境変数のREACT_APP_からVITE_への置換
置換の取りこぼしが最も事故になりやすいのがここです。ルールは次のとおりです。
| CRA | Vite | 補足 |
|---|---|---|
REACT_APP_API_URL | ViteVITE_API_URL | 補足VITE_で始まる変数だけがクライアントに公開される |
process.env.REACT_APP_API_URL | Viteimport.meta.env.VITE_API_URL | 補足値は常に文字列 |
process.env.NODE_ENV | Viteimport.meta.env.MODE / PROD / DEV | 補足modeとNODE_ENVは別の概念 |
%PUBLIC_URL% | Vite不要 | 補足JSではimport.meta.env.BASE_URLが近い役割 |
CRAはREACT_APP_以外の変数を、秘密鍵の流出を避けるために無視します。Viteも同じ発想で、VITE_で始まるものだけを公開します。どちらも値はビルド成果物に埋め込まれます。APIキーなどの秘密情報は、どちらのプレフィックスを付けても守られません。
置換を頼むときは、検索範囲を明示します。
REACT_APP_ を VITE_ に、process.env.REACT_APP_X を import.meta.env.VITE_X に置換して。
対象は src/、.env*、index.html、README。完了したら
grep -rn "REACT_APP_\|process\.env" src public index.html
の出力を貼って。0 件でなければ終わりにしない。Claudeの手が届かない場所が残る
リポジトリ内の置換はgrepで0件を確認できます。残るのはリポジトリの外です。
- ホスティングサービスの環境変数設定(
REACT_APP_の名前のまま登録されている) - CIのシークレットや変数
- Dockerfileやdocker-composeの
ENV
これらは.envと違い、ビルドを回しても気づけません。プレフィックスを変えたままデプロイすると、変数がundefinedのまま本番が動きます。移行PRの説明に「外部で変数名を変える作業」を一覧にして、マージ前に潰してください。
TypeScriptなら、src/vite-env.d.tsで自前の変数に型を付けられます。このファイルにimport文を入れると、型の拡張が効かなくなります。
interface ImportMetaEnv {
readonly VITE_API_URL: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}Jest設定をどう扱うか
react-scripts testはJestを内蔵していました。react-scriptsを外すと、このJestの設定一式もなくなります。選択肢は2つです。
テストランナーの選び方
Vitestへ移す
vite.configにテスト設定を置けます。jest.fnはvi.fnに変わるので、モックを使うテストは書き換えが必要です。
Jestを残す
変換器とjsdom環境を自分で組み直す手間がかかります。Viteの設定とは別の設定ファイルが増えます。
テストが数百本あり、jest.mockを多用しているなら、移行PRを「ビルドの移行」と「テストランナーの移行」に分けるほうが安全です。先にJestを残してViteへ移し、緑になってからVitestへ進みます。Jestのバージョン更新を伴う場合の進め方はJest 30アップグレードの記事も参考になります。
Vitestに移すときの設定
Vitestは既定でグローバルAPIを提供しません。Jestのようにdescribeやexpectをimportなしで使っているテストは、globals: trueを指定しないと動きません。ブラウザ相当の環境が要るReactのコンポーネントテストでは、environmentにjsdomを指定します。
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
},
})@testing-library/reactのようにグローバルの存在を前提に自動クリーンアップするライブラリがあります。globals: falseのまま移すと、この自動クリーンアップが働きません。TypeScriptでグローバルAPIの型を使うなら、tsconfig.jsonのtypesにvitest/globalsを足します。
VitestはViteの6.4.0以上とNode.js 22.12以上を要求します。ViteのNode要件(20.19以上)より厳しい点に注意してください。Vitestのwatchモードで固まらない書き方は、Vitestのテストコマンド設計の記事にまとめています。
テスト移行のプロンプト
テストを Vitest に移して。手順:
1. vitest と jsdom を devDependencies に追加(@testing-library/react は既存のまま)
2. vite.config.js に test.globals: true と test.environment: 'jsdom'
3. jest.fn / jest.mock / jest.spyOn を vi.fn / vi.mock / vi.spyOn に置換
4. package.json の test を "vitest run" に(watch に入れない)
5. npm test が exit 0 になるまで繰り返す。落ちたテストは削除せず原因を報告最後の「削除せず」が要です。落ちるテストをClaudeが黙ってスキップや削除で通すと、完了条件そのものが空洞になります。diffで.skipやit.todoが増えていないかを確認してください。Vue 3側で同じ流れを組んだ例はvue-tscとVitestを検証コマンドにする記事にあります。
build出力先とデプロイ設定
CRAのビルド出力はbuild/ディレクトリです。Viteのbuild.outDirの既定値はdistです。ホスティングの公開ディレクトリ設定やCIの成果物パスがbuildのままだと、ビルドは成功してもデプロイが空になります。
どちらかに揃えます。デプロイ側の設定を変えられない場合は、vite.config.jsで出力先をCRAに合わせます。
export default defineConfig({
plugins: [react()],
build: { outDir: 'build' },
})つまずきやすい箇所
落とし穴
ビルドは通るのに画面で環境変数が空になる
process.env.REACT_APP_Xの置換漏れが1か所でもあると、その参照はViteの環境変数として読まれません。VITE_への書き換え忘れは型エラーにならない書き方もあるため、npm run buildの成功だけでは見つかりません。完了条件にgrepで0件を入れ、さらにnpm run previewでビルド結果を実際に開いて、環境変数を使う画面を目視してください。セッションで/verifyを実行すると、Claudeがアプリをビルド・起動して結果を確かめます。テストや型チェックで拾えない種類の問題を捕まえる用途です。
もう1つは、依存の入れ替えが先に走ってpackage-lock.jsonが大きく動くことです。差分が読めなくなるので、依存の入れ替えは単独のコミットに分けます。
移行後の確認項目
完了報告を受けたら、次を自分の手で確認します。
npm run buildとnpm testがexit 0で終わる(Claudeの報告でなく、手元の出力で)grepでREACT_APP_とprocess.envが0件npm run previewでビルド結果を開き、環境変数に依存する画面が動く- ホスティング・CIの環境変数名を
VITE_に変更した - 出力先ディレクトリがデプロイ設定と一致している
- テストに
.skipやit.todoが増えていない
ここまで揃って初めて、CRAからの移行が終わったと言えます。ベストプラクティスも、Claudeに「通りました」とだけ言わせず、実行したコマンドとその出力を求める運用を勧めています。
まとめ
CRAからViteへの移行は、置換と設定の入れ替えが大半で、Claude Codeに任せやすい作業です。難しいのは作業そのものでなく、終わりの判定です。ビルドとテストが通る、置換のgrepが0件、previewで画面が動く、の3点を先にCLAUDE.mdへ書けば、その判定をClaudeに繰り返させられます。
一方で、ホスティングやCIの環境変数、Node.jsのバージョン、出力先ディレクトリは、リポジトリの外にある設定です。Claudeの手が届かない範囲は、PR説明のチェックリストに書き出して、人間が確認する側に回してください。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。
- react.dev/blog/2025/02/14/sunsetting-create-react-app
- vite.dev/guide
- vite.dev/guide/env-and-mode
- vitest.dev/config/globals
- create-react-app.dev/docs/adding-custom-environment-variables
- code.claude.com/docs/en/best-practices
- code.claude.com/docs/en/common-workflows
- code.claude.com/docs/en/permissions
- vite.dev/config/server-options
- vite.dev/config/shared-options
- create-react-app.dev/docs/proxying-api-requests-in-development
- create-react-app.dev/docs/adding-images-fonts-and-files