crxjs について
CRXJSは、マニフェストを基にChrome拡張機能を構築するViteプラグインです。各種拡張機能の画面に加え、コンテンツスクリプトにも真のHMRを提供し、ブラウザーAPIを薄く扱えます。
主な機能
- 01マニフェストに基づく拡張機能ビルド
- 02ポップアップ、オプション、サイドパネルのHMR
- 03コンテンツスクリプトのHMR
- 04動的なコンテンツスクリプトのインポート
- 05型安全な動的マニフェスト
活用シーン
- 01Viteフレームワークでの拡張機能開発
- 02開発中のコンテンツスクリプト反復
- 03リリース用拡張機能バンドルの作成
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ samber/ cc-skills --skill crxjsインストール後、Claude Code で/crxjsを入力して読み込まれたか確認します。
詳細
CRXJS
CRXJS は、ポップアップ、オプションページ、コンテンツスクリプト、サイドパネルに真の HMR を提供する Chrome 拡張機能開発ツールです。マニフェストを読み取って拡張機能の出力を自動生成し、コンテンツスクリプトの挿入を処理し、サービスワーカーのビルドを管理します。内部では Vite プラグイン(@crxjs/vite-plugin)として動作します。
現在の状況
- パッケージ:
@crxjs/vite-plugin(v2.x は安定版、2026年3月時点の最新は v2.4.0) - スキャフォールディング:
npm create crxjs@latest(常に@latestを使用してください) - メンテナンス担当:@Toumash と @FliPPeDround(2025年半ばから)
- GitHub:github.com/crxjs/chrome-extension-tools(約4kスター)
- Vite との互換性:v3 から v8-beta まで
クイックスタート
# Scaffold new project (picks framework interactively)
npm create crxjs@latest
# Or add to existing Vite project
npm install @crxjs/vite-plugin -D
フレームワーク別の Vite 設定
CRXJS は Vite プラグインとして追加します。セットアップ方法はフレームワークによって少し異なります。
React
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { crx } from "@crxjs/vite-plugin";
import manifest from "./manifest.json";
export default defineConfig({
plugins: [react(), crx({ manifest })],
});
HMR との互換性を最大限に高めるには、@vitejs/plugin-react(plugin-react-swc ではありません)を使用してください。SWC を使用する必要がある場合は、マニフェストをキャストしてください。
import { ManifestV3Export } from "@crxjs/vite-plugin";
const manifest = manifestJson as ManifestV3Export;
Vue
import vue from "@vitejs/plugin-vue";
import { crx } from "@crxjs/vite-plugin";
import manifest from "./manifest.json";
export default defineConfig({
plugins: [vue(), crx({ manifest })],
});
Svelte
import { svelte } from "@sveltejs/vite-plugin-svelte";
import { crx } from "@crxjs/vite-plugin";
import manifest from "./manifest.json";
export default defineConfig({
plugins: [svelte(), crx({ manifest })],
});
Vanilla TypeScript
import { crx } from "@crxjs/vite-plugin";
import manifest from "./manifest.json";
export default defineConfig({
plugins: [crx({ manifest })],
});
defineManifest — 型安全な動的マニフェスト
静的な JSON ファイルの代わりに、CRXJS の defineManifest を使用すると、動的な値と完全な TypeScript の自動補完が利用できます。
// manifest.ts
import { defineManifest } from "@crxjs/vite-plugin";
import pkg from "./package.json";
export default defineManifest((config) => ({
manifest_version: 3,
name: config.command === "serve" ? `[DEV] ${pkg.name}` : pkg.name,
version: pkg.version,
description: pkg.description,
permissions: ["storage", "activeTab", "scripting"],
action: {
default_popup: "src/popup/index.html",
default_icon: {
"16": "public/icons/icon16.png",
"48": "public/icons/icon48.png",
},
},
background: {
service_worker: "src/background/index.ts",
type: "module",
},
content_scripts: [
{
matches: ["https://*/*"],
js: ["src/content/index.ts"],
css: ["src/content/styles.css"],
},
],
options_page: "src/options/index.html",
side_panel: { default_path: "src/sidepanel/index.html" },
icons: {
"16": "public/icons/icon16.png",
"48": "public/icons/icon48.png",
"128": "public/icons/icon128.png",
},
}));
vite.config.ts でインポートします。
import manifest from "./manifest";
// ... crx({ manifest })
型宣言
src/vite-env.d.ts または src/crxjs.d.ts に追加します。
/// <reference types="@crxjs/vite-plugin/client" />
これにより、?script と ?script&module のインポートで型が有効になります。
コンテキスト別の HMR の動作
| コンテキスト | HMR | 動作の仕組み |
|---|---|---|
| ポップアップ | 完全な HMR | WebSocket ベースで、状態が保持されます |
| オプションページ | 完全な HMR | ポップアップと同じです |
| サイドパネル | 完全な HMR | ポップアップと同じです |
| コンテンツスクリプト(マニフェスト) | 真の HMR | CRXJS がローダーと HMR クライアントを挿入します |
| コンテンツスクリプト(動的) | 真の HMR | ?script インポート経由です |
| サービスワーカー | 自動再読み込み | 変更すると拡張機能全体が再読み込みされます |
| メインワールドスクリプト | HMR なし | CRXJS ローダーによってスキップされます |
コンテンツスクリプトで HMR が機能するのは、CRXJS がローダースクリプトを生成し、HMR のプリアンブル、HMR クライアント、実際のスクリプトをインポートするためです。これにより、ページ全体を再読み込みせずに、真のモジュール単位の HMR が可能になります。これが CRXJS の主な差別化要因です。
動的なコンテンツスクリプトのインポート
プログラムで挿入するコンテンツスクリプト(マニフェストには記載しないもの)には、CRXJS が特殊なインポートサフィックスを用意しています。
// background.ts — ?script gives you a resolved path for executeScript
import contentScript from "./content?script";
chrome.action.onClicked.addListener(async (tab) => {
await chrome.scripting.executeScript({
target: { tabId: tab.id! },
files: [contentScript],
});
});
メインワールドへの挿入(HMR なし)には、次を使用します。
import mainWorldScript from "./inject?script&module";
await chrome.scripting.executeScript({
target: { tabId },
world: "MAIN",
files: [mainWorldScript],
});
CRXJS プラグインのオプション
crx({
manifest,
browser: "chrome", // 'chrome' | 'firefox'
contentScripts: {
injectCss: true, // auto-inject CSS for content scripts
hmrTimeout: 5000, // HMR connection timeout (ms)
},
});
開発ワークフロー
# Start dev server (outputs to dist/ with HMR)
npm run dev
# 1. Open chrome://extensions
# 2. Enable "Developer mode"
# 3. Click "Load unpacked"
# 4. Select the dist/ directory
# 5. Edit code — popup/content scripts update instantly via HMR
# 6. Service worker changes trigger automatic extension reload
一度読み込めば、以降の npm run dev セッションは自動的に再接続します。manifest.json に変更がない限り、拡張機能を再読み込みする必要はありません。
本番ビルド
npm run build # outputs to dist/
dist/ ディレクトリは、zip に圧縮して Chrome ウェブストアにアップロードできる状態です。
cd dist && zip -r ../extension.zip .
インラインスクリプトが原因で CWS に拒否されないよう、Vite のモジュールプリロードを無効にします。
build: {
modulePreload: false;
}
既知の問題と回避策
コンテンツスクリプトでの Tailwind CSS HMR
新しい Tailwind クラスを追加しても、コンテンツスクリプトの CSS が更新されないことがあります。回避策:新しいユーティリティクラスを追加した後、開発サーバーを再起動してください。v2.4.0 で改善されていますが、完全には解決していません。設定で injectCss: true が有効になっていることを確認してください。
WebSocket 接続エラー(ws://localhost:undefined/)
原因:開発サーバーと HMR 設定のポートが一致していません。修正方法:両方に同じ値を明示的に設定してください。
server: {
port: 5173,
strictPort: true,
hmr: { port: 5173 },
}
「マニフェスト バージョン 2 は非推奨です」という警告
この警告が表示された場合、マニフェストが MV2 として解釈されています。修正方法:"manifest_version": 3 が設定されていることを確認してください。
file:// URL でコンテンツスクリプトが挿入されない
Chrome では、拡張機能の設定(chrome://extensions)でユーザーが「ファイル URL へのアクセスを許可する」を有効にする必要があります。CRXJS ではこの設定を変更できません。
Chrome の更新後に HMR が機能しなくなる
CRXJS の HMR は、開発サーバーの WebSocket に接続するコンテンツスクリプトの挿入に依存しています。Chrome のセキュリティ更新によって、これが機能しなくなることがあります。修正方法:Chrome の変更に対応した最新バージョンの CRXJS に更新してください。
CRXJS と代替手段の比較
| 機能 | CRXJS | WXT | Plasmo |
|---|---|---|---|
| コンテンツスクリプトの HMR | 真の HMR | ファイルベースのリロード | 部分的 |
| フレームワークのサポート | あらゆる Vite フレームワーク | あらゆるもの | React に特化 |
| 抽象化のレベル | 薄い(Vite プラグイン) | フルフレームワーク | フルフレームワーク |
| メッセージングヘルパー | なし(chrome.* を直接使用) | 組み込み | 組み込み |
| ストレージラッパー | なし | 組み込み | 組み込み |
| クロスブラウザー対応 | Chrome + Firefox | Chrome + Firefox + Safari | Chrome + Firefox |
| ファイルベースのルーティング | なし | あり | あり |
| 学習曲線 | 低い(Vite と CRXJS の知識が必要) | 中程度 | 中程度 |
CRXJS を選ぶ場合:Chrome の生の API に対する抽象化を最小限に抑え、コンテンツスクリプトの HMR を何よりも重視する場合に適しています。CRXJS は邪魔になりません。魔法のようなルーティングも、ラッパー API もなく、HMR を備えたあなたのコードだけです。
WXT を選ぶ場合:規約、組み込みユーティリティ、クロスブラウザー対応を求める場合に適しています。
Plasmo を選ぶ場合:React を中心に開発し、最も高水準の抽象化を求める場合に適しています。
プロジェクト構成(推奨)
my-extension/
├── src/
│ ├── background/
│ │ └── index.ts
│ ├── content/
│ │ ├── index.ts
│ │ └── styles.css
│ ├── popup/
│ │ ├── index.html <- CRXJS resolves HTML entry points
│ │ ├── App.tsx
│ │ └── main.tsx
│ ├── options/
│ │ ├── index.html
│ │ └── main.tsx
│ ├── sidepanel/
│ │ ├── index.html
│ │ └── main.tsx
│ └── shared/
│ ├── messages.ts
│ └── storage.ts
├── public/
│ └── icons/
├── manifest.ts <- or manifest.json
├── vite.config.ts
├── tsconfig.json
└── package.json
CRXJS は、マニフェストで参照される HTML ファイルを自動的に解決します。popup.html では標準の <script type="module" src="./main.tsx"> を使用でき、そのまま動作します。
CRXJS でバグや予期しない動作が発生した場合は、github.com/crxjs/chrome-extension-tools/issues で issue を作成してください。