2
2

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.

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

Last updated at Posted at 2021-12-08

はじめに

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

行った設定は、

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

の4つ。

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

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

Express serverのホットリロード

特に難しい事はなく、webpackのwatchnodemonを使えばいい。

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

※watchはFlags--watchを使っても実現でき、その場合はwebpack --watch --mode=developmentとなる。Flagsの説明の通り、watchも--watchも全く同じのよう。

webpackの設定

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

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

webpack.config.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

configurationの名前(なくてもwebpackは動く)。

mode

webpackをどのモードで行うか?(バンドル≒buildのモード)の設定。

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

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

node-envと併用する形で大体設定する書き方が多くみられる気がする。

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?");

/***/ }),
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
 */

...
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

webpackでbuildしたりロードしたりするものの出力する方法・場所を設定するためのオプション。

output.path

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

output.filename

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

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

output.clean

webapckでbuild後のファイル出力前に、出力先のディレクトリの中身を削除する設定。設定の仕方では削除しないで残したりもできる。

externals

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

詳細は、webpackのページ

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の所でエラーが出たりしてしまうのでこの設定が必要になる。

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
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

plugins

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

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

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

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

ESLintの設定についてはESLintの設定を参照。

ところでwebapckでESLintが動くというけどタイミングは?

webpackでbuildする中でESLintを実行させる1が、ESLintが走るコードがbabel-loaderでトランスパイルされた後のコードなのか?その前のコードなのか?という疑問が出ると思う。
これは実際の動きと、VS CodeでESLintのExtentionsを入れてエラーがソースコード上に出るという事から、ESLintが実行されるコードはトランスパイルされる前のES6等のコードで、babelでトランスパイルされた後のコードではない(と思っている(間違っていたらご指摘下さい))。

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

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

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

ESLintの設定

を参照。

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

補足

設定全体は以下を参照

parserについて

上記の記事の中では基本的にparserは不要と書いたが、必要な場面もあるようで例えば、以下のようなclass内にprivateメソッドを定義した時に出るエラーを解決するのに必要になるようである(何らかの都合で、"ecmaVersion"の設定が変更できないなどの際には)。
image.png

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

VS Code のExtentions

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

  1. eslint-webpack-plugines(元々はeslint-loaderだった)を使う

2
2
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
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?