mizdra's blog

ぽよぐらみんぐ

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 より

「ページの編集は大胆に」、転じて「ドキュメントの整理は大胆に」

「ページの編集は大胆に」という考え方

Wikipedia のページの執筆にあたってのガイドラインの1つに「ページの編集は大胆に」というものがあります。簡単に言えば、ちょっとした編集に留まらず、必要であれば大きな編集もしよう、という指針です。

最初のうちはちょっとした編集を積み重ねていくだけで上手くいっていたはずが、次第に文章がつぎはぎになり、やがて行き詰まる…。このような経験をしたことのある人は多いと思います。文章の元の体裁を尊重して少しずつ手を加えていくのも大事なことです。しかし、どんなに優れたページであっても、編集が積み重なっていくと、思うような体裁にならない時がやってきます。そうした時は大胆な変更をしていくべきだということです。

非常に素朴な指針ですが、文章を変更する上で大胆にやっていくというこの心がけはとても大事なことだと id:mizdra は思っています。当該ガイドラインのページには、このガイドラインにまつわる丁寧な説明が書いてあるので、ぜひ読んでみて下さい。

転じて「ドキュメントの整理は大胆に」

ところでこの「大胆にやる」という考えは、Wikipedia のページの編集に限らず、様々なものについても広げられると id:mizdra は考えています。例えば社内ドキュメントの整理などです。3 年、5年と業務を継続していけば、事業の変化に合わせて自然とドキュメントがつぎはぎになっていくことが多いです。そうして誤読を誘発したり、目の滑るドキュメントができてしまいます。

同様に、定例会のアジェンダについても適用できます。定例会で扱う話題が増えたり、会自体の目的に変化があると、定期的に取り上げる必要がなくなる議題が出てきます。そうした議題をアジェンダに残してしまうと、他の重要な議題に割く時間が減り、定例会の生産性が落ちてしまいます。

こうしたことが起きないよう、id:mizdra はドキュメントの整理を大胆にやることが大事だと思っています。アジェンダで扱うコンテンツの順序を入れ替えたり、思い切って一部のコンテンツを落としてみたり、追加してみたり、格好良い標語を差し込んでみたり…。ついつい何か変化を起こす時は見えざる声に恐れをなしたり、面倒に感じて、変化を控えめにしがちですが、時には大きな変化も必要です。こうすれば絶対に良くなるに違いないと思ったら、その直感を信じて勇気を出して整理していく、ということをよく心がけています。たまに大胆な編集を実践しては、良い体裁に修正することができて、ああやって良かったなとなっています。

大きな変更を加えるのは勇気のいることですが、勇気を振り絞ってやってみると何もかもが良くなるかもしれません。良くならなかったら変更履歴からすぐに戻せば良いだけです。思い切ってやってみましょう。

全部ひっくり返して別物にする、ではない

実践するにあたって 1 つ注意しておきたいのは、「大胆に変更する」が意味するのは「今まで作り上げたものを全部ひっくり返して別物にする」ではなく「大きく変更を加えることを恐れない」だということです。全部を変える必要は当然ないです。良いものは良いものとして引き継いでいくことを心がけると良いと思います。

個人的 Web フロントエンドスキルの獲得方法

ここ2年くらいの話なのですが、仕事で「フロントエンド会」というチーム内委員会のようなものを立ち上げて運営しています。元々1人の Web フロントエンド職人がプロダクトの Web フロントエンドの面倒を見ていたのですが、その方が異動されることになったので、残った人で面倒を見ていける体制を作りましょう、というモチベーションで発足した会でした。この話については以前イベントで発表したので、詳しくはこのスライドをご覧下さい。

speakerdeck.com

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」でフロントエンドのエキスパートのツイートを対象に検索することができて便利です。

学習資料や書籍を読む

さっき何でも片っ端から記事を読んでいけば良い、と書いたのですが、体系的に何かを学ぶ場合は学習資料や書籍を読むと良いです。これはフロントエンドが得意な人に聞いたりすると大体教えてくれます。

作りたいプロダクトのアイデアをメモしておく

継続的に学んでいくためには作りたいプロダクトを絶やさないことが重要で、とにかく手持ち無沙汰にならないようにしておく、というのを意識しています。「そんなこと言われても作りたいものなんか思いつかないよ!」という人にオススメしたいのが、技術的な挑戦のアイデアをメモしておくことです。

id:mizdra は個人 Slack の #idea に日々思いついたネタをメモしていって、面白そうなものをやっていく、という暮らしをしています。アイデアを記録し始めの段階では慣れないかもしれませんが、段々書いていくうちに、これもやってみようだとか、あれ面白そうだとか、意外と自然とアイデアが出てくるようになります。あとタイムラインの話と同じですが、このメモする場も出来るだけ毎日目を通す場の近くにあったほうが良いです。アイデアを書きたいと思ったらすぐ書けるように、メモ帳を開くのが億劫にならないように、ということを意識しています。


という訳で以上が個人的 Web フロントエンドスキルの獲得方法になります。全部が参考にならなくても、これは良いねと思ってもらえるものが1つくらいあれば嬉しいです。

チームでもこうしたスキルの獲得などを日々行っております。もし弊チームの活動に興味があれば、ここから応募できますのでぜひぜひ。

jobs.qiita.com

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

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