Astroでbuild.format: 'file'でもindex.htmlを生成するインテグレーション

## Astro v4.3 （2024/02/02更新）

Astro v4.3で[build.format](https://docs.astro.build/en/reference/configuration-reference/#buildformat)に`'preserve'`が追加された。
`'preserve'`ではファイル構成のままビルドされる。

```
src/pages/about.astro -> dist/about.html
src/pages/about/index.astro -> dist/about/index.html
```


## build.format: 'file'でindex.htmlを生成する

Astroは[build.format](https://docs.astro.build/en/reference/configuration-reference/#buildformat)を`'file'`に設定することで各ページをディレクトリではなくHTMLファイルとしてビルドする。
しかしこの設定ではルート以外のindex.astroからindex.htmlが生成されない。Astroファイルと出力されるHTMLファイルの対応は以下のようになる。

```
src/pages/index.astro -> dist/index.html
src/pages/foo/index.astro -> dist/foo.html
src/pages/foo/bar.astro -> dist/foo/bar.html
```

`src/pages/foo/index.astro`からは`dist/foo.html`を生成したい、つまりpages以下のディレクトリ構造を保ったままHTMLを生成したいときのために、ビルド後のファイルをリネームするAstroインテグレーションを作った。

Astroインテグレーションのhookにはビルドが完了した後に実行される[`astro:build:done`](https://docs.astro.build/en/reference/integrations-reference/#astrobuilddone)がある。  
`astro:build:done`の`routes`オプションで、生成されたルートとそのメタデータのリストが取得できるので、ここからインデックスルートのHTMLファイルを探し、リネームする。

## 環境

- Astro: v4.0.3

## 該当するHTMLファイルを抽出

`astro:build:done`の`routes`オプションは以下のプロパティを持つ。

- `route.type`：ルートがHTMLページかそれ以外のエンドポイントかどうか。
- `route.component`：元のファイルのパス。
- `route.pathname`：出力されたページのパス。動的ルートでは`undefined`になる。

全ルートから`/`以外の`index.(astro|md|mdx|html)`を抽出しリネームする。

## ソースコード

```ts
import fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import type { AstroIntegration } from "astro";

let shouldRename = false;

export default function createIntegration(): AstroIntegration {
  return {
    name: "astro-index-pages",
    hooks: {
      "astro:config:done": ({ config }) => {
        // build.format: 'file'の時のみリネームする
        if (config.build.format === "file") {
          shouldRename = true;
        }
      },
      "astro:build:done": async ({ dir, routes }) => {
        if (!shouldRename) {
          return;
        }

        const outDirPath = fileURLToPath(dir);

        await Promise.all(
          routes
            .filter((route) => {
              return (
                route.type === "page" &&
                route.pathname &&
                route.pathname !== "/" &&
                path.parse(route.component).name === "index"
              );
            })
            .map(async ({ pathname }) => {
              if (!pathname) return;
              // リネーム先のディレクトリパス
              const targetDirPath = path.join(outDirPath, pathname);
              // リネーム前のファイルパス
              const beforeFilePath = path.join(outDirPath, `${pathname}.html`);
              // リネーム先のファイルパス
              const afterFilePath = path.join(outDirPath, pathname, "index.html");
              await fs.mkdir(targetDirPath, { recursive: true });
              await fs.rename(beforeFilePath, afterFilePath);
            }),
        )
          .then(() => {
            logger.info('Success');
          })
          .catch((err) => {
            logger.error(err);
          });
      },
    },
  };
}

```

作成したインテグレーションを設定ファイルに追加する。

```js:astro.config.mjs
import { defineConfig } from 'astro/config'
import indexPages from './astro-index-pages';

export default defineConfig({
  integrations: [
    indexPages()
  ]
})
```

生成されるHTMLファイルは以下になる。

```
src/pages/index.astro -> dist/index.html
src/pages/foo/index.astro -> dist/foo/index.html
src/pages/foo/bar.astro -> dist/foo/bar.html
```

## おまけ

[Write index routes as index.html with build.format: 'file' by matthewp · Pull Request #9209 · withastro/astro](https://github.com/withastro/astro/pull/9209)でインデックスルートを無視しない`build.format: 'preserve'`が考案されている。（まだDraftだが……）
このプルリクがマージされたらこのインテグレーションはいらなくなる。

https://github.com/withastro/astro/pull/9209

→Astro v4.3でbuild.formatに`'preserve'`が追加された。