9
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-02-11

はじめに

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

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

yarn create viteで初期プロジェクトを作成する

まずは公式にある通りの方法(yarn create vite)で初期プロジェクトを作成する。
image.png

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

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の設定
  2. ESLintの設定
  3. VS Codeの設定
  4. npm-check-updatesのインストール
  5. Git hooksの設定
  6. エイリアスの設定(ESLintの設定②)

prettierの設定

ここは目新しい事は特になく、いつも通りの設定をすればいい。好みに応じて、Configuration Fileにあるような設定ファイルの方式を選べばいいだろう。今回は以下のように設定してみた。

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

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

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

ESLintの設定

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

.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に書かれている通り、.cjsはpackage.jsonに"type":"module"を設定してES Modulesを利用できるようにしている場合に利用する拡張子。今回はそういった設定はしないので.jsに変更する。

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

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

  • 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は利用しない

ルールの追加前後でyarn lintの結果を確認してみると、以下の通り変更後ではairbnb-baseのルールでチェックされている事が確認できる。

追加前 追加後
image.png image.png

※上記の追加後のスクショを見ると分かるが、vite@vitejs/plugin-vueがdevDependenciesではなく、dependenciesであるべきというエラーが出ている。その2つは意図してdevDependenciesに入れているので、ESLintのルールを以下のように修正し、エラーを出ないように調整するのがいいだろう。
image.png

vite-plugin-eslint

Viteによるビルド時にESLintのチェックを実行させるためのplugin。
これを導入すると、例えば以下のようにエラーが出るようになる。
image.png
image.png

eslint-plugin-vuetify

VuetifyのESLintのチェックルール。これを導入すると、以下のようにエラーが出るようになる。
image.png

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

.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.jsonjsconfig.jsonを設定したいと思う。ちなみに、yarn create viteを実行した時に、.vscode/extensions.jsonが自動で作成されているが、これがる事で以下のように拡張機能のインストールを勧める通知が出るようになる。チームで開発している時などはこうしたものを利用して拡張機能として入れるべきものの認識を揃える事もできるだろう。
image.png

.vscode/settings.json

今回は以下のように設定した。ESLintのチェックを保存に行い修正可能であれば修正する設定と、同じく保存時にPrettierによる自動整形を行う設定をしている。

.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 の設定などを参照。

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

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

package.jsonに定義しているライブラリの更新のために追加。詳細はJavascript(Node.js)プロジェクトの開発者は必須! ncuコマンドでパッケージを最新化するなどを参照。

Git hooksの設定

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

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

エイリアスの設定(ESLintの設定②)

ESLintはデフォルトでは'@/components/TheWelcome.vue'のような@はパスとして解釈できないので、import/no-unresolvedのエラーが出てしまう。そこでeslint-import-resolver-aliasなどのESLintのライブラリを使い、エイリアスを解決してあげる設定を追加する必要がある。設定自体は簡単で、

$ yarn add -D eslint-import-resolver-alias

でインストールした後、以下のようにESLintのconfigを追加で設定すればいい。

.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

追記

上記の手順で設定したものの、Vuetifyの導入時にエラー(以下のスクショを参照)になって躓いた。
image.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というライブラリがあるようなので、これを利用して実装した。

    ...
	settings: {
		'import/resolver': {
			'eslint-import-resolver-custom-alias': {
				alias: {
					'@': './src'
				},
				extensions: ['.js', '.vue']
			}
		}
	},
    ...

これを導入すると、ESLintがクラッシュする事はなくなったが、以下のスクショのようにVuetifyのimportでエラーが出てしまった。ただ、Viteでのビルドは成功する事から、ESLintの設定不備と考えてよさそうのなので、今回は以下のようにESLintのエラーを明示的にOffするように設定した。

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というのがあったので、このライブラリでうまくいくかなと期待したが、以下のように軒並みimport/no-unresolvedになり、ESLintのimportの解釈が壊れてしまってNGだった。。。
image.png

Vuetifyの導入、ExpressをVite+Vue+Vuetifyの同一PJで開発できるようにする

ここからは開発をしていく際に利用したいライブラリ・フレームワークを導入するという事をやっていきたいと思う。ExpressについてはFirebaseなどを利用したサーバーレスアプリケーションの開発では不要だが、どうしても自前のAPIが欲しくなる事もあるかと思い、導入する事にした(Expressがあるからと言って必ずしもサーバーが必要になるわけではなく、How to run Express.js apps with Netlify Functionsserverless-expressを利用すれば簡単にサーバレスのExpressを構築する事も可能)。

Vuetifyの導入

ここからはUIのフレームワークとしてVuetifyを利用したいので、それを利用できるように設定していく。公式にはないViteのための設定が必要だったりとここは少し躓いた・・・。

まずは、Manual stepsに書かれている通り、vuetifyのインストールとVuetifyをVueでuse()するように設定する。

1点、vuetifyの設定自体をmain.jsに書くとmain.jsが肥大化するので、src/plugins/vuetify.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は確かにあるが、アイコンが表示されない)。

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

これはMaterial Design Iconsに書かれている追加の設定が必要なため。

まず、必要になるライブラリを追加する(yarn add @mdi/font -D)。
後は、vuetify.jsに以下のimportを追加すればいい。

src/plugins/vuetify.js
import '@mdi/font/css/materialdesignicons.css';
...

ここまで設定すると、以下のようにマテリアルデザインのアイコンが利用できるようになり、画面上にも表示されるようになる。
image.png

これで完了と思いきや、今度は以下のような実装をすると、404になってしまう・・・。

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

これはViteのビルド時のpluginが不足しているために、VuetifyのコンポーネントでassetsのURL変換がうまくできていないため。まず、vite-plugin-vuetifyを追加して、以下のようにvite.config.jsを設定すればいい。

vite.config.js
...
import { transformAssetUrls } from 'vite-plugin-vuetify';

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

この設定を追加すると、以下のようにURLが適切に変換されて読み込みできるようになる。
image.png

上記で設定はOKだが、以下のように設定する事でVuetifyのsassの設定を上書きするという事もできる。sassを利用する場合には、ここまで設定しておくといい気がする(Component specific variablesも参照)。

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' }
		})
	],
	...
});
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を追加することでフォントの利用もできるようになる。

src/plugins/webfontloader.js
import webFontLoader from 'webfontloader';

export default () => {
	webFontLoader.load({
		google: {
			families: ['Roboto:100,300,400,500,700,900&display=swap']
		}
	});
};
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で設定していく。

設定自体は簡単で、yarn add -D vue-i18nでvue-i18nを追加し、上記で設定していたvuetify.jsを以下のように変更すればいい(i18nに関する設定はVuetifyのpluginのようなものなので別のファイルに分割するようにした)。

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;
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;
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.jsmessagesをどんどん追記していくとファイルが肥大化してメンテナンス性も悪くなるので、別ファイルに切り出して読み込むようにしてみたいと思う。

ディレクトリとしては、src/locales以下に各言語に対応するJSONファイルを配置するという方法を取る事にする。実装としては以下のようになった。

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()
});
...
study@localhost:~/workspace/vite-vue3-vuetify-express (main *+)
$ tree src/
src/
...
├── locales
│   ├── en.json
│   └── ja.json
...

JSONを読み込む部分についてはGlob のインポートに書かれている方法を利用した。これにより複数のモジュール(今回はsrc/locales以下のJSON)を簡単にインポートできる。ただし、遅延ローディングされるのは困るのでeagerオプションをtrueに設定している。

1点、ファイル名を取得するのにNode.jsのPathを利用しているが、これはデフォルトだと以下のようにエラーになる(これはNode.jsの世界のモジュールはブラウザでは利用できないため)。
image.png

そのため、vite.config.jsの方にvite-plugin-node-polyfillsの設定が必要になる(Node.jsのCore Modulesをブラウザ環境向けにポリフィル=代替コードにする)。

vite.config.js
...
import { nodePolyfills } from 'vite-plugin-node-polyfills';

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

ちなみに、console.log(locales)は以下のようなオブジェクトになる。
image.png

また、console.log(dirPath);console.log(locales[dirPath].default);は以下の通りで、locales[dirPath].defaultにアクセスする事で、JSONの中身を取得できる。
image.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"を追記する。

package.json
{
	...
	"type": "module",
    ...
}

このように設定するだけでES Moduleを使えるようになる(Determining module systemも参照)。そのため、以下のような実装で普通にExpressのサーバーが起動できる。

srv/index.js
import express from 'express';

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

開発時にHMR(保存してサーバーを再起動するやつ)をしたい場合には、nodemonを利用すればいいだろう。

package.json
	"scripts": {
		...
		"express:dev": "nodemon srv/index.js",
		"express": "node srv/index.js",
        ...
   }

ただ、上記の設定変更後にeslintを動かそうとすると、以下のようにエラーになる。これはES Moduleなのにrequire()になってるのでそこで食い違いが出てエラーになっている。

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)にちゃんと書かれており、.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

※昔ながらで、ES Moduleで実装はするものの、runtime時にはWebpackでビルドしたCommonJSの状態のものを動かす、という方法もあった(Node.jsでimport・export(ES6の構文)を使えるようにwebpack × Babelの設定をやってみたを参照)。その場合には、Webpackのpluginにeslint-webpack-pluginというのがあり、ビルド時にESLintを実行してエラーがあれば中断させる、のような設定ができた。ただ、上記のようにES Moduleをそのままruntimeで動かす場合には、ビルドなどの処理がなく、ESLintのチェックもはさめなくなる。そのため、以下のようにlint-stagedを使って、Git hooksをトリガーにESLintのチェックを実行するというのが必須になるだろう。

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

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

試してみた事① vite-plugin-nodeでExpressをビルド

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

詳細は公式を参照してほしいが、ViteでバックエンドのNode.jsもビルドして、開発時にはHMRを利用できるようにするものらしい。

フロントエンドのvite.config.jsとは別のconfigを作成する必要がある。今回は以下のようにvite.config.server.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の必要はないので以下のようになる。

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

プロダクション用のコードを生成する場合は、npx vite build --config vite.config.server.jsでできる。
image.png

ちなみに、フロントエンドと共存するプロジェクトであれば、デフォルトのビルド後のファイル出力のディレクトリがかぶるので、以下のようにvite.config.server.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を行う必要がある。

vite.config.server.js
export default defineConfig({
	ssr: { format: 'cjs' },
    ...
}

ただ、Vite+Vue+Vuetifyの設定をしているプロジェクトで、上記の設定後にビルドしてサーバーを実行しても以下のようにエラーになってしまい起動できなかった。

$ 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のサンプルプロジェクトでは確かにssr: { format: 'cjs' }の設定をしても問題なく動いた。

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

試してみた事② CommonJSのプロジェクトにおいて、vite-nodeで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

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にある通り、再起動前に既存のプロセスをkillするのがうまく動いていないBugがあるようで、以下のように追加で実装が必要になる。

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

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

まとめとして

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

9
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?