Node.jsのトランスパイル時にESLintでエラーがあればBuild停止する設定をWebpackでやってみた

はじめに

こんにちは、SHIFT の開発部門に所属している Katayama です。

Node.jsをES6の構文で実装するために、Webpackを用いてトランスパイル(build)しているという事はあると思います。そしてESLintを静的解析ツールとして導入している場合、eslintコマンドで静的解析を実行した結果エラーが出るのであれば、そのコードはwebpackコマンドでbuildするのは望ましくありません。そこで、今回は ESLint が設定済みの時に、webpack のトランスパイル(Build)時に ESLint を実行しエラーがあれば build を止める設定をやってみました。

また、おまけとして webpack の全体設定について、一部その内容を紹介しています。

※なお、今回設定を行うプロジェクトは、Node.js で ES Modules を利用して実装しているものになります。Node.js で ES Modules を利用するための設定についてはNode.js で import・export(ES6 の構文)を使えるように webpack × Babel の設定をやってみたを参照ください。

webpack の設定

結論:どうすればいいか?

// ./webpack.config.js
const ESLintPlugin = require("eslint-webpack-plugin");

module.exports = {
  // 省略(全体の設定については『おまけ』の項を参照
  plugins: [new ESLintPlugin({ exclude: "node_modules" })],
};

設定について

まず、pluginsとは、build(バンドル)に関する事以外にも幅広い処理を実行させるための設定をするオプション。今回は webpack の build 中に ESLint を使い、エラーがあれば build を止める設定をしている。

今まではeslint-loaderという loader で ESLint のチェックを実行する仕組みだったようが、これは非推奨になったのでeslint-webpack-pluginを使う。

使い方は難しくなく、ESLint の設定(.eslintrc.json など)を作成しておけばそのルールを読み取り、webpack の build 中にソースのチェックをしてくれる。

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

ESLint の設定についてはNode.js(ES6で実装)におけるESLint・Prettierの設定を1からやってみたを参照。

・参考:webpack で ESLint が使える環境を構築してみる

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

webpack で build する中で ESLint を実行させる(eslint-webpack-plugines(元々はeslint-loaderだった)を使う)が、ESLint が走るコードが babel-loader でトランスパイルされた後のコードなのか?その前のコードなのか?という疑問が出るだろう。調べるために実際に ESLint の Extentions を VS Code に入れて ESLint を走らせると、トランスパイル前のコードにエラーが表示される。

この動きから、ESLint のエラーがソースコード上に出るのは、トランスパイルされた後のコードではなくトランスパイル前のコードに対し ESLint が実行されているからなのではと想像している(間違っていたらご指摘下さい)。

ちなみに、今までの eslint-loader では、"enforce: 'pre'"を付けていた場合、babel-loader と併用していれば babel-loader で変換する前にコードを静的解析するという設定になる。

・参考:webpack の基本的な使い方#eslint-loader

まとめとして

今回は webpack を使っている時に、その build を実行する中で ESLint を実行させるための設定についてみてきた。この設定をしておく事で、CI 上で ESLint のエラーがあれば build が止まるので品質の向上にも役立つのではないかと思う。

おまけ

以下のような webpack の設定について、その内容について見てみる。

// ./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" })],
};

上記の設定の概要としては、

・mode で development と production の切り替えができるようにする
・webpack を実行(build を実行)した後に出力されるファイルの出力先をdistディレクトリ以下にし、そのファイル名はentryのindexという名前になるようにする
・node_modules のバンドルをしないようにする

というもの。設定の詳細やその設定の理由については以下で 1 つずつ見ていく。

name

configuration の名前(なくても webpack は動く)。今回は"node-express"という名前を付けている。

mode

webpack をどのモードで行うか?(バンドル ≒build のモード)の設定。productionモードでは実行時に最も早く動くコードをbuild後に出力するが、development時にはソースコードを追える状態にしたいという事があり、モード切替をする。

development モードは、devtool: 'source-map'と共に使われ、ソースコードの圧縮が行われず可読性のある形で build される。

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

node-envと併用する形で、この mode を設定する書き方が多くみられる。

// 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
 */
...
// webpackで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

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

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


_________________________________

執筆者プロフィール:Katayama Yuta
SaaS ERPパッケージベンダーにて開発を2年経験。 SHIFTでは、GUIテストの自動化やUnitテストの実装などテスト関係の案件に従事したり、DevOpsの一環でCICD導入支援をする案件にも従事。 最近開発部門へ異動し、再び開発エンジニアに。座学で読み物を読むより、色々手を動かして試したり学んだりするのが好きなタイプ。

お問合せはお気軽に
https://service.shiftinc.jp/contact/

SHIFTについて(コーポレートサイト)
https://www.shiftinc.jp/

SHIFTのサービスについて(サービスサイト)
https://service.shiftinc.jp/

SHIFTの導入事例
https://service.shiftinc.jp/case/

お役立ち資料はこちら
https://service.shiftinc.jp/resources/

SHIFTの採用情報はこちら
https://recruit.shiftinc.jp/career/