株式会社はてなに入社しました
npm-scripts を書く時の手癖
かれこれ 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 を使う
run-s lint:*
と書けば、lint:*
にマッチする script を全部実行できるrun-s lint:eslint lint:tsc lint:prettier
と手動でリストアップする形にすれば実行順を制御できるが、いつか抜け漏れが出るので極力避ける
- 直列版の
run-s
と並列版のrun-p
が用意されているけど、基本的にはrun-s
を使っているrun-p
にして並列にすると、複数コマンドの出力が入り乱れてデバッグ困難になるため- 直列で実行してストレスに感じる規模のコードは id:mizdra は普段触らないので、特に困っていない
dev
script など並列で実行する必要のあるものについては、素直に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 モードで起動するtest
script の亜種が欲しい」みたいな需要が出てきたときは、以下のように拡張する。
{ "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
が実行されるよう、prebuild
script を追加しておく- 参考: npm-scripts:pre・postプレフィックスを利用して、スクリプト実行前後に別のスクリプトも実行させる方法 - NxWorld
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
dev
script をスクリプトファイルに切り出しているのが見どころ
皆さんも自慢の手癖があれば教えて下さい。
ドッキングステーション導入した
Anker の Thunderbolt 4 対応のドッキングステーションを導入したので、その感想とか。
導入以前の配線状況
元々 4K の外付けディスプレイ (Dell U2720Q) 2枚使って、それを Thunderbolt 3 ケーブルで Mac に繋ぐような構成だった。
+---------+ +----------------+ | Speaker | | Magic Trackpad | +---------+ +----------------+ ^ ^ | | | 3.5mm Audio Cable | Lightning Cable | | | | | | +-------+-------+ +-------+-------+ | | | | | Dell U2720Q |<------------+ +------------>| Dell U2720Q | | | | | | | +---------------+ | | +---------------+ | | | | Type-C, Thunderbolt 3 | | Type-C, Thunderbolt 3 | | | | +---+--------+--+ +---------------+ | MacBook Pro | | MacBook Pro | | 14-inch, 2021 | OR | 16-inch, 2019 | +---------------+ +---------------+
常に Mac から 2 本ケーブルが生えているのが邪魔だったり、私用/社用 Mac 切替時にケーブル差し替えるのが手間というのはあったけど、そこまで不便なく暮らせていた。ただ、最近卓上にデバイスが色々増えてきて (Stream Deck や Shure MV7)、卓上がごちゃごちゃして困っていた。
ドッキングステーションの選定
という訳で、ドッキングステーションを購入することにした。要件や希望は以下の通り。
- Dell U2720Q x 2枚 がホストデバイスから 1本のケーブルで繋がること
- ディスプレイは Thunderbolt 3 で繋いでいるので、最低でも Thunderbolt 3 のポートが 2 つ生えているものを買う必要がある
- できればサイズは小さいと嬉しい
- 大きいと取り回ししづらいので
- 価格も高すぎないと嬉しい
- ハイエンドじゃなくて良い
- できれば色々ポートが付いていると嬉しい
- イーサネットポート、USB Type-A とか
色々探してみて、上 3 つの要件を満たす「Anker PowerExpand 5-in-1 Thunderbolt 4 Mini Dock」を買った。
Thunderbolt 3 で十分そうだけど、数字は大きいほうが良いよねということで Thunderbolt 4 対応のものにした。あといざ買ってみたら、デイジーチェーン周りで複数デバイスを数珠繋ぎにしたら認識されなかったりしたら怖いので、そういうのがケアされてそうな最新規格のものが欲しかった、という背景もある。最小限のポートしか付いていないのが当初の希望から外れてしまうけど、よくよく考えたら Dell U2720Q に沢山ポートが付いているので、そっちを今まで通り使えば良いなという判断になった。Dell U2720Q にイーサネットポートは生えていないけど、イーサネットポート から USB Type-A に変換するケーブルをどこかに繋ぐとかそういう運用で良さそう。
導入後の配線状況
+---------+ +-------------+ +-----------+ +----------------+ | Speaker | | Stream Deck | | Shure MV7 | | Magic Trackpad | +---------+ +-------------+ +-----------+ +----------------+ ^ ^ ^ ^ | | | | | 3.5mm Audio Cable USB Cable | | USB Cable | Lightning Cable | | | | | +----------+ | +------------+ | | | | +-------+-------+ +--+----+----+--+ | | | | | Dell U2720Q |<------------+ +------------>| Dell U2720Q | | | | | | | +---------------+ | | +---------------+ | | | | Type-C, Thunderbolt 3 | | Type-C, Thunderbolt 3 | | | | +------+--------+-----+ | | | Anker Thunderbolt 4 | | Mini Dock | | | +---------------------+ ^ ^ | | Type-C, Thunderbolt 3 | | Type-C, Thunderbolt 4 | +---------------+ | | +-------+-------+ +-------+-------+ | MacBook Pro | | MacBook Pro | | 16-inch, 2019 | OR | 14-inch, 2021 | +---------------+ +---------------+
これで Mac には 1 本のケーブルだけ挿せば、全てのデバイスが機能するようになった。楽ちんだし、机の上がそれなりにスッキリしてよかった。これだけデバイスを数珠繋ぎにしたら、何かしらのデバイスが上手く繋がらなくなるんじゃないか、という心配をしていたけど、特に今の所そういう出来事は起きてなくて安心してる。
皆さんもこの記事読んで欲しくなったら買いましょう。今なら楽天で 5000 円引きです。
Node.js で sourcemap を読み込ませた状態で CLI ツールを起動する
小ネタです。Node.js で CLI ツールを実装する場合、以下のように shebang 付きの実行可能ファイルを用意して、それを package.json
の bin
フィールドから参照する構成にすることが多いと思います。
bin/eslint-interactive.js
:
#!/usr/bin/env node import { run } from '../dist/index.js'; run({ argv: process.argv, }).catch((error) => { console.error(error); process.exit(1); });
package.json
:
{ "name": "eslint-interactive", "scripts": { "build": "tsc -p tsconfig.json", }, "bin": { "eslint-interactive": "bin/eslint-interactive.js" } }
tsconfig.json
:
{ "include": ["src/**/*"], "exclude": ["node_modules"], "compilerOptions": { "outDir": "./dist", "target": "ES2019", "lib": ["ES2019"], "isolatedModules": true, "module": "node12", "moduleResolution": "node12", "sourceMap": true, // デバッグのために sourcemap を出力する "locale": "ja" } }
これで npm i -g eslint-interactive
した時に、eslint-interactive
コマンドがローカルマシンにインストールされ、実行できるようになります。
$ npm i -g eslint-interactive $ eslint-interactive --version 8.1.0
デフォルトでは sourcemap は読み込まれない
CLI ツールを実装する、という話であれば上記対応だけで事足ります。コマンドも問題なくインストールできますし、実行できます。一方で、コマンドを実行時に実行時エラーが発生した場合、そのエラーメッセージを見ると、あれっ?と思う結果になります。
$ eslint-interactive --version file:///Users/mizdra/src/github.com/mizdra/eslint-interactive/dist/cli/run.js:5 throw new Error('test error'); ^ Error: test error at run (file:///Users/mizdra/src/github.com/mizdra/eslint-interactive/dist/cli/run.js:5:11) at file:///Users/mizdra/src/github.com/mizdra/eslint-interactive/bin/eslint-interactive.js:5:1 at ModuleJob.run (node:internal/modules/esm/module_job:195:25) at async Promise.all (index 0) at async ESMLoader.import (node:internal/modules/esm/loader:331:24) at async loadESM (node:internal/process/esm_loader:88:5) at async handleMainPromise (node:internal/modules/run_main:65:12)
デバッグのために sourcemap を出力しているはずですが、エラーメッセージにはトランスパイル後のファイルの位置情報が使われてしまっています。これは Node.js がデフォルトで sourcemap を読み込まないようになっているためです。
sourcemap を読み込ませた状態で CLI ツールを起動する
Node.js が標準で sourcemap を読み込むためのオプション「--enable-source-maps
」を用意してくれているので、これを使うと良いです。Node.js v12.12.0+ でサポートされています。
bin/eslint-interactive.js
:
#!/usr/bin/env -S node --enable-source-maps import { run } from '../dist/index.js'; run({ argv: process.argv, }).catch((error) => { console.error(error); process.exit(1); });
env
コマンドに -S
オプションを渡しているのがミソです。-S
オプションを付けないと、node --enable-source-maps
という名前を持つコマンドを探して実行しようとしてしまいます。詳しくはこのあたり を参照して下さい。
この状態で再度実行してみると、無事 sourcemap が読み込まれていることが確認できます。
$ eslint-interactive --version /Users/mizdra/src/github.com/mizdra/eslint-interactive/src/cli/run.ts:9 throw new Error('test error'); ^ Error: test error at null.run (/Users/mizdra/src/github.com/mizdra/eslint-interactive/src/cli/run.ts:9:9) at file:///Users/mizdra/src/github.com/mizdra/eslint-interactive/bin/eslint-interactive.js:5:1 at ModuleJob.run (node:internal/modules/esm/module_job:195:25) at async Promise.all (index 0) at async ESMLoader.import (node:internal/modules/esm/loader:331:24) at async loadESM (node:internal/process/esm_loader:88:5) at async handleMainPromise (node:internal/modules/run_main:65:12)
余談
デバッグの時に便利なので sourcemap を読み込めるようにしましょう、みたいな紹介の仕方をしましたが、CLI ツールのコードは別に minify している訳でもないですし、デフォルトのエラーメッセージからでも十分デバッグのための情報を得られます。逆に sourcemap を読み込ませると、トランスパイル後のコードのどの位置でエラーが起きたかの情報が失われてしまいます。sourcemap を読み込むと良いケース、悪いケース両方あるはずで、状況によって都度判断すると良いと思います。
参考資料
chezmoi を使って VSCode devcontainer 対応 dotfiles を作る
趣味開発で使っている dotfiles をリニューアルした。
以前までの dotfiles では適切なパスへの設定ファイルの配置や、onetime script の実行タイミングの管理に ansible を使っていた。冪等性を確保するために色々な機能が用意されていて、便利ではあったのだけど、ファイルの配置をするだけで色々なおまじないが必要だったりと、若干冗長だなと感じていた。
あと ansible 自体のインストールにそこそこ時間が掛かるという問題がある。GitHub Actions 上でインストールに掛かる時間を測ったところ、2分くらい掛かっていた。
dotfiles そんなにインストールする機会無いので、今まではそう困ることはなかったけど、VSCode devcontainer との相性が悪くて困っていた。コンテナ起動時にユーザごとの dotfiles を自動でインストールする機能があるのだけど、現行の dotfiles をインストールしようとすると、brew install ansible
に 2 分待たされてしまう *1。開発環境はサクッと立ち上がってほしい。
リニューアル後の dotfiles
まず先に結論、リニューアル後の dotfiles を貼っておく。
以下はリニューアルにあたっての方針とか、使ったツールの感想について書いていく。
リニューアルにあたっての方針 / chezmoi について
前述の課題を踏まえて、まず ansible をやめることにした。とはいえ ansible でやっていたような設定ファイルの配置や onetime script の実行制御はやはり何かしらのツールに任せたい。軽く調べたところ、 chezmoi というツールが良さそうだったので、これを使うことにした。
使い方とかは公式ドキュメントに丁寧に書かれているので、ここでは省略する。日本語だと以下の記事でも軽く使い方が紹介されている。
chezmoi、色々良いところはあるけど、シングルバイナリであることがまず良い。バイナリファイルを DL してくるだけで良いので、インストール時間が短い。GitHub Actions 上だと 2 秒で chezmoi のインストールが完了した *2。
あと template 機能がよく出来ている。dot_zshrc.tmpl
みたいなファイルを作成すると、テンプレートを使って環境ごとに異なる設定を持つ .zshrc
を書くことができる。テンプレートエンジンには text/template が使われているので、{{ if eq .chezmoi.os "darwin" }}
みたいに条件分岐したり、{{ template "part.tmpl" . }}
みたいに他のテンプレートを展開したりできる。テンプレートは dotfiles をインストールする時 (chezmoi apply
) に評価される。
# dot_zshrc.tmpl {{ if eq .chezmoi.os "darwin" }} # darwin export EDITOR="code --wait" # vscode {{ else if eq .chezmoi.os "linux" }} # linux export EDITOR="vim" {{ else }} # other operating system export EDITOR="vi" {{ end }}
sprig で提供されている関数も使えるので、env
で環境変数を参照したりもできる。
# dot_zshrc.tmpl # devcontainer では anyenv を使わないので、devcontainer 以外でだけ anyenv を初期化する。 # devcontainer では REMOTE_CONTAINERS 環境変数に true が設定されているので、これを使い判定する。 {{- if ne (env "REMOTE_CONTAINERS") "true" }} eval "$(anyenv init - zsh)" {{- end }}
こういう形で、環境ごとの違いを上手い具合に吸収できて中々良い。
あと scripts 機能も用意されていて、run_once_
という prefix の付いたファイルを用意すると、今まで実行していないマシンで chezmoi apply
した時にだけ実行される script (onetime script) が作れる。もちろんこの機能も template と組み合わせられる。
VSCode devcontainer 対応
devcontainer では公式が提供している image のほとんどが debian ベースなので、debian に対して対応するつもりで dotfiles を書けば良い。具体的には darwin(macOS) だったら依存パッケージをインストールするのに brew
を使うけど、debian だったら apt
を使う、みたいなことを template で表現してやれば良い。
あと devcontainer に dotfiles をインストールするには settings.json
に dotfiles.*
系の設定を書く必要がある。設定の名前を見る限りは dotfiles.installCommand
に sh -c "$(curl -fsLS chezmoi.io/get)" -- init --apply mizdra
を設定するだけで良さそうに見えるけど、どうやらここには任意のコマンドは渡せなくて、実行ファイルのパスを書かないといけないらしい。仕方がないので install.sh
というシェルスクリプトを用意して、それを dotfiles.installCommand
に設定している。dotfiles.repository
や dotfiles.targetPath
も指定しないといけなかったので、それも指定した。
{ "dotfiles.repository": "mizdra/dotfiles", "dotfiles.targetPath": "~/dotfiles", "dotfiles.installCommand": "~/dotfiles/install.sh", }
それと、devcontainer ではホストマシンにある .gitconfig
がコンテナ内の ~/.gitconfig
に自動でコピーされた状態でコンテナが起動されるので、dotfiles で ~/.gitconfig
インストールしようとすると、コンフリクトしてコケてしまう。どうしたものかと困っていたけど、.chezmoiignore
に .gitconfig
を書けば ~/.gitconfig
のインストールをスキップするよう制御できたので、これで回避した。.chezmoiignore
は template として扱われるので、REMOTE_CONTAINERS
環境変数を見つつ、devcontainer の時だけ ~/.gitconfig
のインストールをスキップするようにしている。本当にかゆいところに手が届く。
{{- if eq (env "REMOTE_CONTAINERS") "true" }} # devcontainer は自動でホストの .gitconfig をコンテナ内にコピーするため、chezmoi aplly の対象から外す /.gitconfig {{- end }}
実際に devcontainer + dotfiles 使い始めてみたけど、爆速で開発環境が立ち上がって、かつ普段使っているようなプロンプト (starship) や alias がそのまま使えて、快適に過ごせている。devcontainer を本格的に使える環境が整ったので、引き続き使いつつ、dotfiles を育てていければなと思う。
*1:CI 上で 2 分だったので、local で動いている docker 上ならもう少し速度出ると思うけど
*2:https://github.com/mizdra/dotfiles/runs/5265645131?check_suite_focus=true の raw logs より
「ページの編集は大胆に」、転じて「ドキュメントの整理は大胆に」
「ページの編集は大胆に」という考え方
Wikipedia のページの執筆にあたってのガイドラインの1つに「ページの編集は大胆に」というものがあります。簡単に言えば、ちょっとした編集に留まらず、必要であれば大きな編集もしよう、という指針です。
最初のうちはちょっとした編集を積み重ねていくだけで上手くいっていたはずが、次第に文章がつぎはぎになり、やがて行き詰まる…。このような経験をしたことのある人は多いと思います。文章の元の体裁を尊重して少しずつ手を加えていくのも大事なことです。しかし、どんなに優れたページであっても、編集が積み重なっていくと、思うような体裁にならない時がやってきます。そうした時は大胆な変更をしていくべきだということです。
非常に素朴な指針ですが、文章を変更する上で大胆にやっていくというこの心がけはとても大事なことだと id:mizdra は思っています。当該ガイドラインのページには、このガイドラインにまつわる丁寧な説明が書いてあるので、ぜひ読んでみて下さい。
転じて「ドキュメントの整理は大胆に」
ところでこの「大胆にやる」という考えは、Wikipedia のページの編集に限らず、様々なものについても広げられると id:mizdra は考えています。例えば社内ドキュメントの整理などです。3 年、5年と業務を継続していけば、事業の変化に合わせて自然とドキュメントがつぎはぎになっていくことが多いです。そうして誤読を誘発したり、目の滑るドキュメントができてしまいます。
同様に、定例会のアジェンダについても適用できます。定例会で扱う話題が増えたり、会自体の目的に変化があると、定期的に取り上げる必要がなくなる議題が出てきます。そうした議題をアジェンダに残してしまうと、他の重要な議題に割く時間が減り、定例会の生産性が落ちてしまいます。
こうしたことが起きないよう、id:mizdra はドキュメントの整理を大胆にやることが大事だと思っています。アジェンダで扱うコンテンツの順序を入れ替えたり、思い切って一部のコンテンツを落としてみたり、追加してみたり、格好良い標語を差し込んでみたり…。ついつい何か変化を起こす時は見えざる声に恐れをなしたり、面倒に感じて、変化を控えめにしがちですが、時には大きな変化も必要です。こうすれば絶対に良くなるに違いないと思ったら、その直感を信じて勇気を出して整理していく、ということをよく心がけています。たまに大胆な編集を実践しては、良い体裁に修正することができて、ああやって良かったなとなっています。
大きな変更を加えるのは勇気のいることですが、勇気を振り絞ってやってみると何もかもが良くなるかもしれません。良くならなかったら変更履歴からすぐに戻せば良いだけです。思い切ってやってみましょう。
全部ひっくり返して別物にする、ではない
実践するにあたって 1 つ注意しておきたいのは、「大胆に変更する」が意味するのは「今まで作り上げたものを全部ひっくり返して別物にする」ではなく「大きく変更を加えることを恐れない」だということです。全部を変える必要は当然ないです。良いものは良いものとして引き継いでいくことを心がけると良いと思います。
個人的 Web フロントエンドスキルの獲得方法
ここ2年くらいの話なのですが、仕事で「フロントエンド会」というチーム内委員会のようなものを立ち上げて運営しています。元々1人の Web フロントエンド職人がプロダクトの Web フロントエンドの面倒を見ていたのですが、その方が異動されることになったので、残った人で面倒を見ていける体制を作りましょう、というモチベーションで発足した会でした。この話については以前イベントで発表したので、詳しくはこのスライドをご覧下さい。
Web フロントエンド職人の異動とともに入社した id:mizdra が Web フロントエンドが得意だったので、ペアプロやペアオペ、定例会などを通じてどんどんスキルや知見を配っていく、という戦略で運営していました。実際に 2 年経過してみてメンバーも徐々にキャッチアップしていって、ちょっとしたパフォーマンス改善をやってみたり、最近 Gulp や jQuery からの脱出を推し進めています。すごすぎる!
一方で識者がスキルや知見を配っていく、という体制だったがゆえに、そのスキルや知識を配る人に依存してしまっているという問題も出てきました。元々1人の Web フロントエンド職人が見る体制から複数人で見る体制になったとはいえ、やはり識者が支えている部分がまだまだ大きいです。
ではどうするかという話なのですが、まあ色々やりようがありますね。本質的な解決ではないですが、採用や異動で工夫して継続的に識者をチームに配備できるようにするのも 1 つの手ですし、レガシーなグッズ (Gulp, jQuery) をどんどん捨てて開発の難易度を下げる/現代的なベストプラクティクスを取り入れやすくするのも手ですし、Next.js のようなフレームワークを導入してレールの上に乗れるようにするのも手です。
一方で、それぞれのメンバーが自発的に Web フロントエンドスキルを獲得していける状態を目指す、その状態に向かえるようサポートする、という手もあります。スキルを伝授するのではなく、スキルを習得する方法を伝授する、ということですね。
そういう経緯で社内向けに id:mizdra の個人的な Web フロントエンドスキルの獲得方法について共有してみました。筋肉で解決みたいな内容で、かつ Web フロントエンド要素もあまりないメタ的な内容が多いので、どういう反応になるか不安だったのですが、思いの外良い反応を頂けました。ああやって良かったなと今になって振り返っています。
で、折角書いたのに社内に置いておくのは勿体ない!と思ったので、折角なのでこの記事で公開してみます。やっぱり公開するのは不安なのですが、誰かの参考になれば良いかなと思います。
まず作りたいプロダクトを決める
いきなりメタ的な話なのですが、作るものがあるのとないのとでは、スキルのキャッチアップの速度が大きく変わってくるので、何を作るかは最初に決めておくのがオススメです。ゴールがあればそこへ至るまでの道筋 (最初にどこから学んでいって、次に何を学べばよいのか) もイメージしやすいし、作りたいものならやる気もあって挫折することなく続けることができます。
作りたいプロダクトの大きさは何でも良いのですが、半年以内で作れるもののような大きすぎないものが良いです。プロダクトによって学べることは変わってくるので、小さいプロダクトを沢山こなすほうが色々学べてお得です。とはいえ作りたいものを作ったほうがやる気の維持には良いので、作りたいものを作ると良いと思います。id:mizdra は小さいものを作っていって、だんだんできることを増やしていく、という戦略が好みです。以前ブログにも書きました。
作りたいものがない場合は、ポートフォリオとかオススメです。プログラミング初心者の方がよく作られているイメージが強いですが、Next.js 使えるし、SSR も活かせるし、完成したものも自分のプロフィールとして今後も使えるし、題材としてはピッタリだと思います。
とにかく記事を読みまくる
フロントエンドは必要な技術スタックが沢山ある上に、歴史的経緯も盛りだくさんという感じで、学ぶことが沢山あります。効率よく学んでいくには、情報を取捨選択して必要なものだけを読んでいく、ということになるのですが、学びはじめの頃はそういう勘もないので、愚直に沢山の記事を読んでいくのがオススメです。
id:mizdra は 2015 年の頃に Web フロントエンドを本格的に学び始めたのですが、その頃は ES2015 が登場してフロントエンドのツールが充実しつつあるタイミングでした。Bower/gulp/Browserify/io.js とか色々あって、次は TypeScript/VSCode/Webpack が来るぞとか盛り上がっていて、Web フロントエンド戦国時代という感じ。とにかく色々なツールの情報があって、人によってオススメしているものが違って情報の取捨選択が大変でした。仕方がないので Qiita などを眺めて、真面目に気になった記事を片っ端から読む暮らしをしていました。
片っ端から集めたものは質の良くないものも多いですが、色々な情報を網羅的に収集したい入門時に参照するのには十分価値のあるものなので、どんどん読んでいくと良いです。記事を読んでいると、審美眼が研ぎ澄まされてどの技術スタックが優れているかとか分かってきて、情報の取捨選択ができるようになってきます。
何が言いたいかというと、がむしゃらにインプットしていくことは全くもって遠回りなんかではない!ということです。がむしゃらにインプットしていくのオススメです。
信頼できる情報が目に入る環境を作る
毎回自分から情報を探しに行くのはコストが掛かり、長続きしません。対策として、id:mizdra は何もせずとも情報が振ってくる環境を整えたりしてました。
例えば RSS を購読したりとか、フロントエンドの情報を発信している人を Twitter でフォローするとか。何でも良いです。ただ、自分が普段目を通す場所にその情報が流れてくるようにしておきましょう。せっかく RSS を購読しても目に入らなければ意味ないので、ちゃんと普段見るところに情報を持ってきます。自分の近くに情報を寄せ、息をしているだけで情報が手に入る、というのが理想です。
id:mizdra の場合は Twitter のタイムラインがそれです。フロントエンドの情報を発信している人をフォローしています。暇さえあれば Twitter を開いているので、息をしているだけで情報が手に入るという環境になってます。フォローすると通知が飛ぶのが嫌で、以前リストを使おうとしたこともあったのですが、リストを開くという行為自体がコストそのものだったのでやめました。とにかく低体力で情報が手に入るようにしています。
フォローする人にも気を配っていて、とにかくこの人は信頼できるという人を積極的にフォローするようにしています。その人が書いた記事を読んでみて、面白いことを書いている!とか、着眼点が良い!とか、そういう基準でどんどんフォローしていきます。折角普段目を通すところに置く情報を選ぶのだから、質の良いものを選ぶと良い、という考えが根底にあります。これでタイムラインには信頼できる情報筋からの質の良い情報が集まるようになります。
ところで自分のスキルが高まるにつれ、情報を見る目も変わってきます。その結果、どの情報が自分にとって信頼できるか、質が良いかは変わってきます。id:mizdra もその時々によって、自分のスキルに応じてフォローする基準を柔軟に変えていっています。
完全に余談ですが、こうやってタイムラインを研ぎ澄ましていくと、「filter:follows react」でフロントエンドのエキスパートのツイートを対象に検索することができて便利です。
学習資料や書籍を読む
さっき何でも片っ端から記事を読んでいけば良い、と書いたのですが、体系的に何かを学ぶ場合は学習資料や書籍を読むと良いです。これはフロントエンドが得意な人に聞いたりすると大体教えてくれます。
- JS
- TS
- React
作りたいプロダクトのアイデアをメモしておく
継続的に学んでいくためには作りたいプロダクトを絶やさないことが重要で、とにかく手持ち無沙汰にならないようにしておく、というのを意識しています。「そんなこと言われても作りたいものなんか思いつかないよ!」という人にオススメしたいのが、技術的な挑戦のアイデアをメモしておくことです。
id:mizdra は個人 Slack の #idea
に日々思いついたネタをメモしていって、面白そうなものをやっていく、という暮らしをしています。アイデアを記録し始めの段階では慣れないかもしれませんが、段々書いていくうちに、これもやってみようだとか、あれ面白そうだとか、意外と自然とアイデアが出てくるようになります。あとタイムラインの話と同じですが、このメモする場も出来るだけ毎日目を通す場の近くにあったほうが良いです。アイデアを書きたいと思ったらすぐ書けるように、メモ帳を開くのが億劫にならないように、ということを意識しています。
という訳で以上が個人的 Web フロントエンドスキルの獲得方法になります。全部が参考にならなくても、これは良いねと思ってもらえるものが1つくらいあれば嬉しいです。
チームでもこうしたスキルの獲得などを日々行っております。もし弊チームの活動に興味があれば、ここから応募できますのでぜひぜひ。