Chrome拡張開発時の躓き箇所のメモ3点

Chrome用のブラウザ拡張を開発するのに [create-chrome-ext](https://github.com/guocaoyi/create-chrome-ext) (の [vue-ts テンプレート](https://github.com/guocaoyi/create-chrome-ext/tree/main/template-vue-ts))をボイラープレートとして使用していたときの躓き箇所の備忘録になります。

## DockerのNode.jsを利用するとHMRが動作しない
最初はホストPC(Windows)にインストールした Node.js(20.x) を使用していたのですが、途中で複数台のPCで開発できるよう Docker を使った環境構築を行うよう方針転換しました。

<details>
<summary>Dockerを使った環境構築について</summary>

複雑なことはせずにviteのデフォルトのポート(5173)のポートフォワーディングを行うくらいのコンテナ環境を作成しています。

```yml:compose.yml
services:
  app:
    build:
      context: ./
      dockerfile: ./.docker/app/Dockerfile
    volumes:
      - .:/app:cached
    ports:
      - '5173:5173'
    tty: true
```
- compose.yml 作成

```Dockerfile:.docker/app/Dockerfile
FROM node:20-slim
SHELL ["/bin/bash", "-oeux", "pipefail", "-c"]

ENV TZ=Asia/Tokyo
RUN { \
        echo "alias ll='ls -l --color=auto'"; \
    } >> /root/.bashrc

WORKDIR /app
```
- compose.yml に記載した Dockerfile を作成
- `今回は WSL2 配下ではなく WindowsPC のボリューム内にプロジェクトを作っているため ホストとコンテナで UID=1000,GID=1000 に揃えるための USER命令(USER node)はなし`

```diff_typescript:vite.config.ts

import { defineConfig } from 'vite'
import { crx } from '@crxjs/vite-plugin'
import vue from '@vitejs/plugin-vue'
import manifest from './src/manifest'
import Components from 'unplugin-vue-components/vite'
import { PrimeVueResolver } from '@primevue/auto-import-resolver'

export default defineConfig(({ mode }) => {
  const production = mode === 'production'

  return {
+    server: {
+      host: true,
+      port: 5173,
+      hmr: {
+        port: 5173,
+      },
+    },
    build: {
      cssCodeSplit: true,
      emptyOutDir: true,
      outDir: 'build',
      rollupOptions: {
        output: {
          chunkFileNames: 'assets/chunk-[hash].js',
        },
      },
    },
    plugins: [
      crx({ manifest }),
      vue(),
      Components({
        resolvers: [PrimeVueResolver()],
      }),
    ],
  }
})
```
- devモードで起動するときのviteにブラウザから到達できるよう `server.host` や `server.port`、`server.hmr.port` などの設定を実施
</details>

---
ホストPCの Node.js を使用していた頃はサイドパネルのvueファイルを保管時のHMRが正常に動作していました。
Dockerを使用したコンテナ環境ではエディタでファイルを編集した際の`hmr update`の出力が行われず、実際にHMRも行われなくなってしまいました。また、サイドパネルを開いた状態で開発者ツールを開いてみてもコンソールにエラーらしいエラーは出ていませんでした。

こちらの件、HMRに関するエラーらしいエラーも出ていなく原因がよくわかっていませんが、以下のように `server.watch.usePolling: true` を追記することで動作するようになりました。

```diff_typescript:vite.config.ts
import { defineConfig } from 'vite'
import { crx } from '@crxjs/vite-plugin'
import vue from '@vitejs/plugin-vue'
import manifest from './src/manifest'
import Components from 'unplugin-vue-components/vite'
import { PrimeVueResolver } from '@primevue/auto-import-resolver'

export default defineConfig(({ mode }) => {
  const production = mode === 'production'

  return {
    server: {
      host: true,
      port: 5173,
+      watch: {
+        usePolling: true,
+      },
      hmr: {
        port: 5173,
      },
    },
    build: {
      cssCodeSplit: true,
      emptyOutDir: true,
      outDir: 'build',
      rollupOptions: {
        output: {
          chunkFileNames: 'assets/chunk-[hash].js',
        },
      },
    },
    plugins: [
      crx({ manifest }),
      vue(),
      Components({
        resolvers: [PrimeVueResolver()],
      }),
    ],
  }
})
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/fcecfeea-116c-d38e-71d2-637d69baaad8.png)

## manualChunkを設定するとdevサーバーが起動しない
UIフレームワークや自動インポートのためのプラグインを導入していった結果として、デフォルトの閾値（500KB）を超過するチャンクが発生するようになりました。

<details>
<summary>build実行時のログ詳細</summary>

```bash
> docker compose exec app npm run build

> [some-ext]@0.0.1 build
> vue-tsc --noEmit && vite build    

vite v5.4.1 building for production...
✓ 209 modules transformed.
The emitted file "img/logo-16.png" overwrites a previously emitted file of the same name.
The emitted file "img/logo-48.png" overwrites a previously emitted file of the same name.
The emitted file "img/logo-34.png" overwrites a previously emitted file of the same name.
The emitted file "img/logo-128.png" overwrites a previously emitted file of the same name.
build/assets/index.ts-loader-CvVIyBbC.js    0.34 kB
build/sidepanel.html                        0.55 kB │ gzip:   0.33 kB
build/img/logo-16.png                       0.73 kB
build/manifest.json                         1.09 kB │ gzip:   0.65 kB
build/.vite/manifest.json                   1.87 kB │ gzip:   0.45 kB
build/img/logo-34.png                       2.04 kB
build/img/logo-48.png                       3.11 kB
build/img/logo-128.png                      7.80 kB
build/assets/primeicons-C6QP2o4f.woff2     35.15 kB
build/assets/primeicons-MpK4pl85.ttf       84.98 kB
build/assets/primeicons-WjwUDZjB.woff      85.06 kB
build/assets/primeicons-DMOk5skT.eot       85.16 kB
build/assets/primeicons-Dr5RGzOO.svg      342.53 kB │ gzip: 105.26 kB
build/assets/sidepanel-6d3Q9_62.css       350.17 kB │ gzip:  36.56 kB
build/assets/chunk-CPt-ohte.js              0.62 kB │ gzip:   0.26 kB
build/assets/chunk-CrPRWnrr.js              2.23 kB │ gzip:   1.10 kB
build/assets/chunk-D__Qv3Tb.js            722.55 kB │ gzip: 163.71 kB

(!) Some chunks are larger than 500 kB after minification. Consider:
- Using dynamic import() to code-split the application
- Use build.rollupOptions.output.manualChunks to improve chunking: https://rollupjs.org/configuration-options/#output-manualchunks
- Adjust chunk size limit for this warning via build.chunkSizeWarningLimit.
✓ built in 5.53s
```
</details>

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/5366cdb9-ad76-f786-fab5-1f44410b30f0.png)

---
これを解消すべく以下のような manualChunk の設定を施しました。
ちなみにチャンクの分割対象を視覚化するための Bundle Analyzer もこのタイミングで導入しました。

```diff_typescript:vite.config.ts
import { defineConfig } from 'vite'
import { crx } from '@crxjs/vite-plugin'
import vue from '@vitejs/plugin-vue'
import manifest from './src/manifest'
import Components from 'unplugin-vue-components/vite'
import { PrimeVueResolver } from '@primevue/auto-import-resolver'
+import { visualizer } from 'rollup-plugin-visualizer'

export default defineConfig(({ mode }) => {
  const production = mode === 'production'

  return {
    server: {
      host: true,
      port: 5173,
      watch: {
        usePolling: true,
      },
      hmr: {
        port: 5173,
      },
    },
    build: {
      cssCodeSplit: true,
      emptyOutDir: true,
      outDir: 'build',
      rollupOptions: {
        output: {
          chunkFileNames: 'assets/chunk-[hash].js',
+          manualChunks: {
+            vue: ['vue'],
+            datatable: ['primevue/datatable'],
+          },
        },
+        plugins: [visualizer()],
      },
    },
    plugins: [
      crx({ manifest }),
      vue(),
      Components({
        resolvers: [PrimeVueResolver()],
      }),
    ],
  }
})
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/a8be75fd-e8af-d8e1-63ce-334e0160df5c.png)

---
上記設定を施した後のビルド結果が以下

<details>
<summary>build実行時のログ詳細</summary>

```bash
> docker compose exec app npm run build

> [some-ext]@0.0.1 build
> vue-tsc --noEmit && vite build

vite v5.4.1 building for production...
✓ 209 modules transformed.
The emitted file "img/logo-34.png" overwrites a previously emitted file of the same name.
The emitted file "img/logo-16.png" overwrites a previously emitted file of the same name.
The emitted file "img/logo-48.png" overwrites a previously emitted file of the same name.
The emitted file "img/logo-128.png" overwrites a previously emitted file of the same name.
build/assets/index.ts-loader-CvVIyBbC.js    0.34 kB
build/sidepanel.html                        0.70 kB │ gzip:   0.35 kB
build/img/logo-16.png                       0.73 kB
build/manifest.json                         1.09 kB │ gzip:   0.65 kB
build/img/logo-34.png                       2.04 kB
build/.vite/manifest.json                   2.16 kB │ gzip:   0.51 kB
build/img/logo-48.png                       3.11 kB
build/img/logo-128.png                      7.80 kB
build/assets/primeicons-C6QP2o4f.woff2     35.15 kB
build/assets/primeicons-MpK4pl85.ttf       84.98 kB
build/assets/primeicons-WjwUDZjB.woff      85.06 kB
build/assets/primeicons-DMOk5skT.eot       85.16 kB
build/assets/primeicons-Dr5RGzOO.svg      342.53 kB │ gzip: 105.26 kB
build/assets/sidepanel-6d3Q9_62.css       350.17 kB │ gzip:  36.56 kB
build/assets/chunk-CPt-ohte.js              0.62 kB │ gzip:   0.26 kB
build/assets/chunk-CrPRWnrr.js              2.23 kB │ gzip:   1.10 kB
build/assets/chunk-DilsI5ti.js             67.85 kB │ gzip:  27.28 kB
build/assets/chunk-F0L3Wnrr.js            200.27 kB │ gzip:  38.53 kB
build/assets/chunk-kB2xJbjL.js            454.92 kB │ gzip: 100.18 kB
✓ built in 5.48s
```
</details>

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/d3da6ae0-f14b-722c-b9a7-2778b54a7eeb.png)

vueとprimevue/datatableについてチャンクを分割したことにより、チャンクの数が2個増え500KBを超えるものが無くなりました

---
build時のログとして表示されていたワーニングメッセージも抑制されるようになりよかったよかったと思いきや、その後の開発の続きを行う際のdevサーバー起動時に以下のようなエラーが出てサーバーが即落ちするようになってしまってました。:sob:

```bash
> docker compose exec app npm run dev

> [some-ext]@0.0.1 dev
> vite

  VITE v5.4.1  ready in 8149 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: http://172.20.0.2:5173/
  ➜  press h + enter to show help
Error: Could not resolve entry module (vue).
    at error (file:///app/node_modules/rollup/dist/es/shared/rollup.js:1858:30)
    at ModuleLoader.loadEntryModule (file:///app/node_modules/rollup/dist/es/shared/rollup.js:22369:20)
    at async Promise.all (index 2)
    at async Promise.all (index 0) {
  code: 'UNRESOLVED_ENTRY'
}
```

こちらの件、エラーメッセージや manualChunk について調べてもこれと言って解決策が見つけられませんでした。
ということで正しい対応か微妙ですが `vite.config.ts` の先頭に定義されているproductionビルドかを判定するproduction変数を元にbuild時のみ manualChunk の設定を有効化されるように修正しました。

```diff_typescript:vite.config.ts
import { defineConfig } from 'vite'
import { crx } from '@crxjs/vite-plugin'
import vue from '@vitejs/plugin-vue'
import manifest from './src/manifest'
import Components from 'unplugin-vue-components/vite'
import { PrimeVueResolver } from '@primevue/auto-import-resolver'

export default defineConfig(({ mode }) => {
  const production = mode === 'production'

  return {
    server: {
      host: true,
      port: 5173,
      watch: {
        usePolling: true,
      },
      hmr: {
        port: 5173,
      },
    },
    build: {
      cssCodeSplit: true,
      emptyOutDir: true,
      outDir: 'build',
      rollupOptions: {
        output: {
          chunkFileNames: 'assets/chunk-[hash].js',
-          manualChunks: {
-            vue: ['vue'],
-            datatable: ['primevue/datatable'],
-          },
+          manualChunks: production
+            ? {
+                vue: ['vue'],
+                datatable: ['primevue/datatable'],
+              }
+            : undefined,
        },
      },
    },
    plugins: [
      crx({ manifest }),
      vue(),
      Components({
        resolvers: [PrimeVueResolver()],
      }),
    ],
  }
})
```

## ストアに公開したブラウザ拡張のインストールでエラー

サイドパネルやタブに関するパーミッションを利用しているため審査に1～2日ほど掛かりましたが、無事に公開できました:relaxed:
そして動作確認のため、自分でブラウザ拡張をインストールしようとしてみたところ以下のようなエラーが出てしまいました。。:sob:

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/7b5994c0-6d12-3b9e-1e41-682dca5e6abd.png)

> ダウンロード エラー: 画像をデコードできませんでした: logo-128.png

拡張機能内に含まれるロゴ画像(logo-128.png)はPhotoshop(CS6)で自作しWeb用に書き出したものでこれと言って特別なことをした覚えもありません。。
先に結論を書くとzipを作成する際に使用していたgulpをおもむろに4→5に移行していたことが原因でした。

以下の記事や公式のリリースノートが参考になりました:bow:
- [（調査メモ）gulp5で画像ファイルが壊れた場合の対処 #Node.js - Qiita](https://qiita.com/nao_t/items/56d1d5859374fe58da03)
- [Release gulp v5.0.0 · gulpjs/gulp · GitHub](https://github.com/gulpjs/gulp/releases/tag/v5.0.0)
- [gulp v5.0.0 リリース！およそ5年ぶりのアップデート - コハム](https://cohamu.com/entry/20240329/1711711115)

<details>
<summary>原因特定に至るまでの調査内容</summary>

1. Chromeのデベロッパーダッシュボードにアップしたエラーが出ているバージョンの `main.crx` をダウンロード
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/da1a608e-26ca-dd0b-821e-291b5efa9894.png)

2. ダウンロードした `main.crx` を `main.zip` にリネームして展開
3. 展開した中身のlogo-128.pngファイルを確認し確かに破損していることを確認
4. 自分の開発環境内に残っていたアップロード元のzipファイルを展開し、中身のlogo-128.pngファイルが破損していることも確認
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/13fadf1a-e680-e47c-27e1-702307592162.png)

5. このことからChromeのデベロッパーダッシュボードにアップロードしたタイミングで破損したわけではなく開発環境で作成したzipファイルに原因があるという問題の切り分けが可能となった
6. zip化のためのコードはnpm-scriptsから呼び出し可能なスクリプト化(`src/zip.js`)されており、主にgulpを使用したシンプルなコードとなっていた
7. そういえばgulp4について脆弱性のアラートが上がってたからgulp5に更新したな？と思い出した
8. gulp5 + png などでググり自身のアップデートが不具合の原因であったことが判明。。
</details>

---
Chromeのブラウザ拡張開発のボイラープレートとして使用している [create-chrome-ext](https://github.com/guocaoyi/create-chrome-ext) を元に作成したプロジェクトについてnpmで管理するパッケージ周りのバージョンが古くなってしまっているものがあり、その中に`gulp: ^4.0.2` も含まれていました。
これを深く考えず `npm install -D gulp@latest` で最新の`^5.0.0`にアップデートだけしたのがエラーの原因でした。

<details>
<summary>create-chrome-ext を使用してブラウザ拡張のプロジェクトを作成したときの出力</summary>

以下は vanilla-ts テンプレートを使用したプロジェクトを作成したときの出力です
```
>npm create chrome-ext

> npx
> create-chrome-ext

√ Project name: ... .
√ Author: ... no one
√ Framework: » vanilla
√ Language: » vanilla-ts

Scaffolding project in H:\workspace_browser_ext\test-project...

Done. Now run:

  npm install
  npm run dev

   Suggest you next step:
    1. cd
    2. Run npm install
    3. Open chrome://extensions/ in your browser
    4. Check the box for Developer mode in the top right.
    5. Click the Load unpacked extension button.
    6. Select the build/ directory that was created.

------------------------------
>npm install
npm warn deprecated inflight@1.0.6: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and 
powerful.
npm warn deprecated source-map-url@0.4.1: See https://github.com/lydell/source-map-url#deprecated
npm warn deprecated urix@0.1.0: Please see https://github.com/lydell/urix#deprecated
npm warn deprecated glob@7.2.3: Glob versions prior to v9 are no longer supported
npm warn deprecated q@1.5.1: You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.
npm warn deprecated
npm warn deprecated (For a CapTP with native promises, see @endo/eventual-send and @endo/captp)
npm warn deprecated source-map-resolve@0.5.3: See https://github.com/lydell/source-map-resolve#deprecated
npm warn deprecated chokidar@2.1.8: Chokidar 2 does not receive security updates since 2019. Upgrade to chokidar 3 with 15x fewer dependencies
npm warn deprecated resolve-url@0.2.1: https://github.com/lydell/resolve-url#deprecated
npm warn deprecated sourcemap-codec@1.4.8: Please use @jridgewell/sourcemap-codec instead

added 457 packages, and audited 458 packages in 23s

44 packages are looking for funding
  run `npm fund` for details

11 vulnerabilities (7 moderate, 4 high)

To address all issues (including breaking changes), run:
  npm audit fix --force

Run `npm audit` for details.
```
</details>
<details>
<summary>gulp: ^4.0.2 を使用していた頃の脆弱性レポート</summary>

```
>npm audit
# npm audit report

braces  <3.0.3
Severity: high
Uncontrolled resource consumption in braces - https://github.com/advisories/GHSA-grv7-fg5c-xmjg
fix available via `npm audit fix --force`
Will install gulp@5.0.0, which is a breaking change
node_modules/braces
  chokidar  1.3.0 - 2.1.8
  Depends on vulnerable versions of anymatch
  Depends on vulnerable versions of braces
  Depends on vulnerable versions of readdirp
  node_modules/chokidar
    glob-watcher  5.0.0 - 5.0.5
    Depends on vulnerable versions of anymatch
    Depends on vulnerable versions of chokidar
    node_modules/glob-watcher
      gulp  4.0.0 - 4.0.2
      Depends on vulnerable versions of glob-watcher
      Depends on vulnerable versions of gulp-cli
      node_modules/gulp
  micromatch  <=4.0.7
  Depends on vulnerable versions of braces
  node_modules/anymatch/node_modules/micromatch
  node_modules/findup-sync/node_modules/micromatch
  node_modules/matchdep/node_modules/micromatch
  node_modules/readdirp/node_modules/micromatch
    anymatch  1.2.0 - 2.0.0
    Depends on vulnerable versions of micromatch
    node_modules/anymatch
    findup-sync  0.4.0 - 3.0.0
    Depends on vulnerable versions of micromatch
    node_modules/findup-sync
    node_modules/matchdep/node_modules/findup-sync
      liftoff  2.2.3 - 3.1.0
      Depends on vulnerable versions of findup-sync
      node_modules/liftoff
        gulp-cli  1.3.0 - 2.3.0
        Depends on vulnerable versions of liftoff
        Depends on vulnerable versions of matchdep
        node_modules/gulp-cli
      matchdep  >=1.0.1
      Depends on vulnerable versions of findup-sync
      Depends on vulnerable versions of micromatch
      node_modules/matchdep
    readdirp  2.2.0 - 2.2.1
    Depends on vulnerable versions of micromatch
    node_modules/readdirp


11 vulnerabilities (7 moderate, 4 high)

To address all issues (including breaking changes), run:
  npm audit fix --force
```
</details>

### gulpを使用したzip化のスクリプトを修正
gulp5ではデフォルトでsrcに指定するGlobパターンのパスに含まれるストリーム形式のファイルについてUTF-8へのエンコードを行うとのこと。
Viteのプロダクションビルド(`npm run build`)によって build ディレクトリに出来上がったファイル一式をソースとした zip を作成するコードとなっているため、src のオプションとして `encoding: false` を追加することで解決できました。

```diff_javascript:src/zip.js
import gulp from 'gulp'
import zip from 'gulp-zip'
import { createRequire } from 'module'
const require = createRequire(import.meta.url)
const manifest = require('../build/manifest.json')

gulp
-  .src('build/**')
+  .src('build/**', { encoding: false })
  .pipe(zip(`${manifest.name.replaceAll(' ', '-')}-${manifest.version}.zip`))
  .pipe(gulp.dest('package'))
```

## 参考サイト

- [サーバーオプション | Vite](https://ja.vitejs.dev/config/server-options.html#server-watch)
  - `server.watch.usePolling` について調べたときの参考情報（公式）
  - 今回はWSL2環境下に環境構築しているわけではないので `server.watch.usePolling: true` が関係するとは思ってなかったですが、実際にはWindows環境下でDockerを使って立てたコンテナ環境でも必要だったみたいです？
- [（調査メモ）gulp5で画像ファイルが壊れた場合の対処 #Node.js - Qiita](https://qiita.com/nao_t/items/56d1d5859374fe58da03)
  - ブラウザ拡張に含まれるロゴ画像が破損していることが原因でブラウザ拡張のインストールに失敗してしまった際に見たページ
  - ブラウザ拡張についてストアに公開した後で発覚したため非常に助かりました:bow:
- [gulp v5.0.0 リリース！およそ5年ぶりのアップデート - コハム](https://cohamu.com/entry/20240329/1711711115)
  - gulp5 の変更点について日本語でまとまっているページ
- [Release gulp v5.0.0 · gulpjs/gulp · GitHub](https://github.com/gulpjs/gulp/releases/tag/v5.0.0)
  - gulp v5.0.0 のリリースノート（公式）
  - 今回のgulp4→5移行で起きた現象についてはBREAKING CHANGESの2行目 `Default stream encoding to UTF-8` に書いてありました
- [Vite製のアプリケーションにBundle Analyzerを設定する](https://zenn.dev/manalink_dev/articles/vite-bundle-analyzer)
  - manualChunk の設定を行う際の設定対象を洗い出すのに使用したBundle Analyzerを導入したときに参考にしたページです
  - 今回の躓きと直接関係のあるものではないです