🌀

create-react-app から Vite への移行

2022/05/17に公開
3

概要

担当プロジェクトでは基本モブプログラミングに取り組んでいるのですが、必要技術の研究や個人でやる方が向く作業用の時間を取っており、その時間を使って React の初期構築周りの知識習得も兼ねて create-react-app で作成したプロジェクトを Vite へ移行する作業を行いました。
参考にした記事とは React のバージョンなど異なる点もありましたので参考になる人もいるかと思い行った作業についてまとめ共有したいと思います。

参考

環境

  • React: 17.0.2
  • craco: 6.4.3
  • typescript: 4.5.5
  • node.js: 16.14.2

Vite への移行手順

パッケージ追加と初期設定

Vite と Vite で React を動かすのに必要なパッケージを追加します。

yarn add --dev vite @vitejs/plugin-react

package.json の起動コマンドを変更します。引数なしの vite が開発サーバーの起動コマンドです。Vite は build 時に型検査を行わないので build 前に tsc --noEmit を追加して別途型検査を行うようにしています。

// package.json
   "scripts": {
-    "start": "craco start",
-    "build": "craco build",
+    "start": "vite",
+    "build": "tsc --noEmit && vite build",
+    "serve": "vite preview",
     "test": "craco test",
   }
   "devDependencies": {
     ...
+    "@vitejs/plugin-react": "^1.3.2",
     ...
+    "vite": "^2.9.8"
   },

プロジェクトルートに Vite の設定ファイル vite.config.ts を以下の内容で作成します。React のプラグインロードと CRA と同じように起動時にブラウザを開くようにしています。

// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  server: {
    open: true
  },
  plugins: [react()]
})

index.html の Vite 対応

index.html はプロジェクトルートに置く必要があるので public/index.html から移動させ、中身も Vite に対応できるよう以下の修正を加えます。

  • %PUBLIC_URL% を削除する
  • React のエントリーポイントとなる index.tsx を script タグで指定する
// index.html
   <head>
     <meta charset="utf-8" />
-    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
+    <link rel="icon" href="/favicon.ico" />


-    <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
+    <link rel="apple-touch-icon" href="/logo192.png" />


-    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
+    <link rel="manifest" href="/manifest.json" />
-    <!--
-      Notice the use of %PUBLIC_URL% in the tags above.
-      It will be replaced with the URL of the `public` folder during the build.
-      Only files inside the `public` folder can be referenced from the HTML.
-
-      Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
-      work correctly both with client-side routing and a non-root public URL.
-      Learn how to configure a non-root public URL by running `npm run build`.
-    -->
     <title>React App</title>
   </head>

   <body>
     <noscript>You need to enable JavaScript to run this app.</noscript>
     <div id="root"></div>
-    <!--
-      This HTML file is a template.
-      If you open it directly in the browser, you will see an empty page.
-
-      You can add webfonts, meta tags, or analytics to this file.
-      The build step will place the bundled scripts into the <body> tag.
-
-      To begin the development, run `npm start` or `yarn start`.
-      To create a production bundle, use `npm run build` or `yarn build`.
-    -->
+    <script type="module" src="/src/index.tsx"></script>
   </body>
 </html>

リセットCSSの適用方法の変更

index.css だとパスが解決できずにエラーとなったので index.tsx に import を移動しました。恐らく設定で解決できると思うのですが css での import にこだわる理由もないので。

// index.css
-@import "~sanitize.css";
-@import "~sanitize.css/forms.css";
-@import "~sanitize.css/typography.css";
// index.tsx
 import React from 'react'
 import ReactDOM from 'react-dom'
+import 'sanitize.css'
+import 'sanitize.css/forms.css'
+import 'sanitize.css/typography.css'
 import './index.css'
 import App from './App'

環境変数の Vite 対応

必要な作業は以下の 3 点です

  • 環境変数の prefix を REACT_APP_ から VITE_ に変更する
  • src/react-app-env.d.tssrc/vite-env.d.ts にリネームし参照先を変える
    • Vite がこのファイル名で特別処理をしているわけではないので別名・別場所でも大丈夫です
  • コードで参照する際の process.envimport.meta.env に変更する
    src/vite-app-env.d.ts の内容は以下の通りでこれを行わないと import.meta.env の型の解決負荷エラーが出ます。
// src/vite-app-env.d.ts
-/// <reference types="react-scripts" />
+/// <reference types="vite/client" />

コード上の変更例です。

-const port = process.env.REACT_APP_PORT
+const port = import.meta.env.VITE_PORT

tsconfig.json の baseUrl 対応

aleclarson/vite-tsconfig-paths: Support for TypeScript's path mapping in Vite
vite-tsconfig-paths は tsconfig.json の allowJs, baseUrl, include/exclude の設定を Vite にも反映してくれる plugin です。移行したプロジェクトでは baseUrl を設定していたので導入しました。

yarn add --dev vite-tsconfig-paths
// vite.config.ts
 import { defineConfig } from 'vite'
 import react from '@vitejs/plugin-react'
 +import tsconfigPaths from 'vite-tsconfig-paths'

 export default defineConfig({
   server: {
     open: true
   },
-  plugins: [react()]
+  plugins: [react(), tsconfigPaths()]
 })

動作確認

ここまでの作業で Vite による開発サーバーの起動やビルドなどを試したところアプリも問題なく動作したのでアプリの動作周りに関しては移行作業が完了です。ただこのままだと Jest が CRA に依存した状態なので続いてそちらの移行作業を行います。

Jest の CRA 依存を外す

  1. yarn eject を実行して内部の設定やスクリプトを出力する
  2. package.json に出力された babel と jest の設定をファイル化する
  3. package.json に出力された本来は devDependencies にあるべきパッケージを移動する
  4. package.json の test コマンドを "test": "jest", に変更する
  5. eject されたパッケージ・設定・スクリプトを読み解き必要なものを残す
    eject されたファイルの中身など全て書き出すと長くなり過ぎるので要点を絞って順番に説明します。

1. yarn eject を実行して内部の設定やスクリプトを出力する

react-scripts
yarn eject で出力される設定やスクリプトは react-scripts パッケージの中身をフィルタリングして出力したものなので、プロジェクトに出力せずに参考にしたい場合はリポジトリ上のコードを直接参考にすることもできます。その際はプロジェクトで使ってるバージョンのタグを探してください。@remove-on-eject-begin@remove-on-eject-end で囲まれたコードは出力時に除外される部分です。

2. package.json に出力された babel と jest の設定をファイル化する

package.json が長くなり過ぎるし専用のファイルにあった方が扱いやすいので個別のファイルにわけました。

3. package.json に出力された本来は devDependencies にあるべきパッケージを移動する

react-scripts の package.json の内容が反映される影響で jest や @types 系のパッケージが dependencies に定義されてしまうので devDependencies に移動しました。後述しますが jest に必要ないとわかっているパッケージがあれば、この時点で削除しても良いと思います。

4. package.json の test コマンドを "test": "jest", に変更する

// package.json
-    "test": "craco test",
+    "test": "jest",

5. eject されたパッケージ・設定・スクリプトを読み解き必要なものを残す

eject される jest の設定の transform の部分が以下のようになっています。

// jest.config.ts
  transform: {
    '^.+\\.(js|jsx|mjs|cjs|ts|tsx)$': '<rootDir>/config/jest/babelTransform.js',
    '^.+\\.css$': '<rootDir>/config/jest/cssTransform.js',
    '^(?!.*\\.(js|jsx|mjs|cjs|ts|tsx|css|json)$)': '<rootDir>/config/jest/fileTransform.js'
  },

最初の babelTransform.js の内容は babel-jest の設定を生成して実行するものになっています。行っている処理は React 17 の新しい JSX トランスフォームを利用するかを判定して babel-preset-react-app の runtime オプションを 'automatic' と 'classic' のどちらにするかの切り替えと babel.config.json や .babelrc といった設定ファイルの無効化です。どちらの処理不要でしたので babel-jest を直接呼び出す形式に変更しました。

// jest.config.ts
  transform: {
-   '^.+\\.(js|jsx|mjs|cjs|ts|tsx)$': '<rootDir>/config/jest/babelTransform.js',
+   '^.+\\.(js|jsx|mjs|cjs|ts|tsx)$': 'babel-jest',
    ...
  },

Jest は Vite の import.meta.env での環境変数へのアクセスに対応していないので babel で変換する必要があり、その為の babel-preset-vite というプラグインを追加します。

yarn add --dev babel-preset-vite

babelTransform.js の内容も合わせた最終的な babel の設定がこちらです。

// babel.config.json
{
  "presets": [
    ["react-app", { "runtime": "automatic" }],
    "babel-preset-vite"
  ]
}

出力された Jest の設定では dotenv が反映されていなかったので設定を追記します。

// jest.config.ts
   setupFiles: [
+    'dotenv/config',
     'react-app-polyfill/jsdom'
   ],

ここまででテストが通るようになったので他の transform の内容も確認したところ利用していないものでしたので、最終的に Jest の transform の設定はデフォルト値で十分ということがわかり項目ごと削除しました。

// jest.config.ts
- transform: {
-   '^.+\\.(js|jsx|mjs|cjs|ts|tsx)$': 'babel-jest',
-   '^.+\\.css$': '<rootDir>/config/jest/cssTransform.js',
-   '^(?!.*\\.(js|jsx|mjs|cjs|ts|tsx|css|json)$)': '<rootDir>/config/jest/fileTransform.js'
- },

transform 以外の設定も <rootDir>/src 以下が対象となるように明示的にしたものが多くデフォルトでも問題はなさそうでしたが、現在はそのままで影響を把握できたら徐々に変更する予定です。
Vite も Jest も問題なく動作する状態になりましたので不要そうなパッケージを順次削除していき最終的な差分が以下の通りです。

   "dependencies": {
-    "@craco/craco": "^6.4.3",
+    "dotenv": "^10.0.0",
+    "dotenv-expand": "^5.1.0",
     "react": "^17.0.2",
     "react-dom": "^17.0.2",
     ...
   },
     
     ...
   "devDependencies": {
+    "@babel/core": "^7.17.10",
     "@testing-library/jest-dom": "^5.14.1",
     "@testing-library/react": "^12.0.0",
     ...
     "@vitejs/plugin-react": "^1.3.2",
+    "babel-jest": "^28.1.0",
+    "babel-loader": "^8.2.3",
+    "babel-plugin-named-asset-import": "^0.3.8",
+    "babel-preset-react-app": "^10.0.1",
+    "babel-preset-vite": "^1.0.4",
     ...
+    "jest": "^27.4.3",
+    "jest-watch-typeahead": "^1.0.0",
+    "ts-node": "^10.7.0",
     "typescript": "^4.6.4",
     "vite": "^2.9.8"

その他

  • ESLint は CRA に依存してなかったので影響なしでした
  • JavaScript/TypeScript/React 経験が浅かったので CRA の内部の処理や設定が知れて勉強になりました
  • プロジェクト初期なので感じた速さの恩恵は少なめでした
    • テストの実行時間は 5s ちょいから 2s ちょいと半減した

Discussion

rtokrtok

しばらく前に参考にさせていただいたのですが、viteを使って色々触っているとvite-app-env.d.tsではなく、vite-env.d.tsの方が正しそうに感じます。
https://ja.vitejs.dev/guide/env-and-mode.html#typescript-用の自動補完

akinekoakineko

コメントありがとうございます。

Vite のドキュメントには確かに vite-env.d.ts と書かれているのですが、自分の知る限りでは Vite はこのファイル名のファイルに対して特別何か処理を行っているわけではありません。恐らく create-vite のテンプレートが src/vite-env.d.ts になっているので、そこも考慮した説明としてこのファイル名及び場所が使われているのだと思います。

この型定義ファイルは TypeScript の為に用意するのですが、TypeScript は tsconfig.jsonfilesincludes などのオプションでコンパイル対象に含まれている .d.ts の型定義ファイルを自動的に認識してくれるので、実はファイル名はなんでもよかったりします。ちなみに外部パッケージの型定義ファイルについてはまたちょっと事情が違うのですが説明すると長くなるのでここでは割愛させて頂きます。

この記事を書いた当時はそこまで知識がなかった事から参考にさせて頂いた記事の通りにしたのですが、公式ドキュメント通りの方が余計な混乱がなくて良さそうにも感じたので訂正とコメントを添えておこうと思います。
改めてご指摘ありがとうございました!

rtokrtok

tsconfig.json の files や includes などのオプションでコンパイル対象に含まれている .d.ts の型定義ファイルを自動的に認識してくれる

言われてみればそうですね。勉強になります。
ありがとうございます!