TypeScript + Preact
GenerateDirectoryListingAction
という GitHubActions を作った話

@yKicchan

Draft

自己紹介

きっちゃそ
:DeNA
Web Frontend
X @yKicchan

目次

  1. 作ったものの紹介 - P4
  2. 技術スタック - P9
  3. 開発知見 - P14
  4. まとめ - P27

1. 作ったものの紹介

GenerateDirectoryListingAction

1. 作ったものの紹介 - GenerateDirectoryListingAction

できること

  • GitHub Actions として利用可能
  • 指定のディレクトリ以下に index.html を生成
    • ディレクトリ内のファイル一覧を表示する
  • オプションで特定ファイルの除外や、見た目の変更が可能
1. 作ったものの紹介 - GenerateDirectoryListingAction
1. 作ったものの紹介 - GenerateDirectoryListingAction

使い方

- name: Generate directory listing
  uses: yKicchan/generate-directory-listing-action@v1
  with:
    target: dist

実際の利用時にはコミットハッシュを指定しよう

1. 作ったものの紹介 - GenerateDirectoryListingAction

利用シーン

  • ルートページのインデックスを自動で更新したい
  • デプロイしたファイルの一覧を手軽に閲覧したい

目的のページまでの繋ぎとして利用するのに便利

2. 技術スタック

2. 技術スタック

選定基準

  • エコシステムの利用や設定が煩雑ではないこと
  • (特にHTMLの)メンテナンスが容易なこと
  • 機能がシンプルなので fat すぎないこと
2. 技術スタック

主な利用技術

項目 利用技術
言語 TypeScript
JSX Preact
ビルド esbuild
CSS PostCSS + cssnano
Lint/Format Biome
Test vitest + happy-dom
項目 利用技術
ファイル検索 glob
スペシャル
アドバイザー
ChatGPT-4-turbo (v2)
スーパー
アシスタント
GitHubCopilot
2. 技術スタック

TypeScript + Preact

  • 宗教上の理由で TypeScript を利用
  • HTML テンプレートエンジンとして Preact(JSX) を採用
  • JSX に対応するために esbuild を利用
2. 技術スタック

AI の活用

  • ChatGPT くんによる実装提案やコードレビュー
  • GitHubCopilot によるコーディング支援

3. 開発知見

3. 開発知見

Preact による HTML 生成

  • Preact の SSR 用の renderToString という関数を使って実装
  • この関数に JSX を食わせることで、文字通り HTMLstring 結果を得ることができる
  • その結果を fs.writeFile を使って index.html を生成した
3. 開発知見

Preact による HTML 生成

import { renderToString } from "preact-render-to-string";
import { App, type AppProps } from "path/to/app";

export const renderHTML = (props: AppProps) => 
  `<!DOCTYPE html>${renderToString(HTML(props))}`;

const HTML = (props: AppProps) => (
  <html>
    <head><!-- 中略 --></head>
    <body>
      <App {...props} />
    </body>
  </html>
);
import fs from "node:fs";
import { renderHTML } from "./html";

const html = renderHTML({ /* props */ });
fs.writeFileSync("index.html", html, "utf-8");

実際の実装:
generator.ts html/index.tsx

3. 開発知見

Preact による HTML 生成

export const renderHTML = async (props: AppProps) => {
  const htmlComponent = await HTML(props);
  return `<!DOCTYPE html>${renderToString(htmlComponent)}`;
}

const HTML = async (props: AppProps) => {
  const appComponent = await App(props);
  return (
    <html>
      <head><!-- 中略 --></head>
      <body>{appComponent}</body>
    </html>
  );
}
3. 開発知見

css の読み込み

  • esbuild の機能で string として読み込み
  • PostCSS と plugin(cssnano など)に食わせて minify

改良余地として、グローバルにしか読んでいない(コンポーネントごとにCSSを読んでいない)ところや未使用クラスの削除には対応できていない
シンプルな View を提供しているのでグローバルCSSで事足りておりわざわざ時間を使わなかったという戦略的撤退でもある

3. 開発知見

css の読み込み

import cssnano from "cssnano";
import postcss from "postcss";
import flexbugsFixes from "postcss-flexbugs-fixes";
import presetEnv from "postcss-preset-env";
import mycss from "path/to/css.pcss";

export async function CSS() {
  const css = await applyPostcssPlugins(mycss);
  return <style>{css}</style>;
}

async function applyPostcssPlugins(css: string) {
  const result = await postcss([
    presetEnv(),
    flexbugsFixes(),
    cssnano()
  ]).process(css, { from: undefined });
  return result.css;
}
import { build } from "esbuild";

build({
  // 中略
  loader: {
    ".tsx": "tsx",
    ".ts": "ts",
    ".css": "text",
    ".pcss": "text",
  },
})

実際の実装:
css/index.tsx esbuild.config.mjs

3. 開発知見

vitest によるテスト

  • テストフレームワークとして vitest を利用(jest互換)
  • @testing-library/preact + happy-dom で UI テスト
    • getByRole によるアクセシビリティテスト
  • ユニットテストをメインに、結合部は適度にモックを行った
  • AAA パターンを意識した記述
3. 開発知見

vitest によるテスト

import { generate } from "./generator";
const setup = (dir: Path) => generate(dir);

it("ファイルがなかったとき、何もしない", async () => {
    const dir = { fullpath: () => "unknown" } as Path;
    await setup(dir);
    expect(mockWriteFile).not.toHaveBeenCalled();
});

it("ディレクトリ内にファイル(またはディレクトリ)が1つ以上あるとき index.html を生成する", async () => {
    const dir = { fullpath: () => "sandbox" } as Path;
    await setup(dir);
    expect(mockWriteFile).toHaveBeenCalledWith("sandbox/index.html", "html", "utf-8");
});
3. 開発知見

vitest によるテスト

3. 開発知見

ログ出力

  • @actions/core を利用したログ出力
  • 良い感じの色付け
  • ログレベルによる出力制御
3. 開発知見

ログ出力

3. 開発知見

ログ出力

3. 開発知見

AI の活用 - ChatGPT

  • 要件を洗い出して雛形の実装を作成
  • エラーなどの解決や実装方法の相談
  • 雑なコードレビュー依頼

4. まとめ

4. まとめ

紹介したこと

  • GitHubActions で利用可能な GenerateDirectoryListingAction を作った
  • TypeScript + Preact で簡易的な SSG を実装した
  • vitest でテストコードを整備した
  • AI による実装提案やコードレビューが意外と役立った
4. まとめ

もう少し詳しい情報

4. まとめ

EOF