詩と創作・思索のひろば

ドキドキギュンギュンダイアリーです!!!

Fork me on GitHub

Makefileの代わりにnpm scripts+zxを使う

そこそこの規模があるプロジェクトで実行すべきタスクを定義するとき、初手として Makefile を使いがち。

  • Pros
    • make は事実上どんな環境にもあることを期待してよい
    • シェルで実行されるコマンドをそのまま書ける
    • タスクの依存関係が明示できる
  • Cons
    • make では positional arguments が使えない
    • 少し複雑なことをしようとすると Makefile 専用の文法を覚える必要がある
    • 現代では、ファイルベースのタスクの依存関係は make が発明されたころほどは必要ではない
      • Docker とか Go とか Webpack がよしなにしてくれることが多い

例: docker compose のラッパー

ちょっとしたコマンドのラッパーを書きたいことがある。Makefile を書きはじめたらすべてのエントリポイントを make にしたい。ということで、以下のような Makefile を発明していた。docker compose のラッパーで、ステージを指定すると docker-compose.*.yaml も読み込んでくれるようなやつだ。

が、make compose STAGE=test COMPOSE_COMMAND='run --rm app go test' と名前付きパラメタとして渡さなきゃいけないのが面倒。

# Makefile
COMPOSE_COMMAND =
DOCKER_COMPOSE_BIN = docker compose
DOCKER_COMPOSE = $(if $(STAGE),$(DOCKER_COMPOSE_BIN) -f docker-compose.yaml -f docker-compose.$(STAGE).yaml,$(DOCKER_COMPOSE_BIN))

.PHONY: compose
compose:
     $(DOCKER_COMPOSE) $(COMPOSE_COMMAND)

google/zx を使う

そもそも make への引数を渡しかた自体あまり自明じゃない気もするし、そろそろ別のものにしてもいいなってことで代替を考える。最近は JavaScript(Node)がユニバーサルなのでまあこれでいいよね。個人的には JS がコードベースに含まれないプロジェクトでもビルドスクリプトのために Node を導入してもいいと思ってる。

代替として make の利点、「シェルコマンドをそのまま書ける」を潰さないように google/zx を使うことにする。シェルスクリプトっぽく JS を書くためのライブラリ集とラッパーコマンドという感じ。

GitHub - google/zx: A tool for writing better scripts

これのインストールのために npm/yarn を使うわけなので、エントリポイントは npm scripts にしてしまおう。自動的に positional arguments も使えることになる。

複雑なものは scripts/ 以下に

package.json に複雑なコマンドを書くことは不可能なので、リポジトリの scripts ディレクトリなどに zx 向けのコマンドを置くことになる。愚直に書くならこんな感じかな。$ でコマンドを実行できたり、トップレベルで await が使えたり、argv が最初から提供されていたりと書きやすい。

#!/usr/bin/env -S npx zx
// vim: set ft=javascript:

const docker_compose =
  argv.stage
    ? [
        "docker",
        "compose",
        "-f",
        "docker-compose.yaml",
        "-f",
        `docker-compose.${argv.stage}.yaml`,
      ]
    : ["docker", "compose"];

await $`${docker_compose} ${argv._.slice(1)}`;

(ちなみにこれはほんとうに初期のバージョンで、docker compose に渡したいフラグが minimist に食われないようにしたり、TTY が必要なコマンドを実行したり、みたいなことを考えると zx ではなく普通に node のスクリプトを書くことになる……。)

これを package.json に書いちゃえばいい。

{
  "scripts": {
    "compose": "zx ./scripts/compose"
  }
}
% yarn compose --stage=test ps
$ zx ./scripts/compose --stage=test ps # yarn の出力
$ docker compose -f docker-compose.yaml -f docker-compose.test.yaml ps # zx の出力
...

ちなみに #!/usr/bin/env -S npx zx でファイルを直接起動することもできるようになる。

細かいのはひとつのファイルにまとめる

まあこれでやりたいことは満たせるわけだけど、シェルスクリプトで2行くらいのものをひとつひとつファイルにするものナー。そういう場合はひとつのファイルにまとめてしまえばいい。zx にはなぜか Markdown に書かれたスクリプトを実行する機能があるので、scripts/README.md などにこんなことを書いてしまう。

# scripts

いろんなタスクをまとめたファイルだよ~

```javascript
const tasks = {};
```

## test

テストを実行するよ~

```javascript
tasks["test"] = async () => {
  await $`go test ./...`;
}
```

## おわりに

```javascript
const task = tasks[argv._[1]];
if (!task) {
  // タスクがなかったら一覧を表示してやる
  await $`cat ${__filename} | sed 's/^## //p;d'`;
} else {
  await task();
}
```

こんなコードを書いとくと、zx ./scripts/README.md test などでテストが実行できてしまう。package.json にはコメントを書きづらいので、ここに説明が書けるのも便利だ。

はてなで一緒に働きませんか?