かれこれ 5 年くらい趣味開発で npm-scripts を書き続けている。長年書き続けているとノウハウが蓄積されてきて、「こう書くとスッキリする」「迷いがなくなる」「後から拡張したくなった時に、簡単に拡張できる」みたいな書き方が身についてきた。自分の型、あるいは手癖のようなものだと思う。
せっかくなので、
id:mizdra の今の npm-scripts を書く時の手癖を書き連ねてみる。
基本形
{ "scripts": { "build": "webpack --mode production", "dev": "webpack-dev-server --mode development", "lint": "eslint .", "test": "jest" } }
一番シンプルな npm-scripts を書く時のパターン。以下の 4 つの script を登録している。
build: 本番ビルドを実行するdev: 開発時に使うビルドモードを立ち上げる- webpack を使ったプロジェクトなら開発サーバを watch ビルドで立ち上げたり、tsc を使ったプロジェクトなら
tsc --watchしたり。
- webpack を使ったプロジェクトなら開発サーバを watch ビルドで立ち上げたり、tsc を使ったプロジェクトなら
lint: lint や型検査など、静的解析を実行する- 静的解析だったら
static-checkが良い気もするけど、長くて type しづらいので避けた checkにしていた時代もあったが、yarn の組み込みコマンドと競合して辛かったのでやめた- ref: yarn check | Yarn
- 界隈的にも lint と呼んでいることのほうが多そうだったので、そっちに合わせている
- 静的解析だったら
test: unit テストを実行する- unit テストだけを扱う
- E2E テストは別 script に逃がす (後述)
趣味開発しているプロジェクトには、基本的にこの 4 つの script を登録するようにしている。どのプロジェクトを触っても、npm i && npm run devすれば開発ビルドが即立ち上がるし、npm run lintすれば静的解析が走る。全プロジェクトで script 名を統一することで、「このプロジェクトではなんて名前の script だっけ?」と迷わないようにしている。
これが基本形で、ここからプロジェクトでやりたいことが広がっていくのに対応して、npm-scripts も拡張していく。
lint や dev で複数コマンドを実行する
「eslintだけでなくtsc --noEmitも実行したい」など、それぞれの基本 script の中で複数コマンドを実行したくなったときは、以下のように拡張する。
{
"scripts": {
"build": "webpack --mode production",
- "dev": "webpack-dev-server --mode development",
+ "dev": "run-p dev:*",
+ "dev:webpack": "webpack-dev-server --mode development",
+ "dev:graphql": "graphql-codegen --watch",
+ "dev:tsm": "typed-scss-modules src --nameFormat none --exportType default --watch"
- "lint": "eslint .",
+ "lint": "run-s -c lint:*",
+ "lint:eslint": "eslint .",
+ "lint:tsc": "tsc --noEmit",
+ "lint:prettier": "prettier --check .",
"test": "jest"
}
}
以下ポイント:
- コマンド 1 つにつき 1 つの script (
lint:eslint/lint:tscなど) を割り当てる- 「開発時に eslint だけ実行しつつデバッグしたい」みたいな需要に答えるため
lint:<name>の<name>の部分には、コマンド名とかそれっぽい名前を割り当てておく
- まとめて実行する script (
lint) も用意する- lint などに関する script を手軽に全部実行できるよう、用意しておく
- script をまとめるのには
npm-run-allを使う- 2024-10-07追記: npm-run-all は現在メンテナンスされていない。代わりにその fork の npm-run-all2 を使うのがオススメ。
run-s lint:*と書けば、lint:*にマッチする script を全部実行できるrun-s lint:eslint lint:tsc lint:prettierと手動でリストアップする形にすれば実行順を制御できるが、いつか抜け漏れが出るので極力避ける
- 直列版の
run-sと並列版のrun-pが用意されているけど、基本的にはrun-sを使っているrun-pにして並列にすると、複数コマンドの出力が入り乱れてデバッグ困難になるため- 直列で実行してストレスに感じる規模のコードは
id:mizdra は普段触らないので、特に困っていない devscript など並列で実行する必要のあるものについては、素直にrun-pを使う
run-sを使うときは-cオプションを付ける- デフォルトでは script を 1 つずつ実行していって、エラーになったら即時中断する挙動になっている
lint:eslint => lint:tsc => lint:prettierという順序で実行される時に、lint:tscでエラーになったらlint:prettierは実行されない
- これだと一部の静的解析の結果が欠損してしまい、効率的にデバッグできない
-cを付けたら、エラーが見つかっても全ての script を実行した上で、エラーが返される挙動になるので、全ての静的解析が実行される
- デフォルトでは script を 1 つずつ実行していって、エラーになったら即時中断する挙動になっている
run-s/run-pに-lオプションは付けない
- CI からはまとめ上げられた script を実行するよう整理する
どうしても lint を並列で実行したくなったら、npx run-p "lint:*" / yarn run-p "lint:*" とターミナルから打てば良いと思う。
lint や test の亜種を用意する
「eslint --fixやprettier --write的なことをするlint script の亜種が欲しい」「watch モードで起動するtestscript の亜種が欲しい」みたいな需要が出てきたときは、以下のように拡張する。
{
"scripts": {
"build": "webpack --mode production",
"dev": "run-p dev:*",
"dev:webpack": "webpack-dev-server --mode development",
"dev:graphql": "graphql-codegen --watch",
"dev:tsm": "typed-scss-modules src --nameFormat none --exportType default --watch"
"lint": "run-s -c lint:*",
"lint:eslint": "eslint .",
"lint:tsc": "tsc --noEmit",
"lint:prettier": "prettier --check .",
+ "lint-fix": "run-s -c lint-fix:*",
+ "lint-fix:eslint": "npm run lint:eslint -- --fix",
+ "lint-fix:prettier": "prettier --write .",
"test": "jest",
+ "test-watch": "npm run test -- --watch"
}
}
以下ポイント:
<拡張したいscript>-<postfix>という名前の script を生やすlintの自動修正版ならlint-fix、testの watch モード版ならtest-watch- 界隈では
lint:fixというパターンも一般的だが、"lint": "run-s -c lint:*"に含まれてしまうので避けている
- 亜種の script とオリジナルの script で共通化できれば共通化する
"lint-fix:eslint": "npm run lint:eslint -- --fix"みたいな感じ- prettier はオプションの都合上共通化が難しいのでやらない。無理せず簡単なほうに倒す。
余談
亜種を用意すると npm-scripts の数が膨大になって見通しが悪くなったり、管理が行き届かなくなるので、趣味では基本的にやらないようにしている。jestを watch モードで起動するならnpm run test -- --watch、みたいなことは
id:mizdra は空で書けるので、別にわざわざ script として用意していない。チームで開発している時などは用意しておくと丁寧だと思う。
コード生成用の script を用意する
GraphQL Code Generator などコード生成系のツールを導入したくなったら、以下のように拡張する。
{
"scripts": {
+ "gen": "run-s gen:*"
+ "gen:graphql": "graphql-codegen",
+ "gen:tsm": "typed-scss-modules src --nameFormat none --exportType default",
"prebuild": "npm run gen"
"build": "webpack --mode production",
"dev": "run-p dev:*",
"dev:webpack": "webpack-dev-server --mode development",
- "dev:graphql": "graphql-codegen --watch",
- "dev:tsm": "typed-scss-modules src --nameFormat none --exportType default --watch"
+ "dev:graphql": "npm run gen:graphql -- --watch",
+ "dev:tsm": "npm run gen:tsm -- --watch",
"lint": "run-s -c lint:*",
"lint:eslint": "eslint .",
"lint:tsc": "tsc --noEmit",
"lint:prettier": "prettier --check .",
"lint-fix": "run-s -c lint-fix:*",
"lint-fix:eslint": "npm run lint:eslint -- --fix",
"lint-fix:prettier": "prettier --write .",
"test": "jest",
"test-watch": "npm run test -- --watch"
}
}
以下ポイント:
genという script を生やす- もちろん generate の略
- 目で見て分かれば何でも良い
dev:*と共通化できることが多いので適当に共通化しておくbuildする際にgenが実行されるよう、prebuildscript を追加しておく- 参考: https://www.nxworld.net/npm-scripts-pre-and-post-hooks.html
genしないとbuildできないので、prebuildで実行されるようにしておくのが妥当
Unit テスト向けの型検査 script を用意する
アプリケーションコードと Unit テストのコードを、それぞれ別々のtsconfig.jsonを使って型検査したくなった時の話。つまりlint:tscを分割したい。
id:mizdra の場合は以下のように拡張している。
{
"scripts": {
"gen": "run-s gen:*"
"gen:graphql": "graphql-codegen",
"gen:tsm": "typed-scss-modules src --nameFormat none --exportType default",
"prebuild": "npm run gen"
"build": "webpack --mode production",
"dev": "run-p dev:*",
"dev:webpack": "webpack-dev-server --mode development",
"dev:graphql": "npm run gen:graphql -- --watch",
"dev:tsm": "npm run gen:tsm -- --watch",
"lint": "run-s -c lint:*",
"lint:eslint": "eslint .",
- "lint:tsc": "tsc --noEmit",
+ "lint:tsc": "run-s -c lint:tsc:*",
+ "lint:tsc:src": "tsc -p tsconfig.src.json --noEmit",
+ "lint:tsc:test": "tsc -p tsconfig.test.json --noEmit",
"lint:prettier": "prettier --check .",
"lint-fix": "run-s -c lint-fix:*",
"lint-fix:eslint": "npm run lint:eslint -- --fix",
"lint-fix:prettier": "prettier --write .",
"test": "jest",
"test-watch": "npm run test -- --watch"
}
}
以下ポイント:
lint:tsc:srcとlint:tsc:testに分割するlint:tscでまとめる
余談
tsconfig.jsonではなくtsconfig.src.json/tsconfig.test.jsonといったファイル名にしてしまうと、tsserver (VSCode で利用されている TypeScript の Language Server) から設定ファイルが検出できなくなり、エディタ上のリアルタイムの型検査がデフォルトの設定で実行されてしまうので注意すること。補完がいつの間にかおかしくなったら、これを疑うと良い。tsserver に設定ファイルが読み込まれているのかは、VSCode の下部にあるツールバーから確認できる。

じゃあtsconfig.jsonにしないといけないのかというと、そうでもない。Solution Style tsconfig.json というものを用意すれば、好きな名前の設定ファイルを tsserver に読み込ませることができる。
{ // files は必ず空にする "files": [], "references": [ // ここに tsserver から参照してほしい設定ファイルのパスを書いていく { "path": "./tsconfig.src.json" }, { "path": "./tsconfig.test.json" } ] }

便利なテクニックだなと思ってよく使ってるけど、
id:mizdra 以外で使っている人見たことない。知られていないだけなのか、そもそもこんな複雑なこと皆やらないだけなのか… 皆さん使ってますか?
E2E テスト用の script を用意する
E2E テストを導入したくなったら、以下のように拡張する。
{
"scripts": {
"gen": "run-s gen:*"
"gen:graphql": "graphql-codegen",
"gen:tsm": "typed-scss-modules src --nameFormat none --exportType default",
"prebuild": "npm run gen"
"build": "webpack --mode production",
"dev": "run-p dev:*",
"dev:webpack": "webpack-dev-server --mode development",
"dev:graphql": "npm run gen:graphql -- --watch",
"dev:tsm": "npm run gen:tsm -- --watch",
"lint": "run-s -c lint:*",
"lint:eslint": "eslint .",
"lint:tsc": "run-s -c lint:tsc:*",
"lint:tsc:src": "tsc -p tsconfig.src.json --noEmit",
"lint:tsc:test": "tsc -p tsconfig.test.json --noEmit",
"lint:prettier": "prettier --check .",
"lint-fix": "run-s -c lint-fix:*",
"lint-fix:eslint": "npm run lint:eslint -- --fix",
"lint-fix:prettier": "prettier --write .",
- "test": "jest",
+ "test": "jest -c jest.test.js",
"test-watch": "npm run test -- --watch",
+ "e2e-test": "jest -c jest.e2e-test.js",
+ "e2e-test-watch": "jest -c jest.e2e-test.js --watch",
}
}
以下ポイント:
testとは別にe2-testを用意するtestやtest-watchで時間の掛かるテストが走ってほしくないため- E2E テストは結構重いので、これが
test-watchなどで走ってしまうと、デバッグ体験が悪い - オプトアウトしておいて、必要に応じて
e2e-testやe2e-test-watchしてもらう、という世界観 - vscode-jest と組み合わせるときも、このように分かれていたほうが扱いやすいという話もある
- vscode-jest については以前記事書いたので読んでください
"jest.jestCommandLine": "npm run test --"で unit テストだけを vscode-jest の実行対象にできる
事前にビルドしてからでないと実行できない script を用意する
さっきさらっと E2E テストの script こんな感じですと紹介したけど、普通は事前にビルドが必要なので、あのままだと動かないと思う。他にもビルドした結果を元に lint したいといった、事前にビルドしてからでないと実行できない script が出てくる状況がたまにある。もしそのようなものが出てきたら、以下のように拡張する。
{
"scripts": {
"gen": "run-s gen:*"
"gen:graphql": "graphql-codegen",
"gen:tsm": "typed-scss-modules src --nameFormat none --exportType default",
"prebuild": "npm run gen"
"build": "webpack --mode production",
+ "postbuild": "run-s -c postbuild:*",
+ "postbuild:e2e-test": "jest -c jest.e2e-test.js",
+ "postbuild:lint": "bundlesize",
"dev": "run-p dev:*",
"dev:webpack": "webpack-dev-server --mode development",
"dev:graphql": "npm run gen:graphql -- --watch",
"dev:tsm": "npm run gen:tsm -- --watch",
"lint": "run-s -c lint:*",
"lint:eslint": "eslint .",
"lint:tsc": "run-s -c lint:tsc:*",
"lint:tsc:src": "tsc -p tsconfig.src.json --noEmit",
"lint:tsc:test": "tsc -p tsconfig.test.json --noEmit",
"lint:prettier": "prettier --check .",
"lint-fix": "run-s -c lint-fix:*",
"lint-fix:eslint": "npm run lint:eslint -- --fix",
"lint-fix:prettier": "prettier --write .",
"test": "jest -c jest.test.js",
"test-watch": "jest --watch",
- "e2e-test": "jest -c jest.e2e-test.js",
- "e2e-test-watch": "jest -c jest.e2e-test.js --watch",
}
}
以下ポイント:
postbuild:という postfix を付けた script を生やすnpm run build && npm run postbuild:e2e-testでワンセットという世界観
postbuildも用意しておくnpm run buildした時に自動で実行される
今の所こういう手癖で書いてるけど、npm run buildで自動で実行されてしまうのがちょっとイマイチだなと思っている。postbuild:lintはまあ許容できるけど、postbuild:e2e-testも毎回走るのはどうなのだろう、みたいな。デプロイ用と lint/test 用の GitHub Workflow がそれぞれあった時に、本来であれば lint/test 用の workflow でだけ postbuild:e2e-test が実行されてほしいのに、デプロイ用の workflow でも buildする都合上 postbuild:e2e-test が実行されてしまうはず。
事前ビルドが必須だとよわかる名前になっていれば良いので、require-build:e2e-testみたいな名前にしたほうが良いのかも。良いアイデア持ってる人が居たら教えてほしい。
コマンドの繋ぎ合わせでは表現が難しい script を用意する
プログラマブルなことしたくなったら、以下のようにスクリプトファイルに追いやれば良い。
{ "scripts": { "dev": "node scripts/dev.js" } }
難しいことをしたくなったら歯を食いしばってスクリプトファイルを書いていけば良いと思う。銀の弾丸はない。
実例
せっかくなのでいくつか実例も紹介。歴史的経緯により、これまで紹介した手癖に沿っていない書き方が多々あるのが気になるかも。目を瞑ってください。
- mizdra/eslint-interactive
- Node.js で動く CLI ツール
postbuild:testとpostbuild:benchmarkがあるのが見どころ
- mizdra/now-playing-for-google-play-music
- Youtube Music で NowPlaying ツイートを可能にするツール
- デスクトップ向けのブラウザ拡張機能と、Android 向けの PWA が用意されていて、同じリポジトリで開発してる
- ブラウザ拡張機能向けの script と PWA 向けの script が混在しているのが見どころ
- monorepo にして npm/yarn workspace 導入したほうがスッキリするとは思う (けど面倒だった)
- まあ趣味なのでこれくらい雑で良いという感覚
- あとブラウザ拡張機能のストアにアップロードする用の zip を作る script があるのも見どころ
- mizdra/scrapbox-userscript-icon-suggestion
- Scrapbox で動く UserScript
devscript をスクリプトファイルに切り出しているのが見どころ
皆さんも自慢の手癖があれば教えて下さい。