harryのブログ

ロードバイクとか模型とかゲームについて何か書いてあるかもしれません

CRAからViteへの移行 - Vite移行編

前回: CRAからViteへの移行 - テスト移行編 - harryのブログ

前書き

前回create-react-app(CRA)に依存せずjest単体でテストできるようにしました。

今回から本格的にVite移行を進めます。参考にしたのは以下の記事や公式ドキュメントです。

移行前環境

  • CRA: 5.0.1
  • React: 18.2.0
  • TypeScript: 5.3.3
  • jest: 29.7.0
  • ESLint: 8.56.0

ビルド環境

  • Node.js: 20系

デプロイ先

移行作業

手順

基本的な流れは以下の通り。

  1. 既存設定の見直し
  2. CRAアンインストール
  3. ESLint plugin アップデート
  4. Vite導入
  5. 個別のエラー対応

既存設定の見直し

こんなタイミングでもないと見直さないので、Vite移行とは無関係に直せそうなところを直しておきます。

tsconfig.json の target

.eslintrc.json の env が "es2021" になってたり、tsconfig.json の target が "es2020" になってたり微妙だったので、"es2021" に統一。

自分のコードではURL safe base64の変換時に String.prototype.replaceAll() を使用しているので、ES2021以降のfeatureが必要ですが、Can I use を見る限り、もう target は ES2021 で問題ないとの判断。

src/service/UrlParamConverter.ts:19:6 - error TS2550: Property 'replaceAll' does not exist on type 'string'. Do you need to change your target library? Try changing the 'lib' compiler option to 'es2021' or later.

19     .replaceAll('+', '-')
        ~~~~~~~~~~

src/service/UrlParamConverter.ts:30:8 - error TS2551: Property 'replaceAll' does not exist on type 'UrlSafeBase64String'. Did you mean 'replace'?

30       .replaceAll('-', '+')
          ~~~~~~~~~~

lint script修正

なんか eslint のコマンドが微妙だったので修正。Viteのサンプルプロジェクトからパクったをインスパイアしました。

package.json

-    "lint": "eslint ./src/**/*.{ts,tsx} --max-warnings=0",
-    "lint:fix": "eslint --fix ./src/**/*.{ts,tsx}"
+    "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings=0",
+    "lint:fix": "eslint . --ext ts,tsx --fix"

--report-unused-disable-directives によってエラーになった箇所も一緒に修正した。*1

CRAアンインストール

後で必要なライブラリをインストールする際にエラーが出るので、先にアンインストールしておきます。そういうとこやぞ、CRA。

> npm rm -D react-scripts

removed 820 packages, and audited 965 packages in 14s

138 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

ばいばい create-react-app!

100年振りに found 0 vulnerabilities になりました。

CRA削除したので、 babel.config.js から 'react-app' を削除します。なぜ前回のテスト移行で追加してしまったのか、コレガワカラナイ。

babel.config.js

  presets: [
-   'react-app',
    '@babel/preset-env',
    ['@babel/preset-react', { runtime: 'automatic' }],
    '@babel/preset-typescript'
  ],

ESLint configのCRA依存の設定も不要になったので削除。

package.json

-  "eslintConfig": {
-    "extends": [
-      "react-app",
-      "react-app/jest"
-    ]
-  },

また、以下のissueのために追加してた overrides も不要になったので削除。

(react-scripts) Support for TypeScript 5.x · Issue #13080 · facebook/create-react-app · GitHub

package.json

-  },
-  "overrides": {
-    "typescript": "^5.3.3"
  }

何故か dependencies にいる @types/node もCRA由来で入ってた可能性があるので消しておきます。

npm rm @types/node

そしてCRAに入ってたnanoidは一緒に消えてしまったので、改めてインストールします。*2

npm i nanoid

この時点でちゃんとテスト(≒ CI)が通ることを確認しておきます。CDは浜で死にました。

> npm run test

> last-origin-unit-viewer@1.2.21 test
> jest

 PASS  src/data/unitBasicData.test.ts
 PASS  src/domain/skill/SkillAreaOfEffectMatcher.test.ts
 PASS  src/data/equipmentData.test.ts
 PASS  src/App.test.tsx

Test Suites: 4 passed, 4 total
Tests:       103 passed, 103 total
Snapshots:   0 total
Time:        5.622 s
Ran all test suites.
> npm run lint    

> last-origin-unit-viewer@1.2.21 lint
> eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings=0

=============

WARNING: You are currently running a version of TypeScript which is not officially supported by @typescript-eslint/typescript-estree.

You may find that it works just fine, or you may not.

SUPPORTED TYPESCRIPT VERSIONS: >=3.3.1 <5.2.0

YOUR TYPESCRIPT VERSION: 5.3.3

Please only submit bug reports when using the officially supported version.

=============

ESLint plugin アップデート

CRAを消し去った所で、npm run lint 実行時に出ている警告が気になるので、ESLint plugin をアップデート。

npm i -D @typescript-eslint/eslint-plugin@latest @typescript-eslint/parser@latest

アップデート後に実行してみると、警告は消えましたが代わりにエラーが出ました。

> npm run lint                                                                     

> last-origin-unit-viewer@1.2.21 lint
> eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings=0


D:\_git\last-origin-unit-viewer\src\state\squad\SquadHook.ts
  254:9  error  Unused eslint-disable directive (no problems were reported from '@typescript-eslint/no-non-null-assertion')1 problem (1 error, 0 warnings)
  1 error and 0 warnings potentially fixable with the `--fix` option.

これは、URLコピー機能に使用している inputRef.current!.select(); などに対してエラーを抑制していた所で*3、抑制が不要になったようなので、eslint-disableeslint-enable のコメント行を削除しました。

Vite導入

準備が整ったのでViteを導入していきます。

Viteには各種テンプレートとそれを試せるオンラインエディタが準備されているので、それを参考に作業を進めます。

手順は以下の通りです。

  1. 必須ライブラリのインストール
  2. 設定ファイル配置/変更
  3. index.htmlの移動と編集
  4. 環境変数対応

必須ライブラリのインストール

今回は react-ts テンプレートを参考に、必須となる vite@vitejs/plugin-react をインストールします。

npm i -D vite @vitejs/plugin-react

設定ファイル配置/変更

設定ファイルの配置

ドキュメントやテンプレートを参考に、以下の設定ファイルを追加します。

  • vite.config.ts
  • src/vite-app-env.d.ts
    • 既存の src/react-app-env.d.ts を変更

vite.config.ts

デプロイ先がGitHub pagesなので base を指定する必要があります。この値は import.meta.env.BASE_URL で参照可能です。

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  base: '/last-origin-unit-viewer/'
});

src/react-app-env.d.ts -> src/vite-app-env.d.ts

- /// <reference types="react-scripts" />
+ /// <reference types="vite/client" />
設定ファイルの変更

package.jsonscripts を以下のように変更します。

package.json

  "scripts": {
-   "start": "react-scripts start",
-   "build": "react-scripts build",
+   "dev": "vite",
+   "build": "tsc && vite build",
    "test": "jest",
-   "eject": "react-scripts eject",
    "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings=0",
-   "lint:fix": "eslint . --ext ts,tsx --fix"
+   "lint:fix": "eslint . --ext ts,tsx --fix",
+   "preview": "vite preview"
  },

特にこだわりはないのでテンプレに準拠しますが、testbuild に変更はないので、CI/CDのコマンド変更は不要でした。

また、Viteではビルド後のファイルが dist ディレクトリに格納されるので、参照してた箇所を変更します。

.gitignore

# production
- /build
+ /dist

.github/workflows/gh-pages.yml

      - name: Deploy
        uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
-         publish_dir: ./build
+         publish_dir: ./dist

index.htmlの移動と編集

index.htmlは色々対応が必要です。

1. index.htmlをrootディレクトリに移動

Viteではindex.htmlはrootディレクトリに配置するようなので移動します。

public/index.html -> index.html

2. index.tsxをscriptタグで追加

テンプレートのプロジェクトと同様、#root のdiv要素直下に追加します。CRAの index.tsx はテンプレートに合わせて main.tsx に変更してもいいと思います。*4

  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
+   <script type="module" src="/src/index.tsx"></script>
  </body>
3. 絶対パス指定のhref属性から %PUBLIC_URL% を削除

前書きで紹介した参考記事でも言及されている通り、public ディレクトリに配置されたassetはルートパス / から提供されるので、%PUBLIC_URL% が不要になります。

静的アセットの取り扱い | Vite

  • 具体例:
    href="%PUBLIC_URL%/manifest.json" -> href="/manifest.json"
4. URLで使用している %PUBLIC_URL%%BASE_URL% に変更

元々 og:image などのURLに %PUBLIC_URL% を使用していたため、これを %BASE_URL% に変更します。

BASE_URL はShared Optionsの base に設定した値のため、両端に / が含まれていることに注意します。

  • 具体例:
    content="https://harry0000.github.io%PUBLIC_URL%/" -> content="https://harry0000.github.io%BASE_URL%"

アプリのURLはそもそも変更することがないので、元々 index.html にハードコーディングしています。

環境変数対応

コード中の process.env から参照している環境変数には undefined が格納されるため、すべて書き換えていきます。

1. process.env.PUBLIC_URLimport.meta.env.BASE_URL に変更

index.htmlのhref属性などはルートディレクトリからのパスを書いておけばビルド時に変換が行われますが、コードで動的に生成してるassetのURLなどは変換が行われません。

基本的には、process.env.PUBLIC_URLimport.meta.env.BASE_URL に置換すればよいのですが、ここでも BASE_URL の両端に / が含まれていることに注意します。

2. REACT_APP 環境変数を変更

Viteでは envPrefix のデフォルト値が VITE_ になっていて、このprefixから始まる環境変数meta.env に自動で読み込まれます。

共通オプション | Vite

そのため、REACT_APP_ prefixで設定していた環境変数VITE_ prefix に書き換えていきます。

もちろん、CDで行っているbuildの環境変数も変更します。

だからGitHubに登録したsecretsにはツール固有のprefixを付けないようにする必要があったんですね(0敗)。

.github/workflows/gh-pages.yml

      - name: Build
        env:
-         REACT_APP_FIREBASE_WEB_API_KEY: ${{ secrets.FIREBASE_WEB_API_KEY }}
-         REACT_APP_GA_MEASUREMENT_ID: ${{ secrets.GA_MEASUREMENT_ID }}
+         VITE_FIREBASE_WEB_API_KEY: ${{ secrets.FIREBASE_WEB_API_KEY }}
+         VITE_GA_MEASUREMENT_ID: ${{ secrets.GA_MEASUREMENT_ID }}
        run: npm run build

個別のエラー対応

やっとViteの導入が一通り終わったので、ビルドしてみて出たエラーに対処していきます。

SVG ファイル

ビルドしてみるとSVGファイルをコンポーネントとしてimportしている箇所でエラーとなりました。

> npm run build

> last-origin-unit-viewer@1.2.21 build
> tsc && vite build

src/component/squad/SquadShareModal.tsx:10:10 - error TS2614: Module '"*.svg"' has no exported member 'ReactComponent'. Did you mean to use 'import ReactComponent from "*.svg"' instead?

10 import { ReactComponent as TwitterSocialIcon } from '../icon/TwitterSocialIcon.svg';
            ~~~~~~~~~~~~~~


Found 1 error in src/component/squad/SquadShareModal.tsx:10

ViteではSVGコンポーネントとして使うことを推奨していないようです。

Performance | Vite

  • Don't transform SVGs into UI framework components (React, Vue, etc). Import them as strings or URLs instead.

ですが、

ので、エラーになってしまうと少し困ります。

そこでSVG画像をReactコンポーネントとしてimportできるようにする vite-plugin-svgr を導入しました。

導入方法はREADME.mdに書かれている通りで、変更箇所などは以下の通りです。

npm i -D vite-plugin-svgr

vite.config.ts

import react from '@vitejs/plugin-react';
+ import svgr from 'vite-plugin-svgr';

// https://vitejs.dev/config/
export default defineConfig({
- plugins: [react()],
+ plugins: [react(), svgr()],
  base: '/last-origin-unit-viewer/'
});

src/vite-app-env.d.ts

/// <reference types="vite/client" />
+ /// <reference types="vite-plugin-svgr/client" />

src\component\squad\SquadShareModal.tsx

- import { ReactComponent as TwitterSocialIcon } from '../icon/TwitterSocialIcon.svg';
+ import TwitterSocialIcon from '../icon/TwitterSocialIcon.svg?react';

ただし、この変更によりSVGのpathが微妙に変わってしまったので、jestの設定も変更しておきます。

jest.config.js

  moduleNameMapper: {
    '\\.css$': 'identity-obj-proxy',
-   '\\.svg$': '<rootDir>/src/__mock__/SvgMock.tsx'
+   '\\.svg?(\\?react)$': '<rootDir>/src/__mock__/SvgMock.tsx'
  },

とりあえずビルドは通る様になりましたね!初回のビルドで16.52sは爆速です!

> npm run build            

> last-origin-unit-viewer@1.2.21 build
> tsc && vite build

The CJS build of Vite's Node API is deprecated. See https://vitejs.dev/guide/troubleshooting.html#vite-cjs-node-api-deprecated for more details.
vite v5.0.12 building for production...
✓ 662 modules transformed.
dist/index.html                                               1.72 kB │ gzip:   0.80 kB
dist/assets/UnitSkillList-8zQ9gu3V.css                        0.69 kB │ gzip:   0.24 kB
dist/assets/CoreLinkSelector-Ujaa1XhX.css                     1.12 kB │ gzip:   0.37 kB
dist/assets/SquadUnitStatusParameterTabPane-UCPTJ5C0.css      2.10 kB │ gzip:   0.66 kB
dist/assets/EquipmentSelector-MCQdzsnC.css                    4.05 kB │ gzip:   1.11 kB
dist/assets/index-N5thVY8F.css                              175.86 kB │ gzip:  27.21 kB
dist/assets/SlotUnavailableOverlay-zgmMeYri.js                0.44 kB │ gzip:   0.33 kB
dist/assets/Col-6c_Lo0lY.js                                   0.68 kB │ gzip:   0.46 kB
dist/assets/FullLinkBonusDropdown-AgXuAz8M.js                 1.64 kB │ gzip:   0.68 kB
dist/assets/CoreLinkSelector-EyLr-jVq.js                      3.48 kB │ gzip:   1.35 kB
dist/assets/SquadUnitStatusParameterTabPane-GbDIXrvO.js       5.47 kB │ gzip:   1.92 kB
dist/assets/EquipmentSelector-VEx41JlM.js                     6.50 kB │ gzip:   2.28 kB
dist/assets/SquadShareModal-b1nutW_c.js                      23.97 kB │ gzip:   8.39 kB
dist/assets/UnitSkillList-oQqVaHuM.js                        29.95 kB │ gzip:   8.52 kB
dist/assets/index-DA70HBsk.js                             2,003.55 kB │ gzip: 346.85 kB

(!) Some chunks are larger than 500 kB after minification. Consider:
- Using dynamic import() to code-split the application
- Use build.rollupOptions.output.manualChunks to improve chunking: https://rollupjs.org/configuration-options/#output-manualchunks
- Adjust chunk size limit for this warning via build.chunkSizeWarningLimit.
✓ built in 16.52s

jestのエラー対応

ビルドが通ったのは良いのですが、またjest殿が死んでおられるぞ!という状況になってしまいました。

    Details:

    D:\_git\last-origin-unit-viewer\src\service\ShareUrlGenerator.ts:18
    var appSiteUrl = new URL(import.meta.env.BASE_URL, 'https://harry0000.github.io').toString();
                                    ^^^^

    SyntaxError: Cannot use 'import.meta' outside a module

このエラーの原因は前書きの参考リンクでも書かれている通り、import.meta に対してBabelのトランスパイルが必要なためです。babel-preset-viteをインストールしましょう。

npm i -D babel-preset-vite

babel.config.js

  presets: [
    '@babel/preset-env',
    ['@babel/preset-react', { runtime: 'automatic' }],
-   '@babel/preset-typescript'
+   '@babel/preset-typescript',
+   'babel-preset-vite'
  ],

またjestのテストが生き返りました。

> npm run test              

> last-origin-unit-viewer@1.2.21 test
> jest

 PASS  src/data/unitBasicData.test.ts
 PASS  src/domain/skill/SkillAreaOfEffectMatcher.test.ts
 PASS  src/data/equipmentData.test.ts
 PASS  src/App.test.tsx (13.114 s)

Test Suites: 4 passed, 4 total
Tests:       103 passed, 103 total
Snapshots:   0 total
Time:        13.95 s, estimated 15 s
Ran all test suites.

ビルド時間の比較

Vite移行後恒例のビルド時間比較のお時間です。

command CRA Vite
npm i 60 s *5 52 s *6 1.15 倍
npm run start (初回) 59820 ms 446 ms 134.12 倍
npm run start (2回目) 6500 ms 288 ms 22.57 倍
npm run build 37.49 s 16.52 s 2.27 倍

※ Viteの開発サーバー起動のcommandは npm run dev です

開発サーバーの初回起動がちょっとおかしいレベルで早くなってますね。最初 400 ms で起動してしまい、もう一度計り直した値を記載しましたが、それでも爆速です。今まで一体どれだけの時間を無駄にしていたんだ…。

Vite移行が完了して

Vite移行もなんやかんやでものすごい色々作業があって時間がかかりました。移行後に改めて npm outdated で各種依存ライブラリを最新化した方が良いでしょう。

ただ時間がかかった分、移行したメリットはあったと思います。ついでに設定ファイルなど色々整理することもできました。

一旦移行が終わって満足しましたが、次回があるとすれば、jestからVitestへの移行かもしれません。

オレはようやくのぼりはじめたばかりだからな このはてしなく遠いVite坂をよ…


*1:誤検知を黙らせるための eslint-disable-next-line がいつの間にか不要になっていた?

*2:お行儀悪くCRAに依存していたnanoidを使っていたため

*3:iOS対応のため、テキスト選択してコピーを行う対応をしていました

*4:この場合、index.cssも一緒にリネームしたいところ

*5:added 1750 packages, and audited 1751 packages in 1m

*6:added 1026 packages, and audited 1027 packages in 52s