Chrome拡張を自作してGitHubのリリースとして公開する備忘録

## Chrome拡張機能を作成＆開発

### プロジェクト作成

`npx create chrome-ext`からのプロジェクト作成を行いました。
`create chrome-ext`というコマンドは辿っていくと以下のリポジトリにたどり着きます。

https://github.com/guocaoyi/create-chrome-ext

- 著名なフロントエンドフレームワーク（React、Vue、Svelte、他多数）＆Viteのビルドを想定したプロジェクトが作成できる（バニラも選択可）
- デフォルトの状態でコンテンツスクリプト、ポップアップ、バックグラウンド、サイドパネル等の機能が有効化されたテンプレートとなっている（Vueのテンプレートでのみ確認）

---
今回は`demo-chrome-ext`という名前でVue.jsを使用したChrome拡張を作成していきます

CLIからプロジェクト作成
```bash
$ npx create chrome-ext demo-chrome-ext
√ Framework: » vue
√ Language: » vue-ts

Scaffolding project in C:\workspace_browser_ext\demo-chrome-ext...

Done. Now run:

  cd demo-chrome-ext
  npm install
  npm run dev

   Suggest you next step:
    1. cd demo-chrome-ext
    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.
```

以下のような形で使用するフレームワークとJavaScript or TypeScriptの選択が行えました。
今回は`vue`⇒`vue-ts`と選択。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/a6e98b49-c8f7-18e5-6bd2-3eced90b0e31.png)
↓
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/e57fef2a-c7a6-32d8-99a5-1e5538381c17.png)

---

プロジェクト作成時のコンソール出力の内容に従い作成したプロジェクトのディレクトリに移動＆npmで管理するパッケージをインストール
```bash
$ cd demo-chrome-ext
$ npm install
```

ひとまずViteの開発サーバーを起動するnpmスクリプトが実行できることを確認
```bash
$ npm run dev

> demo-chrome-ext@0.0.0 dev
> vite


  VITE v4.5.3  ready in 711 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose
  ➜  press h to show help
The emitted file "img/logo-34.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.
The emitted file "img/logo-16.png" overwrites a previously emitted file of the same name.
```

### パッケージ最新化
npmで管理しているパッケージは全体的にやや古い状態だったので以下のコマンドで最新のものを入れ直しました。
6件見つかった脆弱性に関するアラートが消えました。
```bash:dependencies側を更新
$ npm install vue@latest

up to date, audited 479 packages in 1s

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

6 high severity vulnerabilities

To address issues that do not require attention, run:
  npm audit fix

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

Run `npm audit` for details.
```
```bash:devDependencies側を更新
$ npm install -D @crxjs/vite-plugin@^2.0.0-beta.23 @types/chrome@latest @vitejs/plugin-vue@latest gulp@latest gulp-zip@latest prettier typescript vite@latest vue-tsc@latest

added 25 packages, removed 256 packages, changed 71 packages, and audited 248 packages in 19s

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

found 0 vulnerabilities
```

package.jsonのアップデート前後の差分は以下の通りとなりました。
```diff_json:package.json
{
  "name": "demo-chrome-ext",
  "displayName": "demo-chrome-ext",
  "version": "0.0.0",
  "author": "**",
  "description": "",
  "type": "module",
  "license": "MIT",
  "keywords": [
    "chrome-extension",
    "vue",
    "vite",
    "create-chrome-ext"
  ],
  "engines": {
    "node": ">=14.18.0"
  },
  "scripts": {
    "dev": "vite",
    "build": "vue-tsc --noEmit && vite build",
    "preview": "vite preview",
    "fmt": "prettier --write '**/*.{vue,ts,json,css,scss,md}'",
    "zip": "npm run build && node src/zip.js"
  },
  "dependencies": {
-    "vue": "^3.3.4"
+    "vue": "^3.4.27"
  },
  "devDependencies": {
-    "@crxjs/vite-plugin": "^2.0.0-beta.19",
-    "@types/chrome": "^0.0.246",
-    "@vitejs/plugin-vue": "^4.4.0",
-    "gulp": "^4.0.2",
+    "@crxjs/vite-plugin": "^2.0.0-beta.23",
+    "@types/chrome": "^0.0.268",
+    "@vitejs/plugin-vue": "^5.0.5",
+    "gulp": "^5.0.0",
    "gulp-zip": "^6.0.0",
-    "prettier": "^3.0.3",
-    "typescript": "^5.2.2",
-    "vite": "^4.4.11",
-    "vue-tsc": "^1.8.18"
+    "prettier": "^3.3.1",
+    "typescript": "^5.4.5",
+    "vite": "^5.2.13",
+    "vue-tsc": "^2.0.21"
  }
}
```

## Chromeに開発中のChrome拡張をインストール
1. Viteの開発サーバーを起動しておく
2. Chromeで`chrome://extensions`にアクセス
3. `パッケージ化されていない拡張機能を読み込む`ボタンをクリック
4. ダイアログで開発中のChrome拡張のbuildディレクトリを指定

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/49f5c509-a572-4749-9de2-4dbbb2dc4bb2.png)
↓
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/2a7b5eb2-7277-f7aa-93e2-d9efd25dbe92.png)
↓
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/45dd4294-198e-3238-d522-c617323dab7a.png)

## 開発中のChrome拡張の動作確認
`create chrome-ext`コマンドで作成したVue&TypeScriptのテンプレートではChrome拡張の各機能間で共有可能なカウントアップが行えるような拡張機能となっていました。

---
POPUP PAGEというポップアップが表示できることを確認

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/b3226242-76c7-51c9-c020-10da290125ea.png)

---
インストールしたChrome拡張の`Service Worker`をクリックして表示されたdevtoolでカウントアップ時のコンソール出力を確認

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/54ac912a-528a-08ff-7357-9bdc5e861434.png)
↓
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/4c1909ab-5929-1b4f-ce78-b80c3b51c966.png)

---
`Chrome拡張のボタンを右クリック > オプション`からオプションページが表示できることを確認

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/d1cdcb9a-d2dd-3aa9-70bd-048239bc699e.png)

---
`Chrome拡張のボタンを右クリック > サイドパネルを開く`からサイドパネルが表示できることを確認

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/250a602a-19cf-89d5-51e1-a940cf27c7a8.png)

---
新規タブ追加時のページがデフォルトの状態から上書きされていることを確認

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/4046e4e1-3a1d-116f-7db5-cc139898d6e6.png)

## ボイラープレートから不要な機能を取り除く
`create chrome-ext`コマンドで作成したVue&TypeScriptのボイラープレートに不要な機能がある場合の除外手順をまとめてみました

### 新規タブ追加時の画面が不要な場合
<details>
<summary>手順</summary>

不要なファイルを削除

```bash
$ rm -fr newtab.html src/newtab
```

src/manifest.tsを編集
```diff_typescript:src/manifest.ts
import { defineManifest } from '@crxjs/vite-plugin'
import packageData from '../package.json'

//@ts-ignore
const isDev = process.env.NODE_ENV == 'development'

export default defineManifest({
  name: `${packageData.displayName || packageData.name}${isDev ? ` ➡️ Dev` : ''}`,
  description: packageData.description,
  version: packageData.version,
  manifest_version: 3,
  icons: {
    16: 'img/logo-16.png',
    32: 'img/logo-34.png',
    48: 'img/logo-48.png',
    128: 'img/logo-128.png',
  },
  action: {
    default_popup: 'popup.html',
    default_icon: 'img/logo-48.png',
  },
  options_page: 'options.html',
  devtools_page: 'devtools.html',
  background: {
    service_worker: 'src/background/index.ts',
    type: 'module',
  },
  content_scripts: [
    {
      matches: ['http://*/*', 'https://*/*'],
      js: ['src/contentScript/index.ts'],
    },
  ],
  side_panel: {
    default_path: 'sidepanel.html',
  },
  web_accessible_resources: [
    {
      resources: ['img/logo-16.png', 'img/logo-34.png', 'img/logo-48.png', 'img/logo-128.png'],
      matches: [],
    },
  ],
  permissions: ['sidePanel', 'storage'],
-  chrome_url_overrides: {
-    newtab: 'newtab.html',
-  },
})
```

（Git管理している場合）VSCodeで見た差分

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/89424959-e53b-01f3-ea29-eac6c0bf235f.png)
</details>

### ポップアップが不要な場合
<details>
<summary>手順</summary>

不要なファイルを削除

```bash
$ rm -fr popup.html src/popup
```

src/manifest.tsを編集
```diff_typescript:src/manifest.ts
import { defineManifest } from '@crxjs/vite-plugin'
import packageData from '../package.json'

//@ts-ignore
const isDev = process.env.NODE_ENV == 'development'

export default defineManifest({
  name: `${packageData.displayName || packageData.name}${isDev ? ` ➡️ Dev` : ''}`,
  description: packageData.description,
  version: packageData.version,
  manifest_version: 3,
  icons: {
    16: 'img/logo-16.png',
    32: 'img/logo-34.png',
    48: 'img/logo-48.png',
    128: 'img/logo-128.png',
  },
-  action: {
-    default_popup: 'popup.html',
-    default_icon: 'img/logo-48.png',
-  },
  options_page: 'options.html',
  devtools_page: 'devtools.html',
  background: {
    service_worker: 'src/background/index.ts',
    type: 'module',
  },
  content_scripts: [
    {
      matches: ['http://*/*', 'https://*/*'],
      js: ['src/contentScript/index.ts'],
    },
  ],
  side_panel: {
    default_path: 'sidepanel.html',
  },
  web_accessible_resources: [
    {
      resources: ['img/logo-16.png', 'img/logo-34.png', 'img/logo-48.png', 'img/logo-128.png'],
      matches: [],
    },
  ],
  permissions: ['sidePanel', 'storage'],
  chrome_url_overrides: {
    newtab: 'newtab.html',
  },
})
```

（Git管理している場合）VSCodeで見た差分

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/d09604da-0fd0-1066-0362-6c7fd6e6339b.png)
</details>

### オプション画面が不要な場合
<details>
<summary>手順</summary>

不要なファイルを削除

```bash
$ rm -fr options.html src/options
```

src/manifest.tsを編集
```diff_typescript:src/manifest.ts
import { defineManifest } from '@crxjs/vite-plugin'
import packageData from '../package.json'

//@ts-ignore
const isDev = process.env.NODE_ENV == 'development'

export default defineManifest({
  name: `${packageData.displayName || packageData.name}${isDev ? ` ➡️ Dev` : ''}`,
  description: packageData.description,
  version: packageData.version,
  manifest_version: 3,
  icons: {
    16: 'img/logo-16.png',
    32: 'img/logo-34.png',
    48: 'img/logo-48.png',
    128: 'img/logo-128.png',
  },
  action: {
    default_popup: 'popup.html',
    default_icon: 'img/logo-48.png',
  },
-  options_page: 'options.html',
  devtools_page: 'devtools.html',
  background: {
    service_worker: 'src/background/index.ts',
    type: 'module',
  },
  content_scripts: [
    {
      matches: ['http://*/*', 'https://*/*'],
      js: ['src/contentScript/index.ts'],
    },
  ],
  side_panel: {
    default_path: 'sidepanel.html',
  },
  web_accessible_resources: [
    {
      resources: ['img/logo-16.png', 'img/logo-34.png', 'img/logo-48.png', 'img/logo-128.png'],
      matches: [],
    },
  ],
  permissions: ['sidePanel', 'storage'],
  chrome_url_overrides: {
    newtab: 'newtab.html',
  },
})
```

（Git管理している場合）VSCodeで見た差分

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/3d8b07c5-3b7d-0eef-a130-4c01e2fec4c4.png)
</details>

### サイドパネルが不要な場合
<details>
<summary>手順</summary>

不要なファイルを削除

```bash
$ rm -fr sidepanel.html src/sidePanel
```

src/manifest.tsを編集
```diff_typescript:src/manifest.ts
import { defineManifest } from '@crxjs/vite-plugin'
import packageData from '../package.json'

//@ts-ignore
const isDev = process.env.NODE_ENV == 'development'

export default defineManifest({
  name: `${packageData.displayName || packageData.name}${isDev ? ` ➡️ Dev` : ''}`,
  description: packageData.description,
  version: packageData.version,
  manifest_version: 3,
  icons: {
    16: 'img/logo-16.png',
    32: 'img/logo-34.png',
    48: 'img/logo-48.png',
    128: 'img/logo-128.png',
  },
  action: {
    default_popup: 'popup.html',
    default_icon: 'img/logo-48.png',
  },
  options_page: 'options.html',
  devtools_page: 'devtools.html',
  background: {
    service_worker: 'src/background/index.ts',
    type: 'module',
  },
  content_scripts: [
    {
      matches: ['http://*/*', 'https://*/*'],
      js: ['src/contentScript/index.ts'],
    },
  ],
-  side_panel: {
-    default_path: 'sidepanel.html',
-  },
  web_accessible_resources: [
    {
      resources: ['img/logo-16.png', 'img/logo-34.png', 'img/logo-48.png', 'img/logo-128.png'],
      matches: [],
    },
  ],
-  permissions: ['sidePanel', 'storage'],
+  permissions: ['storage'],
  chrome_url_overrides: {
    newtab: 'newtab.html',
  },
})
```

（Git管理している場合）VSCodeで見た差分

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/13f1786f-0e14-bfe3-4305-1045bbad4567.png)
</details>

## タグ作成をトリガーとしたGitHub Actionsのワークフローを作成
<details>
<summary>目的達成に至るまでの備忘録</summary>

1. vite buildの成果物のアップロード先としてGitHubのリリースを作成しAssetsにアップロードするワークフローを作る必要がある
2. [actions/create-release](https://github.com/actions/create-release)アクションでGitHubのリリースを作成し[actions/upload-release-asset](https://github.com/actions/upload-release-asset)アクションでリリースのAssetsにファイルをアップロードできそう
   - `actions/create-release`、`actions/upload-release-asset`の両アクションについて公式のドキュメントを確認したところリポジトリがアーカイブされていた。現在は以下のいずれかのアクションを使用するのが良いとのこと（[actions/create-releaseのREADME.md](https://github.com/actions/create-release/blob/4c11c9fe1dcd9636620a16455165783b20fc7ea0/README.md)より）
     - [elgohr/Github-Release-Action](https://github.com/elgohr/Github-Release-Action)
     - [marvinpinto/action-automatic-releases](https://github.com/marvinpinto/action-automatic-releases)
     - [softprops/action-gh-release](https://github.com/softprops/action-gh-release)
     - [ncipollo/release-action](https://github.com/ncipollo/release-action)
   - 上記4アクションのうち[actions/upload-release-assetアクションのREADME.md](https://github.com/actions/upload-release-asset/blob/bdbc5e2d22a98fa50a14f5528f40f68cadd4b4c1/README.md)でも`softprops/action-gh-release`アクションに関する言及があること、4リポジトリの中で最もスター数が多いことなどから今回は`softprops/action-gh-release`アクションを利用することとしました
3. `softprops/action-gh-release`アクションについて調べ、リリースの作成とAssetsへのアップロードについて1つのアクションで実行できることを確認
</details>

作成したワークフローは以下のようになりました。
```yaml:.github/workflows/create-release.yml
name: Create Release

on:
  push:
    tags:
      - "*"

permissions:
  contents: write

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "20"

      - name: Build
        run: |
          npm install
          npm run build
          mv build demo-chrome-ext
          zip -r demo-chrome-ext-${{ github.ref_name }}.zip demo-chrome-ext

      - name: Release
        uses: softprops/action-gh-release@v2
        with:
          files: demo-chrome-ext-${{ github.ref_name }}.zip
          generate_release_notes: true
```

- `softprops/action-gh-release`アクションのREADMEにも書いてありますがワークフローを実行するのに`contents: write`の権限が必要です
- `generate_release_notes: true`オプションを指定することでリリースノートについて自動生成しています
- `${{ github.ref_name }}`はタグ作成をトリガーとしているため`0.0.1`のようなバージョンが入ります

## リリースノート自動生成用のカテゴリ設定
以下のようなrelease.ymlを作成
```yaml:.github/release.yml
changelog:
  categories:
    - title: 🐛 Bug Fixes
      labels:
        - bug
    - title: 📚 Documentation
      labels:
        - documentation
    - title: 🚫 Duplicates
      labels:
        - duplicate
    - title: 🚀 Enhancements
      labels:
        - enhancement
    - title: 👶 Good First Issues
      labels:
        - good first issue
    - title: 🙋 Help Wanted
      labels:
        - help wanted
    - title: 🚷 Invalid
      labels:
        - invalid
    - title: ❓ Questions
      labels:
        - question
    - title: ⛔ Won't Fix
      labels:
        - wontfix
    - title: Others
      labels:
        - "*"
```

- [GitHubリポジトリにデフォルトで設定されている9つのラベル](https://docs.github.com/ja/issues/using-labels-and-milestones-to-track-work/managing-labels#about-default-labels)に対応するrelease.ymlを作成しました（大枠はAIに出力してもらった）
   - 意味を理解してないラベルがある...
   - 必ずしもデフォルトのまま使い続ける必要はないはず。不要なものは削除しても良いかも
- デフォルト以外のラベルのPRは全てOthersというカテゴリに含まれるような設定としています

## ワークフローの動作確認
ローカルでタグを作成してリモートにプッシュ（`最新のmainブランチにいる状態で以下実施`）
```bash
$ git tag 0.0.1
$ git push origin 0.0.1
Total 0 (delta 0), reused 0 (delta 0), pack-reused 0
To https://github.com/imo-tikuwa/demo-chrome-ext.git
 * [new tag]         0.0.1 -> 0.0.1
```

---
タグ作成をトリガーとしたワークフローが走ったことを確認

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/70b02f16-0ff8-0347-f0c8-9a43bcf7c402.png)

---
ワークフローの実行によって生成されたリリース内にリリースノートが生えてることを確認

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384986/cf8da1a6-c8c7-99ee-312e-aa76a0aac531.png)

- リリースノートの自動生成について`.github/release.yml`で設定したカテゴリごとにPRがまとめられていることを確認
- vite buildによって出来上がったChrome拡張機能のコード一式を固めたzipファイルをAssets欄にアップロードできていることを確認

## 備考
- 今回は手動によるタグ作成をワークフローのトリガーとしていますが、ストアへの公開を目標とした正式なChrome拡張を開発する場合はセマンテックバージョニングのルールに基づいたバージョン管理を行える必要があると思います
- 記事の中で書いたVue&TypeScriptのテンプレートではpackage.jsonのversionに記載した値をChrome拡張のバージョンとして設定するようになっているため、前段としてpackage.jsonのバージョンを書き換えるようなワークフローを組めると良いかなと思いました

今回の記事の中で実際に作成したデモのChrome拡張は以下のリポジトリで公開しています

https://github.com/imo-tikuwa/demo-chrome-ext

## 参考サイト
- [Google Chrome 拡張機能 / Firefox アドオン作成ボイラープレート 2024 - なつねこメモ](https://tech.natsuneko.blog/entry/2024/01/06/make-your-own-browser-extensions-in-2024)
  - Chrome拡張のボイラープレートを探していたときに読んだページ
- [自動生成リリース ノート - GitHub Docs](https://docs.github.com/ja/repositories/releasing-projects-on-github/automatically-generated-release-notes)
  - リリースノートの自動生成について調べたときに読んだページ（公式）
- [ラベルを管理する - GitHub Docs](https://docs.github.com/ja/issues/using-labels-and-milestones-to-track-work/managing-labels#about-default-labels)
  - 9つのデフォルトラベルについて意味を調べたときに読んだページ（公式）