Node.js(Express)（ES6で実装）においてESLint / Prettier / Visual Studio Code / ホットリロードの設定をする

## はじめに
自分でちょっとしたWebアプリを作ったりする事があるが、その時の開発環境は結構適当だったりした。ただ、今後の事を考えるとちゃんと静的解析とかを入れ込んだ開発環境を作れるようになっておくほうがいいと思い今回ちゃんとした環境の構築をやってみたので、その備忘録を残す。

行った設定は、

- Expressサーバのホットリロード対応
- webpack
- ESLint × Prettier
- VS Code

の4つ。

※以下の記事では、webapckでのバンドルをbuildという言い方をしている部分がある（buildツールとしてwebapckが紹介されるのでいいと思っている）。

GitHubのコードは以下（Step2の章の部分がこの記事でやった内容になっている）。

https://github.com/yuta-katayama-23/node-express

## Express serverのホットリロード
特に難しい事はなく、webpackの[watch](https://webpack.js.org/api/cli/#watch)と[nodemon](https://github.com/remy/nodemon)を使えばいい。

```bash
npm install --save-dev nodemon
```
```json:package.json
{
  ...
  "scripts": {
    "watch": "webpack watch --mode=development",
    "start": "nodemon dist/main.js"
  }
}
```

※watchは[Flags](https://webpack.js.org/api/cli/#flags)の`--watch`を使っても実現でき、その場合は`webpack --watch --mode=development`となる。Flagsの説明の通り、watchも--watchも全く同じのよう。

## webpackの設定
以下のように設定した。設定した内容の概要としては、

- modeでdevelopmentとproductionの切り替えの設定
- webpackを実行（buildを実行）した後に出力されるファイルの出力先の設定
- node_modulesのバンドルをしないように設定
- webpackでbuildする時にESLintを実行し、エラーがあればbuildを止める

webpack.config.jsの全体は以下。

```js:webpack.config.js
const path = require('path');
const nodeExternals = require('webpack-node-externals');
const ESLintPlugin = require('eslint-webpack-plugin');

module.exports = {
	target: 'node',
	externals: [nodeExternals()],
	mode: process.env.NODE_ENV === 'production' ? 'production' : 'development',
	name: 'node-express',
	entry: {
		index: './src/index.js',
	},
	output: {
		path: path.resolve(__dirname, 'dist'),
		filename: '[name].js',
		clean: true,
	},
	module: {
		rules: [
			{
				test: /\.m?js$/,
				exclude: /node_modules/,
				use: {
					loader: 'babel-loader',
				},
			},
		],
	},
	plugins: [new ESLintPlugin({ exclude: 'node_modules' })],
};
```

詳細は以下で1つずつ見ていく。

### [name](https://webpack.js.org/configuration/other-options/#name)
configurationの名前（なくてもwebpackは動く）。

### [mode](https://webpack.js.org/configuration/mode/)
webpackをどのモードで行うか？（バンドル≒buildのモード）の設定。

developmentの場合、ソースコードの圧縮が行われず可読性のある形でbuildされる。[`devtool: 'source-map'`](https://webpack.js.org/configuration/devtool/#devtool)と共に使われる事が多い（source-mapを使うと、フロントエンドだとバンドルされて1つになる前のJavaScriptファイルが見れてソースを追える）。ただ、Node.jsの場合は以下のように[eval()](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/eval)でJavaScriptのコードが解釈されるだけなのであまり可読性はない気がする。

productionの場合、`main.js.LICENSE.txt`のようにNode.jsのライブラリのライセンス状況が見れるテキストファイルが作成され＋圧縮されたJavaScript（実行時最も早く動作するコード）が出力される。

※[node-env](https://webpack.js.org/api/cli/#node-env)と併用する形で大体設定する書き方が多くみられる気がする。

```js:webapckでmode=developmentでbuildしたものの一部
/***/ "./src/index.js":
/*!**********************!*\
  !*** ./src/index.js ***!
  \**********************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var regenerator_runtime_runtime_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! regenerator-runtime/runtime.js */ \"./node_modules/regenerator-runtime/runtime.js\");\n/* harmony import */ var regenerator_runtime_runtime_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(regenerator_runtime_runtime_js__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var core_js_modules_es_date_now_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! core-js/modules/es.date.now.js */ \"./node_modules/core-js/modules/es.date.now.js\");\n/* harmony import */ var core_js_modules_es_date_now_js__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_date_now_js__WEBPACK_IMPORTED_MODULE_1__);\n/* harmony import */ var core_js_modules_es_date_to_string_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! core-js/modules/es.date.to-string.js */ \"./node_modules/core-js/modules/es.date.to-string.js\");\n/* harmony import */ var core_js_modules_es_date_to_string_js__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_date_to_string_js__WEBPACK_IMPORTED_MODULE_2__);\n/* harmony import */ var core_js_modules_es_array_from_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! core-js/modules/es.array.from.js */ \"./node_modules/core-js/modules/es.array.from.js\");\n/* harmony import */ var core_js_modules_es_array_from_js__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_array_from_js__WEBPACK_IMPORTED_MODULE_3__);\n/* harmony import */ var core_js_modules_es_string_iterator_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! core-js/modules/es.string.iterator.js */ \"./node_modules/core-js/modules/es.string.iterator.js\");\n/* harmony import */ var core_js_modules_es_string_iterator_js__WEBPACK_IMPORTED_MODULE_4___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_string_iterator_js__WEBPACK_IMPORTED_MODULE_4__);\n/* harmony import */ var core_js_modules_es_object_to_string_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! core-js/modules/es.object.to-string.js */ \"./node_modules/core-js/modules/es.object.to-string.js\");\n/* harmony import */ var core_js_modules_es_object_to_string_js__WEBPACK_IMPORTED_MODULE_5___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_object_to_string_js__WEBPACK_IMPORTED_MODULE_5__);\n/* harmony import */ var core_js_modules_es_promise_js__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! core-js/modules/es.promise.js */ \"./node_modules/core-js/modules/es.promise.js\");\n/* harmony import */ var core_js_modules_es_promise_js__WEBPACK_IMPORTED_MODULE_6___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_promise_js__WEBPACK_IMPORTED_MODULE_6__);\n/* harmony import */ var core_js_modules_web_timers_js__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! core-js/modules/web.timers.js */ \"./node_modules/core-js/modules/web.timers.js\");\n/* harmony import */ var core_js_modules_web_timers_js__WEBPACK_IMPORTED_MODULE_7___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_web_timers_js__WEBPACK_IMPORTED_MODULE_7__);\n/* harmony import */ var express__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! express */ \"./node_modules/express/index.js\");\n/* harmony import */ var express__WEBPACK_IMPORTED_MODULE_8___default = /*#__PURE__*/__webpack_require__.n(express__WEBPACK_IMPORTED_MODULE_8__);\n\n\n\n\n\n\n\n\n\nfunction asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }\n\nfunction _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, \"next\", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, \"throw\", err); } _next(undefined); }); }; }\n\n\nvar app = express__WEBPACK_IMPORTED_MODULE_8___default()();\napp.get('/', /*#__PURE__*/function () {\n  var _ref = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee(req, res) {\n    var reqTime;\n    return regeneratorRuntime.wrap(function _callee$(_context) {\n      while (1) {\n        switch (_context.prev = _context.next) {\n          case 0:\n            reqTime = Date.now();\n            console.log(Array.from('foo'));\n            _context.next = 4;\n            return new Promise(function (resolve) {\n              setTimeout(function () {\n                resolve('sleep');\n              }, 500);\n            });\n\n          case 4:\n            res.status(200).send({\n              msg: 'hello world!',\n              elaptime: Date.now() - reqTime\n            });\n\n          case 5:\n          case \"end\":\n            return _context.stop();\n        }\n      }\n    }, _callee);\n  }));\n\n  return function (_x, _x2) {\n    return _ref.apply(this, arguments);\n  };\n}());\napp.listen(3000, function () {\n  return console.log('listening on port 3000!');\n});\n\n//# sourceURL=webpack://my-webpack-project/./src/index.js?");

/***/ }),
```
```text:main.js.LICENSE.txt
/*!
 * accepts
 * Copyright(c) 2014 Jonathan Ong
 * Copyright(c) 2015 Douglas Christopher Wilson
 * MIT Licensed
 */

/*!
 * body-parser
 * Copyright(c) 2014 Jonathan Ong
 * Copyright(c) 2014-2015 Douglas Christopher Wilson
 * MIT Licensed
 */

...
```
```js:webapckでmode=productionでbuildしたものの一部
...function a(e,a,i,n,t,o,r){try{var s=e[o](r),c=s.value}catch(e){return void i(e)}s.done?a(c):Promise.resolve(c).then(n,t)}var i=__webpack_require__.n(e)()();i.get("/",function(){var e,i=(e=regeneratorRuntime.mark((function e(a,i){var n;return regeneratorRuntime.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return n=Date.now(),console.log(Array.from("foo")),e.next=4,new Promise((function(e){setTimeout((function(){e("sleep")}),500)}));case 4:i.status(200).send({msg:"hello world!",elaptime:Date.now()-n});case 5:case"end":return e.stop()}}),e)})),function(){var i=this,n=arguments;return new Promise((function(t,o){var r=e.apply(i,n);function s(e){a(r,t,o,s,c,"next",e)}function c(e){a(r,t,o,s,c,"throw",e)}s(void 0)}))});return function(e,a){return i.apply(this,arguments)}}()),i.listen(3e3,(function(){return console.log("listening on port 3000!")}))})()})();
```

### [Output](https://webpack.js.org/configuration/output/)
webpackでbuildしたりロードしたりするものの出力する方法・場所を設定するためのオプション。

#### [output.path](https://webpack.js.org/configuration/output/#outputpath)
buildしたものを出力するディレクトリを設定。
`path: path.resolve(__dirname, 'dist')`のようにすれば、`__dirname`がソースコードがあるディレクトリパスが格納されている変数なので、webpack.config.jsがあるディレクトリをルートディレクトリとして、`./dist`にファイルが出力される。

- 参考：[Node.js v17.2.0 documentation　Path](https://nodejs.org/docs/latest/api/path.html)

#### [output.filename](https://webpack.js.org/configuration/output/#outputfilename)
buildして出力されるファイルの名前の設定。
`filename: '[name].js'`のようにすると、[entry](https://webpack.js.org/configuration/entry-context/#entry)に書かれているようにentryのキーの名前（今回だとindex）が`[name]`の部分に補完されるので、出力されるファイル名は`index.js`になる。

※entryのキーは複数のファイルがある時に使われるものな気もするので今回は別に設定しなくてもいいが、出力されるファイル名をindex.jsにしたかったのであえて設定している

#### [output.clean](https://webpack.js.org/configuration/output/#outputclean)
webapckでbuild後のファイル出力前に、出力先のディレクトリの中身を削除する設定。設定の仕方では削除しないで残したりもできる。

### externals
Node.jsではnode_modulesをバンドルする必要はないので、node_modulesを無視するために追加の設定をしている。

詳細は、[webpackのページ](https://webpack.js.org/configuration/externals/#function)に

>  webpack-node-externals, for example, excludes all modules from the node_modules directory and provides options to whitelist packages.

と書かれている通り。また、`webpack-node-externals`の方にも、

> When bundling with Webpack for the backend - you usually don't want to bundle its node_modules dependencies.

と書かれている。

※仮にこの設定をしないと、以下のようにwebpackでbuildを行う際にnode_modulesの所でエラーが出たりしてしまうのでこの設定が必要になる。

```bash:before（webpack-node-externalsを入れる前）
[root@localhost node-express]# yarn dev
yarn run v1.22.17
$ webpack watch --node-env=development
asset index.js 1.11 MiB [compared for emit] (name: index)
...

WARNING in ./node_modules/express/lib/view.js 81:13-25
Critical dependency: the request of a dependency is an expression
 @ ./node_modules/express/lib/application.js 22:11-28
 @ ./node_modules/express/lib/express.js 18:12-36
 @ ./node_modules/express/index.js 11:0-41
 @ ./src/index.js 14:0-30 15:10-17

1 warning has detailed information that is not shown.
...
node-express (webpack 5.64.4) compiled with 1 warning in 3389 ms
```
```bash:after（webpack-node-externalsを入れた後）
[root@localhost node-express]# yarn dev
yarn run v1.22.17
$ webpack watch --node-env=development
asset index.js 11.5 KiB [emitted] (name: index)
runtime modules 937 bytes 4 modules
built modules 2.39 KiB [built]
  modules by path external "core-js/modules/*.js" 294 bytes
    external "core-js/modules/es.date.now.js" 42 bytes [built] [code generated]
    external "core-js/modules/es.date.to-string.js" 42 bytes [built] [code generated]
    external "core-js/modules/es.array.from.js" 42 bytes [built] [code generated]
    external "core-js/modules/es.string.iterator.js" 42 bytes [built] [code generated]
    external "core-js/modules/es.object.to-string.js" 42 bytes [built] [code generated]
    external "core-js/modules/es.promise.js" 42 bytes [built] [code generated]
    external "core-js/modules/web.timers.js" 42 bytes [built] [code generated]
  ./src/index.js 2.02 KiB [built] [code generated]
  external "regenerator-runtime/runtime.js" 42 bytes [built] [code generated]
  external "express" 42 bytes [built] [code generated]
node-express (webpack 5.64.4) compiled successfully in 1231 ms
```

- 参考：[Webpack node modules externals](https://github.com/liady/webpack-node-externals)

### plugins
build（バンドル）に関する事以外にも幅広い処理を実行させるための設定をするオプション。

今回はwebpackのbuild中にESLintを使い、エラーがあればbuildを止める設定をしている。

今までは[eslint-loader](https://github.com/webpack-contrib/eslint-loader)というloaderでESLintのチェックを実行する仕組みだったようが、これは非推奨になったので[eslint-webpack-plugin](https://github.com/webpack-contrib/eslint-webpack-plugin)を使う。
使い方は難しくなく、ESLintの設定（.eslintrc.jsonなど）を作成しておけばそのルールを読み取り、webapckのbuild中にソースのチェックをしてくれる。

※ESLintの公式の通りの設定をしていればどこにESLintの設定ファイルがあるか？を指定する必要はない。

ESLintの設定については[ESLintの設定](#eslintの設定)を参照。

- 参考：[webpackでESLintが使える環境を構築してみる](https://designsupply-web.com/media/programming/6739/)

#### ところでwebapckでESLintが動くというけどタイミングは？
webpackでbuildする中でESLintを実行させる[^1]が、ESLintが走るコードがbabel-loaderでトランスパイルされた後のコードなのか？その前のコードなのか？という疑問が出ると思う。
これは実際の動きと、VS CodeでESLintのExtentionsを入れてエラーがソースコード上に出るという事から、ESLintが実行されるコードはトランスパイルされる前のES6等のコードで、babelでトランスパイルされた後のコードではない（と思っている（間違っていたらご指摘下さい））。

[^1]: [eslint-webpack-plugines](https://github.com/webpack-contrib/eslint-webpack-plugin)（元々は[eslint-loader](https://github.com/webpack-contrib/eslint-loader)だった）を使う

babelでトランスパイルされた後のコードは人が見るものではなく、マシンが読むものなのでそれに対してESLintの静的解析をしても意味ないのではというのも根拠の一つ。

ちなみに、今までのeslint-loaderでは、`enforce: 'pre'`を付けていた場合、babel-loaderと併用していればbabel-loaderで変換する前にコードを静的解析するという設定になるようだが、eslint-webpack-pluginでは設定なしにbabel-loaderでの変換（トランスパイル）前にESLintが実行されているのではないかと思っている。

**※上記の内容は裏付け情報がなく仮説のようなものでもあるのでご注意ください**

- 参考：[webpack の基本的な使い方#eslint-loader](https://www.webdesignleaves.com/pr/jquery/webpack_basic_01.html#h4_index_17)

## ESLintの設定
https://note.com/shift_tech/n/n8bbe1c05ba0b

を参照。

**※Qiitaの記事は全て個人的な記載であり、所属する組織団体とは無関係です。**

### 補足
#### 設定全体は以下を参照
https://github.com/yuta-katayama-23/node-express

#### parserについて
上記の記事の中では基本的に`parser`は不要と書いたが、必要な場面もあるようで例えば、以下のようなclass内にprivateメソッドを定義した時に出るエラーを解決するのに必要になるようである（**何らかの都合で、`"ecmaVersion"`の設定が変更できないなどの際には**）。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/7f45977c-39c1-9484-2659-c54ecba53200.png)

上記のエラーの解消方法については、以下を参照。

https://qiita.com/yuta-katayama-23/items/4a7578ff230e8b4df825

- 参考：[Specifying Parser Options](https://eslint.org/docs/user-guide/configuring/language-options#specifying-parser-options)
- 参考：[Parsing error: Unexpected character '#' #1542](https://github.com/standard/standard/issues/1542)

#### VS Code のExtentions
上記の設定で追加したものも含めて開発をする上で便利そうな有効そうな VS Code の Extentions を列挙してみた。

- [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint)
- [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)
- [Git History](https://marketplace.visualstudio.com/items?itemName=donjayamanne.githistory)
- [Docker](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-docker)
- [gitflow](https://marketplace.visualstudio.com/items?itemName=vector-of-bool.gitflow)
- [GitLens — Git supercharged](https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens)
- [Live Share](https://marketplace.visualstudio.com/items?itemName=MS-vsliveshare.vsliveshare)
- [npm](https://marketplace.visualstudio.com/items?itemName=eg2.vscode-npm-script)
- [npm Intellisense](https://marketplace.visualstudio.com/items?itemName=christian-kohler.npm-intellisense)
- [YAML](https://marketplace.visualstudio.com/items?itemName=redhat.vscode-yaml)