mizdra's blog

ぽよぐらみんぐ

インストールする VS Code 拡張機能を減らした

かつては VS Code 拡張機能をインストールしまくっていて 65 個あったけど、最近はできるだけインストールしないようにしてる。数が多いとセキュリティリスク高まるので。

今インストールしてるのは以下の 24 個:

alefragnani.bookmarks
anthropic.claude-code
dbaeumer.vscode-eslint
esbenp.prettier-vscode
github.codespaces
github.copilot
github.copilot-chat
github.vscode-pull-request-github
golang.go
hediet.vscode-drawio
intellsmi.comment-translate
mizdra.css-modules-kit-vscode
monokai.theme-monokai-pro-vscode
ms-ceintl.vscode-language-pack-ja
ms-vscode-remote.remote-containers
ms-vsliveshare.vsliveshare
oxc.oxc-vscode
pnp.polacode
redhat.vscode-yaml
rust-lang.rust-analyzer
streetsidesoftware.code-spell-checker
stylelint.vscode-stylelint
vadimcn.vscode-lldb
vitest.explorer

普段は JavaScript 書きまくっているので、それ関連の拡張機能が多め。ここ数年で変わったこととしては、Rust で書かれた JavaScript 向けツール (Oxc, Rolldown, Biome, Turbopack, ...) が増えたために rust-lang.rust-analyzervadimcn.vscode-lldb を使うようになったこと。ツールのコードよく読みにいくので、これがないと困る。Go で書かれたツール (typescript-go, esbuild) もよく遭遇するので、golang.go もインストールしてる。

intellsmi.comment-translate は最近 mizchi さんの記事見てインストールしたけど、めっちゃ便利だった。設定はこんな感じでやってる。source"Copilot" にしているのがポイント。

{
  "commentTranslate.targetLanguage": "ja",
  "commentTranslate.hover.concise": true,
  "commentTranslate.multiLineMerge": true,
  "commentTranslate.source": "Copilot"
}

github.vscode-pull-request-github は色々便利な機能があるのでインストールしてる。選択してる行の GitHub 上での URL がコピーできる機能とか。Control+Command+g に keybind 設定してよく叩いてる。

zenn.dev

以前は inline で git blame を表示するために eamodio.gitlens をインストールしていたけど、最近は VS Code に同等の機能が実装されてる。"git.blame.editorDecoration.enabled": true で有効にできる。eamodio.gitlens はアンインストールして今はこれを使ってる。

たまーに Python や Swift など他の言語のリポジトリを触ることになって、色々追加で拡張機能を入れないといけなくなった時は、VS Code の Profile を新たに作ってそっちにインストールすることで凌いでる。こんな感じで、Settings や Keyboard Shortcuts は Default Profile のものを引き継ぐけど、Extension だけ別にしてる。

Default Profile の拡張機能を引き継ぎつつ、新しい Profile 向けに専用の拡張機能をインストールする...みたいなことはできないので、そこがネックではある。今は新しい Profile を作成したあと、Default Profile にあったものでどうしても必要なものを新しい Profile にもインストールし直してる。面倒ではあるけどまあ何とかなってる。


拡張機能のリストは code --list-extensions で出力できます。皆さんもこの際に整理してみてはいかがでしょうか。

css-modules-kit の内部設計: CSS Modules のパースについて

css-modules-kit は CSS Modules のためのツールセットです。何ができるのか、どんな設計で作られているのかは以下の記事を見てください。

www.mizdra.net

www.mizdra.net

この記事では css-modules-kit の内部設計について紹介してみます。今回は CSS Modules のパースについてです。

3種類のツールと core パッケージについて

css-modules-kit は codegen, ts-plugin, linter-plugin (eslint-plugin or stylelint-plugin) の 3 種類のツールから構成される。それぞれ独立したパッケージになってるのだけど、コア部分は core に切り出されてて、3 種類のツールから依存されてる。

.module.css のパーサー

core には色々なロジックや utility が置かれているのだけど、その中に .module.css のパーサーがある。packages/core/src/parser にそのコードがある。

内部では postcss で AST に変換して、それを走査して必要な情報を集めて返してる。一般的に「パーサー」というと AST を返すものをイメージするけど、それとはちょっと違う。css-modules-kit においては AST から余計な情報を削ぎ落としたデータ構造である CSSModule をパーサーから返してる。

CSSModule の構造は packages/core/src/type.ts で定義されてる。

// https://github.com/mizdra/css-modules-kit/blob/89f11a54af1dc86b344e151b63bc2708486c31bb/packages/core/src/type.ts#L98-L121
export interface CSSModule {
  fileName: string;
  text: string;
  /** `.foo {}` や `@keyframes bar {}` などで定義されるトークンの情報  */
  localTokens: Token[];
  /**
   * `@import './other.module.css';` や `@value val from './other.module.css'` などを使った
   * トークンの import 文の情報。今回の説明は省略。
   */
  tokenImporters: TokenImporter[];
  /** パース時に検出された構文エラーなど。今回の説明は省略。 */
  diagnostics: DiagnosticWithLocation[];
}
export interface Token {
  name: string;
  loc: Location;
  declarationLoc?: Location;
}
export interface Location {
  start: Position;
  end: Position;
}
export interface Position {
  line: number;
  column: number;
  offset: number;
}

「Token」という謎の用語があるけど、これは css-modules-kit の内部用語で、.module.css から export されるアイテムを表してる。クラス名とか @keyframes で定義されたキーフレームの名前とか、@value val: #123; による変数とか。export されるのはクラス名だけではないので、「Token」という用語を導入している。

それを踏まえて型を見てもらうと分かるが、トークンの名前や位置情報、構文エラーなどの情報が入ってる。それ以外の情報、例えばプロパティ に関するものは全く持たない。

独自のデータ構造を返している理由

生の postcss の AST を返さずに、独自のデータ構造を返しているのには色々な理由がある。

理由1: CSS Modules として .tsx 側に export されるもの以外の情報が不要だから

単に不要なものは持たないほうが、シンプルになって codegen などから扱いやすい。それはそう。

理由2: postcss に依存しないようにしたかったから

postcss に非依存にすることで、postcss 以外の CSS パーサーに後から差し替え可能にしたかった。

というのも今主要な CSS パーサーには 「postcss」「csstree」「lightningcss」の 3 つがある。css-modules-kit を最初に作るときにどれを使うか悩んだのだけど、結局 postcss を採用した。postcss が優れているからとかではなくて、単に id:mizdra が以前作った happy-css-modules というツールの内部で postcss で CSS Modules をパースしていて、そのコードを参考に実装できたから。

postcss はそこそこ枯れていて良いのだけど、まあまあ微妙なところがある。例えばパフォーマンスや後方互換性を理由にプロパティの value やセレクター部分のパースが本体ではサポートされてなくて、別のライブラリに頼らないといけない。

ここ最近のサプライチェーン攻撃の流行を考えるとあんまり依存関係増やしたくはなくて、困ってる。csstree なら value 部分もセレクターもそれだけでパースできるみたいなので、その点を考えると csstree のほうが良いなーと思ってる。

lightningcss は Rust 製で速くて良いと思うけど、パフォーマンスを理由にパーサーの API が Node.js 向けに提供されてない。Rust 向けにだけ提供されてる。もし css-modules-kit で使うなら、AST にして CSSModule 型のデータを生成するところまでを Rust で書いて、それを Node.js に転送する、みたいな実装をしないといけない。やったらできるけど面倒だし、Rust <=> Node.js 間のデータ転送にコストが掛かるので、速くなるのかよくわからない。

postcss から csstree なり lightningcss なり乗り換えるとしても、ちゃんと互換性を維持したまま乗り換えられるかは正直不透明だなと思う。css-modules-kit が欲しい情報が、AST に含まれていないと安全に移行できないと思う。クラス名やキーフレーム名の位置情報を正確に取得できるのかとかね。postcss も最初は一部情報が欠けてて、id:mizdra が PR 送って直していたりしたくらいなので、csstree やlightningcss もきっと同じようなことになるだろう。とにかく面倒な予感がする。

とまあパーサーを差し替えたい気持ちはあるけど、色々な理由があって進んでない。けど将来的に差し替えられるように、データ構造は工夫してる。

typescript-go が来たら、それを受けて css-modules-kit も Go にしましょうみたいな機運が出てくるかもなとは思ってて、そうしたら lightningcss なり検討するかも。まあまだ先の話だなと思う。

普段使いできる保護レイヤー「restricted shell」の紹介

これは はてなエンジニア Advent Calendar 2025 1日目の記事です。


はてなでフロントエンドエキスパートをしている id:mizdra です。普段は JavaScript を書いてて、趣味で色々なツールを作ってます。

ところで最近、npmjs.com へのサプライチェーン攻撃が話題ですね。以前から npmjs.com ではサプライチェーン攻撃が発生していましたが、今年は規模が大きいものが頻繁に発生しています。

マルウェアをインストールしない・実行しないことが一番ではあるのですが、昨今の npmjs.com への攻撃の様子を踏まえると、そうも言っていられません。そのため、何個も保護レイヤーを設ける「多層防御」という考えが重要です。マルウェアをインストールしない・実行しないための対策とは別に、仮にマルウェアが実行されても、その影響を抑える対策があると良いでしょう。

最も手軽な対策は、ファイルシステム上に機密情報を置かないことです。具体的には、以下のようなものです。

どれも比較的無理なくできるものなので、ぜひやってみると良いでしょう。

一方で、Sandbox を利用するという対策もあります。

  • Development Container (a.k.a. Dev Container) を使う
    • Docker コンテナの中で開発する
  • ブラウザベースの開発環境を使う
    • GitHub Codespaces, Stackblitz, vscode.dev など

Sandbox 内には、そのリポジトリのファイルしか mount されません。そのため、マルウェアの影響をそのリポジトリの中だけに制限できます *1

Sandbox を使った対策の欠点

Sandbox は便利な反面、導入が面倒な技術です。例えば、ホスト側からポートにアクセスできるよう明示的な許可が必要です。ボリュームの mount の設定も必要です。Development Container の場合は、中でシェルを起動して作業できるようになってますから、そのシェルから使う ツール類の設定なども必要でしょう。場合によっては、アプリケーション側のコードの改修も必要かもしれません。

Sandbox は導入と運用にそれなりのコストが掛かってしまいます。マルウェアに対する良い対策だと分かっていても、中々導入は難しいのです。

「restricted shell」の紹介

とはいえ、やはり Sandbox 相当のものは欲しいです。そこで Sandbox と同じような効果が得られて、もっと手軽に使えるものを作ってみました。それが「restricted shell」です。はじめに言っておきますが、macOS 限定、かつ ghq でリポジトリを管理しているユーザ限定です。

restricted shell が動作する様子。

リポジトリを cd すると、restricted shell という制限の掛かったサブシェルが起動します。その中では今いるリポジトリから他のリポジトリのファイルを読み取れなくなります。

他のリポジトリのファイルの読み取りが禁止されるだけで、それ以外の制限はありません。そのため、以下のようなことができます。

  • npm install できる
    • ネットワークアクセスは自由
  • ^R でシェルの履歴が見れる
    • ホームディレクトリ配下のファイルは読み取り制限なし
  • プロセスも起動し放題

Sandbox というよりは、非常に緩い保護レイヤーのようなものです。普段使いできるように、どのリポジトリでも使えるように、意図的に緩い制限にしています。

とはいえ制限を解除して作業したいこともあるでしょう。そこで脱出ハッチとして exit で restricted shell を終了できるようにしてます。また、rsh で再度 restricted shell を起動することも可能です。

exit で restricted shell を終了、rsh で再度起動する様子。

あとサブシェル (restricted shell) から抜ける時に、サブシェルの履歴とカレントディレクトリを親シェルに引き継ぐようにしてます。そのおかげで、restricted shell を終了してもシームレスに作業を継続できます。

普段使いするものなので、使い勝手には拘って設計してます。

仕組み

Apple Seatbelt という技術を使っています。macOS に搭載されているセキュリティ保護機能で、ルールベースでプロセスに制限をかけられます。

yuzuhara.hatenablog.jp

blog.syum.ai

今回はこの Apple Seatbelt で、特定のディレクトリへのアクセスが制限されたサブシェルを立ち上げています。Apple Seatbelt による制限は、親プロセスから子プロセスへと継承されます。そのため、サブシェル内から起動されるあらゆるプロセス (ls .. など) が制限を継承し、ls .. などがエラーでコケるようになります。

実装

実装は Apple Seatbelt の設定ファイル、.zshrc、プロンプトの設定 (starship.toml) に分かれています。まず Apple Seatbelt の設定ファイルから。

;; .config/sandbox/restricted-shell.sb
(version 1)
(allow default)

(deny file-read*
  (subpath (param "GHQ_ROOT"))
)

(allow file-read*
  (subpath (param "REPOSITORY"))
)

;; starship がリポジトリの親ディレクトリのメタデータを読みに行くようなので、例外的に許可。
;; 本当はもうちょっと厳しくしたい。
(allow file-read-metadata
  (subpath (param "GHQ_ROOT"))
)

REPOSITORY が今 cd してるリポジトリのパスを表すパラメータで、GHQ_ROOT が ghq のルートディレクトリを表すパラメータです。一括で GHQ_ROOT の読み取りを禁止し、その上で REPOSITORY を例外的に許可してます。

.zshrc は以下の通り。

PREV_REPOSITORY=""
function start-restricted-shell-if-needed() {
  local ghq_root="$(ghq root)"
  local repository=""
  [[ "$PWD" =~ ^$ghq_root/[^/]+/[^/]+/[^/]+ ]] && repository="$MATCH"
  
  if [[ -z "$repository" ]]; then
    PREV_REPOSITORY=""
  elif [[ "$RESTRICTED_SHELL" != "1" && "$repository" != "$PREV_REPOSITORY" ]]; then
    PREV_REPOSITORY="$repository"
    echo "info: Start a restricted shell. (To exit, type 'exit'.)"
    RESTRICTED_SHELL=1 sandbox-exec -f ~/.config/sandbox/restricted-shell.sb -D GHQ_ROOT="$ghq_root" -D REPOSITORY="$repository" "$SHELL"
    fc -R
    cd "$(</tmp/rsh-pwd)"
  fi
}
function restricted-shell-exit() {
  if [[ "$RESTRICTED_SHELL" == "1" ]]; then
    pwd > /tmp/rsh-pwd
  fi
}
autoload -Uz add-zsh-hook
add-zsh-hook precmd start-restricted-shell-if-needed
add-zsh-hook zshexit restricted-shell-exit
alias rsh='PREV_REPOSITORY="" start-restricted-shell-if-needed'

色々書いてありますが、重要なのは add-zsh-hook precmd ... の部分と、start-restricted-shell-if-needed 関数の中身です。zsh の precmd hook を使い、プロンプトが表示される際にリポジトリのディレクトリかどうかチェックしてます。そして、リポジトリのディレクトリなら RESTRICTED_SHELL=1 sandbox-exec -f ~/.config/sandbox/restricted-shell.sb ... $SHELL でサブシェルを起動してます。

コードの他の部分は、 サブシェルの履歴とカレントディレクトリの引き継ぎ、rsh コマンドに関するものです。色々な機能がありますが、これ全部で 25 行で済んでます。

最後に、プロンプトの設定 (starship.toml) です。RESTRICTED_SHELL=1 なら鍵の絵文字を出してるだけです。これの導入は必須ではないですが、あると便利です。

format = """
$time\
$cmd_duration\
$jobs\
$status\
$username\
$hostname\
$localip\
$shlvl\
$directory\
$git_branch\
$git_state\
$git_metrics\
$git_status\
$custom\
$sudo\
$shell\
$character"""

[custom.restricted_shell]
command = "echo 🔒"
when = ''' test "$RESTRICTED_SHELL" = 1 '''

starship を使ってない人は、なんか上手いことやってください。

実際使ってみてどうか

まだ使い始めて 1 週間くらいですが、趣味でも業務でも苦なく使えています。普段使いできる保護レイヤーとしての役目は果たせてそうです。

ちなみに open . で Finder でディレクトリを開くのも、ちゃんと動きます。当初は Finder が Apple Seatbelt の制限を引き継ぐせいで上手く動かないのでは、と思っていたのですが、制限を引き継がずに起動されるようです。open で起動されたアプリケーションは、シェルではなく launchd が親プロセスになり、その結果 Apple Seatbelt による保護が効かないようです。保護を回避できて安全性が下がってしまってますが、普段使いする分にはこちらのほうが都合が良いとも言えます。

ちょっとしたトラブルもありました。VS Code をシェルから起動する code コマンドが上手く動作しないのです。具体的には、code . と打っても、カレントディレクトリを workspace として開いてくれません。

カレントディレクトリが workspace として開かれない様子。

code コマンドの内部では open -n で VS Code を起動するのですが、その -n オプション (既にアプリケーションが起動していても新しいインスタンスを起動するオプション) の挙動が Apple Seatbelt の有無で変わるようです。

どうやら Apple Seatbelt では (allow default) していても、強制的に課される制限があるようです。id:mizdra が確認した限りでは、ps コマンドや top コマンドの実行が制限されていました。

;; ~/.config/sandbox/allow-default.sb
(version 1)
(allow default)
$ sandbox-exec -f ~/.config/sandbox/allow-default.sb ps
sandbox-exec: execvp() of 'ps' failed: Operation not permitted
$ sandbox-exec -f ~/.config/sandbox/allow-default.sb top
sandbox-exec: execvp() of 'top' failed: Operation not permitted

恐らく、他のプロセスの情報の取得が制限されているのだと思います。そのため open -n のようなコマンドが動かないのでしょう。

色々調べたところ、code コマンドを使わずに直接 open コマンドで VS Code を開けば、問題を回避できるようでした。僕は以下のような alias を貼るようにしてます。

if [[ "$RESTRICTED_SHELL" == "1" ]]; then
  alias code='open -a "Visual Studio Code"'
fi

とはいえこれも完璧ではありません。code --wait のようなオプションが動作しなくなってしまいます *2EDITOR="code --wait" を使ってる人は EDITOR="vim" に切り替えたりと、追加の対策を導入する必要があります。

code . が動かないのはちょっと面倒ですが、回避策もあるのでまあ...という感じですね。

面白いところ

ちょっと面白いのは、restricted-shell.sb を編集することで自由に保護レベルをカスタマイズできるところです。.zshrcrestricted-shell.sb の書き換えを禁止したり、opendocker の起動を禁止したり。好みに応じて変えられます。

やろうと思えばうんと厳しくもできます。Apple Seatbelt では (deny default) と書くと、デフォルトで全てが禁止され、allowlist のような形でルールを記述するモードになります。そうすれば、Sandbox のような厳しい制限も課せると思います。まあ普段使いするには厳しいでしょうが... それやるくらいなら、普通に Development Container 使ったら良いと思います。

macOS 以外への移植

Apple Seatbelt 相当のものがあれば、他の OS へも移植できるはずです。

僕は詳しくないですが、Linux だと AppArmor というものが使えるそうです。後日同僚の id:Windymelt さんが Linux 版の記事を書いて下さるようなので、それを待って下さい。

それ以外の OS についても、興味がある人がいたら移植してみてください。

大事なこと

もちろん、restricted shell は完璧ではありません。restricted shell は (少なくとも今回紹介した sb ファイルでは) ファイルアクセスくらいしか制限してません。マルウェアからネットワークリクエストも、プロセスの起動も仕放題です。open コマンドを使えば保護を回避できます。マルウェアが本気を出せば、きっと色々なことができてしまうでしょう。Sandbox と呼ぶにはお粗末なものです。

セキュリティ対策に銀の弾丸はありません。様々な対策を幾重にも導入し、地道にリスクを下げていくべきです。例えば、以下のようなことをやると良いでしょう。

心構えや勉強も大事です。

  • 信頼できる package をインストールするよう心がける
  • 脆弱性や攻撃の情報に目を光らせ、発見されたらすぐに対応する
  • セキュリティに関する書籍を読む

restricted shell はあくまで保護レイヤーの 1 つです。基本的なセキュリティ対策を疎かにしないよう気をつけましょう。

おわりに

この記事では「restricted shell」を紹介しました。本当にそのような緩い保護レイヤーで役に立つのか、疑問に感じる人も居るかもしれません。一方で、普段使いできる追加の保護レイヤーと考えると、面白い仕組みではないかと思ってます。

僕自身まだ1週間しか使ってないので、本当に使えるものなのか正直よく分かってません。しばらく使ってみて、使用感を確かめたいと思ってます。そのような感じですが、もし興味あれば使ったり、参考にしてみてください。

はてなエンジニア Advent Calendar 2025 2日目の担当は id:pokutuna さんです。

合わせて読みたい

www.mizdra.net

*1:もちろんリポジトリ内に GitHub の Token があって、それにすべてのリポジトリの読み取り権限が付いていたりしたら、その限りではないですが...

*2:--wait のハンドリングが code コマンドの中で行われているためです

YAPC::Fukuoka 2025 で登壇しました

2025/11/14〜15 に福岡工業大学で開催された、YAPC::Fukuoka 2025 に参加してきました。

yapcjapan.org

登壇

ありがたいことにプロポーザルが無事通り、「機密情報の漏洩を防げ! Webフロントエンド開発で意識すべき漏洩パターンとその対策」というタイトルで登壇させて頂きました。

speakerdeck.com

Web フロントエンドはユーザに直接面している領域故に、機密情報の漏洩が起きやすいです。また Next.js など、最近 Web フロントエンドの開発で使われているフレームワークでは、特に機密情報を漏洩させやすいポイントが存在します。そして、Perl の Xslate や Ruby の erb など、テンプレートエンジンを使い慣れている人ほどそれにハマりやすい...と私は考えています。

そこでこの発表では、従来技術 (Xslate や erb) と比較して Next.js などがなぜ漏洩を起こしやすいのか、漏洩を防ぐにはどうしたら良いのか、その他細かい実用的なテクニックについて紹介しています。

id:mizdra は普段の業務で、フロントエンドのエキスパートとして機密情報の漏洩が起きないように注意を払っています。とはいえ、エキスパートだけが注意を払って取り組むのは、健全とは言えません。やはりチーム全体で取り組みたいものです。そのためにも、チームのメンバーにまずはこれを読んでと渡せる資料が欲しかった!

今回のスライドは、まさにその入門資料として使って頂けると思います。ぜひ皆さんのチームでも、このスライドを紹介してみてください。

面白かったセッション

id:sosukesuzuki さんのセッションが良かったです。「時間を可能な限り捻出して、集中して使う」というのは、すごく分かるなーと思いながら聞いてました。

speakerdeck.com

id:Songmu さんの deck の話も良かったです。コツコツ改善を入れて大きな成果が出るまでの道筋が面白かった。

www.docswell.com

観光

YAPC の前日に現地入りして、下関・福岡観光していました。

まず博多駅から特急ソニックで小倉駅まで行き、そこから JR 山陽本線で下関駅に行きました。

JR 山陽本線ですが、門司駅と下関駅の間は海底トンネルを通るようでした。九州から本州にトンネルで渡る体験は面白かったですね。あと途中にデッドセクションがあって電気が消えたりして、なるほどな〜となってました。

www.ku-hibino.com

下関駅前はこんな感じ。

下関駅前の様子。

その後はオーヴィジョン海峡ゆめタワーに。

オーヴィジョン海峡ゆめタワーを下から見上げた様子。

なぜタワーに行ったかというと、僕が「All-Japan タワーズ スタンプラリー」をやっているからです。全国の技術イベント行くついでにやると面白いかなと思ってやってみています。

無事スタンプゲットです。押すのミスってぼやけちゃった。

タワーからは関門海峡が見えました。こうしてみるとすごく狭い海峡なんですね。

どうでもよいですが、スタンプラリーを主催する「全日本タワー連盟」の会議が、当日このタワーで行われていたようです。こんな偶然あるんですね。

そのまま西へと移動し、関門トンネル人道へ。歩いて九州へと渡りました。

その後は門司駅まで行って、電車や新幹線を使い博多へ。博多からタクシーで福岡タワーへ。そう、スタンプラリー2箇所目です。

福岡タワーの様子。夜はライトアップされていました。見えづらいですが、左側にクリスマスツリーの模様が描かれてます。

スタンプゲット。フータくんだそうです。

福岡タワーは夜景が綺麗でした。オススメです。

とまあ観光はこんな感じでした。本当は下関でフグ食べたり、博多で博多ラーメン食べたりしたかったのですが、そのような時間は全く無かった!!! 博多と下関の移動がかなり掛かってしまった... 博多-小倉間の移動には特急ソニックではなく、新幹線を使ったほうが良いです。

名物の食べ物食べられなかったの悔しいので、いつの日かリベンジしたいですね。

JSConf JP

YAPC に続けて何故かJSConf JP があり、YAPC が終わった次の日の朝、福岡から東京まで飛んで駆けつける...ということをやってました。なんか今年の YAPC はフロントエンドや JavaScript 畑の人が多かった気がします。どっちも行ってる人そこそこ見かけました。皆さん元気いっぱいですごい。

JSConf JP はともかく、YAPC で自分と似たような領域の人と沢山会えて、色々お話できて嬉しかったです。来年も色々な方と会えると嬉しいですね。

おわりに

良い発表ができて、面白い発表が聞けて、いろんな人と話せて楽しかったです。来年はベストスピーカー賞取りたいね、と yusukebe さんと話して盛り上がっていたので、頑張りたいと思います。ベストスピーカーが取りやすいトーク、みたいなのはあるんだろうけど、やっぱり自分らしい発表で取れると格好良いなー。

ghq+peco とゴミ置き場

ghq は、git clone したリポジトリを特定のディレクトリ規則に従って管理するコマンドです。そして peco は、インクリメンタル検索をするコマンドです。この 2 つを組み合わせると、git clone したリポジトリを簡単に cd できます。

# ~/.gitconfig
[ghq]
    root = ~/src
# ~/.zshrc
function peco-src() {
  local selected_dir=$(ghq list | peco --query "$LBUFFER")
  if [[ -n "$selected_dir" ]]; then
    BUFFER="cd $(ghq root)/$selected_dir"
    zle accept-line
  fi
  zle redisplay
}
zle -N peco-src
stty stop undef # disable builtin "^s" keybind
bindkey '^s' peco-src

ghq+peco でディレクトリを切り替える様子。

数多くのブログで紹介されている有名なテクニックですので、使っている人も多いでしょう。使ってない人は是非使ってみて下さい。

ゴミ置き場

ghq+peco により、バージョン管理システム (VCS) で管理されるリポジトリを整理できる訳ですが、現実には VCS で管理されないものも扱いたくなります。例えば、ライブラリの動作確認のためのリポジトリや、ライブラリの不具合を再現するためのリポジトリとかです。ちょっとしたコードは書きたいけど、わざわざ git 管理するほどでもない、みたいなやつです。

そうしたリポジトリを管理するため、id:mizdra~/src/localhost/gomi ディレクトリを作ってます。

~/src
├── github.com
│   ├── x-motemen
│   │   └── repository-1
│   └── mizdra
│        ├── repository-1
│        └── repository-2
└── localhost
     └── gomi
          ├── repository-1
          └── repository-2

ゴミ置き場なので gomi です。直球ですね。

気軽に ~/src/localhost/gomi に移動できるよう、zsh で auto_cd と名前付きディレクトリを設定しています。これで ~gomi と打つだけで移動できます *1

# ~/.zshrc
setopt auto_cd
hash -d gomi=~/src/localhost/gomi

VCS 管理しないものは ~gomi に置いていき、不定期に rm -rf ~gomi/* で削除しています。もし何らかの事情で ~gomi に置いたものを VCS 管理したくなったら、git push => rm ~gomi/repo => ghq get ... で ghq 管理のディレクトリへと移動する運用にしてます。

~gomi と ghq+peco の相性問題

~gomi は便利ではあるのですが、冒頭で紹介した ghq+peco を使ったディレクトリ移動のテクニックと組み合わせると、ちょっとした問題が起きます。~gomi にあるリポジトリが一覧されないのです。

~gomi 配下にリポジトリがあるが、ghq+peco によるリポジトリ切り替えの候補にそれが表示されない様子。

これは、リポジトリの一覧を取得するコマンドに ghq list を使用していることが原因です。ghq list は VCS 管理されているディレクトリしか返してくれないのです。ghq は VCS 管理されるリポジトリを整理するためのコマンドなので、自然な挙動ではあります。

とはいえ、~gomi のようなディレクトリを作っている身としては困ります。そこで id:mizdrafind $(ghq root) -mindepth 3 -maxdepth 3 -type d でリポジトリの一覧を取得しています。

# ~/.zshrc
function peco-src() {
  local ghq_root="$(ghq root)"
  local selected_dir=$(find $ghq_root -mindepth 3 -maxdepth 3 -type d | sed "s|$ghq_root/||" | peco --query "$LBUFFER")
  if [[ -n "$selected_dir" ]]; then
    BUFFER="cd $ghq_root/$selected_dir"
    zle accept-line
  fi
  zle redisplay
}
zle -N peco-src
stty stop undef # disable builtin "^s" keybind
bindkey '^s' peco-src

これで、ghq+peco によるリポジトリ切り替えの候補に ~gomi 配下のものが表示されます。

ちょっとした注意点

今回紹介したスクリプトは、https://<domain>/subpath/<org>/<repo>.git というサブパス付き URLを持ち、~/src/<domain>/subpath/<org>/<repo> に配置されるリポジトリには対応してません。-mindepth 3 -maxdepth 3 で深さ 3 にあるディレクトリを抽出してるからです。

サブパスを URL に含むリポジトリなんてそう滅多にないので、ほとんどの場合は問題ないはず...。どうしてもサブパスを URL に含むリポジトリを扱いたい人は、ghq listfind $ghq_root/localhost -mindepth 3 -maxdepth 3 -type d を組み合わせてみてください。ghq list はサブパスを持つディレクトリに対応してるので、それで問題を回避できるはずです。

*1:https://pocke.hatenablog.com/entry/2014/07/23/173811 も合わせて読むと面白いでしょう

Node.js におけるファイル読み取りエラーのクロスプラットフォーム対応の仕組み

Windows の Node.js で存在しないファイルを fs.readFileSync で読み取ろうとすると ENOENT が返ってくる。けど ENOENT は POSIX で定義されてるエラーコードであって、Windows のものではない。どこかで正規化されてるのか? という疑問が出てきたので調べてみた。

答え

Node.js の公式ドキュメントの error.errno の説明に答えが書いてあった。

https://nodejs.org/api/errors.html#errorerrno

On Windows the error number provided by the system will be normalized by libuv.

どうも libuv でエラーコードの正規化がされてるらしい。確かに libuv のコードを見ると、int uv_translate_sys_error(int sys_errno) 関数の中にエラーコードを正規化するコードが書かれていた。

// https://github.com/libuv/libuv/blob/12d1ed1380c59c5ec27503cf149833de6f0e6bb0/src/win/error.c#L134-L144 より
int uv_translate_sys_error(int sys_errno) {
  // ...
  switch (sys_errno) {
    // ...
    case ERROR_BAD_PATHNAME:                return UV_ENOENT;
    case ERROR_DIRECTORY:                   return UV_ENOENT;
    case ERROR_ENVVAR_NOT_FOUND:            return UV_ENOENT;
    case ERROR_FILE_NOT_FOUND:              return UV_ENOENT;
    case ERROR_INVALID_NAME:                return UV_ENOENT;
    case ERROR_INVALID_DRIVE:               return UV_ENOENT;
    case ERROR_INVALID_REPARSE_DATA:        return UV_ENOENT;
    case ERROR_MOD_NOT_FOUND:               return UV_ENOENT;
    case ERROR_PATH_NOT_FOUND:              return UV_ENOENT;
    case WSAHOST_NOT_FOUND:                 return UV_ENOENT;
    case WSANO_DATA:                        return UV_ENOENT;
    // ...
  }
  // ...
}

あれでも libuv ってイベントループを実装するためのライブラリじゃなかったっけ? fs.readFileSync は同期的な API でイベントループ関係ない気がするんだけど。 同期的な API も libuv に依存してるの?

答え2

...という疑問を持ったので調べてみたら、どうも libuv にはファイルシステムに関する機能もあるようだった。README の「Feature highlights」を見ると、「File system events」「Child processes」「Thread pool」など色々書かれてる。

libuv のコードを見てみると、Windows 向けに fs__read という関数が実装されてて、ここでファイルの読み取りやらエラーコードの正規化をしていることがわかる。

そしてこの fs__read 関数が fs.readFileSync の内部で呼ばれている、という訳。

同期的な API の中でも libuv の API が呼び出されてるの知らなかったので、勉強になった。

VS Code でデバッガーを使って oxc の挙動を観察したい

oxc の挙動を観察したいな〜と突然思って、oxc のデバッグ環境を VS Code で用意した。ちょっと躓いたのでメモを残しておく。

やりたいこと

  • oxc のテストを VS Code から実行したい
  • しかも VS Code 上から breakpoint を設定して、テストをステップ実行したい

Requirements

以下は事前にインストールしておく。

  • VS Code
  • Rust
  • rust-analyzer 拡張機能
    • rust-analyzer という Rust 向けの Language Server があって、それを VS Code から使うための拡張機能
    • VS Code からテストを実行するための UI も提供してくれる
      • プロジェクト内のテストケースを抽出して「Test Explorer」にそれを表示してくれたり
      • #[test] のすぐ下にインラインで「Run Test」「Debug (デバッガーを attach しながらテストを実行するモード)」ボタンを表示してくれたり
  • CodeLLDB 拡張機能
    • LLDB というデバッガーがあって、それを VS Code から使うための拡張機能

あと oxc のリポジトリも git clone して Contribution Guide を見ながらセットアップしておく。

breakpoint で止まらない

「Debug」ボタンからテストを実行しても、何故か breakpoint で止まらずに完走してしまう。

「Debug」ボタンからテストを実行したのに、breakpoint で止まらずに完走してしまう様子

最初は rust-analyzer/CodeLLDB 拡張機能の設定が不十分なのかと思ったけど、特に怪しい設定もない。となるとテスト時に実行されるバイナリがなんかおかしいのか? と疑い始める。

テスト時に実行されるバイナリ

じゃあテスト時に実行されるバイナリはどこにあるのか。これは CodeLLDB 拡張機能のログを見るとわかる。

まず VS Code の下部のメニューの「OUTPUT」をクリックし、その次に拡張機能のリストから「LLDB」を選択。そうして表示されるログの先頭のほうに、program という項目があり、その値がテスト時に実行されるバイナリのパスになってる。
OUTPUT > LLDB と辿っていた先に拡張機能のログがあり、そこに書いてある

この program の部分がテスト時に実行されるバイナリになってる。実際に引数を伴って実行してみると、テストが実行できる。

$ ./target/debug/deps/oxc_parser-992051aabfe995a6 module_record::module_record_tests::import_default --exact --show-output

running 1 test
test module_record::module_record_tests::import_default ... ok

successes:

successes:
    module_record::module_record_tests::import_default

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 53 filtered out; finished in 0.00s

バイナリの中身を見る

まあ多分シンボル情報が入ってないんじゃないか、と思うので中身を見てみる。macOS なら dsymutil コマンドなどでできるらしい。

$ dsymutil --statistics target/debug/deps/oxc_parser-992051aabfe995a6
warning: no debug symbols in executable (-arch arm64)
.debug_info section size (in bytes)
-------------------------------------------------------------------------------
Filename                                           Object         dSYM   Change
-------------------------------------------------------------------------------
-------------------------------------------------------------------------------
Total                                                  0b           0b    0.00%
-------------------------------------------------------------------------------

warning: no debug symbols in executable を見るに、やっぱりシンボル情報入ってなかったね... これなら breakpoint 仕掛けても止まらないの当然だ。

何故シンボル情報が含まれてないのか

普通テストを実行する際はデバッグビルドが実行されて、シンボル情報が埋め込まれるはず。なのに何故埋め込まれていないのか。その理由は oxc リポジトリの Cargo.toml に書いてあった

// https://github.com/oxc-project/oxc/blob/ea3f362173247454190a737b9c0737fb26f43a3f/Cargo.toml#L241-L244 より引用
[profile.test]
# Disabling debug info speeds up local and CI builds,
# and we don't rely on it for debugging that much.
debug = false

コメントなるほどね... debug = true にして無事解決しました。

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

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