🧙‍♀️

TypeScript + Vite で Chrome 拡張機能を作る

2022/12/18に公開

この記事は TypeScript Advent Calendar 2022 の19日目です。


今年、簡単な Chrome 拡張機能を開発した。

https://chrome.google.com/webstore/detail/hamacolor/ciofpkbhollmjpaobphhjdngaohglpgp

https://github.com/hamakou108/hama-color

開発初期は公式チュートリアルに従って JavaScript で開発していたが、 CRXJS という Vite プラグインを使えば TypeScript のコードを簡単に Chrome 拡張機能用にビルドできるということを後から知り、コードの書き換えを行った。次の開発時はこの方法を最初から使えるように、 TypeScript + Vite を使った開発手順について改めてまとめる。

前提

  • 今回は最新である Manifest V3 のチュートリアルの内容に合わせた
  • 以下の点については特に触れない
    • Vite の基本的な使い方
    • Chrome 拡張機能の公開申請
  • 環境は以下の通り
    • MacOS 13.0.1
    • Node.js 18.12.1

なお今回書いたコードは以下のリポジトリから確認できる。

https://github.com/hamakou108/chrome-extension-with-typescript-and-vite

インストール

以下のパッケージをインストールする。

$ yarn add --dev typescript vite@^2.9.4 @crxjs/vite-plugin @types/chrome

TypeScript と Vite 以外のパッケージについて補足。

実装

package.json には Vite のビルドのスクリプトを書き足しておく。

package.json
{
  "scripts": {
    "dev": "vite",
    "build": "tsc --noEmit && vite build",
    "preview": "vite preview"
  },
  "devDependencies": {
    "@crxjs/vite-plugin": "^1.0.14",
    "@types/chrome": "^0.0.204",
    "typescript": "^4.9.4",
    "vite": "^2.9.4"
  }
}

tsconfig.json は以下の通り。大抵の拡張機能の処理は DOM 操作を伴うため、 libDOM も指定しておいた方が良い。

tsconfig.json
{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "lib": ["ESNext", "DOM"],
    "moduleResolution": "Node",
    "strict": true
  },
  "include": ["scripts"]
}

vite.config.ts にはビルドの設定に加え、通常は manifest.json (Chrome 拡張機能に関する情報を記述したファイル)に書かれる情報を記述する。 manifest.json ファイルをインポートすることもできるが、 vite.config.ts に記述すると型補完が効くのでオススメ。

vite.config.ts
import { defineConfig } from 'vite'
import { crx, defineManifest } from '@crxjs/vite-plugin'

const manifest = defineManifest({
  manifest_version: 3,
  name: 'Reading time',
  description: 'Add the reading time to Chrome Extension documentation articles',
  version: '1.0',
  icons: {
    '16': 'images/icon-16.png',
    '32': 'images/icon-32.png',
    '48': 'images/icon-48.png',
    '128': 'images/icon-128.png',
  },
  content_scripts: [
    {
      js: ['scripts/content.ts'], // 拡張子を .ts に変更する
      matches: [
        'https://developer.chrome.com/docs/extensions/*',
        'https://developer.chrome.com/docs/webstore/*',
      ]
    }
  ],
})

export default defineConfig({
  plugins: [crx({ manifest })],
})

後はチュートリアルに従ってファイルを追加すると、次のようなファイル構造になる。

$ tree -I 'node_modules'
.
├── images
│   ├── icon-128.png
│   ├── icon-16.png
│   ├── icon-32.png
│   └── icon-48.png
├── package.json
├── scripts
│   └── content.ts
├── tsconfig.json
├── vite.config.ts
└── yarn.lock

2 directories, 10 files

content.ts の内容は以下の通り。 TypeScript の型エラーが出る箇所だけ(雑に)修正している。

content.ts
const article = document.querySelector("article");

// `document.querySelector` may return null if the selector doesn't match anything.
if (article) {
  const text = article.textContent;
  const wordMatchRegExp = /[^\s]+/g; // Regular expression
  const words = text!.matchAll(wordMatchRegExp);
  // matchAll returns an iterator, convert to array to get word count
  const wordCount = [...words].length;
  const readingTime = Math.round(wordCount / 200);
  const badge = document.createElement("p");
  // Use the same styling as the publish information in an article's header
  badge.classList.add("color-secondary-text", "type--caption");
  badge.textContent = `⏱️ ${readingTime} min read`;

  // Support for API reference docs
  const heading = article.querySelector("h1");
  // Support for article docs with date
  const date = article.querySelector("time")?.parentNode;

  ((date ?? heading) as Element).insertAdjacentElement("afterend", badge);
}

ビルドと実行

開発時は yarn dev を実行すると、 dist ディレクトリ下に manifest.json やスクリプト類が生成される。開発中の拡張機能は Loading an unpacked extension に従ってブラウザで実行できる。この手順の Load Unpacked ボタンを押すステップで dist ディレクトリを指定する。


ブラウザに読み込まれた拡張機能


Chrome 拡張機能の公式ページに N min read が表示される

ストアに公開する際は yarn build でビルドし、 dist ディレクトリ以下を zip で固めてアップロードする。

Discussion