0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Chrome拡張機能をAI Agentで作ってみた感想とコツ

0
Last updated at Posted at 2026-01-02

はじめに

この記事は『「バックエンドの型が違う!」を即座に特定するChrome拡張機能を作りました』という記事で投稿したChrome拡張機能の開発秘話みたいなものです。

テキスト・コマンド・MCPのいずれも手が届きにくいChrome拡張機能をCursor Agentで作成するにはどのようなコツがいるのかを紹介していきます。

本記事は公開日時点でのリポジトリの状態で話しています。ストアに公開された拡張機能のバージョンとは前後した実装について言及している可能性があるためご注意ください。

開発環境

  • AI Agent
    • Cursor 2.2.20
  • DevContainer Image
    • node:24.11.1-bookworm
  • ビルド環境
    • node 24.11.1
  • ビルドツール
    • rollup 4.6.1
  • バリデーションライブラリ
    • jsonschema 1.5.0

なぜこの環境を選んだのか順々に説明します。

Cursor

正直、趣味で開発している僕にとってAgentはそこそこな出費の元なので、気になるサービスが出たらスイッチしています。
この拡張機能開発を思い立った時に契約していたのがCursorだったというだけになります。

画面や帳票が乱立する代わりにしっかり設計書が定義されている大規模開発と異なり、思い立ったものを適宜修正し、手動打鍵が必要な拡張機能開発でtmux+Claude Codeのような並列作業をする意味はあまりないので、Cursor Agentは結構良かったと思っています。

DevContainer

ローカル環境を汚さずに開発環境を構築できるDevContainerを使用しました。
node:24.11.1-bookwormイメージをベースに、SSH鍵の設定も.devcontainer/.ssh/に配置するだけで済むようにしています。

Cursorの中でDevContainerを動かすと、AI Agentが影響する範囲もコンテナ内に限定されるのがありがたいですね。余計なファイルを見に行かなくなったり、破壊的な操作を制限できます。

rollup

Chrome拡張機能では生のJSを書いている記事が多いですが、AI Agentに書かせるなら型をしっかり定義したいのでビルドを挟むことにしました。

当初はesbuildを使用していましたが、バリデータがビルド時にエラーを吐いたためビルドツールを再選定しました。色々調べてみたのですが、rollupの方が本番向けの最適化は強いのでより軽量なアウトプットが期待できるみたいです。ビルド速度ではesbuildに負けますが、拡張機能はあまり大規模にならないしホットリロードも効かないのでビルド速度を優先する必要はありません。

viteの本番ビルドではrollupが動いているので、じつは皆さんもよく使っているのではないでしょうか?
Chrome拡張機能以外についても便利なのでぜひ使ってみてください!

rollup設定のポイント

Chrome拡張機能では複数のエントリーポイント(background、devtools、panel)があり、それぞれを個別にビルドする必要があります。

// rollup.config.chrome.js より抜粋
export default [
  backgroundConfig,  // Service Worker
  devtoolsConfig,    // DevTools初期化
  panelConfig        // パネルUI
];

また、静的ファイル(manifest.json、HTML、CSS、アイコン)をdistフォルダにコピーするカスタムプラグインを作成しました。これでビルド後にdist/フォルダをそのままChrome拡張機能として読み込めます。

jsonschema

当初はzodを使用するつもりでした。
ただ、OpenAPI仕様書からzodコードを生成しようとすると拡張機能の中(ブラウザ内)でビルドが必須になり、さまざまな問題に悩まされたので結局JSON Schemaに頼ることとなりました。

JSON SchemaとOpenAPIでは任意項目の定義方法に差があったり、一部パラメータの型の扱いが意図したバリデーションになってくれないなど、厳密に同じものではないのでそこはロジックで吸収しています。

OpenAPIとJSON Schemaの差分吸収

OpenAPIのnullable: trueはJSON Schemaではtype: ["original_type", "null"]に変換する必要があります。また、requiredに含まれていないプロパティもnullを許容するように変換しています。

// validator.ts より抜粋
// null許容の処理
const shouldAllowNull = record.nullable === true || !isRequired;

if (shouldAllowNull && record.type) {
  const originalType = record.type;
  if (Array.isArray(originalType)) {
    if (!originalType.includes('null')) {
      result.type = [...originalType, 'null'];
    }
  } else {
    result.type = [originalType, 'null'];
  }
}

こうした差分を吸収するためにconvertSchemaForJsonSchemaメソッドを実装しました。OpenAPI固有のプロパティ(exampledeprecatedなど)の削除も行っています。

国際化(i18n)対応

地味ですが、日英両対応にしています。Chrome拡張機能のchrome.i18nを使う方法もありますが、DevToolsパネル内で動的に言語を切り替えたかったので独自実装にしました。

// i18n.ts より
const translations: Record<Language, Record<TranslationKey, string>> = {
  ja: {
    title: 'ばりっどーぬ - ValidDog',
    // ...
  },
  en: {
    title: 'ValidDog',
    // ...
  },
};

HTMLではdata-i18n属性を使って翻訳対象の要素を指定し、JavaScript側で一括更新する方式を採用しています。

審査について

バリデーションを実行する際の処理にevalが含まれていたこともあり、権限は重めについてました。
と思っていたら、なんの連絡もなしにいきなり審査が通ってストア公開されていました。
なんじゃそりゃ・・・いつ公開されたのかいまだによく分かってません。

image.png

テストについて

一番苦労したのはテストです。
ブラウザのDevtoolsに対するDevToolsもあるにはありますが、MCPを使用したりCursor内ブラウザでテストしたりする王道アプローチがなかなか見つけられず苦労しました。

結局、ロジックを分離してコアな処理はNode上でも動作できるようにしておき、テストコードを実行してCursor Agentがテストしやすいお膳立てをすることでなんとか品質が向上できました。

テスト設計の工夫

1. コアロジックの分離

バリデーションロジックはvalidator.tsに集約し、ブラウザAPIに依存しない形で実装しています。

// validator.ts
// evalを使用しない純粋なロジック
export class OpenAPIValidator {
  // Node.jsでも動作可能
  static fromFile(content: string): OpenAPIValidator {
    const spec = parseOpenAPISpec(content);
    return new OpenAPIValidator(spec);
  }
}

2. テストケースの共通化

test-cases.mjsにテストケースを定義し、ブラウザ版(test.html)とNode.js版(api-test.mjs)の両方で同じテストケースを使用できるようにしました。

// test-cases.mjs より
export const usersTests = [
  { method: 'GET', path: '/users', desc: 'ユーザー一覧取得' },
  // ...
];

3. 正常系・異常系の分離

npm run startで正常系サーバー(backend-server.js)、npm run missで異常系サーバー(backend-server-miss.js)を起動できるようにしました。異常系サーバーは意図的にOpenAPI仕様書に違反するレスポンスを返します。

残りは結合テスト扱いで手動打鍵です...トホホ

AI Agentのルールについて

linttscによる型チェック・構文チェックで潜在的なエラーを潰し、prettierを用いて整形することでブレの少ないない出力を期待できます。

また、影響範囲の水平展開や都度のテストをルールとして義務付けることでやりとりが減ります。

本来、ある程度の規模の開発から必要なレギュレーションですが、AIという気まぐれな第三者に業務を委託しているので小粒な開発でもルールは厳しくつけた方がいいです。

Chrome拡張機能に限らず、使えそうな考え方はあると思うのでぜひ参考にしてみてください。

---
alwaysApply: true
---

## 概要
本アプリはストア公開を念頭においたChrome拡張機能です。
開発者は日本人なので、基本的にソースコード内は日本語話者むけのコメントを記述し、READMEなどの開発ドキュメントも日本語でお願いします。

## ディレクトリ構造
rollupでビルドしたファイルだけでなく、ストア公開に必要なファイルはdistフォルダにすべてコピーされるようにしてください。
逆に、distにコピーないしビルドされる全てのファイルはsrcフォルダに集約されるようにしてください。

## 単体テスト
拡張機能はテストが難しいため、コアロジックはなるべく**src/validator.tsないしこのファイルから参照するファイル**に括り出しておいてテスト可能な形にしてください。
また、実装を修正したら**このテストケースも修正する**を徹底してください。
修正が完了したら、testフォルダ内で`npm run test`を実施し、修正が正しいことを確認してください。

## 結合テスト
結合テストは手動で打鍵します。
手動打鍵時はルートフォルダの`test.html`を使って画面に入り、testフォルダで`npm run start`で正常系、`npm run miss`で異常系をテストします。これらのコマンドをあなたが実行する必要はありませんが、結合テストを見据えて関連ファイルの修正は毎回お願いします。

## チェックルール

作業終了時に`npm run lint`および`type-check`を実行し、セルフチェックを行なった上で、`npm run format`により定まった規則のコードを生成してください。
srcフォルダとtestフォルダは別個にlintやprettierの設定があるのでそれぞれ実施してください。

ルールのポイント

  1. 言語の統一: 日本人向けなのでコメントやドキュメントは日本語で
  2. ディレクトリ構造の明確化: src → dist の一方向な流れを維持
  3. テストの義務化: 実装修正時にテストケースも必ず更新
  4. 自動チェックの義務化: lint、type-check、formatを毎回実行

技術的な工夫

$ref の解決

OpenAPI仕様書では$refを使ってスキーマを参照することが多いですが、バリデーション時にはこれを解決(展開)する必要があります。循環参照を防ぐため、深さ制限(20階層)を設けています。

// 循環参照を防ぐため深さ制限
if (depth > 20) return obj;

if ('$ref' in record && typeof record.$ref === 'string') {
  const refPath = record.$ref;
  if (refPath.startsWith('#/components/schemas/')) {
    const schemaName = refPath.replace('#/components/schemas/', '');
    const schema = resolved.components?.schemas?.[schemaName];
    if (schema) {
      return resolveRef({ ...schema }, depth + 1);
    }
  }
}

パスパラメータのマッチング

/users/{id}のようなパスパターンと実際のURL/users/123をマッチングさせる処理も実装しました。後方一致でマッチングすることで、ベースURLが異なる場合でも対応できます。

// 後方一致でマッチング(BaseURLに依存しない)
const regex = new RegExp(`${regexPattern}$`);
const match = actualPath.match(regex);

Service Workerの再接続

Chrome拡張機能のManifest V3ではbackgroundがService Workerになり、アイドル時に停止します。DevToolsパネルとの接続が切れた場合の再接続処理を入れています。

port.onDisconnect.addListener(() => {
  port = null;
  setTimeout(() => {
    if (!port) {
      connectToBackground();
    }
  }, 1000);
});

さいごに

Cursorは、ギリ「作った」実感もあるところがいいですね。あと早ければ数ヶ月したらモデルの性能がさらに向上してこの実感すらも薄れるサービス・プロダクトが出現しそうで怖い...

趣味のプログラミングなんてもう編み物みたいなものです。
工場で作られた服があるのに、手編みをするのは楽しいから。こんなかんじで来年もアプリとか作っていきたいと思います!

参考リンク

##【追記】開発内容
具体的にどのように開発したのかは下記記事にまとめて供養しています。
https://qiita.com/HoppingGanon/items/38869715ec87d463e515

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?