🐡

pre-commit+playwrightでUIプレビューを自動生成し、READMEを常に最新に保つ仕組み

に公開

UIを触るたびにREADMEのスクリーンショットが古くなる問題は、地味にストレスが溜まる。
毎回手動でキャプチャして差し替えるのも面倒だし、やらなくなるのが目に見えている。

そこで、pre-commitをフックにしてUIプレビューを自動生成し、そのままREADMEに反映させる仕組みを組んだ。
一度作ってしまえば、以降は「いつの間にかREADMEが最新になっている」状態になる。

例えば、こんな感じのUIプレビューがコミットごとに自動更新される。

GitHub上での成果物の見え方

全体の流れ

やっていることはシンプルで、処理の流れはこうなる。

  1. コードやUIを編集する
  2. pre-commitでローカル開発サーバーを起動し、Playwrightでスクリーンショットを撮る
  3. [Optional] 生成されたPNGをBase64でSVGに埋め込む(GitHub表示対策)
  4. README.mdはそのSVG(またはPNG)を参照するだけ

ポイントは 「pre-commitで必ず実行されるが、無駄な再生成はしない」 ところ。

[Optional] なぜSVGに変換するのか

最初は単純にPNGをREADMEに貼っていたが、GitHubではCSSや影、背景の表現が効かない。
UIの雰囲気が伝わりにくく、せっかくのプレビューがのっぺりして見える。

そこで、PNGを Base64でSVGに埋め込み、枠やシャドウをSVG側で表現 する方式に切り替えた。
これならGitHub上でも見た目をコントロールできる。

pre-commit構成

.pre-commit-config.yaml ではローカルフックを2つ定義している。

  • UIプレビュー生成
  • PNG → SVGカード生成
- repo: local
  hooks:
    - id: make-preview
      name: make preview with PREVIEW_OUTPUT
      language: system
      entry: ./scripts/maybe_make_preview.sh
      pass_filenames: false
    - id: generate-web-editor-card
      name: generate web editor card svg
      language: system
      entry: python3 scripts/generate_web_editor_card.py
      pass_filenames: false

ここでは ファイル差分はpre-commitに任せず、スクリプト側で制御 している。

.pre-commit-config.yaml 全体
.pre-commit-config.yaml
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
-   repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v3.2.0
    hooks:
    -   id: trailing-whitespace
    -   id: end-of-file-fixer
    -   id: check-yaml
    -   id: check-added-large-files
-   repo: local
    hooks:
    -   id: make-preview
        name: make preview with PREVIEW_OUTPUT
        language: system
        entry: ./scripts/maybe_make_preview.sh
        pass_filenames: false
    -   id: generate-web-editor-card
        name: generate web editor card svg
        language: system
        entry: python3 scripts/generate_web_editor_card.py
        pass_filenames: false

無限ループを避けるための工夫

pre-commitで生成物を書き換えると、毎回差分が出てコミットできなくなる。
この問題を避けるために、影響しうるファイル群のハッシュを取る方式を採用した。

scripts/maybe_make_preview.sh の役割は以下。

  • UIに影響するファイルだけを git ls-files で列挙
  • それらのSHA-256をまとめてハッシュ化
  • 前回と同じならプレビュー生成をスキップ
  • 変わっていれば生成し、ハッシュを .preview.hash に保存
CURRENT_HASH=$(printf '%s\0' "${PREVIEW_FILES[@]}" | xargs -0 sha256sum | sha256sum | cut -d ' ' -f1)

if [[ -f "$HASH_FILE" ]] && [[ "$(cat "$HASH_FILE")" == "$CURRENT_HASH" ]]; then
  echo "preview: no relevant changes detected, skipping"
  exit 0
fi

この方式にしてから、pre-commitが「賢くなった」感覚がある。
不要な起動やキャプチャが走らず、体感もかなり軽い。

プレビュー生成の中身

実際のスクリーンショット生成はPythonとPlaywrightに任せている。

  • Pythonでローカル開発サーバーを起動
  • URLが立ち上がるまでポーリング
  • NodeスクリプトでPlaywrightを起動しキャプチャ
  • 終わったらサーバーを確実に終了

preview.py はこのオーケストレーション役だ。

if not wait_for_server(PREVIEW_URL):
    print("Server did not become ready within the timeout window.")
    return 1

subprocess.run(
  ["node", "scripts/capture.mjs", str(OUTPUT_PATH), PREVIEW_URL],
  check=True,
)

Playwright側は極力シンプルにしている。
viewport指定と networkidle 待ちだけで、余計なことはしない。

SVGカード生成

最後に、生成されたPNGをSVGに包む。

scripts/generate_web_editor_card.py では、

  • PNGをBase64エンコード
  • SVGテンプレートに埋め込み
  • 影・角丸・背景をSVG側で定義

という処理をしている。

この段階で「READMEに載せる前提の見た目」を完成させておくのがポイント。
README側はただ <img> で参照するだけになる。

実際に使ってみて

一度この仕組みを入れると、

  • UIを触る
  • そのまま git commit
  • READMEのプレビューが勝手に更新されている

という状態になる。

「ドキュメントは後で直す」が起きにくくなるのが一番の収穫だった。
pre-commitは整形ツールという印象が強いが、ローカルで完結する自動化にはかなり向いている。

UIを持つリポジトリなら、わりと汎用的に使える構成だと思う。


Discussion