The High-Performance, Config-Driven Framework for Modern Cross-Browser Extensions
A production-grade, battle-tested scaffold and toolkit for developing Chrome Manifest V3, Firefox, Safari, and Edge extensions. Powered by Rspack, React 19, Tailwind CSS v4, shadcn primitives, Zustand, and an extensible CLI.
- Overview & Highlights
- Architecture
- Quick Start
- Project Layout
- Configuration Reference
- Core Concepts & Guides
- CLI Reference
- Cross-Browser Compilation
- Testing & Quality Assurance
- Development Commands
- Contributing
- License
Developing browser extensions often involves navigating fragmented APIs across browsers, manual manifest.json synchronizations, complex multi-entry bundling configurations, and tedious cross-context state handling.
CRXKit solves these challenges by treating extension.config.json as the Single Source of Truth for the entire extension lifecycle:
- π Sub-Second Rspack Bundling: Rust-powered Rspack build engine with SWC loader, producing optimized builds in under 0.4s.
- π True Cross-Browser Support: Write once, compile to Chrome (MV3), Firefox (Gecko MV2/MV3), Safari, and Edge.
- π‘οΈ Zero-CSS-Leak Content Scripts: React 19 content scripts rendered inside an isolated Shadow DOM with Tailwind CSS v4 styling.
- β‘ End-to-End Type-Safe RPC: Bidirectional request/response passing with timeout safeguards and error propagation across Background, Content Scripts, Popup, and Side Panels.
- πΎ Reactive Multi-Area Storage: Unified
sync,local,session, andmanagedstorage with the reactiveuseExtensionStorageReact Hook. - π§© Complete MV3 Entity Coverage: Out-of-the-box support for
popup,side-panel,options,new-tab,devtools,offscreen,injected(main-world), anddeclarativeNetRequest. - π©Ί Built-in CLI & Compliance Doctor: Run
pnpm crx doctorfor static health checks, manifest validation, icon verification, and least-privilege permission auditing. - π§ͺ Enterprise Test Suite: Unit tests via Vitest with full
chrome.*mocks, plus real-browser extension sideload testing via Playwright.
flowchart TB
Config["π extension.config.json<br/>(Single Source of Truth)"]
subgraph Build["β‘ Rspack 2.1 Compiler"]
ManifestGen["π οΈ Manifest Generator<br/>(Target: Chrome / Firefox / Safari / Edge)"]
Entries["π¦ Multi-Entry Pipeline"]
end
subgraph Outputs["π dist/"]
ManifestOut["manifest.json"]
BG["background.js (Standalone SW)"]
CS["contentScript.js + contentScript.css (Shadow DOM)"]
Pages["popup.html Β· sidePanel.html Β· options.html Β· newTab.html"]
DevTools["devtools.html Β· offscreen.html"]
end
subgraph Runtime["π Shared Runtime & State"]
RPC["Type-Safe RPC Messaging (messaging.ts)"]
Storage["Reactive Storage Engine (useExtensionStore / useExtensionStorage)"]
Theme["Theme Sync & Tokens (Tailwind v4)"]
end
Config --> ManifestGen --> ManifestOut
Config --> Entries --> Outputs
Outputs -.-> Runtime
- Node.js >= 22.0.0
- Package Manager:
pnpm>= 9.0.0
git clone https://github.com/zh30/crxkit.git my-extension
cd my-extension
pnpm installpnpm devRspack will watch the source files and compile the extension artifacts into dist/.
- Chrome / Edge / Brave / Arc:
- Open
chrome://extensions(oredge://extensions). - Enable Developer mode in the top right corner.
- Click Load unpacked and select the
dist/directory.
- Open
- Firefox:
- Open
about:debugging#/runtime/this-firefox. - Click Load Temporary Add-on... and select
dist/manifest.json.
- Open
pnpm buildβββ extension.config.json # βοΈ Central extension configuration (Entries, Manifest, Settings)
βββ rspack.config.js # β‘ Rspack build config with SWC, CSS extraction & Manifest plugin
βββ schemas/ # π JSON Schema for extension.config.json validation
βββ bin/ & scripts/lib/ # π οΈ CRXKit CLI & Manifest generation utilities
βββ _locales/ # π Internationalization messages (en, zh_CN, etc.)
βββ public/ # πΌοΈ Static assets (Extension icons, fonts, images)
βββ src/
β βββ entries/ # πͺ MV3 Extension Entrypoints
β β βββ background/ # Background service worker (Side panel sync, tab tracking)
β β βββ content/ # React Content script (Mounted in isolated Shadow DOM)
β β βββ popup/ # Browser action toolbar popup UI
β β βββ side-panel/ # Chrome Side Panel UI with host automation
β β βββ options/ # Full-page extension options UI
β β βββ new-tab/ # New Tab override page
β βββ shared/ # π§© Cross-surface shared modules
β β βββ config/ # Typed runtime config from extension.config.json
β β βββ hooks/ # Custom hooks (useExtensionStorage, useThemeSync, useChromeManifest)
β β βββ platform/ # Safe platform wrappers (messaging, storage, offscreen, context-menus)
β β βββ state/ # Zustand store synced with chrome.storage
β β βββ ui/ # shadcn-inspired primitives (Button, Card, Input)
β β βββ lib/ # Utility helpers (cn, parseUrl, runtime checks)
β βββ styles/ # π¨ Tailwind CSS v4 design tokens and theme layers
β βββ __tests__/ # π§ͺ Vitest unit & component test suite
βββ tests/e2e/ # π Playwright real-browser extension E2E tests
All extension metadata, entries, and behavior are configured in extension.config.json:
{
"$schema": "./schemas/extension-config.schema.json",
"namespace": "crxkit",
"minimumChromeVersion": "114",
"defaultLocale": "en",
"manifest": {
"nameMessage": "extension_name",
"descriptionMessage": "extension_description",
"version": "0.1.2",
"icons": {
"16": "public/icon16.png",
"32": "public/icon32.png",
"48": "public/icon48.png",
"128": "public/icon128.png"
}
},
"permissions": ["storage", "activeTab", "tabs", "sidePanel"],
"hostPermissions": ["<all_urls>"],
"webAccessibleResources": ["public/*", "contentScript.css"],
"sidePanel": {
"autoOpenDefault": true,
"allowedHosts": ["localhost", "zhanghe.dev"]
},
"commands": {
"_execute_action": {
"suggested_key": { "default": "Ctrl+Shift+Y", "mac": "Command+Shift+Y" },
"description": "Toggle extension popup"
}
},
"entries": {
"popup": { "kind": "popup", "input": "src/entries/popup/main.tsx", "html": "src/entries/popup/index.html", "output": "popup.html" },
"sidePanel": { "kind": "side-panel", "input": "src/entries/side-panel/main.tsx", "html": "src/entries/side-panel/index.html", "output": "sidePanel.html" },
"options": { "kind": "options", "input": "src/entries/options/main.tsx", "html": "src/entries/options/index.html", "output": "options.html", "openInTab": true },
"newTab": { "kind": "new-tab", "input": "src/entries/new-tab/main.tsx", "html": "src/entries/new-tab/index.html", "output": "newTab.html" },
"background": { "kind": "background", "input": "src/entries/background/index.ts", "output": "background.js" },
"contentScript": { "kind": "content", "input": "src/entries/content/index.ts", "output": "contentScript.js", "css": "contentScript.css", "matches": ["<all_urls>"], "runAt": "document_idle" }
}
}Note
Do not edit dist/manifest.json directly. The manifest is dynamically generated and validated from extension.config.json during the build process.
CRXKit provides a robust, strongly typed messaging pipeline with built-in timeout rejection and exception forwarding:
import { sendMessage, addMessageListener } from '@/shared/platform/messaging';
// 1. Send message from Popup or Content Script to Background
const response = await sendMessage('crxkit:ping', { timestamp: Date.now() }, { timeoutMs: 5000 });
console.log('Pong received:', response.pong);
// 2. Register typed message listener in Background
const unsubscribe = addMessageListener('crxkit:ping', async (payload, sender) => {
return { pong: true, timestamp: payload?.timestamp ?? Date.now() };
});Sync state effortlessly across tabs, popup windows, and side panels using the useExtensionStorage React Hook:
import { useExtensionStorage } from '@/shared/hooks/useExtensionStorage';
export function UserSettings() {
const [apiKey, setApiKey, loading] = useExtensionStorage<string>('apiKey', '', 'sync');
if (loading) return <div>Loading...</div>;
return (
<input
type="text"
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
placeholder="Enter API Key"
/>
);
}To ensure extension styles never conflict with host webpage CSS (and vice versa), the content script mounts into a Shadow DOM:
// src/entries/content/index.ts
const host = document.createElement('div');
const shadow = host.attachShadow({ mode: 'open' });
const mount = document.createElement('div');
const stylesheet = document.createElement('link');
stylesheet.rel = 'stylesheet';
stylesheet.href = chrome.runtime.getURL('contentScript.css');
shadow.append(stylesheet, mount);
document.documentElement.appendChild(host);
const root = createRoot(mount);
root.render(<ContentApp themeTarget={mount} />);Manifest V3 service workers lack DOM access. CRXKit encapsulates chrome.offscreen management for audio, clipboard, or DOM parsing:
import { ensureOffscreenDocument, closeOffscreenDocument } from '@/shared/platform/offscreen';
// Ensure offscreen document is ready
await ensureOffscreenDocument({
path: 'offscreen.html',
reasons: ['DOM_PARSER', 'CLIPBOARD'],
justification: 'Parse web page content in background',
});Register right-click menus and global keyboard shortcuts with type safety:
import { contextMenuManager } from '@/shared/platform/context-menus';
import { registerCommandListener } from '@/shared/platform/commands';
// Context menu registration
contextMenuManager.register([
{
id: 'search-selected-text',
title: 'Search with CRXKit',
contexts: ['selection'],
onClick: (info, tab) => {
console.log('User selected:', info.selectionText);
},
},
]);
// Keyboard shortcut listener
registerCommandListener({
_execute_action: (command, tab) => {
console.log('Action shortcut triggered');
},
});CRXKit includes a developer CLI (pnpm crx):
| Command | Description |
|---|---|
pnpm crx doctor |
Run comprehensive health check (config, missing files, locales, permissions). |
pnpm crx validate |
Validate extension.config.json against JSON Schema and project files. |
pnpm crx manifest --print |
Preview the compiled Manifest V3 JSON output. |
pnpm crx manifest --target firefox --print |
Preview Firefox-targeted manifest with Gecko settings. |
pnpm crx entry add <name> --kind <kind> |
Scaffold a new entry (popup, side-panel, options, devtools, offscreen, injected, page). |
pnpm crx package --target <chrome|firefox> |
Build and create a production ZIP ready for Web Store upload. |
pnpm crx create <target-dir> |
Initialize a fresh CRXKit extension project in a target folder. |
CRXKit supports building targeted bundles for different browser ecosystems:
# Build for Chrome (Default)
pnpm build
# Build for Firefox (generates browser_specific_settings and background scripts)
EXTENSION_TARGET=firefox pnpm build
# Package directly for Firefox Add-ons (AMO)
pnpm crx package --target firefox --out CrxKit-Firefox.zip
# Package for Chrome Web Store
pnpm crx package --target chrome --out CrxKit-Chrome.zipCRXKit features a robust dual-layer testing pipeline:
# 1. TypeScript type check
pnpm typecheck
# 2. Biome linting and formatting check
pnpm check
# 3. Unit & Component tests with Vitest (12 test suites, 41+ tests)
pnpm test:unit
# 4. Playwright End-to-End tests in real Chromium browser
pnpm test:e2e
# 5. Full CI verification pipeline
pnpm test:ci| Script | Command | Purpose |
|---|---|---|
dev |
rspack build --watch |
Start incremental watch build mode |
build |
rspack build --mode production |
Output minified production bundle in dist/ |
typecheck |
tsc --noEmit |
Strict TypeScript validation |
check |
biome check . |
Fast lint, format, and import check |
format |
biome format --write . |
Automatically format all project files |
test:unit |
vitest run |
Run unit tests with Chrome mock environment |
test:e2e |
playwright test |
Run browser sideload E2E tests |
doctor |
node ./bin/crxkit.mjs doctor |
Check extension health and permission compliance |
package:zip |
node ./bin/crxkit.mjs package |
Build and package ZIP for store submission |
Contributions are welcome! Please follow these steps:
- Fork the repository and create a feature branch (
git checkout -b feature/amazing-feature). - Ensure all tests pass:
pnpm test:ci(orpnpm typecheck && pnpm check && pnpm test:unit). - Commit your changes following conventional commits format.
- Open a Pull Request.
Distributed under the MIT License. See LICENSE for more information.