🔧

AIで作ったChrome拡張機能をWXTでリプレイスした話

に公開

はじめに

ChatGPTやGemini、ClaudeといったWeb UIから、CursorやClaude Code、Codex CLIなどのコード生成AIツールまで、AIを活用することでChrome拡張機能は驚くほど簡単に作れるようになりました。「こういう機能が欲しい」と伝えれば、動くコードが出てきます。実際、私もAIを活用して複数のChrome拡張機能を公開してきました。

しかし、運用を続けていくうちに問題が見えてきました。

  • 1つのcontent.jsに数千行のコードが詰め込まれている
  • 機能追加のたびにAIへ「このコードに〇〇を追加して」と依頼 → 書き方にムラが出る
  • どこに何があるか把握しづらく、修正に時間がかかる

手作業でも直せますが、毎回コードを読み解く手間が惜しいと感じるようになりました。

そこで、WXTを使ってリプレイスすることにしました。この記事では、移行の経緯とハマったポイントを共有します。

WXTとは

WXTは、Chrome拡張機能開発のためのフレームワークです。Next.jsのようなファイルベースのルーティング規約を持ち、TypeScript対応、ホットリロード、Manifest V3自動生成などの機能を備えています。

WXTの便利な機能

移行してみて特に便利だと感じた機能を紹介します。

ホットリロードによる快適な開発体験

📖 Browser Startup - WXT

pnpm wxt devで開発サーバーを起動すると、コード変更時に自動でリロードされます。Vanilla JSでは「拡張機能を再読み込み → ページをリロード」が必要でしたが、WXTでは不要です。

pnpm wxt dev

開発用ブラウザのデフォルトページはweb-ext.config.tsで指定できます。また、プロファイルの保存先を設定すると、ログイン状態を保持したまま開発できます。

web-ext.config.ts
import { defineWebExtConfig } from 'wxt';
// import { resolve } from 'node:path';

export default defineWebExtConfig({
  startUrls: ['https://example.com'],
  // Mac
  chromiumArgs: ['--user-data-dir=./.wxt/chrome-data'],
  // Windows(絶対パスが必要)
  // chromiumProfile: resolve('.wxt/chrome-data'),
  // keepProfileChanges: true,
});

デフォルトでは開発用ブラウザを起動するたびにプロファイルがリセットされますが、この設定で認証情報やDevTools拡張機能を維持できます。

サイト別にcontent scriptを分けられる

📖 Content Scripts - WXT

Vanilla JSでは1つのcontent.jsに全サイト向けのコードをまとめていました。WXTでは、サイトごとにcontent scriptを分けて配置できます。

entrypoints/
├── github.content.ts      # GitHub専用
├── twitter.content.ts     # X(Twitter)専用
└── content.ts             # 全サイト共通
entrypoints/github.content.ts
export default defineContentScript({
  matches: ['https://github.com/*'],
  main() {
    console.log('GitHub専用の処理');
  },
});

ファイル名で何のサイト向けか一目瞭然になり、コードの見通しが格段に良くなりました。

@wxt-dev/auto-iconsでアイコン自動生成

📖 Auto Icons - WXT

@wxt-dev/auto-iconsを使えば、1つの高解像度アイコンから全サイズを自動生成してくれます。

wxt.config.ts
export default defineConfig({
  modules: ['@wxt-dev/module-react', '@wxt-dev/auto-icons'],
});

assets/icon.png(128px以上推奨)を配置するだけで、16px、32px、48px、128pxが自動生成されます。

wxt submitでCLIから直接公開

📖 Publishing - WXT

WXTにはwxt submitコマンドがあり、CLIから直接Chrome Web Storeに公開できます。

pnpm wxt zip
pnpm wxt submit

環境変数でAPIキーを設定しておけば、GitHub Actionsと組み合わせてCI/CDも構築できます。

Viteエコシステムとの統合

WXTはViteベースで構築されているため、Viteエコシステムのツールとスムーズに統合できます。

Vitest(公式サポート)

📖 Unit Testing - WXT

WXTはWxtVitestという専用プラグインを提供しており、Vitestとの統合を公式にサポートしています。

vitest.config.ts
import { defineConfig } from 'vitest/config';
import { WxtVitest } from 'wxt/testing/vitest-plugin';

export default defineConfig({
  plugins: [WxtVitest()],
});

このプラグインにより、browser APIのモック、自動インポート、エイリアス設定などが自動で構成されます。

Playwright(公式推奨)

📖 E2E Testing - WXT

WXTはPlaywrightをE2Eテストの推奨ツールとして位置づけています。ビルド後の出力ディレクトリ(.output/chrome-mv3)を指定することで、拡張機能の動作検証が可能です。

Biome(技術的に互換)

Biomeは公式ドキュメントでの言及はありませんが、Viteベースのプロジェクトと問題なく動作するため、WXTプロジェクトでも使用できます。ESLintからの移行もbiome migrate eslintで対応可能です。

移行でハマったポイント

chrome.*ではなくbrowser.*を使う

Vanilla JSからコードを移行するとき、そのままchrome.tabs.getを使っていましたが、WXTでは**browser.*を使うのが推奨**されています。

WXTは内部でwebextension-polyfillを使用しており、browser.*でPromiseベースのAPIが使えます。

// NG: chrome.* を使う(コールバック)
chrome.tabs.query({ active: true }, (tabs) => {
  console.log(tabs[0].url);
});

// OK: browser.* を使う(Promise)
const tabs = await browser.tabs.query({ active: true });
console.log(tabs[0].url);

メッセージング(background ↔ content script)

📖 Messaging - WXT

Vanilla JSではchrome.runtime.sendMessageを直接使っていましたが、何を送受信しているか型がなくて分かりにくい状態でした。@webext-core/messagingを使うと、型安全なメッセージングができます。

utils/messaging.ts
import { defineExtensionMessaging } from '@webext-core/messaging';

interface ProtocolMap {
  getTabInfo: (data: { tabId: number }) => { url: string; title: string };
}

export const { sendMessage, onMessage } = defineExtensionMessaging<ProtocolMap>();
entrypoints/background.ts
import { onMessage } from '@/utils/messaging';

onMessage('getTabInfo', async ({ data }) => {
  const tab = await browser.tabs.get(data.tabId);
  return { url: tab.url ?? '', title: tab.title ?? '' };
});

ストレージ

📖 Storage - WXT

chrome.storage.localを直接使っていると、キー名のタイポや型の不一致に気づきにくいです。@wxt-dev/storageを使うと、型付きのストレージアクセスができます。

utils/storage.ts
import { storage } from '#imports';

export const settingsStorage = storage.defineItem<{
  theme: 'light' | 'dark';
  enabled: boolean;
}>('local:settings', {
  fallback: { theme: 'light', enabled: true },
});
entrypoints/popup/main.tsx
const settings = await settingsStorage.getValue();
await settingsStorage.setValue({ ...settings, theme: 'dark' });

移行してよかったこと

コードの見通しが良くなった

数千行の単一ファイルが、役割ごとに分割されました。「この機能はどこにあるか」が明確になり、バグ修正の心理的ハードルが下がりました。

機能追加が怖くなくなった

TypeScriptの型とエディタの補完により、既存コードを壊すリスクが減りました。新機能を追加する際も、影響範囲が把握しやすくなりました。

AIとの協業がしやすくなった

ファイルが分割されているので、AIに「このファイルを修正して」と依頼しやすくなりました。コンテキストが絞られるため、AIの出力も安定します。

まとめ

AIでChrome拡張機能を作るのは簡単ですが、メンテナンスを続けるには構造化が必要です。

WXTを使うことで、以下のメリットがありました。

  • TypeScriptによる型安全な開発
  • 規約によるファイル分割の強制
  • @webext-core/messagingwxt/storageによる型付きAPI

同じような悩みを抱えている方は、ぜひWXTへの移行を検討してみてください!

参考

GitHubで編集を提案

Discussion