CRAreact-scripts で構築していた React アプリを Vite に移行した際のログです。

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

yarn add -D vite @vitejs/plugin-react

index.html をルートフォルダに移動

Vite ではここに置く必要があるようです。

また public/ ディレクトリにあるファイルへは / でアクセスできるので、以下のような変更をします。

- <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
+ <link rel="icon" href="/favicon.ico" />

import path を ~/ で絶対参照できるようにする

vite.config.ts で alias を設定します。

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: [
      {
        find: "~",
        replacement: path.resolve(__dirname, "./src"),
      },
    ],
  },
});

エラー: sass の import ~@

この状態で起動してブラウザで表示すると以下のエラーが発生しました。

[plugin:vite:css] [sass] Can't find stylesheet to import.
  ╷
6 │ @import '~@progress/kendo-theme-default/scss/core/_index.scss';
  │         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  ╵
  src/index.scss 6:9  root stylesheet

Error: Can’t find stylesheet to import. · Issue #5764 · vitejs/vite · GitHub

vitest.config.ts の alias に以下を追加して解消しました。

      {
        find: /^~@progress\/kendo-theme-default/,
        replacement: '@progress/kendo-theme-default',
      },

エラー: sass の import ~/

起動時に以下のエラーが表示されました。

[plugin:vite:css] [sass] ENOENT: no such file or directory, open '<snip>/src/themes/mixin'
  ╷
1 │ @import '~/src/themes/mixin';
  │         ^^^^^^^^^^^^^^^^^^^^
  ╵

scss ファイルにある import を以下のように修正して解消しました。

-@import '~/src/themes/mixin';
+@import '~/themes/mixin';

エラー: process is not defined

ブラウザ画面が何も表示されなくなり、 devtools の console にエラーが出力されていました:

Uncaught ReferenceError: process is not defined
    at index.ts:2:24

Vite では環境変数の扱いが異なるためです。以下の変更で解消しました。

  • process.envimport.meta.env に変更
  • 環境変数の prefix を REACT_APP_ から VITE_ に変更
  • react-app-env.d.tsvite-app-env.d.ts にリネーム&修正

エラー: require is not defined

一部で require を使っていた箇所があったためエラーになりました。

Uncaught ReferenceError: require is not defined

require を全て import に置き換えて解消しました。なお moment のロケールを条件付きで require していた処理があり、以下のように置き換えました。

+import 'moment/locale/ja';
+import moment from 'moment';

+moment.locale('en');
 if (i18n.language.startsWith('ja')) {
-  require('moment/locale/ja');
+  moment.locale('ja');
 }

Uncaught TypeError: moment.duration(…).format is not a function

ここまでの変更で yarn start で起動してブラウザで動作することを確認できました。

しかし、 devtools の console に以下のエラーが出ています。

Uncaught TypeError: moment.duration(...).format is not a function
    at setDuration (index.tsx:39:37)
    at setTime (index.tsx:42:53)
    at index.tsx:46:42

src/index.tsx に以下を追加して解消しました。 ESM になった影響と思われます。

import momentDurationFormatSetup from "moment-duration-format";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
momentDurationFormatSetup(moment as any);

なお Moment.js 自体が現在は非推奨とされているようです。今回のアプリケーションでは date-fns へと段階的に移行しています。

We now generally consider Moment to be a legacy project in maintenance mode. It is not dead, but it is indeed done.

https://momentjs.com/docs/#/-project-status/

ESLint エラー: require() of ES Module .eslintrc.js from node_modules/@eslint/eslintrc/dist/eslintrc.cjs not supported.

公式サイトに以下の説明がありました。 .eslintrc.cjs にリネームして解消しました。

JavaScript (ESM) - use .eslintrc.cjs when running ESLint in JavaScript packages that specify "type":"module" in their package.json. Note that ESLint does not support ESM configuration at this time.

Configuration Files - ESLint - Pluggable JavaScript Linter

global is not defined

特定の処理で global is not defined というエラーが発生することがありました。 Webpack ではこの変数がデフォルトで含まれていましたが、 Vite では設定をすることで含まれるようになります。

vite.config.ts を修正:

@@ -20,4 +20,7 @@ export default defineConfig({
       },
     ],
   },
+  define: {
+    global: 'window',
+  },
 });

TypeError: Cannot read properties of undefined

class 構文を使っている処理で TypeError: Cannot read properties of undefined が発生することがありました。

TS のコンパイラオプションで useDefineForClassFields というものがあり、Vite+React のテンプレート が true だったためそれに倣って設定していましたが、これにより class のトランスパイル結果が変わってエラーになっていました。

tsconfig.json を修正:

@@ -2,7 +2,7 @@
   "extends": "./tsconfig.paths.json",
   "compilerOptions": {
     "target": "ES2020",
-    "useDefineForClassFields": true,
+    "useDefineForClassFields": false,
     "lib": ["ESNext", "DOM", "DOM.Iterable"],
     "module": "ESNext",
     "skipLibCheck": true,

最近の React では class 構文はほとんど使わなくなっていますが、今回のアプリケーションでは一部にレガシーなコードベースが残っており、その中で以前のトランスパイル結果に依存している箇所があったようです。

デプロイ関連のエラー

ここまでの変更でローカルでは問題なく動作するようになりました。次にデプロイ周りで起きた問題を記載します。

CI デプロイジョブがエラー: The user-provided path ./build does not exist

ビルド結果の出力先ディレクトリ名が Vite のデフォルトは dist になっています。今までは build だったので、デプロイコマンドを変更しました。

Cloudflare の最適化で JS ファイルが壊れる

デプロイはできたものの、アクセスしても何も表示されません。 devtools の console には以下のように出力されていました。

index-SKSNfXQs.js:3
Uncaught SyntaxError: Invalid or unexpected token (at index-SKSNfXQs.js:3:8090)

881 Unchecked runtime.lastError: A listener indicated an asynchronous response by returning true, but the message channel closed before a response was received

エラーの発生箇所を見ると、 minify されたコードですが 1.toString という箇所でエラーになっていました。

...Lot=Math.random(),Rot=jot(1.toString),OA=function(e){return"Symbol(...
                             ^^^^^^^^^^

原因は Cloudflare の最適化機能である minify によって JavaScript ファイルが変わってしまっていたためでした。

このアプリケーションは AWS S3 でホストして Cloudflare で配信する構成になっています。 Cloudflare の設定で minify をオフにすることで解消しました。1

元のコード
Rot=jot(1 .toString) のスペースが除去されて
Rot=jot(1.toString) になって SyntaxError が起きていたようです。

これは esbuild によるビルド特有のようで、 Vite はビルドに esbuild を使用しているためこれが起きるようになりました。

そもそもビルド時に minify しているので Cloudflare 側でさらに minify する意味はなかったわけですが、たまたまオンにしてしまっていました。

ところで 1 .toString というコードは何を意味するのでしょうか。 minify された結果なのではっきりとはわかりませんが、 Number.prototype.toString を関数として取り出しているように見えます。書き方としては (1).toString と等価で、 1 .toString の方が 1 文字節約できるため esbuild がこのような出力をしているのだと思われます。

Browserslist を Vite で使用するための設定

Vite では browserslist を使うのに設定が必要です。

yarn add -D browserslist-to-esbuild

vite.config.ts に以下を追加:

 export default defineConfig({
   plugins: [react()],
+  build: {
+    target:
+      process.env.NODE_ENV === 'production' &&
+      browserslistToEsbuild(['>0.2%', 'not dead', 'not op_mini all']),
+  },

設定変更: ie11 を削除

上記の設定では既に削除されていますが、 ie11 があると Maximum call stack size というエラーが発生するため削除しました。 (既に Internet Explorer はサポート対象外だったのですが、 Browserslist 設定に残ってしまっていた)

エラーログ:

✓ 5956 modules transformed.
✓ built in 6m 29s
error during build:
RangeError: Maximum call stack size exceeded
    at String.substring (<anonymous>)
    at replaceClose (file:///<snip>/node_modules/vite/dist/node/chunks/dep-R0I0XnyH.js:109:21)
    at replaceClose (file:///<snip>/node_modules/vite/dist/node/chunks/dep-R0I0XnyH.js:112:30)
    at replaceClose (file:///<snip>/node_modules/vite/dist/node/chunks/dep-R0I0XnyH.js:112:30)
    at replaceClose (file:///<snip>/node_modules/vite/dist/node/chunks/dep-R0I0XnyH.js:112:30)
    at replaceClose (file:///<snip>/node_modules/vite/dist/node/chunks/dep-R0I0XnyH.js:112:30)
    at replaceClose (file:///<snip>/node_modules/vite/dist/node/chunks/dep-R0I0XnyH.js:112:30)
    at replaceClose (file:///<snip>/node_modules/vite/dist/node/chunks/dep-R0I0XnyH.js:112:30)
    at replaceClose (file:///<snip>/node_modules/vite/dist/node/chunks/dep-R0I0XnyH.js:112:30)
    at replaceClose (file:///<snip>/node_modules/vite/dist/node/chunks/dep-R0I0XnyH.js:112:30)

Jest 関連の移行

のちに Vitest へと移行するのですが、まずは Jest のまま動かすことにしました。

Jest をインストールしてみたがうまく動かない

インストールして多少の設定をしてみましたが、うまく動きませんでした。

yarn add -D jest @types/jest

テスト実行するとエラー:

ReferenceError: Jest: Got error running globalSetup - <snip>/jest-global-setup.js, reason: module is not defined in ES module scope
This file is being treated as an ES module because it has a '.js' file extension and '<snip>/package.json' contains "type": "module". To treat it as a CommonJS script, rename it to use the '.cjs' file extension.
    at file:///<snip>/jest-global-setup.js:3:1
    at ModuleJob.run (node:internal/modules/esm/module_job:194:25)

jest-global-setup.cjs にリネーム。ファイル内容も変更が必要で、 require を import に置き換えました。

この状態でテスト実行すると走り始めるようになりましたが、すべてのテストケースが以下のエラーになります。

Jest encountered an unexpected token

Jest failed to parse a file. This happens e.g. when your code or its dependencies use non-standard JavaScript syntax, or when Jest is not configured to support such syntax.

Out of the box Jest supports Babel, which will be used to transform your files into valid JS based on your Babel configuration.

このアプローチは諦めて、 eject して必要な設定をしていくことにしました。

eject 実行時のエラー

eject コマンドを実行したところエラーになりました。

❯ yarn run eject
NOTE: Create React App 2+ supports TypeScript, Sass, CSS Modules and more without ejecting: https://reactjs.org/blog/2018/10/01/create-react-app-v2.html

✔ Are you sure you want to eject? This action is permanent. … yes
Ejecting...

Out of the box, Create React App only supports overriding these Jest options:

  • clearMocks
  • collectCoverageFrom
  • coveragePathIgnorePatterns
  • coverageReporters
  • coverageThreshold
  • displayName
  • extraGlobals
  • globalSetup
  • globalTeardown
  • moduleNameMapper
  • resetMocks
  • resetModules
  • restoreMocks
  • snapshotSerializers
  • testMatch
  • transform
  • transformIgnorePatterns
  • watchPathIgnorePatterns.

These options in your package.json Jest configuration are not currently supported by Create React App:

  • testPathIgnorePatterns
  • collectCoverage

If you wish to override other Jest options, you need to eject from the default setup. You can do so by running npm run eject but remember that this is a one-way operation. You may also file an issue with Create React App to discuss supporting more options out of the box.

Jest の config から testPathIgnorePatternscollectCoverage を削除して再実行すると eject できました。

eject した設定から必要なものを残す

その後はこちらの記事の Jest の CRA 依存を外す に書いてあるとおり修正を行い、正常にテスト実行ができるようになりました。内容そのままなので詳細は記事に譲ります。

Vitest 移行

まずは Jest 環境を維持したまま移行したのですが、その後 Vitest に移行しました。次の記事を参照してください。


  1. 正確には Cloudflare の設定変更後、再度デプロイすると解消しました。 ↩︎