ViteとTypeScriptを5系にバージョンアップする

# はじめに

本記事では、 Vue.js + TypeScript + Vite で構成された Web アプリケーションについて、 Vite 4 ⇒ 5 系、 TypeScript 4 ⇒ 5 系へとバージョンアップした際に躓いた点とその対処法、そのような対処をした理由についてお伝えします。

:::note warn
本記事の内容は 2024/6/24 時点の情報に基づいています。
:::

# まとめ

- Vite 5 系では Native ESM への移行が推奨されているので、 CJS 形式のファイル( `require/module.exports` 構文を使用している)ファイルに注意が必要
- Vue.js + TypeScript 5 のアプリケーションでは、既存のオプションの廃止と新しいオプションの追加が行われているため、 TypeScript の設定ファイル( tsconfig )に注意が必要
- パッケージのバージョンアップで困らないためには、パッケージを継続してアップデートしていくことが大切だが、そのためには CI や dependabot を用いた自動化が必要

# なぜバージョンアップをするのか

フロントエンド開発のエコシステムに乗っていくためには、主要なパッケージを定期的にバージョンアップする必要があります。なぜなら、古いバージョンを使用したままでいると、以下のリスクがあるからです。

- 有益な OSS やパッケージ製品、クラウドサービス、開発ツールなどを導入できなくなってしまうリスク
- サポート期間の対象外となり、パッケージに新たな脆弱性が発見された場合にサポートが受けられなくなるリスク
- 最新バージョンとの乖離が徐々に増加することでバージョンアップ時の影響範囲が大きくなり、よりバージョンアップを行いにくくなるリスク

メジャーバージョンのアップデートは通例破壊的変更を伴うため、型チェックやビルドが通らなくなり、既存のアプリケーションが実行できなくなることも多々発生します。そのため、公式サイトでバージョンアップに関するドキュメントが提供されています。 Vite と TypeScript についても、[Vite 公式のガイド](https://ja.vitejs.dev/guide/migration#v4-%E3%81%8B%E3%82%89%E3%81%AE%E7%A7%BB%E8%A1%8C) と [TypeScript 公式のガイド](https://devblogs.microsoft.com/typescript/announcing-typescript-5-0/) が提供されているため、それらを使用して破壊的変更の内容や注意点を確認し、バージョンアップ作業を進めていきました。

# アプリケーション構成とパッケージのバージョン
今回バージョンアップを行ったアプリケーションの構成は下図の通りです。
Vue.js + TypeScript をベースにした Web アプリケーションで、バックエンドアプリケーションとは OpenAPI 仕様に従った REST API で連携を行っています。

![フロントエンドの技術スタック](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3810407/df38e889-e8b0-a606-cb2e-df650fcf3f0a.jpeg)

作業時点で、バージョンアップ前後での主要なパッケージのバージョンは下記のようになりました。他にも同じタイミングでバージョンアップを行ったパッケージがありますが、本記事中で言及するパッケージのみを抜粋しています。

| パッケージ | 前 | 後 |
| ---- | ---- | ---- |
| vite | 4.5.2 | 5.2.7 |
| typescript | 4.5.5 | 5.3.3 |
| vitest | 0.34.6 | 1.4.0 |
| vue-tsc | 2.0.5 | 2.0.11 |
| @vue/tsconfig | 0.1.3 | 0.5.1 |
| @tsconfig/node20 | (未使用) | 20.1.4 |
| openapi-generator | 5.4.0 | 7.6.0 |

# Vite 4 ⇒ 5 へのバージョンアップ

Vite 5 から TypeScript 5 への依存関係はなかったので、まずは Vite のバージョンアップを行いました。バージョンアップを行いたい背景として、 2023/12/5 にリリースされた Vitest の v1.0.0 が、 Vite 5 以上を要求することが挙げられます。

## CJS Node API の非推奨化

Vite 5 の破壊的変更の 1 つとして、 CJS Node API が非推奨となり、コールすると警告が出力されるようになりました。[Vite 6ではCJS Node API は廃止が予定](https://vitejs.dev/guide/troubleshooting.html#vite-cjs-node-api-deprecated) されているので、 Vite を継続して使用するのであれば、いまのうちに対応を進めておく必要があります。

公式の移行ガイドに従って、プロジェクトのルートフォルダの package.json に`"type": "module"`を追加しました。`"type": "module"`を追加すると、 Node.js は、拡張子が .js のファイルを CJS ではなく ESM として扱うようになります。すなわち、 ESM を CJS に変換してから動作させる（Fake ESM）のではなく、 ESM を直接動作させる（Native ESM）ようになります。

## postcssの設定ファイルが読み込めない

しかし、この状態でビルドを行ったところ、下記のようなエラーが出力されてビルドに失敗してしまいました。原因として、[postcss](https://github.com/postcss/postcss) の設定ファイル（ postcss.config.js ）が読み込めなくなっているようです。

```terminal:terminal
[Failed to load PostCSS config: Failed to load PostCSS config (searchPath:{パス}):
[ReferenceError] 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 '{パス}\package.json' contains "type": "module". To treat it as a CommonJS script, rename it to use the '.cjs' file extension.
```

エラーメッセージ中でも示唆されているように、この対応として、拡張子を .js から .cjs に変更しました。というのも、 `"type": "module"`を追加したことで、 .js ファイルは ESM とみなされるようになりましたが、 postcss.config.js では下記のように `require` 構文を使用していたので ESM として扱うことができず、 CJS として扱うように明示する必要があったからです。

```js:postcss.config.cjs
module.exports = {
  plugins: [
    require('tailwindcss'),
    require('autoprefixer'),
    require('postcss-nesting'),
  ],
};
```

CJS 形式のファイルについて、本来は ESM（+ TypeScript ）での記述に書き換えていきたいところです。しかし、 postcss は 2023/11/20 の v5.0 で、[ESM + TypeScript での設定に対応済](https://github.com/postcss/postcss-load-config/issues/246) [^1]でしたが、 Vite は [対応中](https://github.com/vitejs/vite/issues/15869) [^2]の状況でした。そのため、 Vite での対応が完了するまでは CJS 形式にしておく必要があります。

## Cypressが起動できない

次に、 Cypress の E2E テストを実行したところ同じようなエラーが出力されてしまいました。

```terminal:terminal
ReferenceError: exports is not defined in ES module scope
```

しかし、 Cypress の設定ファイル（ cypress.config.ts ）は既に `import/export` 構文を使用した ESM 形式で記述されていたので、 postcss の設定ファイルとは異なる原因のようです。

```ts:cypress.config.ts
import { defineConfig } from 'cypress';

export default defineConfig({
  e2e: {
    specPattern: 'cypress/e2e/**/*.{cy,spec}.{js,jsx,ts,tsx}',
    baseUrl: 'http://localhost:XXXX',
  },
});
```

調査を進めたところ、 Cypress が依存している [`ts-node`がProject Referenceに未対応](https://github.com/TypeStrong/ts-node/issues/1057)[^3] であることの制約で、プロジェクトのルートフォルダの tsconfig.json に、`compilerOptions.module`フィールドを設置しておかないとうまく動作しないことがわかりました。

```json:tsconfig.json
  "references": [
    {
      "path": "./tsconfig.node.json"
    },
    {
      "path": "./tsconfig.app.json"
    },
    {
      "path": "./tsconfig.vitest.json"
    }
  ],
  "compilerOptions": {
    "module": "NodeNext"
  }
```

上記の設定を追加することで、 E2E テストが正常に動作するようになりました。

# TypeScript 4 ⇒ 5 へのバージョンアップ

無事 Vite をバージョンアップできたので、次に TypeScript 5 のバージョンアップを行いました。

バージョンアップにあたって苦労したポイントが、 TypeScript の設定ファイル( tsconfig )の更新です。前後で [@vue/tsconfig](https://github.com/vuejs/tsconfig) の構成と設定値が大きく更新されたため、これらを反映する必要がありました。

大きな方向性としては、 Vue 公式のプロジェクトスキャフォールディングツールである [create-vue](https://github.com/vuejs/create-vue) が TypeScript 5 系に対応していたので、こちらで作成される最新のテンプレートに合わせていくように変更を進めました。

## tsconfig の構成の更新

tsconfig の参照先と継承先が変更されたことでビルドができなくなってしまっていたので、まずはこちらの更新を行いました。具体的には下図の 3 箇所に変更が行われていたので、影響するファイルに変更を反映しました。

![TypeScript 設定ファイルの構成](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3810407/2beec3fa-ae98-ec07-912c-bc2a6c3e1a4b.jpeg)

### 1. tsconfig.web.json ⇒ tsconfig.dom.json に変更
    
tsconfig.app.json が継承しているので、ファイル名を変更しました。
```diff:tsconfig.app.json
- "extends": "@vue/tsconfig/tsconfig.web.json"
+ "extends": "@vue/tsconfig/tsconfig.dom.json"
```

### 2. tsconfig.config.json ⇒ tsconfig.node.json にリネーム
tsconfig.json が参照しているので、ファイル名を変更しました。
```diff:tsconfig.json
- "path": "./tsconfig.config.json"
+ "path": "./tsconfig.node.json"
```

### 3. @vue/tsconfig/tsconfig.json ⇒ @tsconfig/node20/tsconfig.json に変更
共通のファイル @vue/tsconfig/tsconfig.node.json が廃止されて、使用する Node.js のバージョンを指定したファイルを使用するように変更になりました。そのため、対象とするバージョンの Node.js の tsconfig のパッケージをダウンロードし、 2 でリネームした tsconfig.node.json が継承するファイルを変更しました。たとえば、 Node 20 を対象とする場合は、@tsconfig/node20 をダウンロードして、下記のように変更します。

```diff:tsconfig.node.json
- "extends": "@vue/tsconfig/tsconfig.node.json"
+ "extends": "@tsconfig/node20/tsconfig.json"
```

## compilerOptions の更新

次に、 tsconfig の `compilerOptions` の設定を更新しました。 `compilerOptions` は、トランスパイラが TypeScript を JavaScript に変換する際の挙動を設定するためのオプションです。

一般的に、 Vue.js + TypeScript + Vite のプロジェクト形式では、型チェックとトランスパイルは別のツールを使用します。型チェックについては、 tsc (Vue.js の場合は vue-tsc)を使用し、トランスパイルについては、 Vite が内部で [esbuild](https://github.com/evanw/esbuild) というツールを使用して行います。

その結果、 compilerOptions は tsc の型チェックの挙動を制御しますが、[Vite のトランスパイルに影響するのは一部のオプションのみ](https://ja.vitejs.dev/guide/features#typescript-%E3%82%B3%E3%83%B3%E3%83%8F%E3%82%9A%E3%82%A4%E3%83%A9%E3%83%BC%E3%82%AA%E3%83%95%E3%82%9A%E3%82%B7%E3%83%A7%E3%83%B3) という挙動になります。

一方で、型チェックとトランスパイルの挙動は一貫している必要があります。そのため、 `compilerOptions` の値は、 vue-tsc の挙動と Vite の挙動ができるだけ一致するように設定する必要があります。

下記では、TypeScript 5 へのバージョンアップにより追加・変更された 2 つのオプションについて説明します。

:::note info
Vite が esbuild を使用する理由
型チェックとトランスパイルで別のツールを使用することにより、想定外の差異が発生するリスクがあります。それでも esbuild を使用する理由は、他のツールに比べて [高速](https://esbuild.github.io/faq/#benchmark-details) であり、開発者体験や開発スピードに影響するからです。 Vite の強みの 1 つとしてスピードが挙げられますが、それを支えているのが esbuild の使用です。
:::

### 1. moduleResolution

このオプションは、 vue-tsc の挙動に影響しますが、Vite でのトランスパイルには影響しないオプションです。 TypeScript 5 での変更点として、設定値 `Bundler` が追加されました。設定のポイントとして、 webpack や Vite といったツールでバンドルをしている場合は、名前の通り `Bundler` に設定するのがベターなので、`Bundler` に設定しました。

```json:tsconfig.node.json
"compilerOptions": {
    "moduleResolution": "Bundler",
}
```
tsconfig.node.json が継承している @tsconfig/node20/tsconfig.json のデフォルト値は `Node16` なので、上書きするように設定する必要があります。一方で、 tsconfig.app.json が継承している @vue/tsconfig/tsconfig.json はデフォルト値が `Bundler` なので、あらためて設定する必要はありません。

`Bundler` に設定するメリットとして、相対パスでの `import` を行った場合にファイルの拡張子を省略できます。逆に、たとえば拡張子を省略したファイルに対して `Node16` を設定して型チェックを実行すると、下記のように拡張子の追加を求めるエラーが発生してしまいます。

```terminal:terminal
error TS2834: Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Consider adding an extension to the import path.
5 } from '../パス/api-client';
```

:::note info
`Bundler` が追加された背景
Node.js において、 CJS では拡張子の補完が可能でしたが、 ESM では拡張子の補完ができず、 tsc もその挙動に合わせていました。一方で、バンドルツールは CJS の仕様に追従して、 ESM のファイルに対しても拡張子を補完する機能を実装しており、 webpack や Vite のような現在の一般的な開発フレームワークでは、バンドルツールを使用しています。その結果として、 tsc の型チェックでのエラーを避けるためだけに拡張子を記載する必要がありましたが、 tsc 側をバンドルツールの挙動に合わせる機能である `Bundler` が登場したことで、このような対応が不要になりました。
:::

### 2. verbatimModuleSyntax

TypeScript 5 で追加された、 Vite でのトランスパイルにも影響するオプションです。
このオプションは、[`importsNotUsedAsValues` と `preserveValueImports` の2つのオプションを廃止して、1つに統合するという目的で導入](https://github.com/microsoft/TypeScript/issues/51479)[^4]されました。そのため、 tsconfig で `importsNotUsedAsValues` や `preserveValueImports` を使用している場合は、下記のような警告が出力されるという破壊的変更が行われています。

```terminal:terminal
error TS5101: Option 'importsNotUsedAsValues' is deprecated and will stop functioning in TypeScript 5.5. Specify compilerOption '"ignoreDeprecations": "5.0"' to silence this error.
  Use 'verbatimModuleSyntax' instead.
```

tsc のデフォルトの動作では、値として使用されていない `import` はすべてトランスパイル時に削除（ import elision ）します。ここで `verbatimModuleSyntax` が `true` の場合、`type` 修飾子がついている `import` のみをトランスパイル時に削除し、それ以外の場合は使用しているかどうかにかかわらず削除しないという挙動に変わります。

このオプションを `true` にする場合の制約として、型情報のみを使用している `import` については、`type` 修飾子を使用してそのことを明示しなければいけません。そうでなければ、該当の `import` を削除してよいものなのか、残すべきものなのか判断できないからです。

このオプションに verbatim (逐語的に)という形容詞が使用されている理由は、 `import` された値が使用されているかどうかというソースファイル全体を見ないとわからない情報を使用して判断するのではなく、ひとつひとつの 
 `import` だけを見て判断するような挙動を行うためです。

継承元の @vue/tsconfig/tsconfig.json の推奨値は `true` なので、それに従って、下記の通り廃止される 2 つのオプションを削除し `true` に設定しました。

```diff_json:tsconfig.app.json
"compilerOptions": {
-    "preserveValueImports": false,
-    "importsNotUsedAsValues": "preserve"
+    "verbatimModuleSyntax": true
}
```

:::note warn
上記のコード例では、`"verbatimModuleSyntax": true` を明示的に設定していますが、 tsconfig.app.json は @vue/tsconfig/tsconfig.json を継承しているので、特に設定しなくても `"verbatimModuleSyntax": true` になります。
:::

しかし、[openapi-generator](https://github.com/OpenAPITools/openapi-generator) の typescript-axios を用いて自動生成していたコードが、型チェックに引っかかってしまいました。というのも、下記のように通常の `import` と `import type` を区別しておらず、実際には型情報しか使用していないのに、`type` 修飾子がついていない `import` が存在したためです。

```package.json
"openapi-client:generate": "openapi-generator-cli generate -g typescript-axios -i {OpenAPI定義のパス} --additional-properties=withSeparateModelsAndApi=true,modelPackage=models,apiPackage=api,supportsES6=true -o {出力先のパス}"
```

```ts:order-response.ts
// 自動生成されたコード
import { OrderItemResponse } from './order-item-response';
// type 修飾子を付加したコード
import type { OrderItemResponse } from './order-item-response';
```

```terminal:terminal
error TS1484: 'OrderItemResponse' is a type and must be imported using a type-only import when
'verbatimModuleSyntax' is enabled.
```

ですが、 openapi-generator を 5.4.0 から 7.6.0 にバージョンアップし、コードの再生成を行ったところ、自動生成コードでも `import` と `import type` が正しく区別されて生成されるようになっており、型チェックを問題なく通過することが判明しました。詳細を確認したところ、 typescript-axios には、 [withSeparateModelsAndApi=true の場合、うまく `import type` が生成されない不具合](https://github.com/OpenAPITools/openapi-generator/issues/17461)[^5] がありましたが、直近の 2024/5/20 にリリースされた 7.6.0 にて解消されていたためでした。

このことにより、 `importsNotUsedAsValues` と `preserveValueImports` について、 @vue/tsconfig/tsconfig.json の推奨値である `"verbatimModuleSyntax": true` に置き換えることができました。

# おわりに

バージョンアップ作業において、具体的にどんな問題が出てくるか、という点は、プロダクトのコードベースによるところも大きいかと思います。そのため本記事のまとめとしては、バージョンアップを行う際に困ったときはどうするべきか、また、バージョンアップを行う際に困らないためにはどうするべきかということを記載します。少しでも参考になれば幸いです。

## リポジトリを検索する

Google 検索や AI チャットで先人の知恵を借りてもわからないとき、頼りになったのが GitHub の issue や Pull Request でした。エラーメッセージや発生している状況、知りたいことやキーワードで検索すると意外とヒットしますし、コメント欄のやり取りに知りたいことが書いてあったりします。また、パッケージのリポジトリを検索した結果、関連する issue や PR が未解決であるということもあります。その場合は、状況を継続して確認することが大切です。たとえば GitHub の場合は、通知を受け取るように設定が可能なほか、 issue から issue や PR へリンクを作成するとステータスが表示されるので、継続的な確認がしやすくなっています。加えて、ハードルは上がってしまいますが、 issue にコメントすることや、修正を行って自ら Pull Request を出すことができれば、 OSS コミュニティーに貢献することができます。

- リンクの例
![issue-link.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3810407/edc8cd29-727b-4ec7-18f7-d297f42cac37.png)

## 継続的にバージョンアップする
最新バージョンとの乖離が増えるほど、バージョンアップ時の影響が大きくなるので、バージョンアップ時に困る可能性が高くなります。このような状況を避けるためには、継続的にパッケージのバージョンアップを行うことが大切です。

継続的なパッケージのバージョンアップは人力では困難なので、 CI パイプラインと [dependabot](https://github.com/dependabot) を用いて、バージョンアップを検知して検証できる環境が必要になります。たとえば dependabot が作成したバージョンアップの PR に対して、 CI 環境で Lint 、型チェック、ビルド、自動テストなどのタスクを実行することで、妥当性の検証をある程度自動化できます。このような環境を構築しておくことで、 CI に失敗した場合には破壊的変更があったと判断できますし、パッチバージョンのアップデートで CI に問題がなかった場合には、プロダクトの運用方針にもよりますが、リリースノートを確認して関係のある内容の変更がなければ、手動の回帰テストのケースを絞っても問題ないと判断できます。

## We Are Hiring!
BIPROGY では一緒に働く仲間を募集しています。
ご興味がある方はこちらをご参照ください。
<https://www.biprogy.com/recruit/recruiting/>

[^1]: 最終閲覧日：2024/6/19 クローズ済
[^2]: 最終閲覧日：2024/6/19 オープン中 Vite が古いバージョンの postcss-load-config を含んでいるためで、 Vite 6 での対応になるだろうということがコメントされている。
[^3]: 最終閲覧日：2024/6/19 オープン中
[^4]: 最終閲覧日：2024/6/19 クローズ済
[^5]: 最終閲覧日：2024/6/24 クローズ済