Addfox + Rsbuild for Manifest V3 browser extensions. addfox.config.ts, manifest (chromium/firefox split), file-based app/ entries (background service worker, content, popup, options, sidepanel, devtools, offscreen), permissions and host_permissions, webextension-polyfill, UI frameworks (React Vue Preact Svelte Solid), Tailwind v4 UnoCSS Sass Less, runtime messaging with a `from` field, content page UI via defineShadowContentUI / defineIframeContentUI / defineContentUI from @addfox/utils, addfox dev/build CLI.
Use this skill whenever the project depends on Addfox (addfox in package.json, addfox.config.ts, defineConfig from addfox) or the user asks about building or configuring extensions with Addfox, Rsbuild plugin setup, .addfox/extension output, or MV3 extension architecture.
Also load it when the user mentions any of the following (including synonyms):
addfox.config, manifest fields, host_permissions, web_accessible_resources, Chrome vs Firefox manifest split, appDir, outDir, zip, envPrefix, hotReload, report / Rsdoctorapp/background, app/content, app/popup, app/options, app/sidepanel, custom entry, HTML template data-addfox-entrywebextension-polyfillbrowser.*addfox dev -baddfox build -b@addfox/utils content UI helpers@tailwindcss/postcss, UnoCSS, scoped CSS in content scriptsextension-functions-best-practices for domain patternsDo not use this skill for pure migration from WXT/Plasmo (use migrate-to-addfox), for test setup (use addfox-testing), or for build/runtime failures (use addfox-debugging first if the user pasted errors).
Read this file for end-to-end patterns. For focused rules, open:
defineShadowContentUI, iframe UI, mount timingRelated skills: addfox-debugging, addfox-testing, migrate-to-addfox, extension-functions-best-practices.
Covers entry discovery, config, manifest, permissions, cross-platform APIs, UI frameworks, styles, messaging, and content UI.
Do not set entry in config when possible. Use default appDir: "app" and place scripts under reserved directories:
app/
├── background/ → Service worker
│ └── index.ts
├── content/ → Content script
│ └── index.ts
├── popup/ → Popup page
│ └── index.tsx
├── options/ → Options page
│ └── index.tsx
├── sidepanel/ → Side panel (Chrome)
│ └── index.tsx
├── devtools/ → DevTools page
│ └── index.tsx
├── offscreen/ → Offscreen document (MV3)
│ └── index.tsx
├── newtab/ → New Tab page
│ └── index.tsx
└── history/ → History page
└── index.tsx
The framework discovers reserved names by directory; do not write source file paths for built-in entries in the manifest — the framework fills output paths automatically.
| Entry | Output Path | HTML Generated | Notes |
|---|---|---|---|
background | background/index.js | ❌ | Service worker (MV3) or background page (MV2) |
content | content/index.js | ❌ | Content script(s) |
popup | popup/index.html | ✅ | Toolbar popup |
options | options/index.html | ✅ | Extension options page |
sidepanel | sidepanel/index.html | ✅ | Chrome side panel |
devtools | devtools/index.html | ✅ | DevTools extension |
offscreen | offscreen/index.html | ✅ | Offscreen document for DOM/audio in MV3 |
sandbox | sandbox/index.html | ✅ | Sandbox page |
newtab | newtab/index.html | ✅ | New Tab override |
bookmarks | bookmarks/index.html | ✅ | Bookmarks override |
history | history/index.html | ✅ | History override |
Use the config entry field for non–built-in entries:
// addfox.config.ts
export default defineConfig({
entry: {
capture: 'capture/index.ts',
worker: 'workers/custom.ts'
},
manifest: {
// Reference by output path
web_accessible_resources: [{
resources: ['capture/index.html'],
matches: ['<all_urls>']
}]
}
});
For popup/options/sidepanel/devtools/offscreen, either:
<!-- app/popup/index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Popup</title>
</head>
<body>
<div id="root"></div>
<!-- This marker tells Addfox where to inject -->
<script data-addfox-entry src="./index.tsx"></script>
</body>
</html>
Details: reference.md.
addfox.config.ts (or .js/.mjs) at project root:
import { defineConfig } from 'addfox';
import { pluginReact } from '@rsbuild/plugin-react';
const manifest = {
manifest_version: 3,
name: 'My Extension',
version: '1.0.0',
description: 'Extension description',
permissions: ['storage', 'activeTab'],
host_permissions: ['<all_urls>'],
action: {
default_popup: 'popup/index.html'
},
options_ui: {
open_in_tab: true
},
content_scripts: [{
matches: ['<all_urls>']
}]
};
export default defineConfig({
// App directory (default: "app")
appDir: 'app',
// Output directory name under .addfox (default: "extension")
outDir: 'extension',
// Manifest configuration - supports chromium/firefox split
manifest: {
chromium: manifest,
firefox: { ...manifest }
},
// Rsbuild plugins (from @rsbuild/plugin-* or @addfox/rsbuild-plugin-*)
plugins: [pluginReact()],
// Override/extend Rsbuild config (optional)
rsbuild: {
// Rsbuild configuration object
// Or function: (base, helpers) => helpers.merge(base, overrides)
}
});
| Field | Type | Description |
|---|---|---|
manifest | object | { chromium, firefox } | Extension manifest. Can be single object or split by browser. |
plugins | Array<RsbuildPlugin> | Rsbuild plugins array. Use @rsbuild/plugin-react, @addfox/rsbuild-plugin-vue, etc. |
rsbuild | object | function | Override/extend Rsbuild config. Object: deep-merged. Function: (base, helpers) => config. |
entry | object | Custom entries. Key = entry name, value = path relative to appDir. |
appDir | string | App directory; default "app". |
outDir | string | Output directory under .addfox; default "extension". |
browserPath | object | Browser executable paths for dev mode. { chrome, firefox, edge, ... } |
zip | boolean | Whether to create zip output; default true. |
envPrefix | string[] | Env var prefixes to expose; default [''] exposes all. Use ['PUBLIC_'] for safety. |
cache | boolean | Cache chromium user data dir; default true. |
hotReload | object | boolean | Hot reload options: { port?: number, autoRefreshContentPage?: boolean } |
debug | boolean | Enable error monitor in dev; default false. |
report | boolean | object | Enable Rsdoctor report; default false. |
The manifest field accepts:
Single object — Used for all browsers:
manifest: {
manifest_version: 3,
name: 'My Extension',
version: '1.0.0'
}
Browser split — Different manifests for Chrome and Firefox:
const baseManifest = {
manifest_version: 3,
name: 'My Extension',
version: '1.0.0'
};
manifest: {
chromium: {
...baseManifest,
minimum_chrome_version: '88'
},
firefox: {
...baseManifest,
browser_specific_settings: {
gecko: { id: '[email protected]' }
}
}
}
Install addfox as dev dependency:
# pnpm (recommended)
pnpm add -D addfox
# npm
npm install -D addfox
# yarn
yarn add -D addfox
强烈推荐: Add webextension-polyfill for cross-browser Promise-based API:
pnpm add webextension-polyfill
pnpm add -D @types/webextension-polyfill
Manifest field reference: rules/manifest-fields.md. Permission guidance: rules/permissions.md.
Request only permissions needed for declared features:
const manifest = {
permissions: [
'storage', // Settings/caching
'activeTab', // Current tab on user gesture
'scripting', // Inject scripts/CSS
'alarms', // Scheduled tasks
'notifications', // Desktop notifications
'contextMenus', // Right-click menu
'offscreen' // MV3 offscreen documents
],
host_permissions: [
'*://*.example.com/*' // Specific sites only
],
optional_permissions: [
'tabs', // Request at runtime if needed
'<all_urls>' // Broad access (optional)
]
};
| Feature | Minimal Permissions | Optional |
|---|---|---|
| Basic popup | storage | activeTab |
| Content script | activeTab or specific host_permissions | scripting |
| Video download | downloads, webRequest/declarativeNetRequest | tabs |
| AI sidebar | sidePanel, storage, activeTab | scripting |
| Screenshot | activeTab | downloads, clipboardWrite |
| Password manager | storage, scripting, activeTab | identity |
| Web3 wallet | storage, scripting, activeTab | alarms |
Note: For detailed implementation of specific features (video, AI, etc.), see the extension-functions-best-practices skill.
Document sensitive permissions (e.g. <all_urls>, tabs) in store listing and privacy policy.
See rules/permissions.md for scenarios and recommendations.
强烈推荐在 Addfox 项目中使用 webextension-polyfill 来实现跨浏览器兼容。
chrome.* API 使用回调,而 Firefox 的 browser.* 支持 Promise;polyfill 让 Chrome 也支持 Promisebrowser.* 命名空间,代码在不同浏览器中表现一致@types/webextension-polyfill 获得完整的 TypeScript 类型# npm
npm install webextension-polyfill
npm install -D @types/webextension-polyfill
# pnpm
pnpm add webextension-polyfill
pnpm add -D @types/webextension-polyfill
# yarn
yarn add webextension-polyfill
yarn add -D @types/webextension-polyfill
app/background/index.ts
import browser from 'webextension-polyfill';
// 使用 Promise API(在 Chrome 和 Firefox 中都可用)
browser.runtime.onInstalled.addListener(async (details) => {
if (details.reason === 'install') {
console.log('Extension installed!');
await browser.storage.local.set({ installedAt: Date.now() });
}
});
// 消息处理
browser.runtime.onMessage.addListener(async (message, sender) => {
if (message.from === 'popup') {
return { received: true, timestamp: Date.now() };
}
});
// 使用 browser.action (Chrome) / browser.browserAction (Firefox 自动映射)
browser.action.onClicked.addListener(async (tab) => {
await browser.scripting.executeScript({
target: { tabId: tab.id! },
func: () => alert('Hello from Addfox!'),
});
});
app/popup/index.tsx
import browser from 'webextension-polyfill';
async function init() {
// 获取当前活动标签页
const [tab] = await browser.tabs.query({ active: true, currentWindow: true });
// 发送消息到 content script
const response = await browser.tabs.sendMessage(tab.id!, {
type: 'GET_PAGE_INFO',
from: 'popup'
});
document.getElementById('title')!.textContent = response.title;
}
init();
app/content/index.ts
import browser from 'webextension-polyfill';
// 监听来自 background/popup 的消息
browser.runtime.onMessage.addListener((message, sender) => {
if (message.type === 'GET_PAGE_INFO' && message.from === 'popup') {
return Promise.resolve({
title: document.title,
url: location.href,
h1: document.querySelector('h1')?.textContent
});
}
});
// 使用 storage API
async function saveData(key: string, value: any) {
await browser.storage.local.set({ [key]: value });
const result = await browser.storage.local.get(key);
console.log('Saved:', result);
}
app/options/index.tsx
import browser from 'webextension-polyfill';
const form = document.getElementById('options-form') as HTMLFormElement;
// 加载保存的设置
async function loadSettings() {
const { apiKey, theme } = await browser.storage.sync.get(['apiKey', 'theme']);
(form.elements.namedItem('apiKey') as HTMLInputElement).value = apiKey || '';
(form.elements.namedItem('theme') as HTMLSelectElement).value = theme || 'light';
}
// 保存设置
form.addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(form);
await browser.storage.sync.set({
apiKey: formData.get('apiKey'),
theme: formData.get('theme')
});
// 显示保存成功提示
await browser.notifications.create({
type: 'basic',
iconUrl: browser.runtime.getURL('icon.png'),
title: '设置已保存',
message: '您的首选项已更新'
});
});
loadSettings();
确保 TypeScript 能正确识别 webextension-polyfill 的类型:
{
"compilerOptions": {
"types": ["webextension-polyfill"]
}
}
Use manifest: { chromium: {...}, firefox: {...} } when fields differ:
const baseManifest = {
manifest_version: 3,
name: 'My Extension',
version: '1.0.0'
};
export default defineConfig({
manifest: {
chromium: {
...baseManifest,
minimum_chrome_version: '88'
},
firefox: {
...baseManifest,
browser_specific_settings: {
gecko: {
id: '[email protected]',
strict_min_version: '109.0'
}
}
}
}
});
Build for specific target:
addfox dev -b chrome # or chromium, firefox, edge, brave
addfox build -b firefox # Firefox-specific build
See reference.md.
| Framework | Plugin Package | Install Command |
|---|---|---|
| React | @rsbuild/plugin-react | pnpm add -D @rsbuild/plugin-react |
| Vue 3 | @addfox/rsbuild-plugin-vue | pnpm add -D @addfox/rsbuild-plugin-vue |
| Preact | @rsbuild/plugin-preact | pnpm add -D @rsbuild/plugin-preact |
| Svelte | @rsbuild/plugin-svelte | pnpm add -D @rsbuild/plugin-svelte |
| Solid | @rsbuild/plugin-solid | pnpm add -D @rsbuild/plugin-solid |
| Vanilla | None | No plugin needed |
// addfox.config.ts
import { defineConfig } from 'addfox';
import { pluginReact } from '@rsbuild/plugin-react';
export default defineConfig({
plugins: [pluginReact()],
manifest: { ... }
});
// package.json
{
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@rsbuild/plugin-react": "^1.2.0"
}
}
// addfox.config.ts
import { defineConfig } from 'addfox';
import vue from '@addfox/rsbuild-plugin-vue';
export default defineConfig({
plugins: [vue()],
manifest: { ... }
});
Entry files remain JS/TS/JSX/TSX/Vue (e.g. app/popup/index.tsx).
Addfox uses Tailwind CSS v4 with PostCSS:
pnpm add -D tailwindcss @tailwindcss/postcss postcss
// postcss.config.mjs
export default {
plugins: {
'@tailwindcss/postcss': {},
},
};
Import Tailwind in your CSS:
/* app/popup/index.css */
@import 'tailwindcss';
// app/popup/index.tsx
import './index.css';
For Tailwind v3, use traditional PostCSS configuration:
pnpm add -D tailwindcss postcss autoprefixer
// postcss.config.mjs
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
// tailwind.config.js
export default {
content: ['./app/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {},
},
plugins: [],
};
pnpm add -D unocss @unocss/postcss
// postcss.config.mjs
export default {
plugins: {
'@unocss/postcss': {},
},
};
Rsbuild has built-in support. Just install and use:
pnpm add -D sass
/* app/popup/index.scss */
$primary: #3b82f6;
.button { background: $primary; }
Prefer scoped styles or BEM/utility classes to avoid leaking into page:
/* Use prefix for content scripts */
.my-extension-popup { ... }
.my-extension-content { ... }
Content scripts should inject minimal, prefixed CSS when not using Shadow DOM or iframe isolation.
Always include a from (or equivalent) field in message payloads:
// popup sending
browser.runtime.sendMessage({
from: 'popup',
action: 'getSettings',
payload: {}
});
// background receiving
browser.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg.from === 'popup' && msg.action === 'getSettings') {
// handle
}
});
Senders can be: "background", "content", "popup", "options", or custom IDs.
| From | To | API | Example |
|---|---|---|---|
| Popup | Background | browser.runtime.sendMessage | Get settings |
| Popup | Content | browser.tabs.sendMessage | Page interaction |
| Content | Background | browser.runtime.sendMessage | Report event |
| Background | Content | browser.tabs.sendMessage | Inject script |
| Background | Popup | Not directly possible | Use storage or message passing via content |
Use window.postMessage + custom events only for content ↔ page script when needed, and validate origin:
// Content script
window.postMessage({
from: 'extension-content',
type: 'REQUEST_DATA'
}, '*');
// Listen from page
window.addEventListener('message', (e) => {
if (e.origin !== location.origin) return;
if (e.data.from === 'page-script') {
// handle
}
});
See rules/messaging.md for detailed patterns.
When the user needs to create content UI or inject DOM into web pages from a content script, use Addfox's built-in helpers from @addfox/utils instead of manually creating shadow roots or iframes.
| Method | Wrapper | Isolation | Use When |
|---|---|---|---|
defineShadowContentUI | Shadow DOM | Style only | Style isolation, single mount root |
defineIframeContentUI | iframe | Full (JS+CSS) | Full isolation from page |
defineContentUI | Plain element | None | No isolation needed |
import { defineShadowContentUI, defineIframeContentUI, defineContentUI } from '@addfox/utils';
// Shadow DOM - most common
const mountShadow = defineShadowContentUI({
name: 'my-content-ui', // Must contain hyphen
target: 'body',
attr: {
id: 'my-root',
style: 'position:fixed;bottom:16px;right:16px;z-index:2147483647;'
},
injectMode: 'append'
});
// React example
const root = mountShadow();
createRoot(root).render(<MyComponent />);
// iframe - full isolation
const mountIframe = defineIframeContentUI({
target: 'body',
attr: { class: 'my-iframe-ui' }
});
// Plain element - no isolation
const mountPlain = defineContentUI({
tag: 'div',
target: 'body',
attr: { id: 'content-ui-root' }
});
Ensure the target exists:
function mountUI() {
const root = mountShadow();
createRoot(root).render(<App />);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', mountUI);
} else {
mountUI();
}
When using defineShadowContentUI or defineIframeContentUI, the framework may not auto-fill content_scripts.css in the manifest (CSS is injected at runtime). Import your styles in the content entry:
// app/content/index.ts
import './styles.css'; // Bundled and injected automatically
import { defineShadowContentUI } from '@addfox/utils';
See rules/content-ui.md for API details and examples.
When implementing specific extension features, combine Addfox best practices with the extension-functions-best-practices skill:
| Feature Category | Addfox Setup | See Also |
|---|---|---|
| Video Enhancement | content entry with Shadow UI | extension-functions-best-practices (Video) |
| Video Download | background + webRequest | extension-functions-best-practices (Video) |
| AI Sidebar | sidepanel entry | extension-functions-best-practices (AI) |
| Page Translation | content entry | extension-functions-best-practices (Translation) |
| Screenshot | activeTab permission | extension-functions-best-practices (Image) |
| Password Manager | storage + scripting | extension-functions-best-practices (Password Manager) |
| Web3 Wallet | content + offscreen | extension-functions-best-practices (Web3) |
Example: Building an AI sidebar extension:
sidepanel entry setupUse the rsbuild field to override or extend Rsbuild configuration:
export default defineConfig({
rsbuild: {
source: {
alias: {
'@': './app',
},
},
output: {
copy: [
{ from: './public', to: '.' }
]
}
}
});
export default defineConfig({
rsbuild: (base, helpers) => {
return helpers.merge(base, {
source: {
define: {
'process.env.API_URL': JSON.stringify('https://api.example.com')
}
}
});
}
});
| Option | Description |
|---|---|
source.alias | Path aliases |
source.define | Global constants |
output.copy | Static asset copying |
output.assetPrefix | Asset URL prefix |
server.port | Dev server port |
dev.hmr | HMR configuration |
app/ with reserved names, or explicit entry in config.rsbuild (not rsbuildConfig) for Rsbuild overrides.{ chromium: {...}, firefox: {...} } for browser split.@rsbuild/plugin-* or @addfox/rsbuild-plugin-* packages.webextension-polyfill with browser.* API.from for routing/security.defineShadowContentUI / defineIframeContentUI / defineContentUI from @addfox/utils.@tailwindcss/postcss in postcss.config.mjs.