mizdra's blog

ぽよぐらみんぐ

TSKaigi Mashup Kansai #2 で「Flow の今」について話しました

少し前の出来事になるのですが、2026/07/24 に TSKaigi Mashup Kansai #2 というイベントに参加してきました。アウトプットの仕方や姿勢の話Effect の話、TSKaigi のイベント運営の裏話といった発表がありました。どれも興味深い話で面白かったです。

かくいう僕も「Flow は今どうなっているか」というタイトルで発表しました。

speakerdeck.com

Flow はもはや新規開発で避けられる技術となっていますが、今も開発が続き、進化しています。その進化は実は TypeScript にも影響を与えているのです。Flow で今どういった変化が起きているのか、それを伝える発表となってます。

本当は TSKaigi 本編で話したかった内容なのですが、id:mizdra の見込みでは 20 分のトークで、一方 TSKaigi は 10分/30分枠しか募集してない...という理由でプロポーザルを出すのを諦めたネタなのでした。とはいえ TSKaigi Mashup Kansai #2 向けに頑張って圧縮したら 10 分に収まりました。本編に 10 分枠でプロポーザル出しておけば良かったですね。後悔してます。

スライドで触れられなかったこぼれ話

せっかくなので、時間が無くて話せなかったことについていくつか書いておきます。

Flow と TypeScript の違いを詳しく知りたい

スライドでも Flow と TypeScript の違いについてざっと説明しましたが、もっと詳しく知りたい方は公式ドキュメントを読んでみてください。「Flow for TypeScript Users」というページがあり、そこに"全て"が書いてあります。

flow.org

全体的に、Flow は TypeScript よりも厳密な型システムを志向しており、それが TypeScript との挙動の違いとして現れてます。例えば mutable array は TypeScript では covariant ですが、Flow では invariant です。

Flow は厳密な型システムが好きな人にオススメです *1

Flow 向けのLinter

Linter は flow コンパイラ本体に組み込まれて、flow start --lints "untyped-type-import=error" などで実行できます。ルールはそれほど多くなく、公式ドキュメントで紹介されているものだけで 27 個です *2

flow コンパイラ本体組み込みの Linter は Flow 向けのカスタムルールを書けませんが、ESLint を使うと書けるようです。hermes-eslint で ESLint で Flow コードを扱えるように拡張し、ESLint のカスタムルールを書く、という手順を踏めば良いようです。また、eslint-plugin-ft-flow という公式の plugin でいくつかのルールも提供してるようです。

Flow 向けの Formatter

Flow 向けの Formatter は Prettier です。@prettier/plugin-hermes という Prettier プラグインとセットで使います。

Flow 向けの Language Server

Language Server は flow lspコマンドで起動できます。VS Code では Flow の拡張機能をインストールすると、拡張機能側でそのコマンドを使って Language Server を起動してくれます。

一点注意があります。それは Flow 記法で書かれた *.js を、エディタが JavaScript の Language Server で処理してしまうことです。JavaScript の Language Server は当然 Flow 記法は理解できないので、「const a: number = 1; は不正な構文だぜ!」などと言ってきます。

対策として、Flow が使われるプロジェクトではエディタ組み込みの JavaScript の Language Server を無効化する必要があります。VS Code であれば "javascript.validate.enable": falsesettings.json に書きます。マジかよって思うかもしれませんが、Flow 拡張機能のドキュメントで推奨されてる方法です。

余談ですが、TypeScript では *.ts という異なる拡張子を選択してるので、こうした問題とは無縁です。*.js に型注釈記法を持ち込んだ Flow 特有の問題です。

.flowconfig 面白オプション集

.flowconfigtsconfig.json みたいなやつです。型チェックやトランスパイルの挙動をカスタマイズできる設定ファイルです。いくつか変わったオプションがあるので紹介します。

  • jest_integration
    • jest.mock(...)などに渡すモジュール名のチェックが有効になる
  • relay_integration
    • Relay (Meta が開発してる GraphQL クライアント) を使っているプロジェクト向けのオプション
    • useFragment という API の型引数が省略可能になる
  • module.missing_module_generators
    • モジュールの型定義が見つからない時に、エラーメッセージに実行すべきコマンドのヒントを追加できる
    • 何かしらのコード生成ツールで型定義ファイルを生成してるプロジェクトでの利用を想定
    • 例えば module.missing_module_generators='.*\.module.css$' -> 'cmk' と書くと、Cannot resolve module `./Button.module.css`. Try running the command `cmk` to generate the missing module. というエラーメッセージに変更できる
  • react.custom_jsx_typing

.flowconfig のコメント

.flowconfig のコメント開始記号は#;、そして💩。そのように公式ドキュメントにも書いてあります

# This is a comment
; This is a comment
💩 This is a comment

Flow は 💩 が好きな人にもオススメです。

*1:まあ Flow は TypeScript ほどエコシステムが成熟してないので、いざ利用するとなると難しいと思いますが...

*2:その他、公式ドキュメントには掲載されていないルールが一部あるようです (uninitialized-instance-property など)。

Secure Enclave で git commit の署名鍵を管理する

id:mizdra は Git の commit 署名をしていて、その署名鍵を 1Password で管理している。commit をする度に 1Password による生体認証を求められるが、その分安全に署名鍵を扱える。

今まではそれで不満は無かったのだけど、Coding Agent を使うようになってからというもの、この仕組みが足かせになっている。具体的には、Coding Agent にタスクを投げて人間が他のことをしている際に commit が試行され、人間が時間内に生体認証できずタイムアウトする、というもの。これのせいで、「人間はこれから寝るからこれやっておいて!」と投げたタスクが途中で止まってしまう。

折角 Coding Agent が備えている自律性が損なわれているのは勿体ないので、なんとかしてみる。

基本的な方針

ファイルシステムに署名鍵置いてそれを使うというのが最も簡単な方法だと思う。けどどうせなら、現代的で格好良い方法を採用したい。具体的には、暗号ハードウェアやセキュリティチップに保存された鍵を使って署名することで、秘密鍵を端末の外に持ち出すことを不可能にし、鍵ファイルの漏洩や窃取によるなりすましができないようにしたい。

実は Macbook Pro にもそうしたセキュリティチップが組み込まれていて、それを制御するシステムがある。それが「Secure Enclave」である。

Secure Enclave 内に鍵を生成して git から利用するテクニックは割と知られていて、インターネット上にもいくつか記事がある。しかし、いずれも Secretive という 3rd-party ソフトウェアを利用する方法だった。悪くないけど、id:mizdra としては 3rd-party ソフトウェアに一切依存しない方法を採用したい。

粘り強く調べてみると、macOS 14.0 Sonoma で追加された sc_auth create-ctk-identity というコマンドで同等のことができるようだった (詳しくは後述)。今回はそれを使ってみる。

やり方

1. Secure Enclave 内に鍵を生成する

以下のコマンドで生成できる。

sc_auth create-ctk-identity -l git-sign -k p-256-ne -t none

sc_auth はスマートカード (IC カードのようなもの) を使ったユーザー認証の設定をするためのコマンド。スマートカード上の証明書を macOS のユーザーアカウントに紐付けて、スマートカードをアカウントのログインに利用できる。

元々はスマートカードを前提としたコマンドであったが、macOS 14.0 Sonoma になって Secure Enclave を扱うためのサブコマンドが追加された *1。それが sc_auth create-ctk-identity である。

sc_auth create-ctk-identity は Secure Enclave 内に CTK identity を生成する。CTK とは CryptoTokenKit の略称で、スマートカードやハードウェアトークンなどの暗号トークンを扱うための Apple のフレームワークのこと。そして CTK identity とは「秘密鍵+それに対応する自己署名証明書」のペアのこと。つまり sc_auth create-ctk-identity は Secure Enclave 内に秘密鍵とそれに対応する自己署名証明書を生成するコマンドということ。

...という前提を踏まえたうえで、オプションの意味について解説する。

  • -l git-sign: 鍵の名前。何でも良い。
  • -k p-256-ne: 鍵の種類。ECDSA P-256 で ne = non-exportable な鍵を意味してる。
  • -t none: 鍵の保護方法の指定。none だと Touch ID やパスコードを要求しなくなる。

まとめると、このステップでは Secure Enclave 内に non-exportable な ECDSA P-256 鍵ペアと自己署名証明書を生成してる。

2. SSH 鍵ファイルの書き出し

Secure Enclave 内に生成された鍵ペアはそのままでは git から扱えない。そこで git から署名鍵として扱える SSH 鍵ファイルを書き出す。以下のコマンドでできる。

ssh-keygen -w /usr/lib/ssh-keychain.dylib -K -N ""

オプションの意味はそれぞれ以下の通り。

  • -w /usr/lib/ssh-keychain.dylib: セキュリティキーとの通信に使うプロバイダライブラリを指定するオプション
    • ssh-keychain.dylib は Secure Enclave を FIDO セキュリティキーとして OpenSSH に見せるプロバイダライブラリ
  • -K: セキュリティキーに保存された鍵を参照するための情報 (key handle) と公開鍵を取得し、ファイルに書き出すオプション
  • -N "": 書き出されるファイルのパスフレーズを空に設定する

これで SSH 鍵ファイルが id_ecdsa_sk_rk, id_ecdsa_sk_rk.pub という名前で書き出される。id_ecdsa_sk_rk は Secure Enclave 内の秘密鍵を指す参照のようなもので、秘密鍵そのものは含まれない。

3. SSH 鍵ファイルの rename & ~/.ssh 配下に移動

このままだと名前があんまりなので、分かりやすい名前に rename し、~/.ssh 配下に移動する。

mv id_ecdsa_sk_rk ~/.ssh/id_git_sign
mv id_ecdsa_sk_rk.pub ~/.ssh/id_git_sign.pub

4. SSH 公開鍵を GitHub に登録する

id_git_sign.pubhttps://github.com/settings/keys から Signing keys として登録する。

5. gitconfig を編集する

作成した鍵を使って commit 署名できるよう、gitconfig を編集する。

[user]
    # ...
    signingkey = ~/.ssh/id_git_sign

[gpg]
    format = ssh

[gpg "ssh"]
    program = ~/.local/bin/ssh-sign

[commit]
    gpgsign = true

[tag]
    gpgSign = true

commit 署名するためのコマンドも必要なので作成する。~/.local/bin/ssh-sign に以下を作成し、実行権限を付与すれば OK。

#!/bin/sh
export SSH_SK_PROVIDER=/usr/lib/ssh-keychain.dylib
exec /usr/bin/ssh-keygen "$@"
$ chmod +x ~/.local/bin/ssh-sign

これで完成。

感想

今のところこれで元気に動いてる。commit 署名するたびに生体認証求められることもないし、安全だし、快適。あと 1Password に鍵を問い合わせることがなくなったので、commit が爆速で完了するようになった。ちゃんと測ってないけど、体感 1s => 0.1s くらいになった。

id_git_sign, id_git_sign.pub は端末ごとに生成する必要があるけど、id_git_sign の置き場さえ揃えておけば gitconfig の書き方はマシン間で共通化できるので、gitconfig の dotfiles 管理は特に支障無くできる。

秘密鍵が Secure Enclave の外に持ち出せないとはいえ、端末内であれば好き勝手に commit 署名可能なので、端末上で動作してるマルウェアから commit 署名されるのは防げない。けどそもそも端末上でマルウェアが動作してる時点でもっと他に心配することがあるので、どうでも良さそう。

同じ方法で Authentication keys (GitHub への push/pull に使うやつ) も Secure Enclave 管理にできると思う。けど push はうっかりすると機密情報の漏洩につながる危険性のある操作なので、個人的には Coding Agent に自由に push させたくない。Secure Enclave 管理にしても良いかもしれないけど、今は一旦様子見するつもり。

参考

TSKaigi 2026 の発表資料の体感半数以上が AI 生成感あるものだった

5/22(金)〜5/23(土)にかけて、TSKaigi 2026 というイベントに行ってきました。興味深いトークがあった一方で、AI 生成と見られる発表資料が多数あったのが印象的でした。ほとんどの登壇者は AI で作ったと言ってないので推測ではあるのですが *1、見るだけで「AI 生成だな」と分かる感じでした。体感6割は AI 生成だったと思います。

去年はこのようなことがなかったので驚いてます。AI でスライドを生成するツール (Claude Design/Google Slides/Genspark など) がここ1年で登場したこと、スライド生成 skills が普及したこと、個々人の中で色々な領域で AI 活用してみようと意識が変化していることなどが原因かなと思ってます。TSKaigi のトークや登壇者の傾向によるところもあるかもしれません。

AI 生成は時短にもなるし、良い資料を書くための補助にもなります。これまで発表するのが億劫に感じていた人も、AI の力を借りて発表しやすくなります。そういった点で、AI を活用してスライドを作るのは良いことだなと思います。実際 id:mizdra も以前、スライド中のイラストサンプルコードの生成に AI を活用しました。満足のいくものがすぐに作れて、とても便利でした。

一方で AI の使い方には注意が必要だと思います。あんまりにも AI 生成そのまま過ぎると、残念なスライドになってしまいます。TSKaigi 2026 でもそのようなスライドが多数見られて、色々思うところがありました。

実際どんなスライドだったのか、どうすべきなのか、何を感じたのか、みたいな話をちょっと書いてみます。

AI 生成スライドの特徴

残念なもの・そうでないものに限らず、AI 生成スライドには以下のような特徴がありました。

  • 「―(ダッシュ)」が多い
  • 会場の大きさに対して文字が小さすぎる
  • ギチギチまで文字やイラストを詰め込んでる
  • 複数カラム構成が多い
  • 一部 (見出しなど) だけが日本語・英語で併記されてる (「型安全な書き方 ― Type-safe coding」みたいな)
  • 絵文字が多い
  • 文章との関連性が不明なイラスト
  • 1文の中で色分けがされてる (「型安全な書き方」の「書き方」だけ青色になってるとか)

実際の例を出すのははばかられるので、気になる方は TSKaigi 2026 の登壇資料を眺めてみてください。なんとなく僕が言いたいことが伝わると思います。

見出しの英語・日本語併記はかなりの資料で見かけました。スライド内で徹底されている訳でもないことから、英語話者向けというわけでもなさそうですし、多分装飾として書かれているのだと思います。1文内での色分けも多かったです。重要なところの強調、あるいは装飾として使われているように見えました。ギチギチまで文字やイラストを詰め込むのは、時々見かけました。話すことがいっぱいあるからこうなっていると思いきや、口頭ではほんの一部しか触れられていなかったので、これといった理由があるわけではなさそうでした。恐らく AI による装飾の一種でしょう。

全体的に装飾過多で目が滑りやすく、さらに装飾でスペースが圧迫されて文字サイズが小さくなり、読みにくさに拍車をかけているように見えました。装飾を削り、文字を大きくして欲しいなーと思う資料が多かったです。

本質的な問題は何か

実際のところ、先ほど書いたような特徴自体が悪くないと思ってます。「―」は AI 感ありますが、それだけですからね。複数カラム構成や日英併記も、見栄えを整えるための装飾として AI が登場する以前から使われています。ギチギチまで文字やイラストを詰め込むのも、話したいことが沢山あるなら問題ないでしょう。

より気になっているのは、AI が書いた台本通りに喋ってる感のある発表がいくつかあったことです。喋る時にたどたどしいというか、自分でも何言ってるのかよく分かってなさそうで言葉に詰まる時があるというか、メリハリや熱量がないというか... 思いを持ってスライドを書き上げていればそうはならなさそうに思います。

なんというか、資料を AI で作って楽しようという点ばかりに目がいって、聞き手に目が向いていないように見えます。本来であれば AI で作ったスライドを手直しして整えるべきですが、それをせずにそのまま発表する。その結果、残念な AI 生成スライドになってしまう...。これが問題の本質のように思います。

本当に時間が無くてどうしようもない時もありますが、できるだけ聞き手に満足してもらえるような発表を心がけるべきだと思います。せっかく発表するのですから、自分にとっても、聞き手にとっても良い発表を目指しましょう。もちろん登壇はボランティアではあるので、どこまで登壇者に求めるのかは難しいのですが...。

今後に思いを馳せる

TSKaigi 2026 では体感6割が AI 生成感のあるスライドでしたが、AI 生成感がないだけで AI を使っているスライドもあるはずです。そう考えるとスライドの作成に AI を使うのは既に当たり前なのかもしれません。時代ですね。一方で、あまりにも AI 生成そのままの残念な発表が多いと、聞き手としてはうんざりするなと思いました (正直 id:mizdra は TSKaigi 2026 でうんざりしていた)。今後他のイベントでもこのようなことが増えるのではと不安に感じています。何か手を打つ必要があるのかもしれません。

立場ごとに色々できることがあると思います。

  • (登壇者) 意識を変える
    • 聞き手に目が向けて資料を作る
    • AI を使う時は (人間だけでやるより) プラスの成果を出すのに使う、という心構えを持っておく
      • マイナスの成果を出していたらダサい、くらいの気持ちでいれば、自然と良い発表資料にしようと思えるのでは
  • (登壇者) 自分の思いを込めた資料を作る
    • 全部を AI 任せではなく、自分が本当に伝えたいことを資料に込める
    • AI が出力してくれたから、ではなく自分が思っていることをちゃんと書く
  • (登壇者) セルフレビューや発表練習をする
    • 以前から言われていたことだけど、AI 時代でも大事
  • (皆) 良い AI の使い方を周知する
    • アウトラインのブラッシュアップに使うとか、スライド中のコードサンプルの生成に使うと良いとか、雛形の作成に使うとか
  • (会社) 社内で登壇資料のレビュー体制を作る
    • 良い資料の書き方を伝授できる
    • 初めて登壇する人は良い資料の書き方を知らないことが多いので、こういうのがあれば助かるはず
  • (スライド生成サービスや skill) 生成される内容を改善する
    • 今は提案資料向けに特化してそうな印象があって、装飾の多用もそういう文化圏から持ち込まれているように見える (憶測)
      • 文字が小さいのは、参加者に発表資料が印刷されて配られたり、会議室が小さいことを前提としているからでは
    • 技術イベントでは資料が印刷されて配られることはなく、会場も比較的大きいので大きな文字が好まれる
    • 技術イベント向けに特化したスライドを生成できると便利そう
  • (皆) 技術イベント向けのスライドテンプレートを用意する・使ってもらう
    • 最初から文字サイズが適切で見やすいものを使う
  • (イベント主催者) ベストトーク賞を作る
    • 賞を取るために登壇者を競わせれば、自然と良い発表をしようと意識が向くのでは
    • 発表資料を作る時に、去年のベストトーク賞の発表を参考にしながら資料を作ってくれる、みたいな効果もありそう

皆で協力しあって大 AI 時代も良い技術イベントができると良いなと思います。

*1:Claude Design で作っていた人が 1 人、Gamma で作っていた人が 1 人居たことは確認してます。前者は登壇中に実際にそれで作ったと発言されていて、後者はスクリーンにサービス名が映っていました。

「Node.js は偶数バージョンが LTS」ではなくなる

長らく Node.js では偶数バージョン (v22, v24 など) が LTS としてメンテナンスされてました。しかし 2027/4 リリース予定の v27 以降はそうではなくなります。Node.js 公式ブログで、v27 以降に取り入れられる新しいリリーススケジュールについて説明されています。

かいつまんで説明すると:

  • major リリースは年2回から年1回となる
  • 奇数番号だけでなく全てのリリースが LTS となる
  • 早期テスト目的で利用されてた奇数リリースは alpha チャンネルで代替される
    • alpha 期間は約半年確保
  • v27 から新スケジュールで運用開始
  • 暦に合わせた番号のバージョンがリリースされる
    • 2027 年なら v27、2028 年なら v28...

公式ブログの説明によると Node.js メンテナーのメンテナンスやリリースの負担軽減が主な目的のようですが、我々ユーザによっても分かりやすくなって良いですね。

サービスに利用する Node.js では奇数バージョンの利用を避ける運用をしていたケースが多かったと思いますが、今後はそういう運用も変えていくことになりそうです。

追記: そもそも「LTS」の定義について

「全バージョンが LTS なら、何と比較して Long Term なサポートなんやねん」と思った人もいるかもしれません。ちょっと混乱するのですが、Node.js における「LTS」は major バージョンに割り当てられるステータスのようなものです。新たな major バージョンがリリースされると、まず半年間 Current というステータスになって、その後 30ヶ月間 LTS になります。Current では新機能がどんどん実装されていきますが、LTS ではセキュリティ修正などを主に受け付ける期間になります *1

これは余談ですが、LTS はセキュリティ修正以外も受け付けることがあります。最近だと LTS であった Node.js 20 に require(ESM) が backport されています。

安全に backport できること、エコシステムが ESM 移行を進めるために重要であることなどを理由に、LTS バージョンに backport されたようです。実際このおかげで ESM 移行がより進んでいますから、ありがたいことですね。

*1:v27 からは Current の前に Alpha というステータスが半年間追加されます。

npm package 実装用のテンプレートを更新した

npm-package-template」という、id:mizdra が爆速で npm package を実装するための開発テンプレートがある。以前ブログで紹介した。

github.com

www.mizdra.net

毎年ちょっとした変更を加えているのだけど、今年は色々な事情があって大きな変更を加えた。高速な formatter/linter が登場したり、npmjs.com へのサプライチェーン攻撃が流行ったり、Coding Agent が普及したり... 時代が変化してるので、それに合わせたテンプレートに変えてみた。

ざっと要点を書き出すと以下のような変更をした。

  • Prettier から Oxfmt に移行
  • ESLint から Oxlint に移行
  • 型チェックは oxlint --type-aware --type-check でやる
  • npm-script に lin-fix を追加
  • AGENTS.md/CLAUDE.md を追加
  • tsconfig.base.json を更新
  • Trusted Publishing でリリースする workflow を追加
  • GitHub Actions のバージョンを pinning
  • GitHub リポジトリの基本的な設定をするためのシェルスクリプトを追加

Oxfmt/Oxlint

Oxfmt/Oxlint が普段使いしても良いレベルに成熟してきたので、Prettier/ESLint に乗り換えてみた。Oxlint 向けには @mizdra/oxlint-config という Shareable Config を作って、それを参照する形にしてる。Oxfmt も Oxlint も特に大きな不満なく動いていていい感じ。

Oxfmt は Shareable Config を実現するための仕組みがないので、設定をパッケージ化せずにベタ書きしてる。けど公式リポジトリを見ると oxfmt.config.ts という形式をサポートする issue が立ってたので、近い将来解決するかも。

多分 oxfmt.config.ts が実装されたら、以下のようなコードで Shareable Config を参照できるようになるはず。

// oxfmt.config.ts
export * from '@mizdra/oxfmt-config';

型チェックは oxlint --type-aware --type-check でやる

TypeScript の型チェックは今まで tsc --noEmit でやってたけど、最近 Oxlint に --type-check というオプションが生えたのでこれを使ってみた。

oxc.rs

このオプションを使うと、なんと lint 結果と一緒に TypeScript のエラーを報告してくれる。Oxlint は型情報を使った lint rule を実行するために内部で TypeScript の型チェックをしていて、その結果をそのまま吐くという仕組みになってる。ESLint 時代はこういうのできなかったので便利になったなあと思う。

ちなみに Oxlint は型チェックに tsgo を fork した tsgolint を使ってるので、型チェック自体も爆速で終わるようになってる。速くて嬉しい。

npm-script に lin-fix を追加

lint-fix": "eslint . --fix" みたいなやつのこと。今までの id:mizdra はまあ npx eslint . --fix と打てば良いだけだし無くても良い派だったのだけど、Coding Agent 向けにはあったら便利そうなので追加してみた。

AGENTS.md/CLAUDE.md

どのリポジトリにもあったら良いものということで、npm-script の使い方や commit message の規則、PR に付けるタグのルールを書いたファイルを置いておいた。AGENTS.md が本体で、CLAUDE.md はそれへの symlink にしてる。symlink できるだけ使いたくない気持ちはあるけどこれ以外良い方法を思いつかなかった。

tsconfig.base.json を更新

"types": ["node"]"noUncheckedSideEffectImports": true を追加したり。以前このブログで紹介した tsconfig.json の推奨オプションをベースに、TypeScript 5.9 で推奨となったオプション を追加しただけ。

近々リリースされる TypeScript 6.0 では "strict": true がデフォルトになったりと色々変わるそうなので、また推奨される tsconfig.json のオプションが変わりそうだなと思ってる。TypeScript 6.0 がリリースされたら見直したい。

Trusted Publishing でリリースする workflow を追加

npm の Trusted Publishing が 2025/7 に GA したので、それを使ったリリース workflow を追加した。

GitHub Actions のバージョンを pinning

サプライチェーン攻撃のリスクを軽減するため、pinact run で GitHub Actions のバージョンを固定した。

GitHub リポジトリの基本的な設定をするためのシェルスクリプトを追加

自分のOSSリポジトリにGitHubのセキュリティ設定を入れ、自分用の手順書を作った - $shibayu36->blog; で紹介されている設定を一発で行うシェルスクリプトを用意した。手でちまちま設定するの面倒だなと思ったのでカッとなって作った。

一部抜粋して紹介。

OWNER=$(gh repo view --json owner -q .owner.login)
REPO=$(gh repo view --json name  -q .name)
# Setup common repository settings
gh repo edit \
  --delete-branch-on-merge \
  --enable-auto-merge \
  --enable-discussions=false \
  --enable-projects=false \
  --enable-secret-scanning \
  --enable-secret-scanning-push-protection \
  --enable-wiki=false
# Enable Code scanning
gh api -X PATCH /repos/$OWNER/$REPO/code-scanning/default-setup -f state=configured
# Enable immutable releases
gh api -X PUT /repos/$OWNER/$REPO/immutable-releases
# Setup rulesets
DEFAULT_BRANCH_PROTECTION=$(gh api /repos/mizdra/npm-package-template/rulesets/13184851)
VERSION_TAG_PROTECTION=$(gh api /repos/mizdra/npm-package-template/rulesets/13184887)
gh api -X POST /repos/$OWNER/$REPO/rulesets --input - <<< $DEFAULT_BRANCH_PROTECTION
gh api -X POST /repos/$OWNER/$REPO/rulesets --input - <<< $VERSION_TAG_PROTECTION
# Require actions to be pinned to a full-length commit SHA
gh api -X PUT /repos/$OWNER/$REPO/actions/permissions -F enabled=true -F sha_pinning_required=true

いくつかの設定は gh repo edit でできるのでそれで設定して、残りは gh api で REST API 叩いて設定してる。欲しかった REST API 全部存在してて GitHub すごい。

ruleset の設定はちょっとハックっぽいことをしてて、mizdra/npm-package-template リポジトリの ruleset を export して、それを新しいリポジトリに import してる。これが一番短く書けるのでこうしてる。mizdra/npm-package-template は公開リポジトリで、公開リポジトリの ruleset は誰でも REST API で読み出せるので、一応このスクリプト自体は誰でも実行できると思う。

あと先ほどのコードには書いてなかったけど、id:mizdra はライセンスファイルの更新をするスクリプトも追加してる。最近 id:anatofuz さんが gh コマンドで gitignore ファイルを生成できると呟いていて、それをみて gh コマンドのリファレンス読んだらライセンスファイルを生成するコマンドを発見したので、それを使ってみている。

# Change license
gh repo license view mit | sed "s/\[year\]/$(date +%Y)/;s/\[fullname\]/mizdra/" > LICENSE
npm pkg set license=MIT && npm i

GitHub のラベルの設定をするスクリプトも追加した。github-label-setup自分向けの label preset を使ってる。

# Setup labels
GITHUB_TOKEN=$(gh auth token) npx \
  -p @azu/github-label-setup \
  -p @mizdra/github-label-presets \
  github-label-setup \
  --labels @mizdra/github-label-presets

おわりに

npm-package-template は CC0-1.0 でライセンスしてるので真似したり参考にしてみてください。

@.css-modules-kit/ts-plugin を Emacs で動かすまでの覚書

@.css-modules-kit/ts-plugin を Neovim で動かすまでの覚書 - mizdra's blog の Emacs 版。

Emacs のインストール

macOS なら brew install --cask emacs でインストールできる *1

一応 https://emacsformacosx.com/ から dmg も落とせるけど、dmg からインストールすると emacs コマンドがパスの通ったディレクトリに配置されなくて、ちょっと面倒だった。brew install --cask emacs であれば emacs コマンドもパス通ったディレクトリに配置してくれる。

設定ファイルの置き場所

~/.config/emacs があれば ~/.config/emacs/init.el に、なければ ~/.emacs/init.el に配置される。~/.config/emacs~/.emacs の両方がある時は後者が優先される。

ちなみに emacs --init-directory=~/.config/emacs-test . のようにして Emacs を起動すると、~/.config/emacs-test が設定ファイルの置き場所として使われる。デバッグ目的で初期設定の Emacs を起動したい時や、設定を切り替えたい時に便利。

Emacs Lisp の書き方

init.el は Emacs Lisp で書く。以下の公式ドキュメントを読んで書き方を学ぶと良い。

実際に実行しながら挙動を確かめたいなら、Emacs で M-: (print "Hello World!") などと打ち込むと良い。Emacs Lisp を実行できる。あとは *scratch* バッファに記入して C-j で実行するのも便利。以下のドキュメントに色々方法がまとまってる。

Mode という概念について

Emacs にはエディタの動作を切り替える仕組みがあり、これを「Mode」と呼ぶ。Mode には major mode と minor mode の二種類がある。major mode は特定のファイルタイプに特化した機能を提供するもので、1つのファイルにつき 1つだけ割り当てられる。一方 minor mode は1つのファイルに複数割り当て可能なもの。

例えば typescript-ts-mode という major mode がある。これは TypeScript 向けにシンタックスハイライトなどを提供したり、forward-sexp / backward-sexp などのコマンドによる文字移動を TypeScript に特化した挙動にする。

一方 Eglot (Emacs 組み込みの Language Client) は major mode ではなく、minor mode として動作する。Language Client が特定のファイルに紐づいて動作するわけではなく、エディタ横断で動作する類のもののため、minor mode として実装されている。

major mode は VS Code でいうところの Language Identifiers のような役割も兼ねている。この major mode の名前を使って、Language Server との紐付けを行うことになる。

major mode について

Emacs 29 以降では以下の major mode が組み込まれており、基本的にはこれらを使うのが良い。

  • js-ts-mode: JavaScript 向けの major mode
  • typescript-ts-mode: TypeScript 向けの major mode
  • tsx-ts-mode: TSX 向けの major mode
  • css-ts-mode: CSS 向けの major mode

ちなみに *-ts-modets の部分は tree-sitter の略。tree-sitter を使ってパースをして AST-aware なシンタックスハイライトをしている実装になってて、このような名前になってる。

補足: major mode の名前に -ts- が付いている理由

何故わざわざ -ts- 付けているのか疑問に思うかもしれないが、どうやら歴史的経緯によるものらしい。元々 Emacs には組み込みや 3rd-party で tree-sitter 実装ではない major mode (js-modetypescript-mode) が存在していた。ただそれらの major mode では正規表現を使ってコードを解析しており、ユーザの期待通りのシンタックスハイライトなどを実現できない問題があったようだ。そこで Emacs 29 になって tree-sitter をベースにした major mode が実装された。

しかし古い major mode と新しい major mode は本質的に挙動が異なるものである。突然 js-mode が tree-sitter ベースになると、古い major mode の挙動に依存した init.el が壊れてしまう。またそもそも Emacs のビルドには tree-sitter は必須ではなく、オプショナルである。tree-sitter がない場合も Emacs が機能するようになっていないといけない。後方互換性、そして tree-sitter なしで Emacs がビルドされているケースに対応するため、tree-sitter ベースの major mode はオプトイン (既存の major mode と名前を分けてデフォルトでは有効化しない) とされているようだった。

tree-sitter ベースの major mode を有効化する

tree-sitter ベースの major mode はオプトインで、デフォルトでは有効化されていない。そのためまずはそれらを有効化する設定を書く必要がある。init.el を以下のように編集すれば良い。

;; tree-sitter のセットアップと構文定義のインストール
(use-package treesit
  :ensure nil
  :config
  (setq treesit-font-lock-level 4)
  (setq treesit-language-source-alist
        (append
         '((javascript "https://github.com/tree-sitter/tree-sitter-javascript" "master" "src")
           (typescript "https://github.com/tree-sitter/tree-sitter-typescript" "master" "typescript/src")
           (tsx        "https://github.com/tree-sitter/tree-sitter-typescript" "master" "tsx/src")
           (css        "https://github.com/tree-sitter/tree-sitter-css" "master" "src"))
         treesit-language-source-alist))
  (add-hook 'emacs-startup-hook
    (lambda ()
      (dolist (lang (mapcar #'car treesit-language-source-alist))
        (unless (treesit-language-available-p lang)
          (treesit-install-language-grammar lang))))))

;; major mode の設定
(add-to-list 'auto-mode-alist '("\\.js\\'"  . js-ts-mode))
(add-to-list 'auto-mode-alist '("\\.mjs\\'" . js-ts-mode))
(add-to-list 'auto-mode-alist '("\\.cjs\\'" . js-ts-mode))
(add-to-list 'auto-mode-alist '("\\.jsx\\'" . js-ts-mode))
(add-to-list 'auto-mode-alist '("\\.ts\\'"  . typescript-ts-mode))
(add-to-list 'auto-mode-alist '("\\.mts\\'" . typescript-ts-mode))
(add-to-list 'auto-mode-alist '("\\.cts\\'" . typescript-ts-mode))
(add-to-list 'auto-mode-alist '("\\.tsx\\'" . tsx-ts-mode))
(add-to-list 'auto-mode-alist '("\\.css\\'" . css-ts-mode))

実は Emacs には tree-sitter の構文定義の情報が組み込まれていない。そのため、ただ *.tstypescript-ts-mode を紐づけてもシンタックスハイライトなどが効かない。そこで構文定義を外部からインストールしてくる必要がある。それが設定の前半部分。後半部分が major mode の紐付け。

これで JavaScript/TypeScript/CSS のシンタックスハイライトが機能するようになる。

補足: 構文定義が Emacs に組み込まれていない理由

tree-sitter の構文定義くらい Emacs に組み込んであったら良いのだが、色々理由があってできてないらしい。構文定義のビルドには Node.js と node-gyp が必要なため、Emacs のビルドにそれらが必須になるのは困るとか。構文定義のビルド方法の詳細に Emacs が依存したくないとか。

補足: jsx-ts-mode がない理由

*.tsxtsx-ts-mode に紐づけるのに *.jsxjsx-ts-mode に紐付けないのがびっくりするかも (id:mizdra はびっくりした)。何故こうしているかというと、Emacs には jsx-ts-mode なる major mode が存在しないから。というかそもそも JSX 用の tree-sitter の構文定義が存在しない。JavaScript と JSX で統一されているらしい。

Emacs 側の議論はちゃんと追ってないけど、多分 tree-sitter の構文定義が統一されているので major mode も統一しているのだろう。

Language Server の設定

先ほど少し触れたが、Emacs には Eglot という Language Client が組み込まれている。major mode と Language Server の起動コマンドの組のリストからなる eglot-server-programs という変数があり、この定義に従って Language Server が起動される。ちなみにデフォルトでは JavaScript と TypeScript では typescript-language-server が Language Server の実装として使われる。

Eglot は minor mode であり、デフォルトでは起動されない。特定の major mode が ON になったら、Eglot を起動するような設定を書く必要がある。

まずは Language Server をインストールしておく。

$ npm i -g typescript-language-server typescript

そして use-package:hookeglot-ensure を使って以下のような設定を書く。

;; tree-sitter のセットアップと構文定義のインストール
(use-package treesit
  :ensure nil
  :config
  (setq treesit-font-lock-level 4)
  (setq treesit-language-source-alist
        (append
         '((javascript "https://github.com/tree-sitter/tree-sitter-javascript" "master" "src")
           (typescript "https://github.com/tree-sitter/tree-sitter-typescript" "master" "typescript/src")
           (tsx        "https://github.com/tree-sitter/tree-sitter-typescript" "master" "tsx/src")
           (css        "https://github.com/tree-sitter/tree-sitter-css" "master" "src"))
         treesit-language-source-alist))
  (add-hook 'emacs-startup-hook
    (lambda ()
      (dolist (lang (mapcar #'car treesit-language-source-alist))
        (unless (treesit-language-available-p lang)
          (treesit-install-language-grammar lang))))))

;; major mode の設定
(add-to-list 'auto-mode-alist '("\\.js\\'"  . js-ts-mode))
(add-to-list 'auto-mode-alist '("\\.mjs\\'" . js-ts-mode))
(add-to-list 'auto-mode-alist '("\\.cjs\\'" . js-ts-mode))
(add-to-list 'auto-mode-alist '("\\.jsx\\'" . js-ts-mode))
(add-to-list 'auto-mode-alist '("\\.ts\\'"  . typescript-ts-mode))
(add-to-list 'auto-mode-alist '("\\.mts\\'" . typescript-ts-mode))
(add-to-list 'auto-mode-alist '("\\.cts\\'" . typescript-ts-mode))
(add-to-list 'auto-mode-alist '("\\.tsx\\'" . tsx-ts-mode))
(add-to-list 'auto-mode-alist '("\\.css\\'" . css-ts-mode))

;; Language Server のセットアップ
(use-package eglot
  :ensure nil
  :hook ((js-ts-mode         . eglot-ensure)
         (typescript-ts-mode . eglot-ensure)
         (tsx-ts-mode        . eglot-ensure)))

これで JavaScript/TypeScript で Language Server が起動するようになったはず。

@css-modules-kit/ts-plugin のセットアップ

@css-modules-kit/ts-plugin は TypeScript Language Service Plugin なので、TypeScript の Language Server に読み込ませる、というステップが必要になる。

まずは @css-modules-kit/ts-plugin をインストールしておく。

npm i -g @css-modules-kit/ts-plugin

そして init.el を以下のように書き換える。

;; tree-sitter のセットアップと構文定義のインストール
(use-package treesit
  :ensure nil
  :config
  (setq treesit-font-lock-level 4)
  (setq treesit-language-source-alist
        (append
         '((javascript "https://github.com/tree-sitter/tree-sitter-javascript" "master" "src")
           (typescript "https://github.com/tree-sitter/tree-sitter-typescript" "master" "typescript/src")
           (tsx        "https://github.com/tree-sitter/tree-sitter-typescript" "master" "tsx/src")
           (css        "https://github.com/tree-sitter/tree-sitter-css" "master" "src"))
         treesit-language-source-alist))
  (add-hook 'emacs-startup-hook
    (lambda ()
      (dolist (lang (mapcar #'car treesit-language-source-alist))
        (unless (treesit-language-available-p lang)
          (treesit-install-language-grammar lang))))))

;; major mode の設定
(add-to-list 'auto-mode-alist '("\\.js\\'"  . js-ts-mode))
(add-to-list 'auto-mode-alist '("\\.mjs\\'" . js-ts-mode))
(add-to-list 'auto-mode-alist '("\\.cjs\\'" . js-ts-mode))
(add-to-list 'auto-mode-alist '("\\.jsx\\'" . js-ts-mode))
(add-to-list 'auto-mode-alist '("\\.ts\\'"  . typescript-ts-mode))
(add-to-list 'auto-mode-alist '("\\.mts\\'" . typescript-ts-mode))
(add-to-list 'auto-mode-alist '("\\.cts\\'" . typescript-ts-mode))
(add-to-list 'auto-mode-alist '("\\.tsx\\'" . tsx-ts-mode))
(add-to-list 'auto-mode-alist '("\\.css\\'" . css-ts-mode))

;; Language Server のセットアップ
(use-package eglot
  :ensure nil
  :hook ((js-ts-mode         . eglot-ensure)
         (typescript-ts-mode . eglot-ensure)
         (tsx-ts-mode        . eglot-ensure)
         (css-ts-mode        . eglot-ensure))
  :config
  (require 'subr-x) ;; string-trim
  (setq npm-root
        (string-trim (shell-command-to-string "npm root -g")))
  (add-to-list
    'eglot-server-programs
    `(((js-ts-mode :language-id "javascript")
       (typescript-ts-mode :language-id "typescript")
       (tsx-ts-mode :language-id "typescriptreact")
       (css-ts-mode :language-id "css"))
      . ("typescript-language-server" "--stdio"
          :initializationOptions
          ((plugins
            . [((name      . "@css-modules-kit/ts-plugin")
                (location  . ,npm-root)
                (languages . ["css"]))]))))))

やってることは Neovim の時とほぼ同じ。CSS でも typescript-language-server が起動されるようにしつつ、@css-modules-kit/ts-plugin を plugin として読み込ませている。:hook(css-ts-mode . eglot-ensure) を足しているのもポイント。これがないとそもそも CSS で Eglot が起動してくれない。

厳密には eglot-server-programs にデフォルトの JavaScript/TypeScript の Language Server を起動する設定が残っているから、それを新しいもので置換したほうが行儀は良いと思う。ただ Eglot は eglot-server-programs の先頭からリストを探索して major mode に一致するものがあったらそれを使う挙動になっている。そのため、add-to-list で先頭に設定を追加してしまえば、古いものがリスト内の残っていても特に問題がない。

という訳でこれで @css-modules-kit/ts-plugin が Emacs でも動くようになった。めでたしめでたし。

youtu.be

おまけ: CSS Language Server と共存できない問題について

勘の良い人は気づいたかもしれないが、Eglot は 1 つの major mode に複数の Language Server を起動することができない。eglot-server-programs の先頭に定義されている Language Server が優先して起動されることになる。

つまり今回 id:mizdra が紹介した init.el では、*.css に対して TypeScript/CSS 両方の Language Server を起動できない。TypeScript の Language Server だけが起動される。そのため @css-modules-kit/ts-plugin が提供する言語機能 (クラスセレクターの Go to Definition や Find All References など) は機能するが、CSS の Language Server が提供する言語機能 (プロパティの補完など) は機能しない。

この問題については長らく Eglot の discussion で議論されていたのだけど、つい最近になって Eglot ではサポートしないという方針になったようだった。

代わりに https://github.com/joaotavora/rassumfrassumhttps://github.com/thefrontside/lspx のような LSP マルチプレクサの使用を推奨している模様。まあ気持ちはわかる... わかるが Emacs 組み込みのパッケージなのだから組み込みで Multiple Server サポートしてほしいなー。

@css-modules-kit/ts-plugin を使いつつ、*.css に対して TypeScript/CSS 両方の Language Server の両方を起動するなら多分以下のような設定になるはず。

;; Language Server のセットアップ
(use-package eglot
  :ensure nil
  :hook ((js-ts-mode         . eglot-ensure)
         (typescript-ts-mode . eglot-ensure)
         (tsx-ts-mode        . eglot-ensure)
         (css-ts-mode        . eglot-ensure))
  :config
  (require 'subr-x) ;; string-trim
  (setq npm-root
        (string-trim (shell-command-to-string "npm root -g")))
  (add-to-list
    'eglot-server-programs
    `(((js-ts-mode :language-id "javascript")
       (typescript-ts-mode :language-id "typescript")
       (tsx-ts-mode :language-id "typescriptreact")
       (css-ts-mode :language-id "css"))
      . ("lspx" "--lsp" "typescript-language-server --stdio" "--lsp" "vscode-css-language-server"
          :initializationOptions
          ((plugins
            . [((name      . "@css-modules-kit/ts-plugin")
                (location  . ,npm-root)
                (languages . ["css"]))]))))))

けどこれ、vscode-css-language-server にも initializationOptions が渡ってしまうし、そもそも vscode-css-language-server*.ts の LSP Request が飛んでしまうし、全然ダメな気がする。lspx では上手くいかなさそう。rassumfrassum もドキュメント読む限りはこういうケースには対応してなさそう。打つ手なし...

まあ CSS の Language Server が提供する言語機能 (プロパティの補完など) は機能しないのは諦めてもらうということで...

おまけ: Language Server のデバッグテクニック

TypeScript Language Service Plugin や typescript-language-server が出力するログを見るには、tsserver.log を出力すると良い。TSS_LOG 環境変数を設定して Emacs を起動すれば良い。

TSS_LOG="-level verbose -file /tmp/tsserver.log" emacs -nw .

*1:Nonfree systems という見出しの下に macOS が書いてあって GNU らしさがある。

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

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