Vite+Vue3+Vuetify3+Expressのプロジェクトを1から作成する（ESLint、Prettier、Node.jsをPJに含めたモノレポ？構成）

## はじめに

個人開発で使うプロジェクトをVue-CLIから移行して、Vueも3.xに上げて新しくテンプレ化してみようと思った。その新プロジェクトの作成時の手順を備忘録として残しておきたく。

完成したプロジェクトは以下。

https://github.com/yuta-katayama-23/vite-vue3-vuetify-express

## `yarn create vite`で初期プロジェクトを作成する
まずは[公式](https://ja.vitejs.dev/guide/#%E6%9C%80%E5%88%9D%E3%81%AE-vite-%E3%83%95%E3%82%9A%E3%83%AD%E3%82%B7%E3%82%99%E3%82%A7%E3%82%AF%E3%83%88%E3%82%92%E7%94%9F%E6%88%90%E3%81%99%E3%82%8B)にある通りの方法（`yarn create vite`）で初期プロジェクトを作成する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/cdfea580-e2fd-d400-d193-eb3320cac101.png)

作成直後は以下のように最低限の状態で、`yarn.lock`すらない状態で作成される。

```console
study@localhost:~/workspace/vite-vue3-vuetify-express (main)
$ tree 
.
├── README.md
├── index.html
├── package.json
├── public
│   └── favicon.ico
├── src
│   ├── App.vue
│   ├── assets
│   │   ├── base.css
│   │   ├── logo.svg
│   │   └── main.css
│   ├── components
│   │   ├── HelloWorld.vue
│   │   ├── TheWelcome.vue
│   │   ├── WelcomeItem.vue
│   │   └── icons
│   │       ├── IconCommunity.vue
│   │       ├── IconDocumentation.vue
│   │       ├── IconEcosystem.vue
│   │       ├── IconSupport.vue
│   │       └── IconTooling.vue
│   ├── main.js
│   ├── router
│   │   └── index.js
│   ├── stores
│   │   └── counter.js
│   └── views
│       ├── AboutView.vue
│       └── HomeView.vue
└── vite.config.js

8 directories, 22 files
```

ここから開発に必要な設定を追加で行っていきたいと思う。内容としては以下の6つになる。

1. [prettierの設定](#prettierの設定)
1. [ESLintの設定](#eslintの設定)
1. [VS Codeの設定](#vs-codeの設定)
1. [npm-check-updatesのインストール](#npm-check-updatesのインストール)
1. [Git hooksの設定](#git-hooksの設定)
1. [エイリアスの設定（ESLintの設定②）](#エイリアスの設定eslintの設定)

### prettierの設定
ここは目新しい事は特になく、いつも通りの設定をすればいい。好みに応じて、[Configuration File](https://prettier.io/docs/en/configuration.html)にあるような設定ファイルの方式を選べばいいだろう。今回は以下のように設定してみた。

```json:.prettierrc.json
{
	"singleQuote": true,
	"useTabs": true,
	"semi": true,
	"bracketSpacing": true,
	"arrowParens": "always",
	"printWidth": 80,
	"trailingComma": "none"
}
```

ついでなのでscriptsも追加する。

```json:package.json
  "scripts": {
    ...
    "style":"npx prettier --ignore-unknown --write ."
  },
```

### ESLintの設定
プロジェクト作成直後は以下のような設定になっている。

```js:.eslintrc.cjs
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution');

module.exports = {
	root: true,
	extends: [
		'plugin:vue/vue3-essential',
		'eslint:recommended',
		'@vue/eslint-config-prettier'
	],
	parserOptions: {
		ecmaVersion: 'latest'
	}
};
```

まず、ファイルの拡張子だが、[Configuration File Formats](https://eslint.org/docs/latest/use/configure/configuration-files#configuration-file-formats)に書かれている通り、`.cjs`はpackage.jsonに`"type":"module"`を設定してES Modulesを利用できるようにしている場合に利用する拡張子。今回はそういった設定はしないので`.js`に変更する。

また、デフォルトで設定されているextends（継承している設定）については以下のように整理される（ESLintに関しては[Node.js（ES6で実装）におけるESLint・Prettierの設定を1からやってみた](https://note.com/shift_tech/n/n8bbe1c05ba0b)などを参照）。

- plugin:vue/vue3-essential
https://eslint.vuejs.org/user-guide/
Vue公式が作成しているESLintのルール

- eslint:recommended
https://eslint.org/docs/latest/rules/
ESLint公式が作成しているルールの内、推奨されるもののルール

- @vue/eslint-config-prettier
ESLintとPrettierのルール衝突を回避するために、ESLintのStyleに関するルールをOffにする[eslint-config-prettier](https://github.com/prettier/eslint-config-prettier)のVue版

これに加えてairbnbのルールも追加しておく事にする。

- [eslint-config-airbnb-base](https://github.com/airbnb/javascript/tree/master/packages/eslint-config-airbnb-base)
airbnbのルール
インストール時は公式の通りで、`yarn add -D eslint-config-airbnb-base`でeslint-config-airbnb-baseをインストールし、`npx install-peerdeps --dev eslint-config-airbnb-base`でpeerDependenciesをインストールすればいい
※ちなみに、Vue-CLIの時に使っていた[@vue/eslint-config-airbnb](https://github.com/vuejs/eslint-config-airbnb/tree/main/packages/eslint-config-airbnb)は利用しない

ルールの追加前後で`yarn lint`の結果を確認してみると、以下の通り変更後ではairbnb-baseのルールでチェックされている事が確認できる。
| 追加前 | 追加後 |
|:-:|:-:|
| ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/402ae0f0-d075-ea8f-7d79-efb8e3ef0bb9.png) | ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/69b3f6c6-4084-40fe-8e6d-48c7eed41175.png) |

※上記の追加後のスクショを見ると分かるが、`vite`と`@vitejs/plugin-vue`がdevDependenciesではなく、dependenciesであるべきというエラーが出ている。その2つは意図してdevDependenciesに入れているので、ESLintのルールを以下のように修正し、エラーを出ないように調整するのがいいだろう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/c0e95efc-f04b-2860-7212-70a5f9afe102.png)

#### [vite-plugin-eslint](https://github.com/gxmari007/vite-plugin-eslint)

Viteによるビルド時にESLintのチェックを実行させるためのplugin。
これを導入すると、例えば以下のようにエラーが出るようになる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/bc04f815-d2e7-7032-9c97-674bcfa1d564.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/d50b386a-71e0-0fd7-dcda-4bf39e70c767.png)

#### [eslint-plugin-vuetify](https://github.com/vuetifyjs/eslint-plugin-vuetify)

VuetifyのESLintのチェックルール。これを導入すると、以下のようにエラーが出るようになる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/f1c588b7-1cf5-3712-7dfc-484cc0a6921a.png)

設定方法としては`yarn add eslint-plugin-vuetify -D`で依存に追加した後、`.eslintrc.js`に`plugin:vuetify/recommended`を追加すればいい（`plugin:vuetify/base`というのもある）。

```js:.eslintrc.js
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution');

module.exports = {
	...
	extends: [
		'plugin:vue/vue3-essential',
		'plugin:vuetify/recommended', // ← 追加
		'eslint:recommended',
		'airbnb-base',
		'@vue/eslint-config-prettier'
	],
	...
};
```

### VS Codeの設定

続いて、`.vscode/settings.json`と`jsconfig.json`を設定したいと思う。ちなみに、`yarn create vite`を実行した時に、`.vscode/extensions.json`が自動で作成されているが、これがる事で以下のように拡張機能のインストールを勧める通知が出るようになる。チームで開発している時などはこうしたものを利用して拡張機能として入れるべきものの認識を揃える事もできるだろう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/0f465da5-831f-9765-59cd-3ab176786b76.png)

#### .vscode/settings.json

今回は以下のように設定した。ESLintのチェックを保存に行い修正可能であれば修正する設定と、同じく保存時にPrettierによる自動整形を行う設定をしている。
```json:.vscode/settings.json
{
	"editor.defaultFormatter": "esbenp.prettier-vscode",
	"editor.formatOnSave": true,
	"editor.codeActionsOnSave": {
		"source.fixAll.eslint": true
	},
	"editor.renderWhitespace": "all",
	"javascript.updateImportsOnFileMove.enabled": "always",
	"jest.autoRun": "off"
}
```

#### jsconfig.json

この後設定していくエイリアスを使って実装する際に、VS Codeからサジェストが出るようにするために設定しておく。詳細は[VS Code の設定](https://note.com/shift_tech/n/nb7a516f34537#557025ed-d21e-400d-850d-7ac1295e51d6)などを参照。

```json:jsconfig.json
{
	"compilerOptions": {
		"baseUrl": "./",
		"paths": {
			"@/*": ["src/*"]
		}
	},
	"exclude": ["node_modules", "dist"]
}
```

### npm-check-updatesのインストール

package.jsonに定義しているライブラリの更新のために追加。詳細は[Javascript(Node.js)プロジェクトの開発者は必須！　ncuコマンドでパッケージを最新化する](https://qiita.com/yuta-katayama-23/items/6defa3d27dce9d67028b)などを参照。

### Git hooksの設定

`simple-git-hooks`によるGit hooksを設定する。動かすタイミングと内容としては以下とした。詳細は[依存モジュール(ライブラリ)のライセンスをチェックする仕組みを導入してみた](https://note.com/shift_tech/n/n6fd1f955658b)などを参照。

- pre-commit
自動整形、ライセンスチェック
- commit-msg
コミットメッセージのチェック

### エイリアスの設定（ESLintの設定②）

ESLintはデフォルトでは`'@/components/TheWelcome.vue'`のような`@`はパスとして解釈できないので、`import/no-unresolved`のエラーが出てしまう。そこで[eslint-import-resolver-alias](https://github.com/johvin/eslint-import-resolver-alias)などのESLintのライブラリを使い、エイリアスを解決してあげる設定を追加する必要がある。設定自体は簡単で、
```console
$ yarn add -D eslint-import-resolver-alias
```
でインストールした後、以下のようにESLintのconfigを追加で設定すればいい。
```js:.eslintrc.js
module.exports = {
	...
	settings: {
		'import/resolver': {
			alias: {
				map: [['@', './src']]
			},
			extensions: ['.js', '.vue']
		}
	},
	...
};
```

※ちなみに、eslint-import-resolver-aliasで以下のように設定した場合、`vite.config.js`を修正しないとエラーになった。これはおそらく、eslint-import-resolver-aliasがNode.jsを前提としているライブラリであるためと思われる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/42ec3af8-30c4-43f9-d649-95aefd1f9f37.png)

- 参考：https://github.com/eslint/eslint/discussions/14667

#### 追記

上記の手順で設定したものの、Vuetifyの導入時にエラー（以下のスクショを参照）になって躓いた。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/044add29-c496-7236-f4b4-dada2746f2bc.png)

おそらくeslint-import-resolver-aliasがNode.jsを前提にしており、`.mjs`を読み込めない（.js or .cjsを期待する）ので、エラーになっているのではないかと考えた（エラーのトレースの中のファイルパスに`node:internal/modules/cjs/loader`がるので）。

> This is a simple Node.js module import resolution plugin for eslint-plugin-import, which supports native Node.js module resolution, module alias/mapping and custom file extensions.（eslint-plugin-import 用のシンプルな Node.js モジュールのインポート解決プラグインで、ネイティブの Node.js モジュール解決、モジュールエイリアス/マッピング、カスタムファイルエクステンションをサポートします。）

仕方ないので、別の方法を取る事にした。具体的には[eslint-import-resolver-custom-alias](https://github.com/laysent/eslint-import-resolver-custom-alias)というライブラリがあるようなので、これを利用して実装した。
```js:
    ...
	settings: {
		'import/resolver': {
			'eslint-import-resolver-custom-alias': {
				alias: {
					'@': './src'
				},
				extensions: ['.js', '.vue']
			}
		}
	},
    ...
```

これを導入すると、ESLintがクラッシュする事はなくなったが、以下のスクショのようにVuetifyのimportでエラーが出てしまった。ただ、Viteでのビルドは成功する事から、ESLintの設定不備と考えてよさそうのなので、今回は以下のようにESLintのエラーを明示的にOffするように設定した。
```js:src/plugins/vuetify.js
// eslint-disable-next-line import/no-unresolved
import 'vuetify/styles';
import { createVuetify } from 'vuetify';
// eslint-disable-next-line import/no-unresolved
import * as components from 'vuetify/components';
// eslint-disable-next-line import/no-unresolved
import * as directives from 'vuetify/directives';
import colors from 'vuetify/lib/util/colors';
...
```

※ちなみに、[eslint-import-resolver-vite](https://github.com/pzmosquito/eslint-import-resolver-vite)というのがあったので、このライブラリでうまくいくかなと期待したが、以下のように軒並み`import/no-unresolved`になり、ESLintのimportの解釈が壊れてしまってNGだった。。。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/cbecc8f3-8216-692c-c7bc-fe052cb398ed.png)

## Vuetifyの導入、ExpressをVite＋Vue＋Vuetifyの同一PJで開発できるようにする
ここからは開発をしていく際に利用したいライブラリ・フレームワークを導入するという事をやっていきたいと思う。ExpressについてはFirebaseなどを利用したサーバーレスアプリケーションの開発では不要だが、どうしても自前のAPIが欲しくなる事もあるかと思い、導入する事にした（Expressがあるからと言って必ずしもサーバーが必要になるわけではなく、[How to run Express.js apps with Netlify Functions](https://www.netlify.com/blog/2018/09/13/how-to-run-express.js-apps-with-netlify-functions/)や[serverless-express](https://github.com/vendia/serverless-express)を利用すれば簡単にサーバレスのExpressを構築する事も可能）。

- [Vuetifyの導入](#vuetifyの導入)
  - [vue-i18nを導入する](#vue-i18nを導入する)
- [Expressを同一PJで実装できるようにする](#expressを同一pjで実装できるようにする)

### Vuetifyの導入

ここからはUIのフレームワークとしてVuetifyを利用したいので、それを利用できるように設定していく。[公式](https://next.vuetifyjs.com/en/getting-started/installation/#manual-steps)にはないViteのための設定が必要だったりとここは少し躓いた・・・。

まずは、[Manual steps](https://next.vuetifyjs.com/en/getting-started/installation/#manual-steps)に書かれている通り、vuetifyのインストールとVuetifyをVueで`use()`するように設定する。

1点、vuetifyの設定自体をmain.jsに書くとmain.jsが肥大化するので、`src/plugins/vuetify.js`に切り出す事にする。

```js:src/plugins/vuetify.js
// eslint-disable-next-line import/no-unresolved
import 'vuetify/styles';
import { createVuetify } from 'vuetify';
// eslint-disable-next-line import/no-unresolved
import * as components from 'vuetify/components';
// eslint-disable-next-line import/no-unresolved
import * as directives from 'vuetify/directives';

const vuetify = createVuetify({
	components,
	directives
});

export default vuetify;
```

基本的な設定は上記でいいが、まだまだ設定が必要な部分がある。ここまで設定しただけでページを実装して開いてもアイコンが表示されない（HTMLにはbuttonは確かにあるが、アイコンが表示されない）。
```vue:src/views/VuetifyView.vue
<template>
	<v-app>
		<v-app-bar>
			<template v-slot:prepend>
				<v-app-bar-nav-icon></v-app-bar-nav-icon>
			</template>

			<v-app-bar-title>Photos</v-app-bar-title>

			<template v-slot:append>
				<v-btn icon="mdi-dots-vertical"></v-btn>
			</template>
		</v-app-bar>
	</v-app>
</template>
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/054bcbd8-456c-fd5e-2ad4-7872b4fa213c.png)

これは[Material Design Icons](https://next.vuetifyjs.com/en/features/icon-fonts/#installing-icon-fonts)に書かれている追加の設定が必要なため。

まず、必要になるライブラリを追加する（`yarn add @mdi/font -D`）。
後は、vuetify.jsに以下のimportを追加すればいい。
```js:src/plugins/vuetify.js
import '@mdi/font/css/materialdesignicons.css';
...
```

ここまで設定すると、以下のようにマテリアルデザインのアイコンが利用できるようになり、画面上にも表示されるようになる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/f51b3376-6a68-1753-840d-f18a68ad553d.png)

これで完了と思いきや、今度は以下のような実装をすると、404になってしまう・・・。
```vue:src/views/VuetifyView.vue
<template>
	<v-app>
		<v-app-bar>
			...
			<v-img src="@/assets/logo.svg"></v-img>
			...
		</v-app-bar>
	</v-app>
</template>
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/7b02a04e-275a-b743-1626-74387cfcea77.png)

これはViteのビルド時のpluginが不足しているために、VuetifyのコンポーネントでassetsのURL変換がうまくできていないため。まず、[vite-plugin-vuetify](https://github.com/vuetifyjs/vuetify-loader/tree/next/packages/vite-plugin)を追加して、以下のようにvite.config.jsを設定すればいい。
```js:vite.config.js
...
import { transformAssetUrls } from 'vite-plugin-vuetify';

export default defineConfig({
	plugins: [
		vue({ template: { transformAssetUrls } })
	],
	...
});
```

この設定を追加すると、以下のようにURLが適切に変換されて読み込みできるようになる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/f7e7e65c-3894-dbf2-fe8a-3e1b8905371d.png)

上記で設定はOKだが、以下のように設定する事でVuetifyのsassの設定を上書きするという事もできる。sassを利用する場合には、ここまで設定しておくといい気がする（[Component specific variables](https://next.vuetifyjs.com/en/features/sass-variables/#component-specific-variables)も参照）。
```js:vite.config.js
...
import vuetify, { transformAssetUrls } from 'vite-plugin-vuetify';

// https://vitejs.dev/config/
export default defineConfig({
	plugins: [
		...
		vuetify({
			autoImport: true,
			styles: { configFile: 'src/styles/settings.scss' }
		})
	],
	...
});
```
```sass:src/styles/settings.scss
/**
 * src/styles/settings.scss
 *
 * Configures SASS variables and Vuetify overwrites
 */

// https://next.vuetifyjs.com/en/features/sass-variables
// @use 'vuetify' with (
// 	$color-pack: false
// );
```

ここまでVuetifyの設定としてはOK（だと思われる）。

※ちなみに、[webfontloader](https://github.com/typekit/webfontloader)を追加することでフォントの利用もできるようになる。
```js:src/plugins/webfontloader.js
import webFontLoader from 'webfontloader';

export default () => {
	webFontLoader.load({
		google: {
			families: ['Roboto:100,300,400,500,700,900&display=swap']
		}
	});
};
```
```js:
import { createApp } from 'vue';
...
import webfontloader from '@/plugins/webfontloader';
import App from './App.vue';
...
const app = createApp(App);
...
webfontloader();
...
app.mount('#app');
```

#### vue-i18nを導入する

多言語対応も最初に行っておくと楽だと思うので、[vue-i18n](https://next.vuetifyjs.com/en/features/internationalization/#vue-i18n)で設定していく。

設定自体は簡単で、`yarn add -D vue-i18n`でvue-i18nを追加し、上記で設定していたvuetify.jsを以下のように変更すればいい（i18nに関する設定はVuetifyのpluginのようなものなので別のファイルに分割するようにした）。
```js:src/plugins/vuetify.js
import { createVuetify } from 'vuetify';
...
import { createVueI18nAdapter } from 'vuetify/locale/adapters/vue-i18n';
import { useI18n } from 'vue-i18n';
import i18n from '@/plugins/i18n';

const vuetify = createVuetify({
	...
	locale: {
		adapter: createVueI18nAdapter({ i18n, useI18n })
	}
});

export default vuetify;
```
```js:src/plugins/i18n.js
import { createI18n } from 'vue-i18n';

const messages = {
	en: {
		$vuetify: {
			...
		}
	},
	sv: {
		$vuetify: {
			...
		}
	}
};

// eslint-disable-next-line new-cap
const i18n = new createI18n({
	legacy: false, // Vuetify does not support the legacy mode of vue-i18n
	locale: 'sv',
	fallbackLocale: 'en',
	messages
});

export default i18n;
```
```js:src/main.js
import { createApp } from 'vue';
...
import vuetify from '@/plugins/vuetify';
import i18n from '@/plugins/i18n';
...
const app = createApp(App);
...
app.use(i18n);
app.use(vuetify);
...
app.mount('#app');
```

以下はおまけ的な話になるが、`src/plugins/i18n.js`の`messages`をどんどん追記していくとファイルが肥大化してメンテナンス性も悪くなるので、別ファイルに切り出して読み込むようにしてみたいと思う。

ディレクトリとしては、`src/locales`以下に各言語に対応するJSONファイルを配置するという方法を取る事にする。実装としては以下のようになった。
```js:src/plugins/i18n.js
...
import path from 'node:path';

const loadLocaleMessages = () => {
	const messages = {};

	const locales = import.meta.glob('@/locales/*.json', { eager: true });
	Object.keys(locales).forEach((dirPath) => {
		messages[path.parse(dirPath).name] = locales[dirPath].default;
	});

	return messages;
};

// eslint-disable-next-line new-cap
const i18n = new createI18n({
	...
	messages: loadLocaleMessages()
});
...
```
```console
study@localhost:~/workspace/vite-vue3-vuetify-express (main *+)
$ tree src/
src/
...
├── locales
│   ├── en.json
│   └── ja.json
...
```

JSONを読み込む部分については[Glob のインポート](https://ja.vitejs.dev/guide/features.html#glob-%E3%81%AE%E3%82%A4%E3%83%B3%E3%83%9B%E3%82%9A%E3%83%BC%E3%83%88)に書かれている方法を利用した。これにより複数のモジュール（今回は`src/locales`以下のJSON）を簡単にインポートできる。ただし、遅延ローディングされるのは困るので`eager`オプションをtrueに設定している。

1点、ファイル名を取得するのに[Node.jsのPath](https://nodejs.org/dist/latest-v18.x/docs/api/path.html)を利用しているが、これはデフォルトだと以下のようにエラーになる（これはNode.jsの世界のモジュールはブラウザでは利用できないため）。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/02276863-53e6-d5b0-e8e2-bc125a7913e8.png)

そのため、vite.config.jsの方に[vite-plugin-node-polyfills](https://github.com/voracious/vite-plugin-node-polyfills)の設定が必要になる（Node.jsのCore Modulesをブラウザ環境向けにポリフィル＝代替コードにする）。
```js:vite.config.js
...
import { nodePolyfills } from 'vite-plugin-node-polyfills';

export default defineConfig({
	plugins: [
		nodePolyfills({ protocolImports: true }),
		...
	],
	...
});
```

ちなみに、`console.log(locales)`は以下のようなオブジェクトになる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/143bfbfc-b057-d39f-96e0-cb63c56dccd6.png)

また、`console.log(dirPath);`と`console.log(locales[dirPath].default);`は以下の通りで、`locales[dirPath].default`にアクセスする事で、JSONの中身を取得できる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/4272e449-3960-bb4c-b63a-692dd001e9b9.png)

### Expressを同一PJで実装できるようにする

Vite＋Vue3＋Vuetify3のプロジェクトに、バックエンドのExpressも共存している状態で開発できると便利だと思ったので設定をしてみた。一番手っ取り早いのはpackage.jsonで`"type": "module"`を設定するのがいいと思われる（ただし、色々なNode.jsライブラリを使って実装したわけではないので、ライブラリがCommonJSにしか対応していない場合は、import時に`Error [ERR_MODULE_NOT_FOUND]: Cannot find module`のようなエラーが出る場合があるかもしれない。その場合には`"type": "module"`以外の方法とるしかないかもしれない）。

実際に設定していくが、package.jsonの設定変更で影響を受ける箇所もあるので合わせて変更していく。まずは、以下のようにpackage.jsonに`"type": "module"`を追記する。
```json:package.json
{
	...
	"type": "module",
    ...
}
```

このように設定するだけでES Moduleを使えるようになる（[Determining module system](https://nodejs.org/dist/latest-v18.x/docs/api//packages.html#determining-module-system)も参照）。そのため、以下のような実装で普通にExpressのサーバーが起動できる。
```js:srv/index.js
import express from 'express';

const app = express();
app.get('/hello', (req, res) => {
	res.json({ msg: 'hello' });
});
app.listen(3000);
```
```console:TerminalA
study@localhost:~/workspace/vite-vue3-vuetify-express (main *+)
$ node srv/index.js 
```
```console:TerminalB
study@localhost:~/workspace/vite-vue3-vuetify-express (main +)
$ curl http://localhost:3000/hello
{"msg":"hello"}
```

開発時にHMR（保存してサーバーを再起動するやつ）をしたい場合には、[nodemon](https://github.com/remy/nodemon)を利用すればいいだろう。
```json:package.json
	"scripts": {
		...
		"express:dev": "nodemon srv/index.js",
		"express": "node srv/index.js",
        ...
   }
```

ただ、上記の設定変更後にeslintを動かそうとすると、以下のようにエラーになる。これはES Moduleなのに`require()`になってるのでそこで食い違いが出てエラーになっている。
```console
study@localhost:~/workspace/vite-vue3-vuetify-express (main *)
$ yarn lint
yarn run v1.22.19
warning ../package.json: No license field
$ eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore

Oops! Something went wrong! :(

ESLint: 8.33.0

Error [ERR_REQUIRE_ESM]: require() of ES Module /home/study/workspace/vite-vue3-vuetify-express/.eslintrc.js from /home/study/workspace/vite-vue3-vuetify-express/node_modules/@eslint/eslintrc/dist/eslintrc.cjs not supported.
.eslintrc.js is treated as an ES module file as it is a .js file whose nearest parent package.json contains "type": "module" which declares all .js files in that package scope as ES modules.
Instead rename .eslintrc.js to end in .cjs, change the requiring code to use dynamic import() which is available in all CommonJS modules, or change "type": "module" to "type": "commonjs" in /home/study/workspace/vite-vue3-vuetify-express/package.json to treat all .js files as CommonJS (using .mjs for all ES modules instead).

    at Object.module.exports [as default] (/home/study/workspace/vite-vue3-vuetify-express/node_modules/import-fresh/index.js:32:59)
    ...
```

対処方法はESLintの設定ファイルの項（[Configuration File Formats](https://eslint.org/docs/latest/use/configure/configuration-files)）にちゃんと書かれており、`.eslintrc.cjs`にファイルの拡張子を変更すればいい（`.cjs`はCommonJSである事を示す拡張子）。

> 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.（JavaScript (ESM) - パッケージ.json で "type": "module" を指定している JavaScript パッケージで ESLint を実行する場合は .eslintrc.cjs を使ってください。ESLintは現時点ではESMの設定をサポートしていないことに注意してください。）

また、commitlintでも同じようにエラーになるが、こちらも`commitlint.config.cjs`に拡張子を変更すればエラーはなくなる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/01f0d230-0f96-ec8c-c546-dd02376f9f48.png)

※昔ながらで、ES Moduleで実装はするものの、runtime時にはWebpackでビルドしたCommonJSの状態のものを動かす、という方法もあった（[Node.jsでimport・export（ES6の構文）を使えるようにwebpack × Babelの設定をやってみた](https://note.com/shift_tech/n/n77562e0926e3)を参照）。その場合には、Webpackのpluginに[eslint-webpack-plugin](https://webpack.js.org/plugins/eslint-webpack-plugin/)というのがあり、ビルド時にESLintを実行してエラーがあれば中断させる、のような設定ができた。ただ、上記のようにES Moduleをそのままruntimeで動かす場合には、ビルドなどの処理がなく、ESLintのチェックもはさめなくなる。そのため、以下のように[lint-staged](https://github.com/okonet/lint-staged)を使って、Git hooksをトリガーにESLintのチェックを実行するというのが必須になるだろう。
```json:package.json
    ...
	"scripts": {
		...
		"prepare": "npx simple-git-hooks"
	},
	"lint-staged": {
		"*": "npx prettier --ignore-unknown --write",
		"*.vue|*.js|*.jsx|*.cjs|*.mjs": "npx eslint --ignore-path .gitignore",
		...
	},
	"simple-git-hooks": {
		"pre-commit": "npx lint-staged --verbose",
		...
	},
    ...
```

上記のように設定しておけば、コミット時にエラーになり、問題のあるコードがpushされる事はなくなる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/62f30d0c-ff76-740e-b61a-6e3877f4b3c9.png)

※以下に参考として、私が試してみた事も残しておこうと思う（`"type": "module"`にできない場合などには、vite-nodeを利用した方法などが選択肢になるかもしれない）。

- 参考：[ESLint fails with ESM and JS eslintrc](https://github.com/eslint/eslint/issues/14137)
- 参考：[ERR_REQUIRE_ESM when using Node.js ESM in tandem with TypeScript config (commitlint.config.ts) ](https://github.com/conventional-changelog/commitlint/issues/3251)

#### 試してみた事①　[vite-plugin-node](https://github.com/axe-me/vite-plugin-node)でExpressをビルド

前提として、**package.jsonには`"type": "module"`を設定しているものとする**。

詳細は[公式](https://github.com/axe-me/vite-plugin-node)を参照してほしいが、ViteでバックエンドのNode.jsもビルドして、開発時にはHMRを利用できるようにするものらしい。

フロントエンドのvite.config.jsとは別のconfigを作成する必要がある。今回は以下のように`vite.config.server.js`として作成して検証してみた。
```js:vite.config.server.js
import { defineConfig } from 'vite';
import { VitePluginNode } from 'vite-plugin-node';

export default defineConfig({
	server: {
		// vite server configs, for details see [vite doc](https://vitejs.dev/config/#server-host)
		port: 3000
	},
	plugins: [
		...VitePluginNode({
			adapter: 'express',
			appPath: './srv/index.js',
			exportName: 'viteNodeApp'
		})
	]
});
```

`./srv/index.js`の実装は、NODE_ENVがdevelopmentの時はViteのdevサーバーを利用して、app.listenの必要はないので以下のようになる。
```js:srv/index.js
import express from 'express';

const app = express();

app.get('/hello', (req, res) => {
	res.json({ msg: 'hello' });
});

if (import.meta.env.PROD) app.listen(3000);

// eslint-disable-next-line import/prefer-default-export
export const viteNodeApp = app;
```

ここまで設定すると、以下のように`npx vite --config vite.config.server.js`でHMRのViteのdevサーバーが立ちあがる。APIもリクエストしてちゃんと動いている事も確認できる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/b0dc5292-fcfe-0eb9-c7bc-73971091c6c3.png)

プロダクション用のコードを生成する場合は、`npx vite build --config vite.config.server.js`でできる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/0281bc80-8ec2-6a62-7373-bb15998a3b85.png)

ちなみに、フロントエンドと共存するプロジェクトであれば、デフォルトのビルド後のファイル出力のディレクトリがかぶるので、以下のようにvite.config.server.jsを設定しておくといいだろう。
```js:vite.config.server.js
export default defineConfig({
	build: {
		outDir: './dist/express'
	},
    ...
}
```

ただ、わざわざvite-plugin-nodeを入れずとも、nodemonでHMRができるのでこの方法を取る意味は薄いなと思った。

※ちなみに、package.jsonに`"type": "module"`を設定していない＝CommonJSとしてNode.jsを動かしたい場合には、vite.config.server.jsに以下のように追加で設定して`vite build`を行う必要がある。
```js:vite.config.server.js
export default defineConfig({
	ssr: { format: 'cjs' },
    ...
}
```

ただ、Vite＋Vue＋Vuetifyの設定をしているプロジェクトで、上記の設定後にビルドしてサーバーを実行しても以下のようにエラーになってしまい起動できなかった。
```console
$ npx vite build --config vite.config.server.js 
vite v4.1.1 building SSR bundle for production...
✓ 344 modules transformed.
dist/express/index.js  922.87 kB
study@localhost:~/workspace/vite-vue3-vuetify-express (main *)
$ node dist/express/index.js 
/home/study/workspace/vite-vue3-vuetify-express/dist/express/index.js:36186
util.inherits(SendStream, Stream);
     ^

TypeError: util.inherits is not a function
    at Module.<anonymous> (/home/study/workspace/vite-vue3-vuetify-express/dist/express/index.js:36186:6)
    at Module._compile (node:internal/modules/cjs/loader:1165:14)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1219:10)
    at Module.load (node:internal/modules/cjs/loader:1043:32)
    at Function.Module._load (node:internal/modules/cjs/loader:878:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)
    at node:internal/main/run_main_module:22:47
```

原因ははっきり分からないが、フロントエンドのソースがあるプロジェクトでvite-plugin-nodeを利用しているためな気がしている。[vite-plugin-nodeのサンプルプロジェクト](https://github.com/axe-me/vite-plugin-node/tree/main/examples/express)では確かに`ssr: { format: 'cjs' }`の設定をしても問題なく動いた。

また、気になるのは`dist/express/index.js`の中身がexpressなどのライブラリをバンドルした状態で生成されていた事。Webpackでビルドする際には[webpack-node-externals](https://github.com/liady/webpack-node-externals)を指定して明示的にバンドルから除外するという事をしていたように、ViteでもNode.jsの場合はバンドル不要なので（runtime時にnode_modulesのJSを実行すればいいので）、バンドルしないようにしなければいけないのに、それができていないのでエラーになっているのでは？とも思った。

#### 試してみた事②　CommonJSのプロジェクトにおいて、[vite-node](https://github.com/vitest-dev/vitest/tree/main/packages/vite-node#readme)でES Moduleで実装したコードをそのまま実行する
　
前提として、package.jsonには`"type": "module"`を設定して**いない**ものとする（**CommonJSのプロジェクトである**）。

今回見ていく方法は、何らかの事情でpackage.jsonに`"type": "module"`を設定できない場合において、でもNode.jsもES Moduleで実装したい場合に取れる方法。

サーバーの起動方法などは簡単で、`npx vite-node srv/index.js`のようにすれば普通に起動できる（vite-nodeは依存に追加している前提）。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/34afc60a-2c24-047c-8ed5-edac9a141358.png)

```js:srv/index.js
import path from 'path';
import { fileURLToPath } from 'url';
import express from 'express';
import snakecaseKeys from 'snakecase-keys';

const filename = fileURLToPath(import.meta.url);
const dirname = path.dirname(filename);

console.log(dirname);
console.log(snakecaseKeys({ fooBar: 'bazs' }));

const app = express();

app.get('/hello', (req, res) => {
	res.json({ msg: 'hello' });
});

app.listen(3000);
```

HMRを行いたい場合には、`-w`オプションを付ければいい（`npx vite-node srv/index.js -w`）。ただし、[vite-node --watch doesn't close process before restarting](https://github.com/vitest-dev/vitest/issues/2334)にある通り、再起動前に既存のプロセスをkillするのがうまく動いていないBugがあるようで、以下のように追加で実装が必要になる。
```js:srv/index.js
...
const server = app.listen(3000);

if (import.meta.hot) {
	import.meta.hot.on('vite:beforeFullReload', () => {
		console.log('Reloading...');
		server.close();
	});
	import.meta.hot.dispose(() => {
		server.close();
	});
}
```

上記のように実装していれば、以下の動画のようにHMRも意図した通りに動く。
![ezgif.com-video-to-gif.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/9d1db184-773b-a73f-8c8e-d090b05e1061.gif)

vite-nodeはVitestの中にあるパッケージなので、Jestでbabelの設定をしていればES Moduleでテストを実装でき、テスト対象のコードがES Moduleでも問題なかった時と同じような仕組みで、Vitest時に使われたそういったノウハウからできたものなのかもしれないとふと思った。

## まとめとして

今回は、Vite＋Vue3＋Vuetify3＋Expressのプロジェクトの設定をやってみた。それなりに時間はかかったが、開発に使えるテンプレートの基本形として完成できて良かった。今後、Expressの方でPrismaを利用した実装などもやっていきたいと思った。