esbuild keepNames の __name ヘルパーが Playwright へ渡す関数のシリアライズを黙って壊す(tsx 実行時)
トラブルシューティング
Playwright
esbuild
知識
Playwright の addInitScript / evaluate に JavaScript の関数を渡すと、関数は toString でシリアライズされブラウザ側で再評価される。このとき実行環境が esbuild の keepNames 変換を通していると(tsx がその代表。ts-node 系や一部バンドラ設定も同様)、関数内部の名前付き関数代入が __name ヘルパー呼び出しでラップされ、シリアライズ結果に モジュール側にしか存在しない __name への参照 が残る。ブラウザにはこのヘルパーが無いため、注入スクリプトは実行冒頭で ReferenceError(__name is not defined)を投げ、注入した機能は丸ごと無効になる。
誤解しやすい条件
- 壊れるのは関数の内部に名前付きの関数代入(const f = () => のような束縛)を含む場合のみ。内部が無名コールバックだけの関数は keepNames の対象にならず無事なので、「他の注入は動くのに一つだけ壊れる」形で現れる。
- 実行経路依存で再現が割れる: tsx 直実行では壊れ、tsup 等でビルドした配布物では keepNames が無効なら壊れない。「開発時だけ壊れて公開版は正常」という気づきにくい分布になる。
- 症状はブラウザ側の pageerror であり Node 側には何も出ない。ページ側のエラーを監視していないと、注入機能(カーソル描画・計測フックなど)が黙って消えるだけで長期間気づけない。実際にページエラー収集を実装した初回走行で初めて発覚した。
対処
- ブラウザ側に __name の恒等シム(globalThis の __name に、受け取った関数をそのまま返す関数を代入する一行)を、壊れる関数の評価より先に注入する。
- シムは var 宣言ではなく globalThis への明示代入にする。Playwright の init script はラッパースコープで評価されるため、var の宣言はグローバルに届かない。
- シムと本体を別々の addInitScript にしない。Playwright は複数 init script の評価順を保証しないため、シム定義と本体呼び出し(toString した関数源の即時実行)を同一スクリプト文字列に連結して、スクリプト内の逐次実行で順序を保証する。
確認方法
- Node 側: 対象関数の toString 結果に __name が含まれるかを実行経路(tsx / ビルド後 dist の両方)で確認する。
- ブラウザ側: pageerror(context レベルなら weberror イベント)を監視して ReferenceError を検知する。注入機能の見た目(カーソルが出るか等)だけの確認は、シナリオが機能を使わない経路で偽陰性になる。