
Chrome Extension Dev
- 40 installs
- 23 repo stars
- Updated August 4, 2026
- aktsmm/agent-skills
chrome-extension-dev is a Claude skill that guides Chrome/browser extension development with the WXT framework and Manifest V3.
About
A guide for building and modifying Chrome and browser extensions with the WXT framework and Manifest V3. A developer uses it to scaffold WXT projects, implement content scripts and service workers, use Chrome APIs like tabs, storage and cookies, and handle Manifest V3 constraints. It recommends a 2025/2026 stack (WXT, TypeScript, React/Vue/Svelte, Tailwind) and covers testing with Vitest and Playwright.
- WXT + Manifest V3 scaffolding for content scripts and service workers
- Chrome API reference table (tabs, storage, runtime, scripting, cookies, offscreen)
- Manifest V3 constraint workarounds (alarms, offscreen, session storage, bundling)
Chrome Extension Dev by the numbers
- 40 all-time installs (skills.sh)
- Ranked #1,377 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
chrome-extension-dev capabilities & compatibility
Free; requires Node.js and the WXT CLI
- Capabilities
- browser max automation · frontend development · extension testing
- Works with
- chrome · playwright
- Use cases
- frontend · testing
- IDEs
- vscode · cursor ide
- Pricing
- Free
What chrome-extension-dev says it does
Chrome/ブラウザ拡張機能開発の包括的ガイド。WXTフレームワーク、Manifest V3、Chrome API、テスト手法をカバー。
**⚠️ 重要**: `permissions`/`host_permissions`変更後は**拡張機能の再読み込みでは不十分**。
npx skills add https://github.com/aktsmm/agent-skills --skill chrome-extension-devAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 40 |
|---|---|
| repo stars | ★ 23 |
| Last updated | August 4, 2026 |
| Repository | aktsmm/agent-skills ↗ |
What it does
Build or migrate a Chrome/browser extension using the WXT framework and Manifest V3.
Who is it for?
Building or migrating Chrome/browser extensions with WXT and Manifest V3
Skip if: Extension end-to-end runtime automation (that is browser-max-automation)
When should I use this skill?
Creating or modifying a browser extension, working with WXT, content scripts or service workers
What you get
A working WXT Manifest V3 extension with correct permissions and constraint handling
- WXT extension project
- Content scripts and service worker
- Store-ready zip package
By the numbers
- 6-API Chrome API table
- 5-item Manifest V3 constraints table
Files
Chrome Extension Dev
ブラウザ拡張機能開発の包括的ガイド。
When to Use
- ブラウザ拡張機能を作りたい、Chrome拡張、browser extension
- WXT フレームワークでの開発
- Content Script / Service Worker の実装
- Chrome API(tabs, storage, cookies等)の使用
- Manifest V3 への対応・移行
推奨技術スタック(2025/2026)
| カテゴリ | 推奨 |
|---|---|
| フレームワーク | WXT (Vite ベース) |
| フロントエンド | React 18+ / Vue 3 / Svelte |
| 言語 | TypeScript |
| スタイリング | Tailwind CSS |
| UIコンポーネント | shadcn/ui / Mantine |
| 状態管理 | Zustand / Jotai |
| テスト | Vitest (ユニット) + Playwright (E2E) |
WXT クイックスタート
# 新規プロジェクト作成
npm create wxt@latest
# テンプレート指定
npm create wxt@latest -- --template react-ts
npm create wxt@latest -- --template vue-ts
# 開発コマンド
npm run dev # HMR付き開発サーバー
npm run build # プロダクションビルド
npm run zip # ストア提出用パッケージプロジェクト構成
project/
├── entrypoints/ # 自動検出エントリポイント
│ ├── background.ts # Service Worker
│ ├── content.ts # Content Script
│ ├── popup/ # Popup UI
│ │ ├── index.html
│ │ └── main.tsx
│ └── options/ # Options Page
├── components/ # 自動インポートUIコンポーネント
├── utils/ # 自動インポートユーティリティ
├── public/ # 静的アセット(icon等)
├── wxt.config.ts # WXT設定
└── package.json主要 Chrome API
| API | 用途 | 権限 |
|---|---|---|
chrome.tabs | タブ操作 | tabs / activeTab |
chrome.storage | データ永続化 | storage |
chrome.runtime | メッセージング | なし |
chrome.scripting | スクリプト/CSS注入 | scripting + host権限 |
chrome.cookies | Cookie操作 | cookies + host権限 |
chrome.offscreen | DOM操作(SW内) | offscreen |
→ 詳細: references/chrome-api.md
Manifest V3 制限事項
| 制限 | 対処法 |
|---|---|
| 30秒タイムアウト | chrome.alarms でウェイクアップ |
| DOMアクセス不可 | chrome.offscreen を使用 |
| 永続化なし | chrome.storage.session を使用 |
| eval()禁止 | 事前にバンドル |
| リモートコード禁止 | 全コードをバンドル |
→ 詳細: references/manifest-v3.md
権限変更時の注意
⚠️ 重要:permissions/host_permissions変更後は拡張機能の再読み込みでは不十分。
削除→再インストールが必要。
Key References
| トピック | リファレンス |
|---|---|
| Chrome API 詳細 | references/chrome-api.md |
| Manifest V3 | references/manifest-v3.md |
| テスト | references/testing.md |
| 公開 | references/publishing.md |
| よくあるパターン | references/patterns.md |
Done Criteria
- [ ] WXT プロジェクト構成が正しい
- [ ] 必要な権限が
wxt.config.tsに設定されている - [ ] Manifest V3 制限を考慮した実装
- [ ] 開発モードで動作確認済み
- [ ] ビルドエラーがない
External Resources
# Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0)
## English
Copyright (c) 2025-2026 yamapan (aktsmm)
This work is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0
International License.
You are free to:
- **Share** — copy and redistribute the material in any medium or format
- **Adapt** — remix, transform, and build upon the material
Under the following terms:
- **Attribution** — You must give appropriate credit, provide a link to the
license, and indicate if changes were made. You may do so in any reasonable manner,
but not in any way that suggests the licensor endorses you or your use.
- **NonCommercial** — You may not use the material for commercial purposes.
*(Please contact the author if you wish to use this material for commercial purposes.)*
- **ShareAlike** — If you remix, transform, or build upon the material, you must
distribute your contributions under the same license as the original.
No additional restrictions — You may not apply legal terms or technological
measures that legally restrict others from doing anything the license permits.
**AI/ML Training Restriction** — Use of this content for AI/ML training, data
mining, or other analytical purposes is prohibited without explicit permission.
Full license text: https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode
---
## 日本語
Copyright (c) 2025-2026 yamapan (aktsmm)
この作品はクリエイティブ・コモンズ 表示-非営利-継承 4.0 国際ライセンスの下に提供されています。
あなたは以下の条件に従う限り、自由に:
- **共有** — どのようなメディアやフォーマットでも資料を複製・再配布できます
- **翻案** — 資料をリミックス、変形、および加工することができます
以下の条件に従ってください:
- **表示** — あなたは適切なクレジットを表示し、ライセンスへのリンクを提供し、
変更があったらその旨を示さなければなりません。これらは合理的であればどのような方法で
行っても構いませんが、許諾者があなたやあなたの利用行為を支持していると示唆するような
方法は除きます。
- **非営利** — あなたは営利目的でこの資料を利用してはなりません。
(※商用利用をご希望の場合は、別途ご連絡ください。)
- **継承** — もしあなたがこの資料をリミックス、変形、または加工した場合、
あなたはあなたの貢献部分を元の作品と同じライセンスの下で配布しなければなりません。
追加的な制約は課せません — あなたは、このライセンスが他の者に許諾することを法的に
制限するような法的条項や技術的手段を適用してはなりません。
**AI/MLトレーニング制限** — 本コンテンツをAI/MLモデルのトレーニング、データマイニング、
その他の解析目的での使用は明示的な許可なく禁止されています。
ライセンス全文: https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode.ja
---
## Special Permission for Microsoft Employees / Microsoft 社員向け特別許諾
### English
Microsoft Corporation employees are granted permission to use, copy, modify, and
distribute this material for any purpose within the scope of their employment
duties at Microsoft, including internal business use and customer-facing
activities, without the NonCommercial restriction of this license.
This special permission applies only to work performed as part of official
Microsoft business activities.
### 日本語
Microsoft Corporation の社員は、Microsoft での業務の範疇において、本資料を社内業務
および顧客対応を含むあらゆる目的で使用、複製、改変、配布することが許諾されます。
この場合、本ライセンスの「非営利」制限は適用されません。
この特別許諾は、Microsoft の公式な業務活動の一環として行われる作業にのみ適用されます。
---
## Disclaimer / 免責事項
### English
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR
A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
### 日本語
本ソフトウェアは「現状のまま」で提供され、明示または黙示を問わず、商品性、
特定目的への適合性、および権利非侵害についての保証を含むがこれに限定されない、
いかなる種類の保証も伴いません。作者または著作権者は、契約行為、不法行為、
またはそれ以外であろうと、ソフトウェアに起因または関連し、あるいはソフトウェアの
使用またはその他の扱いによって生じる一切の請求、損害、その他の責任について
責任を負いません。
Chrome API Reference
Chrome拡張機能で使用する主要APIの詳細ガイド。
---
chrome.tabs API
タブの作成・変更・再配置など、ブラウザのタブシステムを操作。
権限
{
"permissions": ["tabs"], // url, title等のセンシティブ情報
"permissions": ["activeTab"], // ユーザー操作時の一時的アクセス
"host_permissions": ["*://*/*"] // ホストへの完全アクセス
}よく使うメソッド
// 現在のタブを取得
async function getCurrentTab() {
const [tab] = await chrome.tabs.query({
active: true,
lastFocusedWindow: true,
});
return tab;
}
// 新しいタブを作成
chrome.tabs.create({ url: "https://example.com" });
// タブにメッセージを送信
const response = await chrome.tabs.sendMessage(tabId, { type: "getData" });
// タブを更新
await chrome.tabs.update(tabId, { url: "https://new-url.com" });
// タブを閉じる
await chrome.tabs.remove(tabId);イベント
// タブがアクティブになった時
chrome.tabs.onActivated.addListener((activeInfo) => {
console.log("Tab activated:", activeInfo.tabId);
});
// タブが更新された時
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
if (changeInfo.status === "complete") {
console.log("Tab loaded:", tab.url);
}
});
// タブが閉じられた時
chrome.tabs.onRemoved.addListener((tabId, removeInfo) => {
console.log("Tab closed:", tabId);
});---
chrome.storage API
ユーザーデータの保存・取得・変更追跡。
権限
{
"permissions": ["storage"]
}Storage エリア
| エリア | 容量 | 同期 | 用途 |
|---|---|---|---|
local | 10MB | ❌ | 大きなデータ |
sync | 100KB (8KB/item) | ✅ | ユーザー設定 |
session | 10MB | ❌ | 一時データ(メモリ) |
managed | - | - | 管理者ポリシー(読取専用) |
使用例
// ローカルストレージに保存
await chrome.storage.local.set({ key: "value" });
// 取得
const result = await chrome.storage.local.get(["key"]);
console.log(result.key);
// Sync ストレージ(ブラウザ間同期)
await chrome.storage.sync.set({ settings: { theme: "dark" } });
// Session ストレージ(メモリ、再起動でクリア)
await chrome.storage.session.set({ tempData: {} });
// 変更を監視
chrome.storage.onChanged.addListener((changes, areaName) => {
for (const [key, { oldValue, newValue }] of Object.entries(changes)) {
console.log(`${key} changed from ${oldValue} to ${newValue}`);
}
});⚠️ 注意: localStorage は使わない
- Service Worker で使用不可
- Content Script はホストページとストレージを共有
- ブラウザ履歴削除でデータ消失
---
chrome.cookies API
Cookieの取得・設定・変更通知。
権限
{
"permissions": ["cookies"],
"host_permissions": ["*://*.example.com/"]
}使用例
// Cookie取得
const cookie = await chrome.cookies.get({
url: "https://example.com",
name: "session_id",
});
// 全Cookie取得
const cookies = await chrome.cookies.getAll({ domain: "example.com" });
// Cookie設定
await chrome.cookies.set({
url: "https://example.com",
name: "my_cookie",
value: "my_value",
expirationDate: Date.now() / 1000 + 3600,
});
// Cookie削除
await chrome.cookies.remove({
url: "https://example.com",
name: "my_cookie",
});
// 変更監視
chrome.cookies.onChanged.addListener((changeInfo) => {
console.log("Cookie changed:", changeInfo.cookie.name, changeInfo.cause);
});---
chrome.offscreen API
Service WorkerでDOM操作が必要な場合に使用。
権限
{
"permissions": ["offscreen"]
}使用理由
| 理由 | 説明 |
|---|---|
CLIPBOARD | クリップボードAPI使用 |
DOM_PARSER | DOMParser使用 |
DOM_SCRAPING | iframe内のDOM取得 |
AUDIO_PLAYBACK | 音声再生 |
USER_MEDIA | getUserMedia() |
WEB_RTC | WebRTC使用 |
使用例
// オフスクリーンドキュメントが存在するか確認
async function setupOffscreenDocument(path: string) {
const existingContexts = await chrome.runtime.getContexts({
contextTypes: ["OFFSCREEN_DOCUMENT"],
documentUrls: [chrome.runtime.getURL(path)],
});
if (existingContexts.length > 0) return;
await chrome.offscreen.createDocument({
url: path,
reasons: ["CLIPBOARD"],
justification: "クリップボード操作のため",
});
}
// 使用
await setupOffscreenDocument("offscreen.html");
await chrome.runtime.sendMessage({ type: "copy", data: "text" });---
chrome.runtime API
拡張機能のライフサイクル・メッセージング。
メッセージング
// メッセージ送信
const response = await chrome.runtime.sendMessage({
type: "getData",
key: "foo",
});
// メッセージ受信
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === "getData") {
sendResponse({ value: "bar" });
}
return true; // 非同期レスポンス用
});インストール・更新イベント
chrome.runtime.onInstalled.addListener(({ reason }) => {
if (reason === "install") {
chrome.tabs.create({ url: "onboarding.html" });
}
if (reason === "update") {
console.log("Extension updated");
}
});---
chrome.scripting API
Content Script やCSSの動的注入。
権限
{
"permissions": ["scripting"],
"host_permissions": ["<all_urls>"]
}使用例
// スクリプト注入
await chrome.scripting.executeScript({
target: { tabId: tabId },
func: () => {
document.body.style.backgroundColor = "red";
},
});
// CSS注入
await chrome.scripting.insertCSS({
target: { tabId: tabId },
css: "body { background: blue !important; }",
});---
chrome.action API
ツールバーアイコンの制御。
使用例
// バッジテキスト設定
await chrome.action.setBadgeText({ text: "5" });
await chrome.action.setBadgeBackgroundColor({ color: "#FF0000" });
// アイコン変更
await chrome.action.setIcon({ path: "icons/active.png" });
// ポップアップ変更
await chrome.action.setPopup({ popup: "popup2.html" });
// クリックイベント(ポップアップなしの場合)
chrome.action.onClicked.addListener((tab) => {
console.log("Action clicked on tab:", tab.id);
});---
chrome.sidePanel API
サイドパネルの表示制御。
権限
{
"permissions": ["sidePanel"]
}マニフェスト設定
{
"side_panel": {
"default_path": "sidepanel.html"
}
}使用例
// サイドパネルを開く
await chrome.sidePanel.open({ windowId: windowId });
// 特定タブでパネル変更
await chrome.sidePanel.setOptions({
tabId: tabId,
path: "custom-panel.html",
enabled: true,
});---
外部リソース
Manifest V3 ガイド
Manifest V3(MV3)は2025年以降の必須標準。
---
MV2 → MV3 主要変更点
| 機能 | MV2 | MV3 |
|---|---|---|
| バックグラウンド処理 | Background Pages | Service Workers |
| ネットワークリクエスト | webRequest(ブロッキング) | declarativeNetRequest |
| リモートコード | 許可 | 禁止 |
| コード実行 | eval() 許可 | 禁止 |
| CSP | 柔軟 | 厳格化 |
---
Service Worker 制限事項
| 制限 | 内容 | 対処法 |
|---|---|---|
| 30秒タイムアウト | イベント処理後30秒でスリープ | chrome.alarms でウェイクアップ |
| DOMアクセス不可 | document/window 不可 | chrome.offscreen を使用 |
| 永続化なし | 変数はスリープ時にクリア | chrome.storage.session を使用 |
| eval()禁止 | 動的コード実行不可 | 事前にバンドル |
| リモートコード禁止 | CDNからのスクリプト不可 | 全コードをバンドル |
30秒タイムアウト対策
// chrome.alarms でウェイクアップ
chrome.alarms.create("keepAlive", { periodInMinutes: 0.5 });
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === "keepAlive") {
// 定期的な処理
}
});DOMアクセスが必要な場合
// offscreen ドキュメントを使用
await chrome.offscreen.createDocument({
url: "offscreen.html",
reasons: ["DOM_PARSER"],
justification: "HTML解析のため",
});
// offscreen.js 内でDOM操作
const parser = new DOMParser();
const doc = parser.parseFromString(html, "text/html");状態の永続化
// ❌ グローバル変数(スリープでクリア)
let counter = 0;
// ✅ storage.session を使用
await chrome.storage.session.set({ counter: 0 });
const { counter } = await chrome.storage.session.get("counter");
await chrome.storage.session.set({ counter: counter + 1 });---
マニフェスト構成
基本構成
{
"manifest_version": 3,
"name": "Extension Name",
"version": "1.0.0",
"description": "説明文",
"icons": {
"16": "icons/16.png",
"48": "icons/48.png",
"128": "icons/128.png"
},
"action": {
"default_popup": "popup.html",
"default_icon": {
"16": "icons/16.png",
"48": "icons/48.png"
}
},
"background": {
"service_worker": "background.js",
"type": "module"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"]
}
],
"permissions": ["storage", "activeTab"],
"host_permissions": ["<all_urls>"]
}WXT での設定
// wxt.config.ts
import { defineConfig } from "wxt";
export default defineConfig({
manifest: {
name: "Extension Name",
permissions: ["storage", "activeTab", "scripting"],
host_permissions: ["<all_urls>"],
},
});---
権限のベストプラクティス
最小権限の原則
{
// ❌ 過剰な権限
"permissions": ["tabs", "history", "bookmarks"],
"host_permissions": ["<all_urls>"]
}
{
// ✅ 必要最小限
"permissions": ["activeTab"],
"host_permissions": ["*://*.example.com/*"]
}オプショナル権限
{
"optional_permissions": ["tabs", "history"],
"optional_host_permissions": ["*://*.newsite.com/*"]
}// 必要時にリクエスト
const granted = await chrome.permissions.request({
permissions: ["tabs"],
origins: ["*://*.newsite.com/*"],
});---
MV3 移行時の一般的な問題
| 問題 | 原因 | 解決策 |
|---|---|---|
| バックグラウンドスクリプトが動かない | Background Page → SW 移行 | background.service_workerを使用 |
| ネットワーク操作が動かない | webRequest ブロッキング廃止 | declarativeNetRequest に移行 |
| 外部スクリプトが読み込めない | リモートコード禁止 | 全コードをバンドルに含める |
| localStorage 使用不可 | SW で Web Storage API 不可 | chrome.storage に移行 |
eval() エラー | 動的コード実行禁止 | 事前コンパイル |
---
declarativeNetRequest
ネットワークリクエストのルールベース制御。
権限
{
"permissions": ["declarativeNetRequest"],
"host_permissions": ["<all_urls>"]
}ルール定義
// rules.json
[
{
"id": 1,
"priority": 1,
"action": { "type": "block" },
"condition": {
"urlFilter": "*://ads.example.com/*",
"resourceTypes": ["script", "image"]
}
},
{
"id": 2,
"priority": 1,
"action": {
"type": "redirect",
"redirect": { "url": "https://example.com/blocked.html" }
},
"condition": {
"urlFilter": "*://blocked.com/*"
}
}
]マニフェスト設定
{
"declarative_net_request": {
"rule_resources": [
{
"id": "ruleset_1",
"enabled": true,
"path": "rules.json"
}
]
}
}---
外部リソース
よくあるパターン集
ブラウザ拡張機能開発でよく使う実装パターン。
---
メッセージングパターン
Content Script ↔ Service Worker
// content.ts - メッセージ送信
const response = await chrome.runtime.sendMessage({
type: "GET_DATA",
payload: { key: "value" },
});
// background.ts - メッセージ受信
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === "GET_DATA") {
// 非同期処理
fetchData(message.payload).then((data) => {
sendResponse({ success: true, data });
});
return true; // 非同期レスポンスを示す
}
});Service Worker → Content Script
// background.ts
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
const response = await chrome.tabs.sendMessage(tab.id!, {
type: "UPDATE_UI",
data: { theme: "dark" },
});型安全なメッセージング
// types/messages.ts
type MessageMap = {
GET_DATA: { request: { key: string }; response: { value: string } };
SET_DATA: { request: { key: string; value: string }; response: void };
};
type MessageType = keyof MessageMap;
async function sendMessage<T extends MessageType>(
type: T,
payload: MessageMap[T]["request"]
): Promise<MessageMap[T]["response"]> {
return chrome.runtime.sendMessage({ type, payload });
}
// 使用
const result = await sendMessage("GET_DATA", { key: "settings" });---
ストレージパターン
型安全なストレージラッパー
// utils/storage.ts
interface StorageSchema {
settings: {
theme: "light" | "dark";
notifications: boolean;
};
lastSync: number;
}
export async function getStorage<K extends keyof StorageSchema>(
key: K
): Promise<StorageSchema[K] | undefined> {
const result = await chrome.storage.local.get(key);
return result[key];
}
export async function setStorage<K extends keyof StorageSchema>(
key: K,
value: StorageSchema[K]
): Promise<void> {
await chrome.storage.local.set({ [key]: value });
}
// 使用
const settings = await getStorage("settings");
await setStorage("settings", { theme: "dark", notifications: true });リアクティブストレージ(React)
// hooks/useStorage.ts
import { useState, useEffect } from "react";
export function useStorage<T>(key: string, defaultValue: T) {
const [value, setValue] = useState<T>(defaultValue);
useEffect(() => {
// 初期値を読み込み
chrome.storage.local.get(key).then((result) => {
if (result[key] !== undefined) {
setValue(result[key]);
}
});
// 変更を監視
const listener = (
changes: { [key: string]: chrome.storage.StorageChange },
areaName: string
) => {
if (areaName === "local" && changes[key]) {
setValue(changes[key].newValue);
}
};
chrome.storage.onChanged.addListener(listener);
return () => chrome.storage.onChanged.removeListener(listener);
}, [key]);
const updateValue = async (newValue: T) => {
await chrome.storage.local.set({ [key]: newValue });
setValue(newValue);
};
return [value, updateValue] as const;
}
// 使用
function SettingsComponent() {
const [theme, setTheme] = useStorage("theme", "light");
return <button onClick={() => setTheme("dark")}>Dark Mode</button>;
}---
Content Script パターン
Shadow DOM でスタイル分離
// content.ts
export default defineContentScript({
matches: ["<all_urls>"],
main() {
// Shadow DOM でホストページのスタイルから分離
const container = document.createElement("div");
const shadow = container.attachShadow({ mode: "closed" });
shadow.innerHTML = `
<style>
.ext-panel { /* スタイル */ }
</style>
<div class="ext-panel">
<!-- UI -->
</div>
`;
document.body.appendChild(container);
},
});WXT の createShadowRootUi
// content.ts
import { createShadowRootUi } from "wxt/client";
export default defineContentScript({
matches: ["<all_urls>"],
cssInjectionMode: "ui",
async main(ctx) {
const ui = await createShadowRootUi(ctx, {
name: "my-extension-ui",
position: "inline",
anchor: "body",
onMount: (container) => {
const root = createRoot(container);
root.render(<App />);
return root;
},
onRemove: (root) => {
root.unmount();
},
});
ui.mount();
},
});ページコンテキストでの実行
// ページのグローバル変数にアクセスする必要がある場合
export default defineContentScript({
matches: ["<all_urls>"],
world: "MAIN", // ページコンテキストで実行
main() {
// window オブジェクトはページと共有
console.log(window.somePageVariable);
},
});---
ブラウザ自動操作パターン
ref 番号システム
DOM要素に一意の ref 番号を付与し、LLMが確実に要素を特定できるようにする。
// DOM解析でref番号を付与
function assignRefNumbers() {
const interactiveElements = document.querySelectorAll(
'button, a, input, select, [role="button"], [role="link"], [role="checkbox"]'
);
interactiveElements.forEach((el, i) => {
el.setAttribute("data-copilot-ref", `e${i}`);
});
// 出力形式
// [e0] button "次へ"
// [e5] radio "そう思わない"
}
// LLMからの指示を解析
// [ACTION: click, e5]
function executeAction(action: string, ref: string) {
const element = document.querySelector(`[data-copilot-ref="${ref}"]`);
if (!element) return;
switch (action) {
case "click":
(element as HTMLElement).click();
break;
case "focus":
(element as HTMLElement).focus();
break;
}
}ボット検出回避
// ループ間に3-5秒のランダム待機
async function humanLikeDelay() {
const waitTime = 3000 + Math.random() * 2000;
await new Promise((resolve) => setTimeout(resolve, waitTime));
}
// マウス移動をシミュレート
async function humanLikeClick(element: HTMLElement) {
// ホバーイベント
element.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
await new Promise((resolve) => setTimeout(resolve, 100 + Math.random() * 200));
// クリック
element.click();
}---
Service Worker パターン
長時間処理の分割
// 30秒制限を回避するため、処理を分割
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === "LONG_TASK") {
// 即座にレスポンスを返す
sendResponse({ status: "started" });
// バックグラウンドで処理を継続
processInChunks(message.data);
return false; // 同期レスポンス
}
});
async function processInChunks(data: any[]) {
const CHUNK_SIZE = 100;
for (let i = 0; i < data.length; i += CHUNK_SIZE) {
const chunk = data.slice(i, i + CHUNK_SIZE);
await processChunk(chunk);
// 進捗を保存
await chrome.storage.session.set({ progress: i + CHUNK_SIZE });
}
// 完了通知
await chrome.runtime.sendMessage({ type: "TASK_COMPLETE" });
}Keep-Alive パターン
// 定期的にウェイクアップ
chrome.alarms.create("keepAlive", { periodInMinutes: 0.5 });
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === "keepAlive") {
// セッションストレージをチェック
chrome.storage.session.get("pendingTasks").then((result) => {
if (result.pendingTasks?.length > 0) {
processPendingTasks(result.pendingTasks);
}
});
}
});---
スクリーンショットパターン
現在のタブをキャプチャ
// background.ts
async function captureTab(): Promise<string> {
const dataUrl = await chrome.tabs.captureVisibleTab(undefined, {
format: "png",
});
return dataUrl;
}
// 使用(権限: activeTab または tabs + host_permissions)
chrome.action.onClicked.addListener(async (tab) => {
const screenshot = await captureTab();
// Base64 データURL が返る
});フルページキャプチャ
// Content Script でスクロールしながらキャプチャ
async function captureFullPage(): Promise<string[]> {
const screenshots: string[] = [];
const viewportHeight = window.innerHeight;
const totalHeight = document.documentElement.scrollHeight;
for (let y = 0; y < totalHeight; y += viewportHeight) {
window.scrollTo(0, y);
await new Promise((resolve) => setTimeout(resolve, 100));
const screenshot = await chrome.runtime.sendMessage({ type: "CAPTURE" });
screenshots.push(screenshot);
}
return screenshots;
}---
外部リソース
Chrome Web Store 公開ガイド
拡張機能の公開準備とストア提出プロセス。
---
公開前チェックリスト
必須項目
- [ ] マニフェストの
name,version,descriptionが正確 - [ ] アイコン(16x16, 48x48, 128x128 px)が用意されている
- [ ] プライバシーポリシーが用意されている(ユーザーデータを扱う場合)
- [ ] 権限が最小限に設定されている
- [ ] 動作テストが完了している
推奨項目
- [ ] スクリーンショット(1280x800 または 640x400)を用意
- [ ] プロモーションタイル画像(440x280 small、920x680 large)
- [ ] 詳細な説明文(多言語対応推奨)
- [ ] カテゴリが適切に選択されている
---
Release Gate
- 既存 tag に後続修正が入った場合、tag 付け替えではなく patch version を上げる。
- 公開前に
npm run test,npm run lint,npm run typecheck,npm run validate:bridge,npm audit --omit=dev,npm run zipを通す。 - full
npm auditが WXT 経由の dev-only 脆弱性を返す場合は、--omit=devの runtime audit と分けて判断する。semver-major の WXT 更新は別サイクルで扱う。 - CLI stdout や success marker だけで gate 完了を判定しない。ZIP の存在、サイズ、更新時刻、checksum、プロセス残存なしを別経路で確認する。
ZIP パッケージ作成
WXT の場合
# プロダクションビルド
npm run build
# ZIP 生成
npm run zip
# → .output/[name]-[version]-chrome.zip が生成される手動の場合
# PowerShell
Compress-Archive -Path ".output/chrome-mv3/*" -DestinationPath "extension.zip"---
Chrome Web Store Developer Dashboard
アカウント設定
1. Chrome Web Store Developer Dashboard にアクセス 2. 初回は $5 の登録料が必要 3. デベロッパーアカウントを設定
新規アイテム追加
1. 「新しいアイテム」をクリック 2. ZIP ファイルをアップロード 3. ストアリスティング情報を入力:
- 詳細説明
- カテゴリ
- 言語
- スクリーンショット
- プライバシーポリシー URL
権限の正当化
ストア審査では、使用する各権限の正当化が求められる。
| 権限 | 正当化例 |
|---|---|
tabs | タブのURL/タイトルを表示するため |
<all_urls> | すべてのウェブサイトでコンテンツスクリプトを実行するため |
storage | ユーザー設定を保存するため |
cookies | ログイン状態を確認するため |
activeTab | ユーザーがクリックした時のみ現在のタブにアクセスするため |
---
審査プロセス
審査期間
- 通常: 1〜3 営業日
- 複雑な権限を使用する場合: 1〜2 週間
よくあるリジェクト理由
| 理由 | 対策 |
|---|---|
| 権限の過剰要求 | 必要最小限の権限に変更 |
| プライバシーポリシー不備 | ユーザーデータの取り扱いを明記 |
| 機能の説明不足 | ストア説明を詳細化 |
| リモートコード | すべてのコードをバンドルに含める |
| 誤解を招く説明 | 正確な機能説明に修正 |
| 低品質のUI | UIを改善 |
---
商標セーフな命名 (公開後の takedown 対策)
審査通過後でも、商標権者の代理 (例: Microsoft 代理の Tracer microsoft@tracer.ai) が Google 経由で商標侵害を申し立てると、7 日以内に是正しないと item が suspend される。
- 苦情が狙うのは item の Title に当たる箇所: manifest
name/action.default_title/
ストアリスティング名。ここから他社商標 (GITHUB, Copilot, Microsoft 等) を外す。
description/ README / keywords での nominative な互換性言及 は許容
(例: Works with GitHub Copilot or local LLMs)。エスカレートしたら description も中立化するが、 Title 修正だけで suspend リスクは消える。
- 商標対応で manifest `name` の内部 ID や設定キー prefix を変えない —
既存ユーザーのインストール / 設定が壊れる。変えるのは人間が見る Title 文字列だけ。
- リネーム後は全面 grep して、ビルド生成物 (
.outputの manifest、コンパイル済み JS) からも
旧 Title が消えていることを確認する。
- ZIP 反映と リスティング項目は別物。Title だけ直して submit しても、CWS の
説明文の見出し / Privacy policy URL / Store icon / Screenshots 画像 / Website・Support URL に旧名が残ったままだと再申し立ての火種になる。再申請前に Dashboard 上で全項目を点検する。 Screenshots は画像内に写り込んだタブ title やパネル見出しも対象。新名で撮り直して差し替える。
- ストアの URL slug (
/detail/<slug>/<itemId>) は Title 由来で 公開後に自動再生成される
(item ID は不変、旧 URL は redirect で生き続ける)。slug を直接変更する API は無いため、 Title 中立化 + 審査通過後に新 slug を検証する。
- Dashboard の Screenshots スロットを自動操作する場合、削除はサムネイル hover で出るボタン、
追加は drop zone の click → expect_file_chooser 経由が確実。input[type=file] への 直接 set_input_files は隠し input に当たって反映されないことがある。
- ただし UI 部品ごとに動作が違う: Store icon (128×128) は
input[type=file]への直接
set_input_files が通る。Screenshots だけ drop zone + file chooser 必須。アップロード後は 必ず thumbnail / preview の有無で反映を検証する (SCREENS_AFTER_UP が増えない=失敗)。
- hover overlay を出すときは sticky header が pointer を奪う。
scroll_into_view_if_needed
+ window.scrollBy で対象を viewport 中央 ~300px 下へ移動し、座標ベースの page.mouse.move(cx, cy) で hover してから click すると安定する。
- ダッシュボードを撮ったスクショには publisher email / extension ID が写る。
証跡として残す画像(メール添付用など)は repo の store-assets/evidence/ に隔離し、 そのパスを .gitignore に追加して public repo に絶対に commit しない。
---
アップデート公開
バージョン更新
// wxt.config.ts
export default defineConfig({
manifest: {
version: "1.1.0", // バージョンを更新
},
});更新手順
1. npm run zip で新しいZIPを生成 2. Developer Dashboard で該当アイテムを選択 3. 「パッケージ」タブで新しいZIPをアップロード 4. 変更履歴を入力 5. 送信して審査を待つ
---
Artifact Hygiene
- ZIP の中身を列挙し、
src/,tests/,.github/,.vscode/,store-assets/,*.map, log が入っていないことを確認する。 publish-extensionは.env.submitを自動読込する。OAuthinvalid_grantは refresh token 失効として扱い、同じ ZIP を保持したまま再認可後に再実行する。- 再認可時の auth code や refresh token はチャットやログへ貼らず、ターミナルへ直接入力する。更新後は secret を表示せず token exchange の成功だけ確認する。
- live retry 前に
publish-extension --dry-run --chrome-zip <zip>を通し、認証と設定だけ先に確認する。 - CWS publish が止まった場合でも、ZIP と SHA256 を GitHub Release に残して再開可能にする。
- 審査中、認証、権限、duplicate など外部状態で publish だけ止まる場合は、ZIP / SHA256 / version / commit / tag / upload の状態を分けて記録し、再開条件を明記する。
- ブロッカー解消後の再開では、最新タグの ZIP を rollup 提出する。ブロック時点の古い ZIP を蘇生せず、間に積まれた patch をまとめて出す(実例: v0.1.11→v0.1.15 で 4 版分を一括公開)。
publish-extensionが400 "Publish condition not met: You may not edit or publish an item that is in review."で失敗する場合は、前回の draft が審査キュー残留中。先に?projection=DRAFTで stuck しているcrxVersionを確認し、Dashboard UI からmore_vert→ 審査をキャンセル → 確認ダイアログで取り下げる。ステータスバッジが「公開済み」に戻り次第publish-extensionを再実行できる。- publish 後の CWS API 確認は item endpoint に
?projection=DRAFTを付ける。crxVersionが対象版でitemErrorが 0 件なら API 側の確認は通過扱いにし、uploadState: NOT_FOUNDだけで失敗判定や再アップロードをしない。 - API 応答が疎または stale な場合、最終ステータスは Chrome Web Store Developer Dashboard で確認する。
自動パブリッシング
WXT Auto-Publishing
# Chrome Web Store API を設定
npm install -D chrome-webstore-upload-cli
# 環境変数設定
export EXTENSION_ID="your-extension-id"
export CLIENT_ID="your-client-id"
export CLIENT_SECRET="your-client-secret"
export REFRESH_TOKEN="your-refresh-token"
# アップロード&公開
npx chrome-webstore-upload upload --source .output/*-chrome.zip --auto-publishGitHub Actions 自動公開
# .github/workflows/publish.yml
name: Publish to Chrome Web Store
on:
release:
types: [created]
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- run: npm ci
- run: npm run build
- run: npm run zip
- name: Upload to Chrome Web Store
uses: mnao305/chrome-extension-upload@v5.0.0
with:
file-path: .output/*-chrome.zip
extension-id: ${{ secrets.EXTENSION_ID }}
client-id: ${{ secrets.CLIENT_ID }}
client-secret: ${{ secrets.CLIENT_SECRET }}
refresh-token: ${{ secrets.REFRESH_TOKEN }}---
非公開配布
開発者モード(ローカル)
1. chrome://extensions を開く 2. 「デベロッパーモード」を有効化 3. 「パッケージ化されていない拡張機能を読み込む」 4. ビルドフォルダを選択
CRX パッケージ(社内配布)
# Chrome で CRX を生成
# chrome://extensions → パック拡張機能
# または
npx crx pack .output/chrome-mv3 -o extension.crx---
外部リソース
テストガイド
ブラウザ拡張機能のテスト戦略(Vitest + Playwright)。
---
テスト戦略
| テスト種別 | ツール | 対象 |
|---|---|---|
| ユニットテスト | Vitest | ユーティリティ、ロジック、状態管理 |
| 統合テスト | Vitest | コンポーネント、Chrome API モック |
| E2Eテスト | Playwright | 拡張機能全体、実際のブラウザ操作 |
---
Vitest セットアップ
インストール
npm install -D vitest jsdom @testing-library/react @testing-library/jest-dom設定
// vitest.config.ts
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
environment: "jsdom",
include: ["**/*.test.ts", "**/*.test.tsx"],
globals: true,
setupFiles: ["./test/setup.ts"],
},
});セットアップファイル
// test/setup.ts
import "@testing-library/jest-dom";
// Chrome API モック
const chromeMock = {
storage: {
local: {
get: vi.fn(),
set: vi.fn(),
},
sync: {
get: vi.fn(),
set: vi.fn(),
},
onChanged: {
addListener: vi.fn(),
},
},
runtime: {
sendMessage: vi.fn(),
onMessage: {
addListener: vi.fn(),
},
},
tabs: {
query: vi.fn(),
sendMessage: vi.fn(),
},
};
vi.stubGlobal("chrome", chromeMock);テスト例
// utils/storage.test.ts
import { describe, it, expect, vi, beforeEach } from "vitest";
import { saveSettings, getSettings } from "./storage";
describe("Storage Utils", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("should save settings to local storage", async () => {
const settings = { theme: "dark" };
await saveSettings(settings);
expect(chrome.storage.local.set).toHaveBeenCalledWith({ settings });
});
it("should get settings from local storage", async () => {
vi.mocked(chrome.storage.local.get).mockResolvedValue({
settings: { theme: "light" },
});
const result = await getSettings();
expect(result).toEqual({ theme: "light" });
});
});---
React コンポーネントテスト
// components/Popup.test.tsx
import { render, screen, fireEvent } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";
import { Popup } from "./Popup";
describe("Popup Component", () => {
it("should render correctly", () => {
render(<Popup />);
expect(screen.getByText("Settings")).toBeInTheDocument();
});
it("should toggle theme on button click", async () => {
render(<Popup />);
const button = screen.getByRole("button", { name: /toggle theme/i });
fireEvent.click(button);
expect(chrome.storage.local.set).toHaveBeenCalled();
});
});---
Playwright E2E テスト
インストール
npm install -D @playwright/test
npx playwright install chromium設定
// playwright.config.ts
import { defineConfig } from "@playwright/test";
export default defineConfig({
testDir: "./e2e",
use: {
headless: false, // 拡張機能テストは headless: false 必須
},
projects: [
{
name: "chromium",
use: { browserName: "chromium" },
},
],
});拡張機能読み込み
// e2e/fixtures.ts
import { test as base, chromium, type BrowserContext } from "@playwright/test";
import path from "path";
export const test = base.extend<{
context: BrowserContext;
extensionId: string;
}>({
context: async ({}, use) => {
const pathToExtension = path.join(__dirname, "../.output/chrome-mv3");
const context = await chromium.launchPersistentContext("", {
headless: false,
args: [
`--disable-extensions-except=${pathToExtension}`,
`--load-extension=${pathToExtension}`,
],
});
await use(context);
await context.close();
},
extensionId: async ({ context }, use) => {
// 拡張機能のService Worker URLから ID を取得
let [background] = context.serviceWorkers();
if (!background) {
background = await context.waitForEvent("serviceworker");
}
const extensionId = background.url().split("/")[2];
await use(extensionId);
},
});
export const expect = test.expect;E2E テスト例
// e2e/popup.spec.ts
import { test, expect } from "./fixtures";
test("popup should open and display content", async ({
context,
extensionId,
}) => {
// ポップアップページを開く
const popupPage = await context.newPage();
await popupPage.goto(`chrome-extension://${extensionId}/popup.html`);
// 要素を確認
await expect(popupPage.locator("h1")).toContainText("My Extension");
});
test("content script should inject into page", async ({ context }) => {
const page = await context.newPage();
await page.goto("https://example.com");
// Content Script が注入した要素を確認
const injectedElement = page.locator("[data-extension-injected]");
await expect(injectedElement).toBeVisible();
});サイドパネルテスト
// e2e/sidepanel.spec.ts
import { test, expect } from "./fixtures";
test("side panel should display", async ({ context, extensionId }) => {
// サイドパネルページを直接開く(実際のサイドパネル操作は制限あり)
const page = await context.newPage();
await page.goto(`chrome-extension://${extensionId}/sidepanel.html`);
await expect(page.locator("main")).toBeVisible();
});---
CI/CD 設定
GitHub Actions
# .github/workflows/test.yml
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
- run: npm ci
- name: Run unit tests
run: npm run test
- name: Build extension
run: npm run build
- name: Install Playwright
run: npx playwright install chromium
- name: Run E2E tests
run: npm run test:e2e
env:
# E2Eテストは headless: false が必要なため xvfb を使用
DISPLAY: ":99"
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: playwright-report/---
WXT Typecheck
WXT の生成型を含めた型検査は、root tsconfig.json や .wxt/tsconfig.json だけでは不足することがある。専用 tsconfig.typecheck.json を用意し、.wxt/wxt.d.ts と必要な browser API 型を include する。
{
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": true,
"types": ["wxt/client", "chrome"],
"jsx": "react-jsx"
},
"include": ["**/*.ts", "**/*.tsx", ".wxt/wxt.d.ts"]
}entrypoints/ 配下に補助 .d.ts を置くと WXT が entrypoint と誤認することがあるため、shim は root か型専用フォルダに置く。
テスト Tips
Chrome API の詳細モック
// 詳細なモック設定
vi.mocked(chrome.storage.local.get).mockImplementation(async (keys) => {
if (keys.includes("settings")) {
return { settings: { theme: "dark" } };
}
return {};
});非同期処理のテスト
// メッセージング のテスト
it("should handle message response", async () => {
vi.mocked(chrome.runtime.sendMessage).mockResolvedValue({ success: true });
const result = await sendAction("doSomething");
expect(result.success).toBe(true);
});---
外部リソース
Related skills
FAQ
What framework does it recommend?
WXT (Vite-based) with TypeScript and React 18+, Vue 3 or Svelte, styled with Tailwind CSS.
What is the gotcha with permission changes?
After changing permissions or host_permissions, reloading the extension is not enough; you must remove and reinstall it.