mizdra's blog

ぽよぐらみんぐ

チームに提案をする時は傾聴することが大事

「こういう取り組みをチームで始めてみたい」だとか、「新しいコーディング規約を考えてきたので、これを導入してみたい」だとか、「こういうツールを導入してみたい」だとか、チームに対して提案をする場面というのはよくあると思います。実際に提案する際には、以下のような 4 つのフェーズからなるフローで進めることが多いと思います。

  1. 提案の概要をまとめた資料 (提案書) を作成する
    • 何故それをやりたいのか (背景・動機)
    • どんなことをやりたいのか、やってみてどんなメリットがあるのか
    • 懸念やデメリット、代替案などはあるか
  2. 1の提案書を誰かにレビューしてもらう
    • 意見をもらって、提案書を適時修正するフェーズ
  3. 再度レビューに出して、チームから承諾をもらう
  4. 提案を実行する

で、この 2 についてなのですが、id:mizdra はこのフェーズで傾聴を大事にしています。レビューに出したら、色々な人からフィードバックコメントを貰えると思うのですが、それを傾聴するようにします。どんな些細なことでも、相手の気持ちや考えに寄り添って、丁寧な解決策を提示してみる。レビューする人のスキルは大抵バラバラで、時にはちょっとずれた意見をもらうこともあると思うのですが、そういう意見にも突き返すことなく、真摯に対応することを大事にしています。

意見をくれた人を突き返すようでは、あきれられたり、意見を言うことを恐れるようになってしまって、逆に意見が貰えなくなってしまいます。意見を貰うために提案をしているのだから、意見を気軽に言ってもらえる状態を目指すべきです。些細な意見でも、コミュニケーションを繰り返すうちに思わぬ発見があることもよくあります。しかもそれが思いの外重要なことだったり… どんな意見も大切にすることが重要です。

そもそも意見を無視して提案を押し通そうとするのなら、意見を聞く機会を設ける必要もないので、提案なんかせずに勝手に進めれば良いですからね。何が言いたいかというと、意見を突き返すようであれば、それは提案とは言えないということです。

傾聴は提案以外でも色々な場面で重要なスキルなので、提案以外の場面でも意識していけると良いですね。

見つけた GitHub の Issue を片っ端から subscribe している

あるライブラリを使っていてバグっぽい挙動に遭遇した時、ほぼ必ず当該ライブラリの Issue を検索するようにしている。加えて、見つけた Issue の subscribe ボタンを押して、https://github.com/notifications に通知がいくようにしている。バグ遭遇時以外にも、何らかの理由で Issue に到達した時にその Issue を subscribe してる。

たったこれだけのことだけど、これを地道に続けていると https://github.com/notifications を見れば無限に最新お得情報が流れてくる、面白ニュースフィードにできる。バグ修正されたらすぐ修正されたバージョンを取り込みにいけるし、欲しい機能が実装されたらすぐ触りにいける。id:mizdra は /notifications を見ては、趣味や業務で困っていたバグの修正情報を即座にキャッチして、その修正を即取り入れる、という活動に役立てている。

裏話が自然と頭に入ってくる

バグ修正されるまで/機能が実装されるまでの議論も逐次追っていけるので、たまに面白い裏話とか、実装の背景とか、設計の意図とか裏話的なものが自然と頭に入ってくるのも嬉しい。あんまり興味ないバグや機能の裏話は中々頭に入ってこないけど、興味のあるものを選んで購読するので、そんなに苦なく続けられると思う。id:mizdra はこの subscribe 活動を初めて 3 年くらい経ったけど、今の所楽しくやれている。

躊躇せずどんどん subscribe していく

沢山 subscribe していくと通知で溢れかえるんじゃないか、と思うかもしれないけど、今の所 id:mizdra はそんなことになっていないので、躊躇せず subscribe している。毎日 3〜5 issues くらいしか更新ないので、普通に数日おきに見ていれば取りこぼすこともない。むしろにいろんな issue を subscribe することで、毎日色々な面白情報がやってきて楽しいのでどんどん subscribe している。

/notifications には watch しているリポジトリの情報も流れてくるので、watch しているリポジトリが多い人だと、爆速で情報が流れていくので使いづらい、とかはあるかもしれない。僕は watch しているリポジトリほとんどないので困っていない。watch しまくっている人は /notifications 以外で見る、とか工夫が必要かも。

おまけ (2022/06/29 追記)

subscribe した Issue は以下の URL から一覧できる。「以前 subscribe したあの Issue どれだっけ...」という時に便利。

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したり。
  • lint: lint や型検査など、静的解析を実行する
    • 静的解析だったらstatic-checkが良い気もするけど、長くて type しづらいので避けた
    • 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 は普段触らないので、特に困っていない
      • devscript など並列で実行する必要のあるものについては、素直にrun-pを使う
    • run-sを使うときは-cオプションを付ける
      • デフォルトでは script を 1 つずつ実行していって、エラーになったら即時中断する挙動になっている
        • lint:eslint => lint:tsc => lint:prettierという順序で実行される時に、lint:tscでエラーになったらlint:prettierは実行されない
      • これだと一部の静的解析の結果が欠損してしまい、効率的にデバッグできない
      • -cを付けたら、エラーが見つかっても全ての script を実行した上で、エラーが返される挙動になるので、全ての静的解析が実行される
    • run-s / run-p-l オプションは付けない
      • -l オプションを付けると、script 名付きの出力が得られる
      • これを使えば run-p で複数コマンドの出力が入り乱れてもある程度見やすくなる
      • ただし、実行されるコマンドの色の出力が抑えられてしまう問題がある
      • 微妙に不便なので使わないようにしてる
  • CI からはまとめ上げられた script を実行するよう整理する

どうしても lint を並列で実行したくなったら、npx run-p "lint:*" / yarn run-p "lint:*" とターミナルから打てば良いと思う。

lint や test の亜種を用意する

eslint --fixprettier --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-fixtestの 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"
   }
 }

以下ポイント:

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:srclint:tsc:testに分割する
  • lint:tscでまとめる

余談

tsconfig.jsonではなくtsconfig.src.json/tsconfig.test.jsonといったファイル名にしてしまうと、tsserver (VSCode で利用されている TypeScript の Language Server) から設定ファイルが検出できなくなり、エディタ上のリアルタイムの型検査がデフォルトの設定で実行されてしまうので注意すること。補完がいつの間にかおかしくなったら、これを疑うと良い。tsserver に設定ファイルが読み込まれているのかは、VSCode の下部にあるツールバーから確認できる。

f:id:mizdra:20220324012931p:plain

じゃあtsconfig.jsonにしないといけないのかというと、そうでもない。Solution Style tsconfig.json というものを用意すれば、好きな名前の設定ファイルを tsserver に読み込ませることができる。

{
  // files は必ず空にする
  "files": [],
  "references": [
    // ここに tsserver から参照してほしい設定ファイルのパスを書いていく
    { "path": "./tsconfig.src.json" },
    { "path": "./tsconfig.test.json" }
  ]
}

f:id:mizdra:20220324012952p:plain

便利なテクニックだなと思ってよく使ってるけど、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を用意する
    • testtest-watchで時間の掛かるテストが走ってほしくないため
    • E2E テストは結構重いので、これがtest-watchなどで走ってしまうと、デバッグ体験が悪い
    • オプトアウトしておいて、必要に応じてe2e-teste2e-test-watchしてもらう、という世界観
    • 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:testpostbuild: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 をスクリプトファイルに切り出しているのが見どころ

皆さんも自慢の手癖があれば教えて下さい。

ドッキングステーション導入した

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

(この図は asciiflow で作りました)

常に 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 |
                        +---------------+    +---------------+

(この図も asciiflow で作りました)

これで Mac には 1 本のケーブルだけ挿せば、全てのデバイスが機能するようになった。楽ちんだし、机の上がそれなりにスッキリしてよかった。これだけデバイスを数珠繋ぎにしたら、何かしらのデバイスが上手く繋がらなくなるんじゃないか、という心配をしていたけど、特に今の所そういう出来事は起きてなくて安心してる。

皆さんもこの記事読んで欲しくなったら買いましょう。今なら楽天で 5000 円引きです。

item.rakuten.co.jp

Node.js で sourcemap を読み込ませた状態で CLI ツールを起動する

小ネタです。Node.js で CLI ツールを実装する場合、以下のように shebang 付きの実行可能ファイルを用意して、それを package.jsonbin フィールドから参照する構成にすることが多いと思います。

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 をリニューアルした。

github.com

以前までの dotfiles では適切なパスへの設定ファイルの配置や、onetime script の実行タイミングの管理に ansible を使っていた。冪等性を確保するために色々な機能が用意されていて、便利ではあったのだけど、ファイルの配置をするだけで色々なおまじないが必要だったりと、若干冗長だなと感じていた。

あと ansible 自体のインストールにそこそこ時間が掛かるという問題がある。GitHub Actions 上でインストールに掛かる時間を測ったところ、2分くらい掛かっていた。

dotfiles そんなにインストールする機会無いので、今まではそう困ることはなかったけど、VSCode devcontainer との相性が悪くて困っていた。コンテナ起動時にユーザごとの dotfiles を自動でインストールする機能があるのだけど、現行の dotfiles をインストールしようとすると、brew install ansible に 2 分待たされてしまう *1。開発環境はサクッと立ち上がってほしい。

qiita.com

code.visualstudio.com

リニューアル後の dotfiles

まず先に結論、リニューアル後の dotfiles を貼っておく。

github.com

以下はリニューアルにあたっての方針とか、使ったツールの感想について書いていく。

リニューアルにあたっての方針 / chezmoi について

前述の課題を踏まえて、まず ansible をやめることにした。とはいえ ansible でやっていたような設定ファイルの配置や onetime script の実行制御はやはり何かしらのツールに任せたい。軽く調べたところ、 chezmoi というツールが良さそうだったので、これを使うことにした。

www.chezmoi.io

使い方とかは公式ドキュメントに丁寧に書かれているので、ここでは省略する。日本語だと以下の記事でも軽く使い方が紹介されている。

zenn.dev

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 と組み合わせられる。

www.chezmoi.io

VSCode devcontainer 対応

devcontainer では公式が提供している image のほとんどが debian ベースなので、debian に対して対応するつもりで dotfiles を書けば良い。具体的には darwin(macOS) だったら依存パッケージをインストールするのに brew を使うけど、debian だったら apt を使う、みたいなことを template で表現してやれば良い。

github.com

あと devcontainer に dotfiles をインストールするには settings.jsondotfiles.* 系の設定を書く必要がある。設定の名前を見る限りは dotfiles.installCommandsh -c "$(curl -fsLS chezmoi.io/get)" -- init --apply mizdra を設定するだけで良さそうに見えるけど、どうやらここには任意のコマンドは渡せなくて、実行ファイルのパスを書かないといけないらしい。仕方がないので install.sh というシェルスクリプトを用意して、それを dotfiles.installCommand に設定している。dotfiles.repositorydotfiles.targetPath も指定しないといけなかったので、それも指定した。

{
  "dotfiles.repository": "mizdra/dotfiles",
  "dotfiles.targetPath": "~/dotfiles",
  "dotfiles.installCommand": "~/dotfiles/install.sh",
}

github.com

それと、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 }}

github.com

実際に devcontainer + dotfiles 使い始めてみたけど、爆速で開発環境が立ち上がって、かつ普段使っているようなプロンプト (starship) や alias がそのまま使えて、快適に過ごせている。devcontainer を本格的に使える環境が整ったので、引き続き使いつつ、dotfiles を育てていければなと思う。

f:id:mizdra:20220222021608p:plain

*1:CI 上で 2 分だったので、local で動いている docker 上ならもう少し速度出ると思うけど

*2:https://github.com/mizdra/dotfiles/runs/5265645131?check_suite_focus=true の raw logs より

ポケットモンスター・ポケモン・Pokémon・は任天堂・クリーチャーズ・ゲームフリークの登録商標です.

当ブログは @mizdra 個人により運営されており, 株式会社ポケモン及びその関連会社とは一切関係ありません.