Nuxt3で一部のページを本番ビルドで丸ごと除外する備忘録

1. 勉強用だけどAWS(Lambda)にデプロイしているサーバーレスアプリケーションの中に新たにWebRTCを使ったクローズドなチャット機能を作った
2. クローズドなチャットはインターネット上に公開してしまうと電気通信事業法に引っかかってしまうかもしれない
3. 本番ビルドからチャットに関するページを除きたくなった
    - ブランチ切って避けたりはしたくない（存在を忘れそうなので）
    - ローカルの開発サーバーでのみ動作確認可能としたい
    - リバースエンジニアリングも不可能にしたい

という流れ。

:::note warn
2024/03/26追記
Nuxt3の公式ドキュメント読んだら`.nuxtignore`というファイルを設定することでも実現できることがわかりました。
https://nuxt.com/docs/guide/directory-structure/nuxtignore

こちらを利用するのが正解かも？（ドキュメントに書いてあるので）
:::

## フォルダ構成とチャット画面について
関係しているもののみ抜粋。以下のような感じ
```
components/
  ┣ layouts/
  ┃ ┗ error/
  ┃   ┗ WebRTCProductionExcludeError.vue ← 本番環境でクローズドチャットのURLにアクセスしたときのエラー画面
  ┗ WebRTC-chat/ ← webrtc-chat.vueで使用するコンポーネントをまとめたディレクトリ
    ┣ publish/
    ┃ ┗ WarnMessage.vue ← 除外したくない
    ┣ ChatMessagesTimeline.vue ← 除外したい
    ┣ ClipboardCopy.vue ← 除外したい
    ┣ MessageEditor.vue ← 除外したい
    ┗ MessageView.vue ← 除外したい
pages/
  ┗ demo/
    ┗ appendix/
      ┗ webrtc-chat.vue ← 除外したい
types/
  ┗ webrtc-chat.d.ts
package.json
nuxt.config.ts
```

作成したチャット画面は以下のような形になっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/cefe4fed-44e3-c8ca-dc3b-bf91c1cc405d.png)

## 修正内容について
- Nuxt3においてビルドを担当しているのはViteの中の[ESBuild](https://esbuild.github.io/)と[rollup](https://rollupjs.org/)
    - Nuxt3 ⇒ Vite ⇒ ESBuild,rollup という関係
- 本番ビルドを行うときに使用されるrollupのオプションとして特定のページコンポーネントを外部から読み込むコンポーネントと定め、ビルドに含めないようにすれば良さそう
  - 具体的には`nuxt.config.ts`のViteを使ったビルドオプション（`vite.build.rollupOptions.external`）の設定を環境変数を元にした分岐によってセットする/しないといった処理を書くことで解決できました
- `※今回の件には関係ない気がしますがサーバーサイドレンダリングは無効化しているプロジェクトになります。`
```diff_typescript:nuxt.config.ts
import { createResolver } from "@nuxt/kit";

const { resolve } = createResolver(import.meta.url);

// 除外したいページコンポーネントについてnuxt.config.tsからの相対パスの配列を作成
// createResolverから取り出したresolve関数で絶対パスに変換
// components以下のコンポーネントは使って無ければ自動で除外されるはず
const pagesToExcludeInProduction = [
  "pages/demo/appendix/webrtc-chat.vue",
].map((path) => resolve(path));

export default defineNuxtConfig({
  ssr: false,

  vite: {
    build: {
      rollupOptions: {
        external: process.env.NODE_ENV === "production" ? pagesToExcludeInProduction : undefined,
      },
    },
  },
});
```

- `NODE_ENV`環境変数は`nuxi dev`のとき`development`、`nuxi build`のとき`production`を取り、今回のやりたいことに合致するので採用しました
- `NODE_ENV`環境変数が`production`のとき除外対象となるページのVueファイルのパス配列が渡り、そうでないとき初期値のundefinedが渡ります

## 動作確認
① ローカルの開発サーバー上では特に問題なく動作することを確認
```bash
$ npx nuxt dev
Nuxt 3.10.3 with Nitro 2.9.1                                                                                           9:03:05 PM
                                                                                                                       9:03:05 PM
  ➜ Local:    http://localhost:3000/
  ➜ Network:  use --host to expose

ℹ Using yup with vee-validate                                                                                         9:03:07 PM
  ➜ DevTools: press Shift + Alt + D in the browser (v1.0.8)                                                            9:03:07 PM

ℹ Compiled plugins/server.mjs in 527.94ms                                                                             9:03:09 PM
ℹ Compiled plugins/client.mjs in 536.66ms                                                                             9:03:09 PM
ℹ Compiled types/plugins.d.ts in 592.84ms                                                                             9:03:09 PM
ℹ Start build:done hook.                                                                                              9:03:10 PM

~~~省略~~~

ℹ End build:done hook.                                                                                                9:03:11 PM
ℹ Vite client warmed up in 9691ms                                                                                     9:03:19 PM
✔ Nuxt Nitro server built in 3550 ms                                                                            nitro 9:03:22 PM
```
|チャット画面||
|---|---|
|初期表示<br />![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/f62ba4d3-6f47-69be-c6d3-cb1a61182cb2.png)|ホスト⇒クライアントのキー渡してる画面<br />![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/d205f0b5-bbf1-5b86-f7e0-6703ca43f789.png)|
|チャット①<br />![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/9ff2a1e1-b5da-293f-23b8-0f0a29d2b8fe.png)|チャット②<br />![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/66bb784d-2f7d-5ca3-5c2a-dd72bcf96b32.png)|

---
② node-server向けのプリセットで本番ビルドを実施＆node-serverを起動しクローズドチャットのページにアクセスし404エラーとなることを確認
```bash
$ NITRO_PRESET=node-server npx nuxt build
Nuxt 3.10.3 with Nitro 2.9.1                                                                                           9:01:36 PM
ℹ Using yup with vee-validate                                                                                         9:01:37 PM
ℹ Building client...                                                                                                  9:01:39 PM
ℹ vite v5.1.4 building for production...                                                                              9:01:39 PM

~~~省略~~~

✔ You can preview this build using node .output/server/index.mjs                                                nitro 9:01:53 PM

$ node .output/server/index.mjs
Listening on http://[::]:3000
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/36da1bb0-99b6-407b-b518-94cc0e0b4c1b.png)

```
クローズドなチャットのURLにアクセス
↓
app.vueの表示処理が走る
↓
TypeError: error loading dynamically imported moduleのエラーが発生
↓
app.vueと同階層に配置したerror.vueの表示処理が走る
↓
error.vueの中でuseRouteコンポーザブルから取得できるルートのパスが`/demo/appendix/webrtc-chat`のとき特別なエラー画面を表示
```
という流れ（のはず:thinking:）


---
③ AWS Lambda向けのプリセットで本番ビルドを実施＆生成物(`.outputディレクトリ以下のリソース`)について除外したいページのテキストが検索で引っかからないことを確認
```bash
$ NITRO_PRESET=aws-lambda npx nuxt build
Nuxt 3.10.3 with Nitro 2.9.1                                                                                                10:31:50 PM
ℹ Using yup with vee-validate                                                                                              10:31:51 PM
ℹ Building client...                                                                                                       10:31:53 PM
ℹ vite v5.1.4 building for production...                                                                                   10:31:53 PM

~~~省略~~~

ℹ ✓ built in 1.05s                                                                                                         10:32:15 PM
✔ Server built in 1103ms                                                                                                   10:32:15 PM
ℹ Start build:done hook.                                                                                                   10:32:15 PM
ℹ copied PrimeVue themes from /app/node_modules/primevue/resources/themes to /app/public/themes                            10:32:15 PM
ℹ End build:done hook.                                                                                                     10:32:15 PM
✔ Generated public .output/public                                                                                    nitro 10:32:15 PM
ℹ Building Nuxt Nitro server (preset: aws-lambda)                                                                    nitro 10:32:16 PM
✔ Nuxt Nitro server built                                                                                            nitro 10:32:24 PM
  ├─ .output/server/chunks/_/articles.mjs (1.89 kB) (680 B gzip)
  ├─ .output/server/chunks/_/articles.mjs.map (1.38 kB) (496 B gzip)
~~~省略~~~
  ├─ .output/server/index.mjs (233 B) (175 B gzip)
  └─ .output/server/package.json (4.14 kB) (1.11 kB gzip)
Σ Total size: 5.22 MB (1.22 MB gzip)
```

|||
|---|---|
|`.output`をエディタで開いた状態<br />![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/10b0271c-573d-4ecc-f819-029c728bc035.png)|`.output/public/_nuxt`以下にWebRTCチャット用のjsファイルが生成されていないことを確認<br />![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/85e619e8-e7c7-9d83-fbe6-6abad331dd15.png)|
|WebRTC chat内のテキストで検索(`WebRTCを使ったP2Pチャットです`)<br />ヒットしないことを確認<br />![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/55ef4016-26c0-0401-2473-19643f581695.png)|ソース側で`WebRTCを使ったP2Pチャットです`を検索したときの結果<br />ヒットすることを確認![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/2a3e6ef8-ec2c-523a-ccab-24e92ee028b9.png)|

## 参考リンク
- [javascript - How to tell Vite to exclude a subset of files in a directory from build? - Stack Overflow](https://stackoverflow.com/questions/75412767/how-to-tell-vite-to-exclude-a-subset-of-files-in-a-directory-from-build)
  - 今回の解決方法を思い当たるに至った情報:pray:
- [個人開発でクローズドなチャットを作るので電気通信事業に届出](https://zenn.dev/saikou_kunisaki/articles/3c39a2f83006d4)
  - クローズドなチャットって悪用できそうだよなーと思ってググったときに見つけた情報:pray:
- [Error Handling · Get Started with Nuxt](https://nuxt.com/docs/getting-started/error-handling#error-page)
  - Nuxt3のエラーハンドリングに関するページ（公式）:pray:

## 2024/03/26追記
記事の先頭に追記した`.nuxtignore`について自分のプロジェクトでも少し使ってみました。

- 書き方は`.gitignore`と一緒
- `pages/`ディレクトリの他に`layouts/`、`middleware/`などのディレクトリのファイルも除外できるみたい

### .nuxtignore設定
以下のようなファイルをプロジェクトのルートに作成
```:.nuxtignore
pages/demo/appendix/webrtc-chat.vue
```

### 動作確認1
まずは開発サーバーを起動して確認
```bash
$ npx nuxt dev
```

↓アプリケーション内のサイドバーに設置した[NuxtLink](https://nuxt.com/docs/api/components/nuxt-link)から除外対象のページに遷移しようとしたところ用意した固有のエラー画面ではなく404エラー画面が表示されました

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/6463dfb3-27b8-59ac-eb24-ca3fa8f2157c.png)
- この挙動は`vite.build.rollupOptions.external`を利用してページを除外したときには見られなかった動作でした
  - error.vueの実装次第かもしれない

---
↓除外対象のページのURLに直アクセスしようとしてこの間作ったerror.vueを経由したエラー画面が表示できることを確認

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/2297052e-65f7-ddac-2868-7af0b809ec2e.png)

---
↓その他、開発サーバー起動中に`.nuxtignore`を編集すると保存時に再読み込みされることを確認

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/7394c51c-5632-d03b-1092-51869f39741b.png)

### 動作確認2
次に`node-server`向けの本番ビルドを実施して確認
```bash
$ NITRO_PRESET=node-server npx nuxt build
$ node .output/server/index.mjs
```

開発サーバーを起動したときの動作確認と全く同一の動作となることを確認できました。

### メモ
`.nuxtignore`について除外対象をGit管理しつつ「開発サーバーでは除外しない/本番ビルドでは除外する」を実現するのは以下のような自分の環境由来の問題もあり、やや手間な気がしました。

- 切りのいいところでローカルの開発環境からServerless Frameworkを使用して手作業で本番デプロイしている
- 本番ビルドで除外したいファイルについて`.nuxtignore`で定義していると、開発サーバーを起動する際にも除外されてしまう

---
以下の内容は少し考えただけで試せていませんが、本番ビルドのデプロイ手順について以下のような形に作り替えることで開発サーバー/本番ビルドについて解決できそう

1. 本番環境で除外したいページについて`.nuxtignore`ではなく`.nuxtignore.production`みたいなファイルを用意
2. GitHub Actionsのワークフローの中で本番ビルド（`npx nuxt build`）の前の手順として`cp .nuxtignore.production .nuxtignore`する
3. Serverless Frameworkで本番ビルドの成果物一式をデプロイする

```:ワークフローのイメージ
$ cp .nuxtignore.production .nuxtignore
$ NITRO_PRESET=aws-lambda npx nuxt build
$ npx serverless deploy
```

---
追記の内容と直接関係ないけどServerless FrameworkについてGitHub Actionsのワークフローから利用する場合、以下のアクションを利用するのが良さそう？

https://github.com/serverless/github-action