はじめに
自分でちょっとしたWebアプリを作ったりする事があるが、その時の開発環境は結構適当だったりした。ただ、今後の事を考えるとちゃんと静的解析とかを入れ込んだ開発環境を作れるようになっておくほうがいいと思い今回ちゃんとした環境の構築をやってみたので、その備忘録を残す。
行った設定は、
Expressサーバのホットリロード対応
webpack
ESLint × Prettier
VS Code
の4つ。
※以下の記事では、webapckでのバンドルをbuildという言い方をしている部分がある(buildツールとしてwebapckが紹介されるのでいいと思っている)。
GitHubのコードは以下(Step2の章の部分がこの記事でやった内容になっている)。
Express serverのホットリロード
特に難しい事はなく、webpackのwatchとnodemonを使えばいい。
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にファイルが出力される。
参考:Node.js v17.2.0 documentation Path
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
参考:Webpack node modules externals
plugins
build(バンドル)に関する事以外にも幅広い処理を実行させるための設定をするオプション。
今回はwebpackのbuild中にESLintを使い、エラーがあればbuildを止める設定をしている。
今まではeslint-loaderというloaderでESLintのチェックを実行する仕組みだったようが、これは非推奨になったのでeslint-webpack-pluginを使う。
使い方は難しくなく、ESLintの設定(.eslintrc.jsonなど)を作成しておけばそのルールを読み取り、webapckのbuild中にソースのチェックをしてくれる。
※ESLintの公式の通りの設定をしていればどこにESLintの設定ファイルがあるか?を指定する必要はない。
ESLintの設定についてはESLintの設定を参照。
参考:webpackで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が実行されているのではないかと思っている。
※上記の内容は裏付け情報がなく仮説のようなものでもあるのでご注意ください
参考:webpack の基本的な使い方#eslint-loader
ESLintの設定
静的解析・構文チェックツールを入れていると入れていないでは、後々でコードの品質や読みやすさにも大きな違いが出るのでその設定をしておく。JavaScriptの静的解析と言えばESLintという感じなのでESLintで静的解析のルール設定をしていく。
はじめの導入はGetting Started with ESLintを見れば簡単にできる。
yarn add --dev eslint
yarn run eslint --init
yarn run eslint --initを実行すると、CLI上で質問が出てきて初期の構成を作ってくれる。今回は一旦以下のように設定した。
Q
A
How would you like to use ESLint?
What type of modules does your project use?
Which framework does your project use?
Does your project use TypeScript?
How would you like to define a style for your project?
How would you like to define a style for your project?
Which style guide do you want to follow?
初期設定時の.eslintrc.jsonは以下のようになる。ここからルールを変更していく。
.eslintrc.json
{
"env": {
"es2021": true,
"node": true
},
"extends": [
"airbnb-base"
],
"parserOptions": {
"ecmaVersion": 13,
"sourceType": "module"
},
"rules": {
}
}
※ちなみに今の状態でESLintを実行すると、
[root@localhost node-express]# yarn lint
yarn run v1.22.17
$ eslint src/*
/root/workspace/node-express/src/index.js
7:3 warning Unexpected console statement no-console
21:24 warning Unexpected console statement no-console
✖ 2 problems (0 errors, 2 warnings)
Done in 1.38s.
のようになる。
参考:ESLint 最初の一歩
結論:ESLintの設定内容とprettierの設定内容
.eslintrc.json
{
"root": true,
"env": {
"es2021": true
},
"extends": [
"eslint:recommended",
"airbnb-base",
"plugin:prettier/recommended"
],
"parserOptions": {
"ecmaVersion": 12
},
"rules": {
"no-console": "off"
},
"ignorePatterns": ["/node_modules/", "/dist/"]
}
.prettierrc
{
"singleQuote": true,
"useTabs": true,
"semi": true,
"bracketSpacing": true,
"arrowParens": "always",
"printWidth": 80,
"trailingComma": "none"
}
以下の項で、上記の設定の詳細についてみていく。
root: true
ESLintを特定のプロジェクトに制限するための設定。
ESLintはデフォルトでは公式に書かれている通り、ルートディレクトリまでのすべての親フォルダにある設定ファイルを探す。これはすべてのプロジェクトを特定の規則に従わせたい場合に便利だが、意図しないルール適用になったりするので、それを防止するためにroot: trueとして、ESLintを特定のプロジェクトに限定する(ESLintは "root": trueで親フォルダまで探しに行くのをStopさせられる)。
By default, ESLint will look for configuration files in all parent folders up to the root directory. This can be useful if you want all of your projects to follow a certain convention, but can sometimes lead to unexpected results. To limit ESLint to a specific project, place "root": true inside the .eslintrc.* file or eslintConfig field of the package.json file or in the .eslintrc.* file at your project's root level. ESLint will stop looking in parent folders once it finds a configuration with "root": true.
参考:Cascading and Hierarchy
env
静的検証の前提条件(JavaScriptのコードが、ブラウザで動くコードである・ES6である、など)を設定する事ができるオプション。
今回はNode.jsでES6の構文(constとかletとか)も使う+最新のES2021も使うかもしれないので、node, es6, es2021と設定する。
"env": {
"es6": true,
"es2021": true,
"node": true
}
と言いたいところだが、この後見ていくextendsの設定でairbnb-baseについて解説するが、このairbnb-baseにnode, es6は組み込み済みなのでここで明示的に宣言せずで問題ない(分かりやすさを考えて宣言するのはありだが、同じ事を書いている事になるので冗長な気もしたり…)。
なので、結論としてはes2021だけ記載するようにする。
"env": {
"es2021": true
},
※ちなみに、今回はextendsのairbnb-baseが諸々いい感じに設定してくれるので不要だが、知識として以下は知っておく必要がありそうなので書いておく。
ES Moduleの機能(簡単に言ってしまえばimport/exportのやつ)を静的解析でチェックするには、後に出てくるparserOptionsの設定で"sourceType": "module"とする必要がある。
ESLintは、その解析対象のJavaScriptのコードがECMAScript 5(ES5)の構文で書かれているものと想定している。ので、この設定を上書きするにはenvで"es2021": trueのようにするだけではダメで、parserOptionsの設定も必要になりecmaVersion: 12と書く必要がある(←これについては不要という文献もあり、ESLintのversionによって違うのかもしれない)、以下のように書く。ちなみに、ESLintの公式には「ESLint allows you to specify the JavaScript language options you want to support. By default, ESLint expects ECMAScript 5 syntax.」と書かれている。
"parserOptions": {
"ecmaVersion": 12,
"sourceType": "module"
}
参考:Specifying Environments
参考:Specifying Parser Options
🌟 環境設定をする
参考:webpack の基本的な使い方
extends
eslintの設定(.eslintrc.jsonなどに書く設定)で、他の設定内容を継承させるための設定をする部分。
普通のプログラムの継承と同じように、envとかrulesとかparserOptionsとか、eslintの設定項目の各項目に対する設定を、他のファイルから読み込んで継承させられる。
例えば、今回extendsにしてしようとしているairbnb-base(eslint-config-airbnb-base)では、index.jsの方に、rulesフォルダ内のパスが書かれており、そのrulesフォルダ内には、es6.jsというのがあるが、この中身を継承するので、envのes6: trueや、parserOptionsのecmaVersion: 6, sourceType: 'module', ...や、rulesの各項目が、自分のESLintの設定ファイル(.eslintrc.json)に書かれているのと同じ扱いになる。
※airbnb-baseの詳細についてはairbnb-baseの中身を参照。
今回は、eslintの方のrecommendedになっているルール(eslint:recommended)と、airbnb-base、後フォーマッタはprettierを使いたいのでprettierの3つを継承させる。
※prettierの使い方は今までの使われ方から含めてprettierについてに詳細を書いている。
eslint:recommendedeslintの推奨ルールなので設定に組み込む
airbnb-baseJavaScriptの静的解析ルールとして厳しいと言われており、これで書いておくと後から楽なので設定に組み込む
prettier(eslint-config-prettie)コードのフォーマッターであるprettierのルールにコードスタイルを統一するために設定に組み込む
以下でそれぞれについて少し詳細を見ていく。
参考:Extending Configuration Files
参考:Using a configuration from a plugin
eslint:recommendedの中身
ルールの中身全体はRulesに書かれている。
ソースコードとしてはeslint-recommended.jsがeslint:recommendedの定義をしているものと思われる。
※eslint:recommendedを始めとするeslint-config-*系の比較表としてはeslint-config-* の比較表が分かりやすいと思う。
airbnb-baseの中身
ルールの中身全体はAirbnb JavaScript Style Guideに書かれている。
ソースコードとしては、eslint-config-airbnb-baseのrulesフォルダ内のものになる(extendsの項で説明した通り)。
※ちなみに、より具体的にルールを見ていくと、『関数の引数にそのまま値を代入』の方はエラーになるが、Expressのmiddlewareを定義の場合はエラーにならない。これは、https://github.com/airbnb/javascript/blob/master/packages/eslint-config-airbnb-base/rules/best-practices.js#L226 の設定のおかげ。
関数の引数にそのまま値を代入
const foo = (bar) => {
bar = 'text';
return bar;
};
foo();
expressのmiddlewareの例
app.use((req, res, next) => {
req.time = Date.now();
next();
});
prettierについて
コードフォーマッターのルールに沿っているか?も静的解析でチェックをしたいのでprettierをextendsに入れる。
ESLintにprettierを入れるという事になると、ググってよく出てくるのはeslint-plugin-prettierだが、Integrating with Lintersに書かれているように、『These are generally not recommended(一般に推奨されない)』となっている。
ただ、Integrating with Linters Notesの項に書かれている内容を読むと、but can be useful in certain circumstances(特定の状況では役に立つことがある)とも書かれている。
これはどういうことなのか?を考えてみると、例えば、eslint-plugin-prettier等だと、
・You end up with a lot of red squiggly lines in your editor, which gets annoying. Prettier is supposed to make you forget about formatting – and not be in your face about it!
・They are slower than running Prettier directly.
・They’re yet one layer of indirection where things may break.
というようなデメリットがあると書かれている。
この、
フォーマッターなのに赤い線がエディタに出てしまって気になってしまう(本来フォーマットの事は忘れるべきなのに)
prettierを直接実行するより遅い
という内容から、prettierとしては、基本的にはESLintとprettierは分けて使われるべきという考え方をしているのだろうと思われる。そのためeslint-plugin-prettierではなく、eslint-config-prettierを使って、prettierと衝突するようなESLintのルールをoffにしつつ、フォーマットの事で(ESLintがprettierのフォーマットルール違反を検知して)警告がでないような形でフォーマッティングする(普通にprettierのコマンドでフォーマッティングする)という考え方をしているのだろうと思われる。
確かに、eslint-config-prettierのみを入れてESLintにあるprettierと衝突するルールをoffにし、後は.prettierrcにフォーマットルールを書いてExtentionsのPrettierを入れておけば(または prettierのコマンドを実行すれば)、ESLintとprettierを分離してフォーマッティングを動かすのは可能。
がしかし、実際問題として、開発の現場ではprettierのコマンドを実行し忘れたり、VS CodeのExtentionsが入っていなかったり、でソースコードのフォーマットがバラバラになってしまうという事は起きうる。そう考えると、現実的にはESLintにフォーマットチェックのルール(prettier)も組み込まざる負えないのではないかと思う。その場合、eslint-config-prettierを使う事になる。
※個人的には、これが『but can be useful in certain circumstances(特定の状況では役に立つことがある)』の事なのかな?と思ったりしている。
ではどのように設定するかだが、まず大前提としてprettierのルールと衝突するESLintやその他のルールはoffにする必要がある。これはeslint-plugin-prettierのRecommended Configurationにも書かれている。
This plugin works best if you disable all other ESLint rules relating to code formatting, and only enable rules that detect potential bugs. (If another active ESLint rule disagrees with prettier about how code should be formatted, it will be impossible to avoid lint errors.) You can use eslint-config-prettier to disable all formatting-related ESLint rules.(このプラグインは、コードのフォーマットに関する他のESLintルールをすべて無効にし、潜在的なバグを検出するルールのみを有効にした場合に最適です。(他のアクティブなESLintルールがコードのフォーマットについてprettierと意見が合わない場合、lintエラーを回避することができません)。eslint-config-prettierを使って、フォーマットに関するESLintルールをすべて無効にすることができます。)
という事で、手順にある通りeslint-config-prettierを依存に追加する。
yarn add --dev eslint-config-prettier
ちなみに、このeslint-config-prettierの中身は、index.jsを見ると分かるが、単にいくつかのルールをoffにしているだけ。
続いて、eslint-plugin-prettierを依存に追加する。
yarn add --dev eslint-plugin-prettier prettier
あとは、eslint-plugin-prettierのRecommended Configurationに書かれている通り、extendsにplugin:prettier/recommendedを追記するだけ。
このplugin:prettier/recommendedは、公式にも書いてあるように、単に
{
"extends": ["prettier"],
"plugins": ["prettier"],
"rules": {
"prettier/prettier": "error",
"arrow-body-style": "off",
"prefer-arrow-callback": "off"
}
}
と同じ事を自分のESLintの設定に追加してくれるだけで、ソースコードで言えばeslint-plugin-prettier.jsがそれ。
これでESLintにprettierの設定を組み込んだ状態になり、以下のようにeslintコマンド時にprettierのエラーも出力されるようになる。
[root@localhost node-express]# yarn lint
yarn run v1.22.17
$ eslint src/*
/root/workspace/node-express/src/index.js
1:21 error Replace `"express"` with `'express'` prettier/prettier
5:9 error Replace `"/"` with `'/'` prettier/prettier
7:3 warning Unexpected console statement no-console
7:26 error Replace `"foo"` with `'foo'` prettier/prettier
11:15 error Replace `"sleep"` with `'sleep'` prettier/prettier
16:10 error Replace `"hello·world!"` with `'hello·world!'` prettier/prettier
21:24 warning Unexpected console statement no-console
21:36 error Replace `"listening·on·port·3000!"` with `'listening·on·port·3000!'` prettier/prettier
✖ 8 problems (6 errors, 2 warnings)
6 errors and 0 warnings potentially fixable with the `--fix` option.
※prettierの設定やESLintの設定全体は結論:ESlintの設定内容とprettierの設定内容の項を参照。
※ちなみに、eslint-plugin-prettierだと赤線が出るというが確かに以下のように赤線が出る...。2
しかし、基本的にVS Codeであれば、設定でソースの保存時には自動でフォーマッティングを走らせることができるので、新規で自分がコードを書いている時に赤線が出る以外は基本でないのでそこまで煩わしくもないと思う。
※このESLint・prettierを使う時のVS Codeの設定については、VS Codeの設定を参照。
※ESLintとprettierを分けて考える場合には(prettierとしてはこれが推奨)、eslint-config-prettierをextendsの末尾に追記し、prettierのフォーマッティングルールを.prettierrcに記載し、後は以下のようなコマンドを実行する(以下はprettier本体のpackage.jsonを参考に書いたもの)。
package.json
"scripts": {
"fix": "run-s fix:eslint fix:prettier",
"fix:eslint": "eslint --fix",
"fix:prettier": "prettier --write",
}
参考:How To Set Up ESLint & Prettier In VS Code
Prettier と ESLint の組み合わせの公式推奨が変わり plugin が不要になった
parser
ググってみると、babelを使っていると@babel/eslint-parserを導入して、babelの独自の書き方をESLintで解釈できるようにする必要がありそうな雰囲気を感じるが、注意書きに
Note: You only need to use @babel/eslint-parser if you are using Babel to transform your code. If this is not the case, please use the relevant parser for your chosen flavor of ECMAScript (note that the default parser supports all non-experimental syntax as well as JSX).
と書かれているように、Babelでコードを変換している時にのみ必要となっているので今回は不要。
※確かにwebpackにbabel-loaderを組み込んでbuildしているが、それはESLintのチェック対象のコードではない(build前のコードを静的解析してくれないと意味ない)ので不要と思っている。
※@babel/eslint-parserは昔はbabel-eslintというライブラリだった。
参考:Specifying Parser Options
VS Codeの設定
今回はESLintの中でprettierのフォーマットルールに則っているか?のチェックもしているので、source.fixAll.eslintの設定+ ExtentionsのESLintの追加のみでOK(prettierはpackage.jsonに追加しているのでそれが動く)。
その場合のsetting.jsonは以下のようになる。
./.vscode/setting.json
{
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true,
}
}
※ただ、この状態だと、
ESLintが走るファイル以外(jsonなど)でprettierのフォーマッティングができない
といった事があるので、editor.formatOnSaveの設定+ExtentionsのPrettierを入れるという事も一つの手に思える。この場合、ソースコードの保存時にどちらが先に走るのか?が分からないのと、思わぬ副作用があるかもしれないので注意は必要かもしれない。
上記の方法でやる場合、PrettierのExtentionsを入れて、"editor.formatOnSave": trueを追記したsetting.jsonは以下のようになる(editor.defaultFormatterを設定しているが、ESLintのフォーマットチェックも同じprettierなのでこれの設定が衝突する事はないと思っている)。
両方書いた場合
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
}
}
今回は他のjsonなどでもprettierでフォーマッティングしたかったので、editor.formatOnSaveを追記した形で設定した。
※ルールの設定などをしていて気づいたが、VS CodeのExtentionsでESLintを入れて、コマンドではなくそのExtentionsを介してsource.fixAll.eslintでフォーマッティングを走らせている時に、.prettierrcの設定内容を変えてソースコードを保存しても変更後のルールでフォーマッティングされなかった...。しょうがないので.prettierrcを変えたら再起動するとちゃんとsource.fixAll.eslintでフォーマッティングが自動修正されるようになった。
VS Code のExtentions
上記の設定で入れたもの以外でも有効そうなものを列挙すると、以下のようなExtentionsは入れておくといいかもしれない。
ESLint
Prettier
Git History
eslint-webpack-plugines(元々はeslint-loaderだった)を使う ↩
extendsにplugin:prettier/recommendedを記載.prettierrcに{ "singleQuote": true }を記載 ↩
↧