Learn-skills.dev addfox-best-practices
git clone https://github.com/NeverSight/learn-skills.dev
T=$(mktemp -d) && git clone --depth=1 https://github.com/NeverSight/learn-skills.dev "$T" && mkdir -p ~/.claude/skills && cp -r "$T/data/skills-md/addfox/skills/addfox-best-practices" ~/.claude/skills/neversight-learn-skills-dev-addfox-best-practices && rm -rf "$T"
data/skills-md/addfox/skills/addfox-best-practices/SKILL.mdWhen to use
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):
- Config & manifest:
, manifest fields,addfox.config
,host_permissions
, Chrome vs Firefox manifest split,web_accessible_resources
,appDir
,outDir
,zip
,envPrefix
,hotReload
/ Rsdoctorreport - Entries:
,app/background
,app/content
,app/popup
,app/options
, customapp/sidepanel
, HTML templateentrydata-addfox-entry - Cross-browser:
,webextension-polyfill
API,browser.*
/addfox dev -baddfox build -b - UI on pages: floating panel, inject UI into page, Shadow DOM, iframe UI,
content UI helpers@addfox/utils - Styles: Tailwind v4
, UnoCSS, scoped CSS in content scripts@tailwindcss/postcss - Feature-heavy extensions (video, AI, downloads, etc.): use this skill together with
for domain patternsextension-functions-best-practices
Do 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).
How to use
Read this file for end-to-end patterns. For focused rules, open:
- rules/manifest-fields.md — manifest keys and Addfox-specific behavior
- rules/permissions.md — permission choices and least-privilege patterns
- rules/messaging.md — message shapes, async responses, tab vs runtime messaging
- rules/content-ui.md —
, iframe UI, mount timingdefineShadowContentUI - reference.md — CLI, config table, cross-platform notes
Related skills: addfox-debugging, addfox-testing, migrate-to-addfox, extension-functions-best-practices.
Addfox Best Practices
Covers entry discovery, config, manifest, permissions, cross-platform APIs, UI frameworks, styles, messaging, and content UI.
1. Entry
File-based Entry (Recommended)
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.
Reserved Entry Names
| Entry | Output Path | HTML Generated | Notes |
|---|---|---|---|
| | ❌ | Service worker (MV3) or background page (MV2) |
| | ❌ | Content script(s) |
| | ✅ | Toolbar popup |
| | ✅ | Extension options page |
| | ✅ | Chrome side panel |
| | ✅ | DevTools extension |
| | ✅ | Offscreen document for DOM/audio in MV3 |
| | ✅ | Sandbox page |
| | ✅ | New Tab override |
| | ✅ | Bookmarks override |
| | ✅ | History override |
Custom Entries
Use the config
field for non–built-in entries:entry
// 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>'] }] } });
HTML Templates
For popup/options/sidepanel/devtools/offscreen, either:
- Omit HTML — framework generates it automatically
- Provide template with entry marker:
<!-- 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.
2. addfox.config and manifest
Config File
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) } });
Config Fields Reference
| Field | Type | Description |
|---|---|---|
| | Extension manifest. Can be single object or split by browser. |
| | Rsbuild plugins array. Use , , etc. |
| | Override/extend Rsbuild config. Object: deep-merged. Function: . |
| | Custom entries. Key = entry name, value = path relative to appDir. |
| | App directory; default . |
| | Output directory under ; default . |
| | Browser executable paths for dev mode. |
| | Whether to create zip output; default . |
| | Env var prefixes to expose; default exposes all. Use for safety. |
| | Cache chromium user data dir; default . |
| | Hot reload options: |
| | Enable error monitor in dev; default . |
| | Enable Rsdoctor report; default . |
Manifest Configuration
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: 'myextension@example.com' } } } }
Dependencies
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.
3. Permissions
Least Privilege Principle
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) ] };
Permission Patterns by Feature
| Feature | Minimal Permissions | Optional |
|---|---|---|
| Basic popup | | |
| Content script | or specific | |
| Video download | , / | |
| AI sidebar | , , | |
| Screenshot | | , |
| Password manager | , , | |
| Web3 wallet | , , | |
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.
4. Cross-platform (Chrome / Firefox)
4.1 webextension-polyfill(强烈推荐)
强烈推荐在 Addfox 项目中使用
来实现跨浏览器兼容。webextension-polyfill
为什么使用 polyfill
- 统一的 Promise API: Chrome 的
API 使用回调,而 Firefox 的chrome.*
支持 Promise;polyfill 让 Chrome 也支持 Promisebrowser.* - 一致的命名空间: 统一使用
命名空间,代码在不同浏览器中表现一致browser.* - 更好的类型支持: 配合
获得完整的 TypeScript 类型@types/webextension-polyfill
安装
# 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();
配置 tsconfig.json
确保 TypeScript 能正确识别
webextension-polyfill 的类型:
{ "compilerOptions": { "types": ["webextension-polyfill"] } }
4.2 Manifest 差异处理
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: 'myextension@example.com', 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.
5. UI frameworks
Supported Frameworks
| Framework | Plugin Package | Install Command |
|---|---|---|
| React | | |
| Vue 3 | | |
| Preact | | |
| Svelte | | |
| Solid | | |
| Vanilla | None | No plugin needed |
Setup Example (React)
// 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" } }
Setup Example (Vue)
// 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).
6. Styles
Tailwind CSS v4
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';
Tailwind CSS v3 (Legacy)
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: [], };
UnoCSS
pnpm add -D unocss @unocss/postcss
// postcss.config.mjs export default { plugins: { '@unocss/postcss': {}, }, };
Sass/Less
Rsbuild has built-in support. Just install and use:
pnpm add -D sass
/* app/popup/index.scss */ $primary: #3b82f6; .button { background: $primary; }
CSS Scoping
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.
7. Messaging
Include Message Origin
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.
Message Types by Context
| From | To | API | Example |
|---|---|---|---|
| Popup | Background | | Get settings |
| Popup | Content | | Page interaction |
| Content | Background | | Report event |
| Background | Content | | Inject script |
| Background | Popup | Not directly possible | Use storage or message passing via content |
Content ↔ Page Script
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.
8. Content UI (injecting DOM in pages)
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
instead of manually creating shadow roots or iframes.@addfox/utils
Three Isolation Levels
| Method | Wrapper | Isolation | Use When |
|---|---|---|---|
| Shadow DOM | Style only | Style isolation, single mount root |
| iframe | Full (JS+CSS) | Full isolation from page |
| Plain element | None | No isolation needed |
Usage
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' } });
When to Call Mount
Ensure the target exists:
function mountUI() { const root = mountShadow(); createRoot(root).render(<App />); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', mountUI); } else { mountUI(); }
Manifest and CSS
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.
9. Common Feature Implementations
When implementing specific extension features, combine Addfox best practices with the extension-functions-best-practices skill:
| Feature Category | Addfox Setup | See Also |
|---|---|---|
| Video Enhancement | entry with Shadow UI | extension-functions-best-practices (Video) |
| Video Download | + | extension-functions-best-practices (Video) |
| AI Sidebar | entry | extension-functions-best-practices (AI) |
| Page Translation | entry | extension-functions-best-practices (Translation) |
| Screenshot | permission | extension-functions-best-practices (Image) |
| Password Manager | + | extension-functions-best-practices (Password Manager) |
| Web3 Wallet | + | extension-functions-best-practices (Web3) |
Example: Building an AI sidebar extension:
- Use Addfox for
entry setupsidepanel - Use extension-functions-best-practices for AI SDK integration (Vercel AI SDK, LangChain)
10. Rsbuild Configuration (Advanced)
Use the
rsbuild field to override or extend Rsbuild configuration:
Object Form (Deep Merge)
export default defineConfig({ rsbuild: { source: { alias: { '@': './app', }, }, output: { copy: [ { from: './public', to: '.' } ] } } });
Function Form (Full Control)
export default defineConfig({ rsbuild: (base, helpers) => { return helpers.merge(base, { source: { define: { 'process.env.API_URL': JSON.stringify('https://api.example.com') } } }); } });
Common Rsbuild Options
| Option | Description |
|---|---|
| Path aliases |
| Global constants |
| Static asset copying |
| Asset URL prefix |
| Dev server port |
| HMR configuration |
Checklist (when generating or modifying extension)
- Entry: file-based under
with reserved names, or explicitapp/
in config.entry - Config: use
(notrsbuild
) for Rsbuild overrides.rsbuildConfig - Manifest: use
for browser split.{ chromium: {...}, firefox: {...} } - Plugins: use
or@rsbuild/plugin-*
packages.@addfox/rsbuild-plugin-* - Permissions: minimal set; sensitive ones documented.
- Cross-browser: use
withwebextension-polyfill
API.browser.* - Messaging: payload includes
for routing/security.from - Content UI: use
/defineShadowContentUI
/defineIframeContentUI
fromdefineContentUI
.@addfox/utils - Styles: Tailwind v4 uses
in@tailwindcss/postcss
.postcss.config.mjs - Feature implementation: reference extension-functions-best-practices for video/AI/etc.
Additional resources
- Detailed config, CLI, and manifest: reference.md.
- Manifest field usage: rules/manifest-fields.md.
- Permissions by scenario: rules/permissions.md.
- Messaging patterns: rules/messaging.md.
- Content UI: rules/content-ui.md.
- Debugging: addfox-debugging skill.
- Testing: addfox-testing skill.
- Feature implementations: extension-functions-best-practices skill.