📘 WSI v1.3.0 対応

WSI プラグイン開発ガイド

Chrome 拡張 Web System Injection に自分だけの機能を追加しよう。
数十行の JavaScript で、任意の Web サイトにボタン・パネル・外部API連携を実装できます。

🚀5分で始める#

最小のプラグインは 2ファイルplugin.jsonmain.js)だけで動きます。 ここでは example.com を開くと右下に 👋 ボタンを表示する「Hello World」を作ってみましょう。

📥 事前準備: WSI 拡張機能のインストール

このガイドは WSI 本体がインストール済みである前提です。未インストールの場合は、 Chrome Web Store で「Web System Injection」を検索 してインストールしてください。インストール後、ブラウザのツールバーに WSI アイコンが表示されます。

Step 1. フォルダを作る

terminal
mkdir my-first-plugin
cd my-first-plugin

Step 2. plugin.json を作る

プラグイン定義ファイル。ID・対象ドメイン・エントリースクリプトを指定します。

plugin.json
{
  "id": "my-first-plugin",
  "name": "My First Plugin",
  "version": "1.0.0",
  "description": "初めてのWSIプラグイン",
  "domains": ["example.com"],
  "scripts": { "main": "main.js" }
}

Step 3. main.js を作る

プラグイン本体。WSI オブジェクト経由で拡張機能のAPIにアクセスします。

main.js
WSI.addButton({
  text: '👋',
  position: 'bottom-right',
  onClick: () => {
    alert('Hello from WSI!');
  },
});

WSI.log('プラグインが読み込まれました');

Step 4. ZIPにまとめる

2ファイルを ZIP 圧縮します。フォルダを圧縮するのではなく、ファイルを選択して圧縮してください(plugin.json が ZIP のルートに来る必要があります)。

⚠️ よくあるミス

my-first-plugin/ フォルダごと圧縮すると、ZIP内部が my-first-plugin/plugin.json の階層になってしまい、WSIが plugin.json を見つけられません。必ず中身のファイルを選択して圧縮してください。

Step 5. WSIにインポート

  1. Chrome で chrome://extensions を開き、WSI が読み込まれていることを確認
  2. WSI アイコンをクリックしてポップアップを開く
  3. 「プラグインを追加」をクリック
  4. 作った ZIP ファイルをドラッグ&ドロップ
  5. プレビュー内容を確認して「インポート」
  6. https://example.com を開く → 右下に 👋 ボタンが出たら成功 🎉
💡 Tip

ボタンはドラッグで移動可能です。使いやすい位置に配置できます。位置は chrome.storage.local に保存され、次回以降は覚えてくれます。

📦プラグインの構造#

WSI プラグインは以下の最大3ファイルで構成されます。

plugin-zip-内部
my-plugin.zip
├── plugin.json    # 必須: プラグイン定義
├── main.js        # 必須: プラグイン本体
└── style.css      # 任意: CSS(複数可)

plugin.json のフィールド

フィールド必須説明
idstring 必須 一意識別子。英数字とハイフンのみ (/^[a-zA-Z0-9-]+$/)
namestring 必須 表示名。WSI ポップアップに表示されます
versionstring 必須 セマンティックバージョニング(例: 1.0.0
descriptionstring 任意 説明文。プラグインカードに表示
authorstring 任意 作者名
domainsstring[] 必須 対象ドメインの配列(1つ以上)。詳細は ドメインマッチング
scripts.mainstring 必須 メインJSファイル名(通常 main.js
scripts.runAtstring 任意 注入タイミング。デフォルト document_idle
stylesstring[] 任意 CSSファイル名の配列。複数指定可
configobject 任意 プラグイン固有の設定オブジェクト。WSI.getConfig() で取得

main.js の実行環境

プラグインコードは対象ページの メインワールド(ページ自身のJSと同じ実行コンテキスト)で実行されます。 つまり window, document, localStorage など、ページ側の全APIにアクセス可能です。

ただし chrome.* 拡張機能APIは直接使えません。 これは WSI がセキュリティのため意図的に切っているもので、代わりに必要な機能は WSI.* SDK 経由で提供されます。

ℹ️ メインワールドとは

Chrome 拡張には「Isolated World」(拡張機能専用のサンドボックス)と「Main World」(ページ自身の世界)があります。WSI プラグインは後者で動くため、ページのJSと同じDOM・同じJSグローバルを共有します。結果、ページ側の React や Vue の内部状態にもアクセス可能です(自己責任)。

🧰SDK リファレンス#

プラグインコードには WSI という引数でSDKオブジェクトが渡されます。以下のAPIが利用可能です。

WSI.addButton(options)

WSI.addButton({ text, icon, position, onClick }) → HTMLButtonElement

画面にフローティングボタンを追加します。ユーザはドラッグで自由に移動可能で、位置は chrome.storage.local に永続化されます。

パラメータ説明
textstringボタンのラベル
iconstring任意のアイコン文字(絵文字など)。指定するとtextの前に表示
positionstring初期位置: bottom-right / bottom-left / top-right / top-left
onClickfunctionクリック時コールバック(ドラッグ直後は自動抑制)
使用例
WSI.addButton({
  text: 'Say Hi',
  icon: '👋',
  position: 'bottom-right',
  onClick: () => alert('Hi!'),
});

WSI.addPanel(options)

WSI.addPanel({ title, width, position, content, onOpen, onClose }) → HTMLDivElement

画面サイドにスライドパネルを追加します。目次・検索結果・設定画面などに使えます。

パラメータ説明
titlestringパネルヘッダーのタイトル
widthstringCSS幅(例: "300px", "25vw"
positionstringright(デフォルト)または left
contentstringパネル本文のHTML文字列(innerHTMLにセット)
onOpenfunction表示直後のコールバック
onClosefunction×ボタンで閉じた直後のコールバック
使用例
const panel = WSI.addPanel({
  title: '設定',
  width: '320px',
  position: 'right',
  content: '<p>ここに内容</p>',
  onClose: () => console.log('panel closed'),
});
⚠️ XSS注意

contentinnerHTML にセットされるため、ユーザ入力や外部文字列を直接渡すと XSS 脆弱性になります。必ずエスケープしてください。

WSI.storage

プラグイン固有の永続ストレージ。chrome.storage.local にプラグインID名前空間で保存されます。 すべて非同期(Promise を返す)です。

WSI.storage.get(key) → Promise<value | undefined>
WSI.storage.set(key, value) → Promise<true>
WSI.storage.remove(key) → Promise<true>
WSI.storage.getAll() → Promise<{ [key]: value }>
使用例
// 保存
await WSI.storage.set('userName', '山田太郎');

// 取得
const name = await WSI.storage.get('userName');

// 全件取得(プラグインのストレージ全体)
const all = await WSI.storage.getAll();

// 削除
await WSI.storage.remove('userName');
💡 Tip

URL別にデータを分けたいときは、キー名に location.origin + location.pathname を含めると自然に名前空間を切れます。Highlighter サンプルがこの方式を使っています。

WSI.fetch(url, options)

WSI.fetch(url, { method, redirect, headers, body }) → Promise<Response>

CORS制限をバイパスして任意のURLにリクエストを送れる特別な fetch。 内部的には Service Worker で fetch() を実行するため、ページのCORSポリシーに縛られません。

オプション説明
methodstringHTTPメソッド。デフォルト "HEAD"
redirectstring"follow"(デフォルト)/ "manual" / "error"
headersobjectリクエストヘッダー
bodystringリクエストボディ(POST/PUT 等)

戻り値(成功時)

response shape
{
  ok: boolean,         // HTTP 2xx なら true
  status: number,      // HTTPステータスコード
  url: string,         // リダイレクト後の最終URL
  redirected: boolean, // リダイレクトが発生したか
  body: string,        // レスポンスボディ(HEAD時は空文字列)
}

戻り値(失敗時)

error shape
{ error: string, ok: false, status: 0 }
使用例: 外部APIを叩いてJSONを取得
const res = await WSI.fetch(
  'https://jisho.org/api/v1/search/words?keyword=猫',
  { method: 'GET' }
);

if (res.ok) {
  const data = JSON.parse(res.body);
  console.log(data);
} else {
  console.error('fetch error', res.error || res.status);
}

WSI.onPageLoad(callback)

WSI.onPageLoad((newUrl) => void) → void

SPA等で location.href が変わった瞬間に callback が呼ばれます。 React Router や Next.js のクライアントサイド遷移にも反応します(MutationObserver + popstate 実装)。

使用例
WSI.onPageLoad((url) => {
  WSI.log(`ナビゲーション: ${url}`);
  // 再初期化処理
});
ℹ️ 初回実行について

onPageLoad最初のページロード時には発火しません。最初の処理は main.js のトップレベルコードに書き、SPA遷移への追従のみ onPageLoad で処理してください。

WSI.getConfig()

WSI.getConfig() → object

plugin.jsonconfig フィールドの値を deep-copy で返します。 プラグインをユーザー設定可能にするための主要な仕組みです。

plugin.json
{
  "id": "my-plugin",
  "config": {
    "message": "こんにちは",
    "color": "#4688F1"
  }
}
main.js
const config = WSI.getConfig();
console.log(config.message); // "こんにちは"
console.log(config.color);   // "#4688F1"

WSI.log(message)

WSI.log(message: string) → void

プラグインIDをプレフィックスとして付けた console.log。どのプラグインが何をログったか明確になります。

使用例
WSI.log('プラグイン起動');
// コンソール出力: [WSI:my-plugin] プラグイン起動

🎯ドメインマッチング#

domains 配列に書いたパターンのどれかに一致するドメインで、プラグインが実行されます。

パターンマッチする例マッチしない例
"example.com" example.com www.example.com / example.org
"*.example.com" example.com / www.example.com / foo.bar.example.com example.org
"*" (v1.2.0〜) あらゆるドメイン

複数パターンの組み合わせ

plugin.json
{
  "domains": [
    "qiita.com",
    "*.qiita.com",
    "zenn.dev",
    "*.hatena.ne.jp"
  ]
}
ℹ️ chrome:// / edge:// では動きません

Chrome 内部ページや拡張機能ストアなど、特権ページには拡張機能のスクリプト注入自体がブロックされます(Chrome仕様)。ドメイン指定に関係なく、これらのページでは WSI プラグインは動きません。

⚙️設定 (config)#

plugin.jsonconfig フィールドは任意のJSONオブジェクトです。 プラグイン内から WSI.getConfig() で取得し、振る舞いを変えられる仕組みに使えます。

典型的な使い方

plugin.json
{
  "id": "highlighter",
  "config": {
    "color": "#fff59d",
    "buttonPosition": "bottom-left",
    "maxHighlightsPerPage": 100
  }
}
main.js
const config = WSI.getConfig();
const color = config.color || '#fff59d';               // デフォルト値
const buttonPosition = config.buttonPosition || 'bottom-right';
const maxHighlights = config.maxHighlightsPerPage ?? 50;
💡 設定可能なプラグインの書き方

ユーザーが簡単にカスタマイズできるよう、ハードコードせず config から読む習慣をつけると、自分や他人が再利用しやすくなります。色、閾値、対象セレクタなどは config に出しましょう。

📦パッケージング#

プラグインは ZIP形式で WSI にインポートします。ZIP の作り方をOS別・方法別にまとめます。

Windows (エクスプローラ)

  1. plugin.json, main.js, style.css を選択
  2. 右クリック → 「送る」→「圧縮 (zip 形式) フォルダー」
  3. できた .zip ファイル名を任意のものに変更

macOS / Linux (コマンドライン)

bash
cd my-plugin
zip ../my-plugin.zip plugin.json main.js style.css

Windows PowerShell

PowerShell
Compress-Archive -Path plugin.json, main.js, style.css -DestinationPath my-plugin.zip

Node.js (jszip) で自動化

開発が加速してくると、スクリプトでZIPを作るのが便利です。WSI リポジトリのサンプル ZIP もこの方式で作っています。

build-zip.js
const fs = require('fs');
const JSZip = require('jszip');

const zip = new JSZip();
['plugin.json', 'main.js', 'style.css'].forEach((f) => {
  zip.file(f, fs.readFileSync(f));
});

zip.generateAsync({ type: 'nodebuffer' }).then((buf) => {
  fs.writeFileSync('my-plugin.zip', buf);
  console.log('ZIP built:', buf.length, 'bytes');
});

バージョニングのルール

ZIPを再作成するたびに plugin.jsonversion をインクリメントしましょう。

変更タイプ
パッチ修正(バグ修正)1.0.01.0.1
機能追加(後方互換あり)1.0.51.1.0
破壊的変更1.3.02.0.0
💡 同一IDは上書きインポート

id が同じプラグインをインポートすると、WSI は上書き確認を出します。設定値・ストレージはそのまま残り、コードだけが新しくなります。

🎨サンプルギャラリー#

公式サンプル 6 個は、それぞれ異なる WSI SDK 機能を学べるよう設計されています。 ZIPをインポートすればすぐ動きます。ソースコードは各カードから GitHub で読めます。

📂 GitHub でリポジトリを開く

🔍デバッグ#

プラグインコードのログを見る

対象ページで F12 で DevTools を開き「Console」タブ。 WSI.log() の出力は [WSI:your-plugin-id] プレフィックス付きで表示されます。

ストレージの中身を確認する

  1. chrome://extensions を開く
  2. WSI の詳細 → 「Service Worker」リンクをクリック → DevTools が開く
  3. Console で以下を実行:
Service Worker Console
// 全ストレージを表示
chrome.storage.local.get(null, console.log);

// 特定プラグインのデータ
chrome.storage.local.get('pluginData_my-plugin', console.log);

// ボタン位置キャッシュ
chrome.storage.local.get('wsiButtonPositions', console.log);

よくあるエラー

症状原因・対処
インポート時「plugin.json が見つかりません」 ZIP内部がフォルダ階層になっている。ファイルを選択して圧縮し直す
ボタンが表示されない (1) プラグインが有効か確認 (2) ドメインパターンが合っているか (3) DevTools Consoleでエラー確認
コード変更が反映されない プラグインを一度削除してから ZIP を作り直して再インポート。version バンプも忘れずに
WSI is not defined 注入タイミングが早すぎる可能性。scripts.runAtdocument_idle
WSI.fetch がエラー 戻り値の error フィールドにメッセージあり。CORSではなくネットワーク自体のエラーの可能性

ベストプラクティス#

DO

  • 1プラグイン = 1機能 に絞る(テストしやすく、再利用しやすい)
  • 設定値は config(色、閾値、セレクタなどはハードコードしない)
  • エラーは try-catch で握りつぶさない(WSI.logで見えるようにする)
  • CSSクラスにプラグインID等のプレフィックスをつける(.wsi-xxxxx-)。ページ側のCSSと衝突しないよう
  • innerHTML に入れる前にエスケープ(XSSを防ぐ)
  • storage キーに location.pathname を含める(URL別の状態管理)
  • ZIP再作成時に version をバンプ(何が変わったか追跡できる)

DON'T

  • chrome.* API を直接呼ぼうとしない(メインワールドからは使えない)
  • ページのグローバル変数を書き換えないwindow.$ 等との衝突要因)
  • 永続化なしの重いDOM計算を onPageLoad で毎回(キャッシュする)
  • 外部ライブラリをCDNから動的 <script> 読み込み(Manifest V3 のリモートコード禁止ルールに抵触する可能性。ZIPに同梱すべき)
  • signalhigh-frequencyなイベント(scroll/mousemove)にデバウンスなしリスナ(パフォーマンス劣化)
💡 Tip: main.js は短いほうが良い

公式サンプルの main.js はいずれも 200行以下に収めています。何をするプラグインか一目でわかる粒度が理想です。

FAQ#

WSI 本体(Chrome拡張)はどこで入手できますか?

Chrome Web Store で「Web System Injection」を検索 してインストールしてください。インストール後、ブラウザツールバーの WSI アイコンからポップアップを開いて、プラグインZIPをインポートできます。

プラグインは同時に何個まで入れられますか?

明確な上限はありません。chrome.storage.local の容量上限(約10MB)まで。実用上は数十個まで問題なく動きます。

他のユーザーが作ったプラグインを使うのは安全ですか?

WSI プラグインは JavaScript がフル実行されるため、ソースコードを読めない相手から配られた ZIP は絶対に入れないでください。Chrome拡張機能本体と同じレベルのリスクがあります。公式サンプルや自分で書いたものだけ使うのが安全です。

プラグイン同士はデータを共有できますか?

デフォルトでは別名前空間で完全分離されています(pluginData_<pluginId> キーで分離)。どうしても共有したい場合は、メインワールド上で独自の window.myShared = ... 的な仕組みを使うことは可能です(非推奨)。

React/Vue で書かれたページにも機能追加できますか?

はい、問題なく動きます。ただし SPA では onPageLoad で URL 変更を検知する必要があります。React の state や props に直接触ることもできますが、内部実装に依存するので脆いです。可能ならDOM経由でアクセスしましょう。

プラグインで外部ライブラリ (lodash, axios 等) を使いたい

CDNから動的ロードはManifest V3 のルール違反になる可能性があります。ライブラリのJSファイルをプラグインZIPに同梱し、plugin.jsonscripts.main より前に別のscriptとして読む… という仕組みは WSI に無いため、必要な関数をインライン実装するか、ZIP内の別ファイルを import() するのが現実解です。

ボタンの位置をリセットしたい

Service Worker Console で以下を実行:

console
chrome.storage.local.remove('wsiButtonPositions');

または該当プラグインを削除 → 再インポートでもリセットされます。

サンプルと同じディレクトリ構造で作るべきですか?

いいえ、必須ではありません。ZIPの内部構造がフラット(plugin.json がルート)であれば、開発時のフォルダ配置は自由です。

プラグインをChrome Web Storeで配布できますか?

いいえ、WSIプラグインは WSI 本体に取り込まれる形で動作します。配布したい場合は、ZIPファイルを自分のサイト/GitHub等でホストし、「WSIをインストールした上でこのZIPをインポートしてください」と案内する形になります。

どのSDKを使うべきか迷ったら?

以下が目安です:

  • 単発のアクションaddButton(例: 1クリックでコピー/通知/検索)
  • 情報を表示するUIaddPanel(目次、検索結果、詳細)
  • 状態を永続化storage(設定、履歴、マーキング)
  • 外部サイトへリクエストfetch(API連携、URL検証)
  • SPAに対応onPageLoad(URL変更時に再処理)
既存のプラグインをベースに作りたい

サンプルZIPを展開して、plugin.jsonidnameを変更、main.jsを書き換え、ZIP化すればオリジナルプラグインになります。サンプルギャラリーの気に入ったものから始めるのがおすすめ。