webpack で実行可能な単一ファイルを作る

node.js で cli ツールを作った時に 簡単に実行する方法を考えてみました。 nexe を使う方法もありますが、ファイルサイズが大きくなるので javascript のコードのみまとめる事を想定します。

方針としては

  • ライブラリも使いたいので webpack で単一ファイルにまとめる
  • まとまったファイルに shebang をつける
  • ファイルを path の通った所に置いて実行権限をつける

て感じです。 これを node.js で実現するには以下をやります。

  • webpack で単一ファイルにまとめる
  • webpack.BannerPlugin で shebang を追加する
  • ファイルを path の通った所に置いて実行権限をつける

実行するスクリプトを作成する

いつも通り node.js の package を作ります。

$ mkdir webpack-test && cd webpack-test
$ npm init -y
$ npm install --save lodash
$ mkdir src
$ vim src/index.js

ソースを src/index.js に書いたのは後で使う webpack のデフォルト設定に合わせるためです。 src/index.js の内容は webpack の Getting Started を 参考に、こんな感じで。

const _ = require('lodash');
const main = () => {
   const message = _.join(['hello', 'world'], ' ');
   console.log(message);
};
main();

試しに実行してみます。

$ node index.js
hello world

表示されました。

webpack で単一ファイルにまとめる

webpack をインストールして実行してみます。

$ npm install --save-dev webpack webpack-cli
$ npx webpack
...
$ node ./dist/main.js
hello world

./dist/main.js が webpack によって出力されたファイルです。 中身を見ると、lodash もこのファイルに入っている事が分かります。 これで 単一ファイルにはできましたが、 実行可能にするには shebang を追加する必要があります。

実行可能にする

shebang を追加するための webpack の設定をします。 以下のようにコマンドラインでやってもいいのですが、

$ echo '#!/usr/bin/env node' | cat - ./dist/main.js > ./dist/main

今回は webpack の BannerPlugin を使い、さらにファイルに実行権限を追加するところまで自動でやります。 webpack.config.js を作り、以下のようにします。

const webpack = require('webpack');

module.exports = {
    mode: 'production', // to minify
    target: 'node',
    plugins: [
        new webpack.BannerPlugin({
            banner: '#!/usr/bin/env node', // add shebang
            raw: true,
        }),
        function() {
            this.hooks.done.tap('chmod', () => {
                fs.chmodSync(path.resolve(__dirname, 'dist', 'main.js'), '755');
            });
        },
    ],
};

もう一度 webpack を実行し、 直接実行できるか試してみます。

$ npx webpack
$ ./dist/main.js
hello world

実行できました。 あとはこのファイルを PATH の通った所に配置し、 ファイル名を適当に変えれば OK です。

補足: hook の書き方

chmod する所は最初は

function() {
    this.plugin('done', () => {
        fs.chmodSync(path.resolve(__dirname, 'dist', 'main.js'), '755');
    });
},

と書いていたんですが、

$ nps webpack
DeprecationWarning: Tapable.plugin is deprecated. Use new API on `.hooks` instead

のようなメッセージが出たので調べたところ、 最初の例の書き方に変わったみたいです。

バージョン

$ node -v
v10.13.0
$ npx webpack -v
4.26.1