Astroで極力フレームワークっぽさを無くす

いままで、HTMLの生成はpugを使用していたのですが、
このご時世、そろそろ他にいいものがあるのでは…？
と調べてみたところ、今更ながらAstroを知ったので、さっそく案件に取り入れてみた記録

https://docs.astro.build/ja/getting-started/


極力生成されたソースにAstroで作ったという痕跡を残してほしくないということで、
デフォルトと異なる設定にした場所中心の備忘となります。

node -v 22.13.1
npm -v 10.9.2
astro@5.2.5

での内容です

## 要件
1. APIなどのつなぎ込みのない、HMTLとCSSとJSで静的なモックを作成する
2. 生成したHTMLを直接ブラウザで閲覧出来るようにする
4. 相対パスでパス指定する
5. IE11は非対応でOK


### HTML関連
#### インデント有にしてほしい
とのことだったので、`astro.config`にてインデントありの設定に変更
`compressHTML: false,`　（[公式](https://docs.astro.build/ja/reference/configuration-reference/#compresshtml)）
 結果、生成されたHTMLはインデントがついてはいる。が、結構ぐちゃぐちゃでした。
なので、astroでビルド→prettierで整形という処理をbuild時に実行するように修正。
 prettierは最大80文字と、空白処理をコマンドで上書き
 （`--print-width 1000 --html-whitespace-sensitivity ignore`）
（一応）先方の納得のいく形になりました。

#### 改行コードはCRLFにしたい
Astroでbuildをしたソースは、LFになるようです。
ビルド後のソースの改行コード変更に関するconfigの設定は公式でなさそう…
CRLFに変更という処理を自前で設定（node convertLine.cjs）
こちらも、build時にastroでビルド→LFをCRLFに変更という処理を自前で追加しました


<details>
<summary>convertLine.cjs</summary>

```convertLine.cjs

const fs = require('fs');
const path = require('path');

// 設定（対象フォルダ & 拡張子）
const targetDir = './dist'; // 変換したいフォルダのパス
const extensions = ['.html', '.css', '.js']; // 変換対象の拡張子リスト

// 再帰的にフォルダ内のファイルを処理
function convertLineEndings(dir) {
  fs.readdirSync(dir, { withFileTypes: true }).forEach((entry) => {
    const fullPath = path.join(dir, entry.name);
    if (entry.isDirectory()) {
      // サブフォルダがあれば再帰処理
      convertLineEndings(fullPath);
    } else if (extensions.includes(path.extname(entry.name))) {
      // 指定の拡張子に一致するファイルを変換
      const content = fs.readFileSync(fullPath, 'utf8');
      // 改行を CRLF に統一
      const convertedContent = content.replace(/\r?\n/g, '\r\n');
      fs.writeFileSync(fullPath, convertedContent, 'utf8');
    }
  });
}
// 実行
convertLineEndings(targetDir);
```
</details>

#### 相対パス指定

公式では相対パス指定は対応していないそうです。
「Astro　相対パス」などでググったらでてきます。
以下のパッケージを使用させていただきました。
[astro-relative-links
](https://www.npmjs.com/package/astro-relative-links)


### CSS関連

#### スコープ対応しないでほしい

…お客さんの要望なら聞くしかありません。
.astro内に`<style>`タグを記述するのがAstroでは一般的など思われます。
その場合、`<style is:global>`とすると、スコープ対応されないようです。
[公式：グローバルスタイル](https://docs.astro.build/ja/guides/styling/#%E3%82%B0%E3%83%AD%E3%83%BC%E3%83%90%E3%83%AB%E3%82%B9%E3%82%BF%E3%82%A4%E3%83%AB)

外部のCSS（node_modules）や、そのほか作成したCSSやSCSSなどは
スクリプトの先頭にimportで読み込みを追加すればOKです
```
ex
---
import '@/assets/style/style.scss'
---
```


:::note info
dev開発時の`<style is:global>`

- devで開発中はバッティングしていなかったのに、ビルドしたらCSSがバッティングしてしまう
- 別ファイルでis:globalにしたはずなのに、Styleが当たらない

ということが度々ありました。
dev開発時はグローバル指定が反映されない？ようです。
:::




#### 別ファイルでCSSにしてほしい

こちらもデフォルトだと、ビルド後は.cssファイルが出来上がるのではなく`head`内にstyleタグでの追加となるようです。
公式に設定の変更方法がありました：[build.inlineStylesheets](https://docs.astro.build/ja/reference/configuration-reference/#buildassets)
ということで、上記を参考にastro.configに以下を追記しました。
```
export default defineConfig({
    build: {
        inlineStylesheets: 'never',
        assets: 'assets',
    }
})
```

上記設定をすると、`head`内CSSが`dist/assets/XXXX.css`というディレクトリに生成されます。
ここで新たな問題が発生です

#### 別ファイルにしたCSSを指定の場所に格納したい

`dist/assets/XXXX.css`ではなく、`dist/assets/style/XXXX.css`に変更したい。
上記設定の`build.assets: 'assets'`を`assets/style`に変更すれば解決します。
が、画像も同様に`assets => assets/style`となってしまうのです。

なので、viteのbuild設定にて、assets配下のファイル格納先を変更するように追記しました。

<details>
<summary>astro.config.mjs</summary>

```astro.config.mjs
import { defineConfig } from "astro/config";

export default defineConfig({
  build: {
    inlineStylesheets: 'never',
    assets: 'assets',
  },
  vite: {
    build: {
      rollupOptions: {
        output: {// ★★以下を追記★★
          assetFileNames: (assetInfo) => {
            let extType = assetInfo.names[0].split('.').at(-1);
            if (extType && /png|jpe?g|svg|gif|webp|ico/i.test(extType)) extType = 'img';
            if (extType && /css|scss/i.test(extType)) extType = 'css';
            return `assets/${extType}/[name][extname]`;
          },
        },
      },
    },
  },
});
```
</details>

#### 別ファイルにしたCSSを1つにまとめてほしい

どんどん実装していくと、CSSが増え、生成されたファイルが長くなります。
すると、生成されたCSSが分割されるようになりました。
原因はたぶん、Astroが使用してるviteのデフォルトでCSSコード分割が有効になっているからっぽいです。
https://ja.vite.dev/config/build-options.html#build-csscodesplit
こちらを無効にし、かつ、インライン記述するMAX値も0としておきます。

```
export default defineConfig({
  vite: {
    build: {
      rollupOptions: {
      assetsInlineLimit: 0,　★
      cssCodeSplit: false,　★
```

### JS関連

#### scriptの読み込みを`type="module"`ではなくしたい

ビルドされたファイルのscript読み込みは`type="module"`での読み込みとなるようです。
これが直接ブラウザでみるとCORSのエラーになるようでした。

https://docs.astro.build/ja/guides/client-side-scripts/#_top

つまり、公式のように
.astroに`<script>~<script>`でもNG。
sciptタグでの読み込みもNG。
ファイル内の先頭にimportしてもNG。
となると、publicに.jsを作って、そこを外部JSとして読み込むようにするしかない…
という結論になりました。（もしかしたら他にいい方法があったのかも。）

なので、AstroでのJSビルドは行わず、rollupで別途ビルドするように設定ファイルを作成
ローカル開発時は`public/assets/js/script.js`が出来上がるように設定
ビルド時は`dist/assets/js/script.js`が出来上がるように設定
としました

<details>
<summary>rollup.config.js</summary>

```rollup.config.js
import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import terser from '@rollup/plugin-terser';
import watcher from 'rollup-plugin-watcher';

const srcDir = './src/assets/js/';
const publicDir = './public/assets/js/';
const distDir = './dist/assets/js/';

const isProduction = process.env.NODE_ENV === 'prod';

const pluginOptsion = [
  resolve(),
  commonjs({
    sourceMap: false,
  }),

  isProduction &&
    terser({
      compress: {
        drop_console: true,
        drop_debugger: true,
      },
    }),
  !isProduction && watcher(['src/assets/**/**.js']),
];

const watchOption = isProduction
  ? false
  : {
      include: 'src/assets/**/**.js',
      chokidar: {
        usePolling: false,
      },
    };

const config = [
  {
    input: `${srcDir}AAA.js`,
    output: {
      file: isProduction ? `${distDir}mock.js` : `${publicDir}mock.js`,
      format: 'iife',
      name: 'AAA',
      sourcemap: !isProduction,
    },
    watch: watchOption,
    plugins: pluginOptsion,
  },
];
export default config;

```
</details>

### asset関連

src配下に複数のディレクトリに分けて画像を格納し、astroの`Image`で読み込むと、
distの中に生成されるときは全て1階層になります

``` src/assets/images/common/icon.png => dist/assetes/images/icon.png ```

distでもディレクトリ構造を保ったままにしたい場合、
public内に画像を格納し、Imageでの読み込みではなく普通にimgタグとして読み込むようにすればディレクトリを保ったままbuildができます。
Imageを使う&&ディレクトリ構造を保つ。というのは実現が難しいためどちらかしかできません。
という内容で相談のうえ、今回はImageを使用し、distの中は1階層になるで譲歩いただきました。


### 最終config

<details>
<summary>astro.config.mjs</summary>

```astro.config.mjs

import relativeLinks from 'astro-relative-links';
import { defineConfig } from 'astro/config';
import autoprefixer from 'autoprefixer';
import path, { dirname } from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

// https://astro.build/config
export default defineConfig({
  server: {
    open: true,
  },
  compressHTML: true,
  integrations: [relativeLinks()],
  build: {
    format: 'file',
    inlineStylesheets: 'never',
    assets: 'assets',
  },

  vite: {
    resolve: {
      alias: {
        '@/': `${path.resolve(__dirname, 'src')}/`,
      },
    },
    build: {
      assetsInlineLimit: 1,
      cssCodeSplit: false,
      rollupOptions: {
        output: {
          // entryFileNames: `assets/js/script.js`,
          manualChunks: undefined,
          assetFileNames: (assetInfo) => {
            let extType = assetInfo.names[0].split('.').at(-1);

            if (extType && /png|jpe?g|svg|gif|tiff|webp|ico/i.test(extType)) {
              extType = 'img';
            }
            if (extType && /css|scss/i.test(extType)) {
              extType = 'css';
            }

            return `assets/${extType}/[name][extname]`;
          },
        },
      },
    },
    css: {
      preprocessorOptions: {
        scss: {
          // see: https://vite.dev/config/shared-options.html#css-preprocessoroptions
          api: 'modern',
        },
      },
      postcss: {
        plugins: [autoprefixer()],
      },
    },
  },
});


```

</details>


## 感想
これからは、静的なHTMLの作成とか、ちょっとしたサイトの作成など
全然Astro使っていきたいなと思いました…！