chrome-extension-wxt
Build Chrome extensions using WXT framework with TypeScript, React, Vue, or Svelte. Use when creating browser extensions, developing cross-browser add-ons, or working with Chrome Web Store projects. Triggers on phrases like "chrome extension", "browser extension", "WXT framework"
Install
npx skills add https://github.com/tenequm/skills/tree/main/skills/chrome-extension-wxt
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install tenequm-skills@llmmart
git clone https://github.com/tenequm/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole tenequm/skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Chrome Extension Development with WXT
Build modern, cross-browser extensions using WXT - the next-generation framework that supports Chrome, Firefox, Edge, Safari, and all Chromium browsers with a single codebase.
When to Use This Skill
Use this skill when:
- Creating a new Chrome/browser extension
- Setting up WXT development environment
- Building extension features (popup, content scripts, background scripts)
- Implementing cross-browser compatibility
- Working with Manifest V3 (mandatory standard as of 2025, V2 deprecated)
- Integrating React 19, Vue, Svelte, or Solid with extensions
Quick Start Workflow
1. Initialize WXT Project
# Create new project with framework of choice
npm create wxt@latest
# Or with specific template
npm create wxt@latest -- --template react-ts
npm create wxt@latest -- --template vue-ts
npm create wxt@latest -- --template svelte-ts
2. Project Structure
WXT uses file-based conventions:
project/
├── entrypoints/ # Auto-discovered entry points
│ ├── background.ts # Service worker
│ ├── content.ts # Content script
│ ├── popup.html # Popup UI
│ └── options.html # Options page
├── components/ # Auto-imported UI components
├── utils/ # Auto-imported utilities
├── public/ # Static assets
│ └── icon/ # Extension icons
├── wxt.config.ts # Configuration
└── package.json
3. Development Commands
npm run dev # Start dev server with HMR
npm run build # Production build
npm run zip # Package for store submission
Core Entry Points
WXT recognizes entry points by filename in entrypoints/ directory:
Background Script (Service Worker)
// entrypoints/background.ts
export default defineBackground({
type: 'module',
persistent: false,
main() {
// Listen for extension events
browser.action.onClicked.addListener((tab) => {
console.log('Extension clicked', tab);
});
// Handle messages
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
// Handle message
sendResponse({ success: true });
return true; // Keep channel open for async
});
},
});
Content Script
// entrypoints/content.ts
export default defineContentScript({
matches: ['*://*.example.com/*'],
runAt: 'document_end',
main(ctx) {
// Content script logic
console.log('Content script loaded');
// Create UI
const ui = createShadowRootUi(ctx, {
name: 'my-extension-ui',
position: 'inline',
anchor: 'body',
onMount(container) {
// Mount React/Vue component
const root = ReactDOM.createRoot(container);
root.render(<App />);
},
});
ui.mount();
},
});
Popup UI
// entrypoints/popup/main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
<!-- entrypoints/popup/index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Extension Popup</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="./main.tsx"></script>
</body>
</html>
Configuration
Basic wxt.config.ts
import { defineConfig } from 'wxt';
export default defineConfig({
// Framework integration
modules: ['@wxt-dev/module-react'],
// Manifest configuration
manifest: {
name: 'My Extension',
description: 'Extension description',
permissions: ['storage', 'activeTab'],
host_permissions: ['*://example.com/*'],
},
// Browser target
browser: 'chrome', // or 'firefox', 'edge', 'safari'
});
Common Patterns
Type-Safe Storage
// utils/storage.ts
import { storage } from 'wxt/storage';
export const storageHelper = {
async get<T>(key: string): Promise<T | null> {
return await storage.getItem<T>(`local:${key}`);
},
async set<T>(key: string, value: T): Promise<void> {
await storage.setItem(`local:${key}`, value);
},
watch<T>(key: string, callback: (newValue: T | null) => void) {
return storage.watch<T>(`local:${key}`, callback);
},
};
Type-Safe Messaging
// utils/messaging.ts
interface Messages {
'get-data': {
request: { key: string };
response: { value: any };
};
}
export async function sendMessage<K extends keyof Messages>(
type: K,
payload: Messages[K]['request']
): Promise<Messages[K]['response']> {
return await browser.runtime.sendMessage({ type, payload });
}
Script Injection
// Inject script into page context
import { injectScript } from 'wxt/client';
await injectScript('/injected.js', {
keepInDom: false,
});
Building & Deployment
Production Build
# Build for specific browser
npm run build -- --browser=chrome
npm run build -- --browser=firefox
# Create store-ready ZIP
npm run zip
npm run zip -- --browser=firefox
Multi-Browser Build
# Build for all browsers
npm run zip:all
Output: .output/my-extension-{version}-{browser}.zip
Modern Stacks (2025)
Popular technology combinations for building Chrome extensions:
WXT + React + Tailwind + shadcn/ui
Most popular stack in 2025. Combines utility-first styling with pre-built accessible components.
npm create wxt@latest -- --template react-ts
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
npx shadcn@latest init
Best for: Modern UIs with consistent design system Example: https://github.com/imtiger/wxt-react-shadcn-tailwindcss-chrome-extension
WXT + React + Mantine UI
Complete component library with 100+ components and built-in dark mode.
npm create wxt@latest -- --template react-ts
npm install @mantine/core @mantine/hooks
Best for: Feature-rich extensions needing complex components Example: https://github.com/ongkay/WXT-Mantine-Tailwind-Browser-Extension
WXT + React + TypeScript (Minimal)
Clean setup for custom designs without UI library dependencies.
npm create wxt@latest -- --template react-ts
Best for: Simple extensions or highly custom designs
Advanced Topics
For detailed information on advanced topics, see the reference files:
- React Integration: See
references/react-integration.mdfor complete React setup, hooks, state management, and popular UI libraries - Chrome APIs: See
references/chrome-api.mdfor comprehensive Chrome Extension API reference with examples - Chrome 140+ Features: See
references/chrome-140-features.mdfor latest Chrome Extension APIs (sidePanel.getLayout(), etc.) - WXT API: See
references/wxt-api.mdfor complete WXT framework API documentation - Best Practices: See
references/best-practices.mdfor security, performance, and architecture patterns
Troubleshooting
Common issues and solutions:
- Module not found errors: Ensure modules are installed and properly imported
- CSP violations: Update
content_security_policyin manifest - Hot reload not working: Check browser console for errors
- Storage not persisting: Use
storage.localorstorage.synccorrectly
For deeper guidance on avoiding these issues, see references/best-practices.md.
Resources
Official Documentation
- WXT Docs: https://wxt.dev
- Chrome Extension Docs: https://developer.chrome.com/docs/extensions
- Firefox Extension Docs: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons
Bundled Resources
- references/: Detailed documentation for advanced features
Use these resources as needed when building your extension.
Files (skills)
-
references
-
best-practices.md 7.8 KB
# Chrome Extension Best Practices with WXT Security, performance, and architecture recommendations. ## Security ### Content Security Policy Always configure CSP for extension pages: ```typescript export default defineConfig({ manifest: { content_security_policy: { extension_pages: "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'", }, }, }); ``` ### Minimal Permissions Request only necessary permissions: ```typescript // Good - specific permissions permissions: ['storage', 'activeTab'] // Bad - excessive permissions permissions: ['<all_urls>', 'tabs', 'history', 'bookmarks'] ``` Use `optional_permissions` for features that might not be needed: ```typescript manifest: { permissions: ['storage'], optional_permissions: ['tabs', 'bookmarks'], } ``` ### Input Validation Always sanitize user input: ```typescript function sanitizeInput(input: string): string { return input .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } ``` Use DOMPurify for HTML content: ```typescript import DOMPurify from 'dompurify'; const clean = DOMPurify.sanitize(html, { ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'], ALLOWED_ATTR: ['href'], }); ``` ### Secure API Calls Never hardcode API keys: ```typescript // Store in browser.storage, not in code const { apiKey } = await browser.storage.local.get('apiKey'); const response = await fetch(url, { headers: { 'Authorization': `Bearer ${apiKey}`, }, }); ``` ## Performance ### Service Worker Optimization Keep service worker lightweight: ```typescript export default defineBackground({ main() { // Use alarms for long delays browser.alarms.create('daily-sync', { periodInMinutes: 1440, }); // Unregister listeners when not needed let listener: any; function enable() { listener = (msg: any) => handleMessage(msg); browser.runtime.onMessage.addListener(listener); } function disable() { if (listener) { browser.runtime.onMessage.removeListener(listener); listener = null; } } }, }); ``` ### Lazy Loading Load heavy dependencies only when needed: ```typescript export default defineContentScript({ matches: ['*://*.example.com/*'], async main(ctx) { // Wait for user interaction document.querySelector('#button')?.addEventListener('click', async () => { // Lazy load React const React = await import('react'); const ReactDOM = await import('react-dom/client'); const { App } = await import('./components/App'); const root = ReactDOM.createRoot(document.getElementById('root')!); root.render(React.createElement(App)); }); }, }); ``` ### Bundle Splitting Configure Vite for optimal chunks: ```typescript export default defineConfig({ vite: () => ({ build: { rollupOptions: { output: { manualChunks: { vendor: ['react', 'react-dom'], utils: ['date-fns', 'lodash-es'], }, }, }, }, }), }); ``` ### Caching Strategy Cache API responses appropriately: ```typescript const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes async function getCachedData(key: string) { const cached = await storage.getItem<{data: any, timestamp: number}>(`cache:${key}`); if (cached && Date.now() - cached.timestamp < CACHE_DURATION) { return cached.data; } const freshData = await fetchData(key); await storage.setItem(`cache:${key}`, { data: freshData, timestamp: Date.now(), }); return freshData; } ``` ## Architecture ### File Organization ``` src/ ├── entrypoints/ │ ├── background/ # Complex background logic │ │ ├── index.ts │ │ ├── handlers.ts │ │ └── utils.ts │ ├── content/ # Complex content script │ │ ├── index.ts │ │ ├── ui.tsx │ │ └── injector.ts │ └── popup/ # Popup UI │ ├── index.html │ ├── main.tsx │ └── App.tsx ├── components/ # Shared UI components │ ├── Button.tsx │ └── Modal.tsx ├── utils/ # Shared utilities │ ├── storage.ts │ ├── messaging.ts │ └── api.ts └── types/ # TypeScript types └── index.ts ``` ### Type-Safe Communication Define message interfaces: ```typescript // types/messages.ts export interface MessageMap { 'fetch-data': { request: { url: string }; response: { data: any }; }; 'save-settings': { request: { settings: Record<string, any> }; response: { success: boolean }; }; } // utils/messaging.ts export async function sendMessage<K extends keyof MessageMap>( type: K, payload: MessageMap[K]['request'] ): Promise<MessageMap[K]['response']> { return await browser.runtime.sendMessage({ type, payload }); } ``` ### Error Handling Implement comprehensive error handling: ```typescript // utils/errors.ts export class ExtensionError extends Error { constructor( message: string, public code: string, public context?: any ) { super(message); this.name = 'ExtensionError'; } } // Usage try { await riskyOperation(); } catch (error) { if (error instanceof ExtensionError) { // Handle known error console.error(`Error ${error.code}:`, error.message, error.context); } else { // Handle unknown error console.error('Unexpected error:', error); } // Report to user await browser.notifications.create({ type: 'basic', title: 'Error', message: 'Something went wrong', }); } ``` ### State Management For complex state, use proper state management: ```typescript // utils/store.ts import { create } from 'zustand'; import { persist } from 'zustand/middleware'; interface State { settings: Record<string, any>; updateSettings: (updates: Record<string, any>) => void; } export const useStore = create<State>()( persist( (set) => ({ settings: {}, updateSettings: (updates) => set((state) => ({ settings: { ...state.settings, ...updates }, })), }), { name: 'extension-storage', } ) ); ``` ## Testing ### Unit Tests ```typescript // vitest.config.ts import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { environment: 'jsdom', setupFiles: ['./test/setup.ts'], globals: true, }, }); // test/setup.ts import { vi } from 'vitest'; global.browser = { runtime: { sendMessage: vi.fn(), }, storage: { local: { get: vi.fn(), set: vi.fn(), }, }, } as any; ``` ### E2E Tests ```typescript // e2e/extension.spec.ts import { test, expect } from '@playwright/test'; test('popup loads correctly', async ({ page, extensionId }) => { await page.goto(`chrome-extension://${extensionId}/popup.html`); await expect(page.locator('h1')).toHaveText('My Extension'); }); ``` ## Deployment ### Version Management Use semantic versioning: ```json { "version": "1.0.0" // MAJOR.MINOR.PATCH } ``` ### Store Submission Checklist - [ ] Icons provided (16, 32, 48, 128) - [ ] Permissions justified in description - [ ] Privacy policy provided (if handling user data) - [ ] Screenshot and promotional images - [ ] Tested on target browsers - [ ] No hardcoded secrets - [ ] CSP properly configured - [ ] Manifest complete and valid ### CI/CD Pipeline ```yaml # .github/workflows/release.yml name: Release on: push: tags: - 'v*' jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '20' - run: npm ci - run: npm run build - run: npm run zip:all - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: extensions path: .output/*.zip ``` -
chrome-140-features.md 6.6 KB
# Chrome 140+ Features (September 2025+) New Chrome Extension APIs introduced in Chrome 140 and later versions. ## Chrome 140 (September 2025) ### sidePanel.getLayout() Determines the position of the side panel (left or right) in the browser window. **Official Documentation:** https://developer.chrome.com/docs/extensions/reference/api/sidePanel#method-getLayout #### API Signature ```typescript chrome.sidePanel.getLayout(): Promise<{ side: 'left' | 'right'; }> ``` #### Basic Usage ```typescript // Get current side panel layout const layout = await chrome.sidePanel.getLayout(); console.log('Side panel is positioned on the:', layout.side); if (layout.side === 'right') { console.log('Side panel is on the right'); } else { console.log('Side panel is on the left'); } ``` #### Use Cases ##### 1. RTL Language Support ```typescript export default defineContentScript({ matches: ['*://*'], async main() { const layout = await chrome.sidePanel.getLayout(); const documentDir = document.documentElement.dir; // Adjust UI based on panel side and text direction if (layout.side === 'right' && documentDir === 'rtl') { // Apply RTL-optimized positioning applyRTLStyles(); } }, }); ``` ##### 2. Dynamic Content Positioning ```typescript // Popup component function App() { const [panelSide, setPanelSide] = useState<'left' | 'right'>('left'); useEffect(() => { chrome.sidePanel.getLayout().then(({ side }) => { setPanelSide(side); }); }, []); return ( <div className={`panel-${panelSide}`}> <p>Panel is positioned on the {panelSide}</p> {/* Adjust UI layout based on panel side */} </div> ); } ``` ##### 3. Optimal Notification Placement ```typescript // Background script browser.alarms.onAlarm.addListener(async (alarm) => { const layout = await chrome.sidePanel.getLayout(); // Position notifications away from side panel const notificationPosition = layout.side === 'right' ? 'bottom-left' : 'bottom-right'; await chrome.notifications.create({ type: 'basic', title: 'Reminder', message: 'Task is due', iconUrl: '/icon/128.png', }); }); ``` #### Browser Compatibility - **Chrome:** 140+ (September 2025) - **Firefox:** Not yet supported - **Edge:** 140+ (follows Chromium) - **Safari:** Not applicable (no side panel API) #### Feature Detection Always check if the API is available: ```typescript async function getSidePanelSide(): Promise<'left' | 'right' | null> { if (chrome.sidePanel?.getLayout) { try { const layout = await chrome.sidePanel.getLayout(); return layout.side; } catch (error) { console.error('Failed to get side panel layout:', error); return null; } } return null; // API not available } ``` #### Integration with WXT ```typescript // entrypoints/sidepanel/main.tsx import { useState, useEffect } from 'react'; function SidePanel() { const [side, setSide] = useState<'left' | 'right'>('left'); useEffect(() => { // Get initial side chrome.sidePanel.getLayout().then(({ side }) => { setSide(side); }); // Note: Chrome doesn't fire events when user changes panel side // You may need to periodically check or reload when panel is opened }, []); return ( <div className={`sidepanel-container side-${side}`}> <header className={side === 'right' ? 'rtl' : 'ltr'}> <h1>Side Panel Content</h1> </header> <main> <p>Current side: {side}</p> </main> </div> ); } ``` #### Default Behavior - **New Chrome installations (2025+):** May default to right side - **Upgraded Chrome installations:** Retains user's previous preference - **User can change:** Users can move side panel between left and right at any time #### Common Patterns ##### Responsive Layout Adjustment ```typescript // hooks/useSidePanelPosition.ts import { useState, useEffect } from 'react'; export function useSidePanelSide() { const [side, setSide] = useState<'left' | 'right'>('left'); const [isLoading, setIsLoading] = useState(true); useEffect(() => { if (chrome.sidePanel?.getLayout) { chrome.sidePanel .getLayout() .then(({ side }) => { setSide(side); }) .catch((error) => { console.error('Failed to get side panel side:', error); }) .finally(() => { setIsLoading(false); }); } else { setIsLoading(false); } }, []); return { side, isLoading }; } // Usage in component function MyComponent() { const { side, isLoading } = useSidePanelSide(); if (isLoading) return <LoadingSpinner />; return ( <div className={`content-${side}`}> {/* Content positioned based on panel location */} </div> ); } ``` #### Styling Based on Side ```css /* CSS for panel-aware layouts */ .content-left { /* Panel is on left, content flows from right */ margin-left: 20px; margin-right: 0; text-align: left; } .content-right { /* Panel is on right, content flows from left */ margin-left: 0; margin-right: 20px; text-align: right; } /* RTL support */ [dir="rtl"] .content-left { direction: rtl; } ``` ## Staying Updated To stay informed about new Chrome Extension features: 1. **Chrome Extensions What's New:** https://developer.chrome.com/docs/extensions/whats-new 2. **Chrome Developers Blog:** https://developer.chrome.com/blog 3. **Chrome Platform Status:** https://chromestatus.com/features 4. **WXT Changelog:** https://github.com/wxt-dev/wxt/releases ## Migration Guide If your extension currently assumes side panel is always on the left: ### Before (Assumed Left Side) ```typescript // Old code - assumes left side function positionContent() { const content = document.getElementById('content'); content.style.marginLeft = '400px'; // Fixed left margin } ``` ### After (Side-Aware) ```typescript // New code - adapts to panel side async function positionContent() { const content = document.getElementById('content'); if (chrome.sidePanel?.getLayout) { const { side } = await chrome.sidePanel.getLayout(); if (side === 'right') { content.style.marginRight = '400px'; content.style.marginLeft = '0'; } else { content.style.marginLeft = '400px'; content.style.marginRight = '0'; } } } ``` ## Related APIs - **chrome.sidePanel.open()** - Open side panel programmatically - **chrome.sidePanel.close()** - Close side panel - **chrome.sidePanel.setOptions()** - Configure side panel behavior - **chrome.sidePanel.getOptions()** - Get current side panel configuration **Full Side Panel API:** https://developer.chrome.com/docs/extensions/reference/api/sidePanel -
chrome-api.md 20.6 KB
# Chrome Extension API Reference Comprehensive guide to Chrome Extension APIs with WXT. Based on official Chrome Extension documentation at https://developer.chrome.com/docs/extensions. ## Core APIs ### chrome.action (Manifest V3) Control the extension's toolbar icon. **Official Docs:** https://developer.chrome.com/docs/extensions/reference/api/action ```typescript // Set badge text (shows number on icon) await browser.action.setBadgeText({ text: '5' }); // Set badge background color await browser.action.setBadgeBackgroundColor({ color: '#FF0000' }); // Set icon await browser.action.setIcon({ path: { 16: '/icon/16.png', 32: '/icon/32.png', } }); // Set title (tooltip) await browser.action.setTitle({ title: 'Extension tooltip' }); // Enable/disable for specific tabs await browser.action.enable(tabId); await browser.action.disable(tabId); // Listen for icon clicks browser.action.onClicked.addListener((tab) => { console.log('Extension icon clicked in tab:', tab.id); }); // Set popup programmatically await browser.action.setPopup({ popup: 'popup.html' }); ``` ### chrome.tabs Interact with browser tabs. **Official Docs:** https://developer.chrome.com/docs/extensions/reference/api/tabs ```typescript // Query tabs const tabs = await browser.tabs.query({ active: true, currentWindow: true }); // Get specific tab const tab = await browser.tabs.get(tabId); // Create new tab const newTab = await browser.tabs.create({ url: 'https://example.com', active: true, pinned: false, }); // Update tab await browser.tabs.update(tabId, { url: 'https://example.com', active: true, }); // Close tab await browser.tabs.remove(tabId); // Duplicate tab await browser.tabs.duplicate(tabId); // Send message to content script const response = await browser.tabs.sendMessage(tabId, { type: 'getMessage', data: 'hello' }); // Note: tabs.executeScript and tabs.insertCSS are deprecated in MV3 // Use chrome.scripting API instead (see scripting section below) // Tab events browser.tabs.onCreated.addListener((tab) => { console.log('Tab created:', tab.id); }); browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { if (changeInfo.status === 'complete') { console.log('Tab loaded:', tab.url); } }); browser.tabs.onRemoved.addListener((tabId, removeInfo) => { console.log('Tab closed:', tabId); }); browser.tabs.onActivated.addListener((activeInfo) => { console.log('Tab activated:', activeInfo.tabId); }); ``` ### chrome.runtime Access extension runtime information and communicate between components. **Official Docs:** https://developer.chrome.com/docs/extensions/reference/api/runtime ```typescript // Get extension ID const extensionId = browser.runtime.id; // Get manifest const manifest = browser.runtime.getManifest(); console.log('Version:', manifest.version); // Get URL of extension resource const iconUrl = browser.runtime.getURL('icon/128.png'); // Send message to background const response = await browser.runtime.sendMessage({ type: 'getData', payload: { key: 'value' } }); // Listen for messages browser.runtime.onMessage.addListener((message, sender, sendResponse) => { console.log('Message from:', sender.tab?.url || 'extension'); if (message.type === 'getData') { // Handle async with Promise (async () => { const result = await fetchData(message.payload); sendResponse(result); })(); return true; // Keep channel open for async } }); // Connect for long-lived connections const port = browser.runtime.connect({ name: 'my-channel' }); port.postMessage({ data: 'hello' }); port.onMessage.addListener((msg) => { console.log('Received:', msg); }); // Listen for connection browser.runtime.onConnect.addListener((port) => { console.log('Connected:', port.name); port.onMessage.addListener((msg) => { console.log('Message:', msg); port.postMessage({ response: 'received' }); }); }); // Install/update events browser.runtime.onInstalled.addListener((details) => { if (details.reason === 'install') { console.log('Extension installed'); } else if (details.reason === 'update') { console.log('Extension updated to version:', manifest.version); } }); // Extension suspend warning browser.runtime.onSuspend.addListener(() => { console.log('Service worker about to suspend'); // Clean up resources }); ``` ### chrome.storage Store and sync data. **Official Docs:** https://developer.chrome.com/docs/extensions/reference/api/storage ```typescript // Local storage (not synced) await browser.storage.local.set({ key: 'value' }); const result = await browser.storage.local.get('key'); console.log(result.key); // 'value' // Sync storage (synced across devices) await browser.storage.sync.set({ settings: { theme: 'dark' } }); const settings = await browser.storage.sync.get('settings'); // Get multiple items const data = await browser.storage.local.get(['key1', 'key2']); console.log(data.key1, data.key2); // Get all items const all = await browser.storage.local.get(null); // Remove items await browser.storage.local.remove('key'); await browser.storage.local.remove(['key1', 'key2']); // Clear all await browser.storage.local.clear(); // Get bytes in use const bytes = await browser.storage.local.getBytesInUse('key'); // Listen for changes browser.storage.onChanged.addListener((changes, area) => { console.log('Storage area:', area); // 'local' or 'sync' for (const [key, { oldValue, newValue }] of Object.entries(changes)) { console.log(`${key} changed from ${oldValue} to ${newValue}`); } }); // Storage limits // local: ~10MB // sync: 100KB total, 8KB per item ``` ### chrome.alarms Schedule periodic tasks. **Official Docs:** https://developer.chrome.com/docs/extensions/reference/api/alarms ```typescript // Create alarm that fires once await browser.alarms.create('reminder', { delayInMinutes: 1, }); // Create periodic alarm await browser.alarms.create('daily-sync', { periodInMinutes: 1440, // 24 hours }); // Create alarm at specific time await browser.alarms.create('scheduled', { when: Date.now() + 60000, // 1 minute from now }); // Get alarm const alarm = await browser.alarms.get('reminder'); // Get all alarms const alarms = await browser.alarms.getAll(); // Clear alarm await browser.alarms.clear('reminder'); // Clear all alarms await browser.alarms.clearAll(); // Listen for alarms browser.alarms.onAlarm.addListener((alarm) => { console.log('Alarm fired:', alarm.name); if (alarm.name === 'daily-sync') { performDailySync(); } }); ``` ### chrome.notifications Display system notifications. **Official Docs:** https://developer.chrome.com/docs/extensions/reference/api/notifications ```typescript // Basic notification await browser.notifications.create({ type: 'basic', iconUrl: '/icon/128.png', title: 'Notification Title', message: 'This is the notification message', priority: 2, }); // Notification with buttons await browser.notifications.create('my-notification-id', { type: 'basic', iconUrl: '/icon/128.png', title: 'Action Required', message: 'Click a button to respond', buttons: [ { title: 'Accept' }, { title: 'Decline' } ], requireInteraction: true, // Don't auto-dismiss }); // Progress notification await browser.notifications.create({ type: 'progress', iconUrl: '/icon/128.png', title: 'Downloading...', message: 'File download in progress', progress: 50, }); // List notification await browser.notifications.create({ type: 'list', iconUrl: '/icon/128.png', title: 'Multiple Items', message: 'Summary message', items: [ { title: 'Item 1', message: 'First item' }, { title: 'Item 2', message: 'Second item' }, ], }); // Image notification await browser.notifications.create({ type: 'image', iconUrl: '/icon/128.png', title: 'Image Notification', message: 'Notification with image', imageUrl: '/images/preview.png', }); // Update notification await browser.notifications.update('my-notification-id', { progress: 75, }); // Clear notification await browser.notifications.clear('my-notification-id'); // Notification events browser.notifications.onClicked.addListener((notificationId) => { console.log('Notification clicked:', notificationId); }); browser.notifications.onButtonClicked.addListener((notificationId, buttonIndex) => { console.log(`Button ${buttonIndex} clicked on ${notificationId}`); }); browser.notifications.onClosed.addListener((notificationId, byUser) => { console.log(`Notification ${notificationId} closed by user: ${byUser}`); }); ``` ### chrome.contextMenus Add items to browser context menu (right-click menu). **Official Docs:** https://developer.chrome.com/docs/extensions/reference/api/contextMenus ```typescript // Create context menu in background script browser.runtime.onInstalled.addListener(() => { // Simple menu item browser.contextMenus.create({ id: 'search-selection', title: 'Search "%s"', contexts: ['selection'], }); // Menu with submenu browser.contextMenus.create({ id: 'parent', title: 'Extension Actions', contexts: ['page', 'selection'], }); browser.contextMenus.create({ id: 'child1', parentId: 'parent', title: 'Action 1', contexts: ['page'], }); browser.contextMenus.create({ id: 'child2', parentId: 'parent', title: 'Action 2', contexts: ['page'], }); // Menu for specific URL patterns browser.contextMenus.create({ id: 'github-actions', title: 'GitHub Actions', contexts: ['page'], documentUrlPatterns: ['*://github.com/*'], }); }); // Listen for clicks browser.contextMenus.onClicked.addListener((info, tab) => { if (info.menuItemId === 'search-selection') { const query = info.selectionText; browser.tabs.create({ url: `https://www.google.com/search?q=${encodeURIComponent(query)}`, }); } }); // Context types // 'all', 'page', 'selection', 'link', 'editable', 'image', 'video', 'audio' ``` ### chrome.webRequest Intercept and modify network requests. **Official Docs:** https://developer.chrome.com/docs/extensions/reference/api/webRequest **Requires permission:** `"webRequest"` and host permissions ```typescript // Block requests browser.webRequest.onBeforeRequest.addListener( (details) => { // Block requests to certain URLs if (details.url.includes('ads.com')) { return { cancel: true }; } }, { urls: ['<all_urls>'] }, ['blocking'] ); // Modify request headers browser.webRequest.onBeforeSendHeaders.addListener( (details) => { const headers = details.requestHeaders || []; // Add custom header headers.push({ name: 'X-Custom-Header', value: 'my-value', }); // Remove header const filtered = headers.filter(h => h.name !== 'User-Agent'); return { requestHeaders: filtered }; }, { urls: ['*://*.example.com/*'] }, ['blocking', 'requestHeaders'] ); // Modify response headers browser.webRequest.onHeadersReceived.addListener( (details) => { const headers = details.responseHeaders || []; // Modify CORS headers headers.push({ name: 'Access-Control-Allow-Origin', value: '*', }); return { responseHeaders: headers }; }, { urls: ['*://*.api.com/*'] }, ['blocking', 'responseHeaders'] ); // Redirect requests browser.webRequest.onBeforeRequest.addListener( (details) => { return { redirectUrl: 'https://alternative.com' }; }, { urls: ['*://blocked.com/*'] }, ['blocking'] ); ``` ### chrome.cookies Manage browser cookies. **Official Docs:** https://developer.chrome.com/docs/extensions/reference/api/cookies ```typescript // Get cookie const cookie = await browser.cookies.get({ url: 'https://example.com', name: 'session', }); // Get all cookies for URL const cookies = await browser.cookies.getAll({ url: 'https://example.com', }); // Set cookie await browser.cookies.set({ url: 'https://example.com', name: 'session', value: 'abc123', expirationDate: Date.now() / 1000 + 3600, // 1 hour httpOnly: true, secure: true, sameSite: 'lax', }); // Remove cookie await browser.cookies.remove({ url: 'https://example.com', name: 'session', }); // Listen for cookie changes browser.cookies.onChanged.addListener((changeInfo) => { console.log('Cookie changed:', changeInfo.cookie.name); console.log('Removed:', changeInfo.removed); }); ``` ### chrome.downloads Manage file downloads. **Official Docs:** https://developer.chrome.com/docs/extensions/reference/api/downloads ```typescript // Download file const downloadId = await browser.downloads.download({ url: 'https://example.com/file.pdf', filename: 'downloaded-file.pdf', saveAs: true, // Show save dialog }); // Search downloads const downloads = await browser.downloads.search({ query: ['pdf'], orderBy: ['-startTime'], limit: 10, }); // Pause download await browser.downloads.pause(downloadId); // Resume download await browser.downloads.resume(downloadId); // Cancel download await browser.downloads.cancel(downloadId); // Show download in folder await browser.downloads.show(downloadId); // Open downloaded file await browser.downloads.open(downloadId); // Listen for download changes browser.downloads.onChanged.addListener((delta) => { if (delta.state?.current === 'complete') { console.log('Download complete:', delta.id); } }); browser.downloads.onCreated.addListener((item) => { console.log('Download started:', item.filename); }); ``` ### chrome.bookmarks Access and modify bookmarks. **Official Docs:** https://developer.chrome.com/docs/extensions/reference/api/bookmarks ```typescript // Get bookmarks const bookmarks = await browser.bookmarks.getTree(); // Search bookmarks const results = await browser.bookmarks.search('github'); // Create bookmark const bookmark = await browser.bookmarks.create({ parentId: '1', title: 'GitHub', url: 'https://github.com', }); // Create folder const folder = await browser.bookmarks.create({ parentId: '1', title: 'My Folder', }); // Update bookmark await browser.bookmarks.update(bookmark.id, { title: 'GitHub - Updated', url: 'https://github.com/explore', }); // Move bookmark await browser.bookmarks.move(bookmark.id, { parentId: folder.id, index: 0, }); // Remove bookmark await browser.bookmarks.remove(bookmark.id); // Remove folder recursively await browser.bookmarks.removeTree(folder.id); // Listen for changes browser.bookmarks.onCreated.addListener((id, bookmark) => { console.log('Bookmark created:', bookmark.title); }); browser.bookmarks.onRemoved.addListener((id, removeInfo) => { console.log('Bookmark removed:', id); }); ``` ### chrome.scripting Inject JavaScript and CSS into web pages (replaces deprecated tabs.executeScript/insertCSS). **Official Docs:** https://developer.chrome.com/docs/extensions/reference/api/scripting **Required permission:** `"scripting"` ```typescript // Execute script in tab await chrome.scripting.executeScript({ target: { tabId: tabId }, files: ['content.js'], }); // Execute inline function await chrome.scripting.executeScript({ target: { tabId: tabId }, func: () => { console.log('Hello from injected script'); }, }); // Execute with arguments await chrome.scripting.executeScript({ target: { tabId: tabId }, func: (color) => { document.body.style.backgroundColor = color; }, args: ['red'], }); // Inject CSS file await chrome.scripting.insertCSS({ target: { tabId: tabId }, files: ['styles.css'], }); // Inject inline CSS await chrome.scripting.insertCSS({ target: { tabId: tabId }, css: 'body { background: red; }', }); // Remove CSS await chrome.scripting.removeCSS({ target: { tabId: tabId }, css: 'body { background: red; }', }); // Register content scripts dynamically await chrome.scripting.registerContentScripts([{ id: 'my-script', matches: ['*://example.com/*'], js: ['content.js'], runAt: 'document_idle', }]); // Get registered scripts const scripts = await chrome.scripting.getRegisteredContentScripts(); // Unregister scripts await chrome.scripting.unregisterContentScripts({ ids: ['my-script'], }); // Update existing scripts await chrome.scripting.updateContentScripts([{ id: 'my-script', matches: ['*://example.com/*', '*://example.org/*'], }]); ``` ### chrome.history Access browser history. **Official Docs:** https://developer.chrome.com/docs/extensions/reference/api/history ```typescript // Search history const history = await browser.history.search({ text: 'github', startTime: Date.now() - 7 * 24 * 60 * 60 * 1000, // Last 7 days maxResults: 100, }); // Get visits for URL const visits = await browser.history.getVisits({ url: 'https://github.com', }); // Add URL to history await browser.history.addUrl({ url: 'https://example.com', title: 'Example Domain', }); // Remove URL from history await browser.history.deleteUrl({ url: 'https://example.com', }); // Remove all history in time range await browser.history.deleteRange({ startTime: Date.now() - 24 * 60 * 60 * 1000, // Last 24 hours endTime: Date.now(), }); // Delete all history await browser.history.deleteAll(); // Listen for history changes browser.history.onVisited.addListener((result) => { console.log('Page visited:', result.url); }); ``` ## Chrome 140+ Features (September 2025) ### sidePanel.getLayout() New in Chrome 140 - determine side panel side (left or right). **Official Docs:** https://developer.chrome.com/docs/extensions/reference/api/sidePanel#method-getLayout ```typescript // Get side panel layout const layout = await chrome.sidePanel.getLayout(); console.log('Side panel side:', layout.side); // 'left' or 'right' // Useful for RTL language support if (layout.side === 'right') { // Apply RTL-specific styling or behavior } ``` **Use Cases:** - Adapting UI for RTL languages - Adjusting panel content based on side - Optimizing user experience based on panel location **Browser Support:** Chrome 140+ (September 2025) ## Permission Patterns ### Required Permissions **manifest.json:** ```json { "permissions": [ "storage", "tabs", "activeTab", "alarms", "notifications", "contextMenus" ], "host_permissions": [ "*://example.com/*", "*://api.example.com/*" ], "optional_permissions": [ "downloads", "bookmarks", "history" ] } ``` ### Request Optional Permissions ```typescript // Check if permission granted const hasPermission = await browser.permissions.contains({ permissions: ['downloads'], origins: ['*://downloads.example.com/*'], }); // Request permission const granted = await browser.permissions.request({ permissions: ['downloads'], origins: ['*://downloads.example.com/*'], }); if (granted) { // Permission granted, use the API await browser.downloads.download({ url: 'https://example.com/file.pdf' }); } // Remove permission await browser.permissions.remove({ permissions: ['downloads'], }); // Listen for permission changes browser.permissions.onAdded.addListener((permissions) => { console.log('Permissions added:', permissions); }); browser.permissions.onRemoved.addListener((permissions) => { console.log('Permissions removed:', permissions); }); ``` ## Content Script Communication ### Sending Messages ```typescript // Content script → Background const response = await browser.runtime.sendMessage({ type: 'getData', payload: { key: 'value' }, }); // Background → Content script const response = await browser.tabs.sendMessage(tabId, { type: 'updateUI', payload: { theme: 'dark' }, }); ``` ### Long-Lived Connections ```typescript // Content script const port = browser.runtime.connect({ name: 'my-channel' }); port.postMessage({ type: 'init' }); port.onMessage.addListener((msg) => { console.log('Received:', msg); }); port.onDisconnect.addListener(() => { console.log('Disconnected'); }); // Background script browser.runtime.onConnect.addListener((port) => { if (port.name === 'my-channel') { port.onMessage.addListener((msg) => { // Handle message port.postMessage({ response: 'acknowledged' }); }); } }); ``` ## Official Documentation Links - **Get Started:** https://developer.chrome.com/docs/extensions/get-started - **API Reference:** https://developer.chrome.com/docs/extensions/reference/api - **Manifest V3:** https://developer.chrome.com/docs/extensions/develop/migrate/what-is-mv3 - **Content Scripts:** https://developer.chrome.com/docs/extensions/develop/concepts/content-scripts - **Service Workers:** https://developer.chrome.com/docs/extensions/develop/concepts/service-workers - **Message Passing:** https://developer.chrome.com/docs/extensions/develop/concepts/messaging - **Storage:** https://developer.chrome.com/docs/extensions/reference/api/storage - **Permissions:** https://developer.chrome.com/docs/extensions/develop/concepts/declare-permissions - **Publishing:** https://developer.chrome.com/docs/webstore/publish - **Best Practices:** https://developer.chrome.com/docs/extensions/develop/concepts/best-practices -
react-integration.md 21 KB
# React Integration with WXT Complete guide for building Chrome extensions with React and WXT. ## Setup ### Initialize with React Template ```bash npm create wxt@latest -- --template react-ts cd my-extension npm install ``` ### Manual Setup ```bash npm install react react-dom npm install -D @types/react @types/react-dom @wxt-dev/module-react ``` **wxt.config.ts:** ```typescript import { defineConfig } from 'wxt'; export default defineConfig({ modules: ['@wxt-dev/module-react'], manifest: { content_security_policy: { extension_pages: "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'", }, }, }); ``` ## Entry Point Patterns ### Popup with React **Directory structure:** ``` entrypoints/popup/ ├── index.html ├── main.tsx # Entry point ├── App.tsx # Root component └── components/ # UI components ├── Header.tsx └── Settings.tsx ``` **entrypoints/popup/index.html:** ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Extension Popup</title> </head> <body> <div id="root"></div> <script type="module" src="./main.tsx"></script> </body> </html> ``` **entrypoints/popup/main.tsx:** ```typescript import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App'; import './style.css'; ReactDOM.createRoot(document.getElementById('root')!).render( <React.StrictMode> <App /> </React.StrictMode> ); ``` **entrypoints/popup/App.tsx:** ```typescript import { useState, useEffect } from 'react'; export default function App() { const [data, setData] = useState<any>(null); const [loading, setLoading] = useState(true); useEffect(() => { loadData(); }, []); async function loadData() { try { // Get current tab const [tab] = await browser.tabs.query({ active: true, currentWindow: true }); // Load from storage const result = await browser.storage.local.get('settings'); setData(result.settings); } catch (error) { console.error('Error loading data:', error); } finally { setLoading(false); } } async function handleSave(newSettings: any) { await browser.storage.local.set({ settings: newSettings }); setData(newSettings); } if (loading) { return <div className="loading">Loading...</div>; } return ( <div className="app"> <h1>My Extension</h1> <div className="content"> {/* Your UI here */} </div> </div> ); } ``` ### Options Page with React **entrypoints/options/index.html:** ```html <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Extension Options</title> </head> <body> <div id="root"></div> <script type="module" src="./main.tsx"></script> </body> </html> ``` **entrypoints/options/App.tsx:** ```typescript import { useState, useEffect } from 'react'; export default function Options() { const [settings, setSettings] = useState({ theme: 'light', notifications: true, apiKey: '', }); useEffect(() => { // Load settings browser.storage.sync.get('settings').then((result) => { if (result.settings) { setSettings(result.settings); } }); }, []); async function handleSave() { await browser.storage.sync.set({ settings }); // Show success notification await browser.notifications.create({ type: 'basic', title: 'Settings Saved', message: 'Your settings have been saved successfully', iconUrl: '/icon/128.png', }); } return ( <div className="options-page"> <h1>Extension Settings</h1> <section> <label> Theme: <select value={settings.theme} onChange={(e) => setSettings({ ...settings, theme: e.target.value })} > <option value="light">Light</option> <option value="dark">Dark</option> </select> </label> </section> <section> <label> <input type="checkbox" checked={settings.notifications} onChange={(e) => setSettings({ ...settings, notifications: e.target.checked })} /> Enable Notifications </label> </section> <section> <label> API Key: <input type="password" value={settings.apiKey} onChange={(e) => setSettings({ ...settings, apiKey: e.target.value })} placeholder="Enter your API key" /> </label> </section> <button onClick={handleSave}>Save Settings</button> </div> ); } ``` ### Content Script with React UI **entrypoints/content.ts:** ```typescript import ReactDOM from 'react-dom/client'; import { ContentScriptApp } from './ContentScriptApp'; export default defineContentScript({ matches: ['*://*.example.com/*'], cssInjectionMode: 'ui', async main(ctx) { // Create shadow root UI const ui = await createShadowRootUi(ctx, { name: 'my-extension-overlay', position: 'overlay', anchor: 'body', onMount: (container) => { // Mount React app in shadow DOM const root = ReactDOM.createRoot(container); root.render(<ContentScriptApp />); return root; }, onRemove: (root) => { // Cleanup root?.unmount(); }, }); // Mount UI ui.mount(); }, }); ``` **entrypoints/ContentScriptApp.tsx:** ```typescript import { useState } from 'react'; import './content.css'; export function ContentScriptApp() { const [visible, setVisible] = useState(false); return ( <div className="extension-overlay"> <button className="toggle-button" onClick={() => setVisible(!visible)} > Toggle Panel </button> {visible && ( <div className="extension-panel"> <h2>Extension Panel</h2> <p>This is injected into the page!</p> </div> )} </div> ); } ``` ## React Hooks for Extensions ### useStorage Hook ```typescript // hooks/useStorage.ts import { useState, useEffect } from 'react'; export function useStorage<T>(key: string, defaultValue: T) { const [value, setValue] = useState<T>(defaultValue); const [loading, setLoading] = useState(true); useEffect(() => { // Load initial value browser.storage.local.get(key).then((result) => { if (result[key] !== undefined) { setValue(result[key]); } setLoading(false); }); // Listen for changes const listener = (changes: any, area: string) => { if (area === 'local' && changes[key]) { setValue(changes[key].newValue); } }; browser.storage.onChanged.addListener(listener); return () => { browser.storage.onChanged.removeListener(listener); }; }, [key]); const updateValue = async (newValue: T) => { await browser.storage.local.set({ [key]: newValue }); setValue(newValue); }; return [value, updateValue, loading] as const; } // Usage function MyComponent() { const [theme, setTheme, loading] = useStorage('theme', 'light'); if (loading) return <div>Loading...</div>; return ( <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}> Current theme: {theme} </button> ); } ``` ### useMessage Hook ```typescript // hooks/useMessage.ts import { useEffect, useCallback } from 'react'; type MessageHandler<T = any> = ( message: T, sender: browser.Runtime.MessageSender ) => any | Promise<any>; export function useMessage<T = any>( type: string, handler: MessageHandler<T> ) { useEffect(() => { const listener = ( message: any, sender: browser.Runtime.MessageSender, sendResponse: (response?: any) => void ) => { if (message.type === type) { Promise.resolve(handler(message.payload, sender)) .then(sendResponse) .catch((error) => { console.error(`Error handling message ${type}:`, error); sendResponse({ error: error.message }); }); return true; // Keep channel open } }; browser.runtime.onMessage.addListener(listener); return () => { browser.runtime.onMessage.removeListener(listener); }; }, [type, handler]); } // Usage function MyComponent() { useMessage('get-data', async (payload, sender) => { console.log('Message from:', sender.tab?.url); return { data: 'some data' }; }); return <div>Component listening for messages</div>; } ``` ### useTabs Hook ```typescript // hooks/useTabs.ts import { useState, useEffect } from 'react'; export function useTabs() { const [tabs, setTabs] = useState<browser.Tabs.Tab[]>([]); useEffect(() => { // Load initial tabs browser.tabs.query({}).then(setTabs); // Listen for tab changes const onCreated = (tab: browser.Tabs.Tab) => { setTabs((prev) => [...prev, tab]); }; const onRemoved = (tabId: number) => { setTabs((prev) => prev.filter((t) => t.id !== tabId)); }; const onUpdated = (tabId: number, changeInfo: any, tab: browser.Tabs.Tab) => { setTabs((prev) => prev.map((t) => (t.id === tabId ? tab : t)) ); }; browser.tabs.onCreated.addListener(onCreated); browser.tabs.onRemoved.addListener(onRemoved); browser.tabs.onUpdated.addListener(onUpdated); return () => { browser.tabs.onCreated.removeListener(onCreated); browser.tabs.onRemoved.removeListener(onRemoved); browser.tabs.onUpdated.removeListener(onUpdated); }; }, []); return tabs; } // Usage function TabList() { const tabs = useTabs(); return ( <ul> {tabs.map((tab) => ( <li key={tab.id}> {tab.title} - {tab.url} </li> ))} </ul> ); } ``` ## State Management ### Context API for Extension State ```typescript // contexts/ExtensionContext.tsx import { createContext, useContext, useEffect, useState } from 'react'; interface ExtensionState { settings: any; user: any; updateSettings: (settings: any) => Promise<void>; } const ExtensionContext = createContext<ExtensionState | null>(null); export function ExtensionProvider({ children }: { children: React.ReactNode }) { const [settings, setSettings] = useState({}); const [user, setUser] = useState(null); useEffect(() => { // Load initial state browser.storage.local.get(['settings', 'user']).then((result) => { setSettings(result.settings || {}); setUser(result.user || null); }); // Listen for storage changes const listener = (changes: any) => { if (changes.settings) { setSettings(changes.settings.newValue); } if (changes.user) { setUser(changes.user.newValue); } }; browser.storage.onChanged.addListener(listener); return () => { browser.storage.onChanged.removeListener(listener); }; }, []); const updateSettings = async (newSettings: any) => { await browser.storage.local.set({ settings: newSettings }); setSettings(newSettings); }; return ( <ExtensionContext.Provider value={{ settings, user, updateSettings }}> {children} </ExtensionContext.Provider> ); } export function useExtension() { const context = useContext(ExtensionContext); if (!context) { throw new Error('useExtension must be used within ExtensionProvider'); } return context; } // Usage in App function App() { return ( <ExtensionProvider> <YourComponents /> </ExtensionProvider> ); } ``` ### Zustand for Extension State ```typescript // store/useStore.ts import { create } from 'zustand'; import { persist } from 'zustand/middleware'; interface ExtensionStore { theme: 'light' | 'dark'; notifications: boolean; apiKey: string; setTheme: (theme: 'light' | 'dark') => void; setNotifications: (enabled: boolean) => void; setApiKey: (key: string) => void; } export const useStore = create<ExtensionStore>()( persist( (set) => ({ theme: 'light', notifications: true, apiKey: '', setTheme: (theme) => set({ theme }), setNotifications: (notifications) => set({ notifications }), setApiKey: (apiKey) => set({ apiKey }), }), { name: 'extension-storage', getStorage: () => ({ getItem: async (name) => { const result = await browser.storage.local.get(name); return result[name] || null; }, setItem: async (name, value) => { await browser.storage.local.set({ [name]: value }); }, removeItem: async (name) => { await browser.storage.local.remove(name); }, }), } ) ); // Usage function SettingsComponent() { const { theme, setTheme } = useStore(); return ( <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}> Toggle Theme (current: {theme}) </button> ); } ``` ## Styling ### Popular UI Libraries (2025) Modern Chrome extensions commonly use these UI libraries with React: #### shadcn/ui Most popular choice for Chrome extensions in 2025. ```bash npx shadcn@latest init npx shadcn@latest add button card dialog ``` **Benefits:** - Tailwind CSS-based components - Full customization and ownership of code - Copy-paste philosophy - components live in your codebase - Excellent TypeScript support - Works seamlessly with WXT **Example Setup:** ```typescript // components/ui/button.tsx (generated by shadcn) import * as React from "react" import { cn } from "@/lib/utils" export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {} const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( ({ className, ...props }, ref) => { return ( <button className={cn( "inline-flex items-center justify-center rounded-md px-4 py-2", className )} ref={ref} {...props} /> ) } ) ``` **Popular Template:** https://github.com/imtiger/wxt-react-shadcn-tailwindcss-chrome-extension #### Mantine UI Complete component library with 100+ components. ```bash npm install @mantine/core @mantine/hooks npm install -D postcss-preset-mantine postcss-simple-vars ``` **Benefits:** - Rich component ecosystem (100+ components) - Built-in dark mode support - Comprehensive form management with @mantine/form - Accessible by default - Excellent documentation **Example Setup:** ```typescript // entrypoints/popup/main.tsx import { MantineProvider } from '@mantine/core'; import '@mantine/core/styles.css'; function App() { return ( <MantineProvider> <YourApp /> </MantineProvider> ); } ``` **Popular Template:** https://github.com/ongkay/WXT-Mantine-Tailwind-Browser-Extension #### Tailwind CSS (Utility-First) Most flexible option for custom designs. ```bash npm install -D tailwindcss postcss autoprefixer npx tailwindcss init -p ``` **Benefits:** - Maximum design flexibility - Small bundle size with purging - No additional component library needed - Works with shadcn/ui for pre-built components ### Tailwind CSS Setup ```bash npm install -D tailwindcss postcss autoprefixer npx tailwindcss init -p ``` **tailwind.config.js:** ```javascript export default { content: [ './entrypoints/**/*.{html,tsx}', './components/**/*.tsx', ], theme: { extend: {}, }, plugins: [], }; ``` **entrypoints/popup/style.css:** ```css @tailwind base; @tailwind components; @tailwind utilities; /* Extension-specific styles */ .popup-container { @apply w-96 h-[500px] p-4; } ``` ### CSS Modules WXT supports CSS Modules automatically: **Button.module.css:** ```css .button { padding: 8px 16px; background: blue; color: white; border: none; border-radius: 4px; } .button:hover { background: darkblue; } ``` **Button.tsx:** ```typescript import styles from './Button.module.css'; export function Button({ children, onClick }: any) { return ( <button className={styles.button} onClick={onClick}> {children} </button> ); } ``` ### Styled Components ```bash npm install styled-components npm install -D @types/styled-components ``` ```typescript import styled from 'styled-components'; const Button = styled.button` padding: 8px 16px; background: ${(props) => props.theme.primary}; color: white; border: none; border-radius: 4px; &:hover { opacity: 0.8; } `; export function MyComponent() { return <Button>Click me</Button>; } ``` ## Performance Optimization ### Code Splitting ```typescript // Lazy load heavy components import { lazy, Suspense } from 'react'; const HeavyComponent = lazy(() => import('./HeavyComponent')); function App() { return ( <Suspense fallback={<div>Loading...</div>}> <HeavyComponent /> </Suspense> ); } ``` ### React.memo for Expensive Components ```typescript import { memo } from 'react'; const ExpensiveComponent = memo(({ data }: { data: any }) => { // Expensive rendering logic return <div>{/* rendered content */}</div>; }); ``` ### useMemo and useCallback ```typescript import { useMemo, useCallback } from 'react'; function DataTable({ data }: { data: any[] }) { // Memoize expensive calculations const sortedData = useMemo(() => { return data.sort((a, b) => a.value - b.value); }, [data]); // Memoize callbacks const handleClick = useCallback((id: number) => { console.log('Clicked:', id); }, []); return ( <table> {sortedData.map((item) => ( <tr key={item.id} onClick={() => handleClick(item.id)}> <td>{item.name}</td> </tr> ))} </table> ); } ``` ## Common Patterns ### Loading States ```typescript function DataLoader() { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState<Error | null>(null); useEffect(() => { fetchData() .then(setData) .catch(setError) .finally(() => setLoading(false)); }, []); if (loading) return <LoadingSpinner />; if (error) return <ErrorMessage error={error} />; if (!data) return <EmptyState />; return <DataDisplay data={data} />; } ``` ### Form Handling ```typescript import { useState, FormEvent } from 'react'; function SettingsForm() { const [formData, setFormData] = useState({ name: '', email: '', notifications: true, }); const handleSubmit = async (e: FormEvent) => { e.preventDefault(); try { await browser.storage.sync.set({ settings: formData }); console.log('Settings saved'); } catch (error) { console.error('Failed to save:', error); } }; return ( <form onSubmit={handleSubmit}> <input type="text" value={formData.name} onChange={(e) => setFormData({ ...formData, name: e.target.value })} /> <input type="email" value={formData.email} onChange={(e) => setFormData({ ...formData, email: e.target.value })} /> <label> <input type="checkbox" checked={formData.notifications} onChange={(e) => setFormData({ ...formData, notifications: e.target.checked })} /> Enable Notifications </label> <button type="submit">Save</button> </form> ); } ``` ### Modal/Dialog Patterns ```typescript function ConfirmDialog({ isOpen, onConfirm, onCancel }: any) { if (!isOpen) return null; return ( <div className="modal-overlay" onClick={onCancel}> <div className="modal-content" onClick={(e) => e.stopPropagation()}> <h2>Are you sure?</h2> <div className="modal-actions"> <button onClick={onConfirm}>Confirm</button> <button onClick={onCancel}>Cancel</button> </div> </div> </div> ); } ``` ## Testing React Extensions ### Component Testing with Vitest ```typescript // Button.test.tsx import { render, screen, fireEvent } from '@testing-library/react'; import { describe, it, expect, vi } from 'vitest'; import { Button } from './Button'; describe('Button', () => { it('renders children', () => { render(<Button>Click me</Button>); expect(screen.getByText('Click me')).toBeInTheDocument(); }); it('calls onClick when clicked', () => { const handleClick = vi.fn(); render(<Button onClick={handleClick}>Click me</Button>); fireEvent.click(screen.getByText('Click me')); expect(handleClick).toHaveBeenCalledTimes(1); }); }); ``` ## Common Issues & Solutions ### Issue: React DevTools not working **Solution:** Add to manifest: ```typescript manifest: { content_security_policy: { extension_pages: "script-src 'self' 'unsafe-eval'; object-src 'self'", }, } ``` ### Issue: Hot reload breaks React state **Solution:** Use React Fast Refresh properly: ```typescript // In components, ensure proper export export default function MyComponent() { // Component logic } ``` ### Issue: Storage not syncing between components **Solution:** Use storage change listeners: ```typescript useEffect(() => { const listener = (changes: any) => { if (changes.key) { setState(changes.key.newValue); } }; browser.storage.onChanged.addListener(listener); return () => browser.storage.onChanged.removeListener(listener); }, []); ``` -
wxt-api.md 6.2 KB
# WXT API Reference Complete API documentation for WXT framework functions and utilities. ## Core APIs ### defineBackground() Define service worker (background script) behavior. ```typescript export default defineBackground({ type: 'module' | 'esm', persistent: boolean, main(ctx) { // Background logic } }); ``` ### defineContentScript() Define content script that runs on web pages. ```typescript export default defineContentScript({ matches: string[], excludeMatches?: string[], runAt: 'document_start' | 'document_end' | 'document_idle', world: 'ISOLATED' | 'MAIN', cssInjectionMode: 'ui' | 'inline' | 'manual', main(ctx: ContentScriptContext) { // Content script logic } }); ``` ### createShadowRootUi() Create isolated UI components in content scripts. ```typescript const ui = createShadowRootUi(ctx, { name: string, position: 'inline' | 'overlay' | 'modal', anchor: string | HTMLElement, onMount(container: HTMLElement) { // Mount UI framework }, onRemove?(container: HTMLElement) { // Cleanup } }); ui.mount(); ui.remove(); ``` ### storage WXT storage API with type safety. ```typescript import { storage } from 'wxt/storage'; // Get item const value = await storage.getItem<T>('local:key'); // Set item await storage.setItem('local:key', value); // Remove item await storage.removeItem('local:key'); // Watch for changes const unwatch = storage.watch<T>('local:key', (newValue, oldValue) => { // Handle change }); ``` ### injectScript() Inject scripts into page context. ```typescript import { injectScript } from 'wxt/client'; await injectScript('/script.js', { keepInDom: boolean, }); ``` ## Browser API WXT provides unified `browser` API that works across all browsers: ```typescript // Tabs await browser.tabs.query({ active: true }); await browser.tabs.sendMessage(tabId, message); // Runtime await browser.runtime.sendMessage(message); browser.runtime.onMessage.addListener(handler); // Storage await browser.storage.local.get(key); await browser.storage.local.set({ key: value }); await browser.storage.sync.set({ key: value }); // Action (toolbar icon) browser.action.onClicked.addListener(handler); await browser.action.setBadgeText({ text: '5' }); await browser.action.setIcon({ path: '/icon.png' }); ``` ## Configuration API ### defineConfig() ```typescript import { defineConfig } from 'wxt'; export default defineConfig({ // Target browser browser: 'chrome' | 'firefox' | 'edge' | 'safari', // Modules (framework integration) modules: ['@wxt-dev/module-react'], // Manifest overrides manifest: { name: string, description: string, version: string, permissions: string[], host_permissions: string[], content_security_policy: { extension_pages: string, }, }, // Vite configuration vite: () => ({ // Vite config }), }); ``` ### defineWebExtConfig() Configure browser runner behavior during development. ```typescript import { defineWebExtConfig } from 'wxt'; export default defineWebExtConfig({ // Custom browser binary paths binaries: { chrome: '/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta', firefox: 'firefoxdeveloperedition', edge: '/usr/bin/microsoft-edge-dev', }, // Chrome/Chromium-specific arguments chromiumArgs: [ '--user-data-dir=./.wxt/chrome-data', '--disable-features=DialMediaRouteProvider', ], // Firefox-specific arguments firefoxArgs: [ '--profile', './.wxt/firefox-profile', ], // Keep profile data between runs (preserves logins, storage) keepProfileChanges: true, // Browser to launch (overrides wxt.config.ts) target: 'chrome-mv3', // Start URL when browser opens startUrl: 'https://example.com', // Additional preferences chromiumProfile: './.wxt/chrome-profile', firefoxProfile: './.wxt/firefox-profile', }); ``` **Common Use Cases:** #### Development with Chrome Beta/Canary ```typescript // web-ext.config.ts export default defineWebExtConfig({ binaries: { chrome: '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary', }, chromiumArgs: [ '--user-data-dir=./.wxt/chrome-canary-data', ], }); ``` #### Persistent Login Sessions ```typescript // web-ext.config.ts export default defineWebExtConfig({ keepProfileChanges: true, chromiumProfile: './.wxt/dev-profile', // Start with test page open startUrl: 'https://app.example.com/dashboard', }); ``` #### Firefox Developer Edition ```typescript // web-ext.config.ts export default defineWebExtConfig({ binaries: { firefox: 'firefoxdeveloperedition', }, firefoxArgs: [ '--profile', './.wxt/firefox-dev-profile', ], keepProfileChanges: true, }); ``` **Configuration File Location:** `web-ext.config.ts` in project root ## Context APIs ### ContentScriptContext Available in content scripts: ```typescript interface ContentScriptContext { addEventListener<K extends keyof WindowEventMap>( target: Window | Document | HTMLElement, type: K, listener: (event: WindowEventMap[K]) => void ): void; isValid: boolean; signal: AbortSignal; } ``` ## Utility Functions ### MatchPattern Pattern matching for URLs: ```typescript import { MatchPattern } from 'wxt/match-pattern'; const pattern = new MatchPattern('*://*.youtube.com/*'); if (pattern.includes('https://www.youtube.com/watch')) { // Matches } ``` ### Location Change Detection Detect SPA navigation: ```typescript ctx.addEventListener(window, 'wxt:locationchange', ({ newUrl }) => { console.log('Navigated to:', newUrl); }); ``` ## Build APIs ### Environment Variables ```typescript // Available at build time import.meta.env.MODE // 'development' | 'production' import.meta.env.BROWSER // 'chrome' | 'firefox' | etc. import.meta.env.VITE_* // Custom env variables ``` ### Asset URLs ```typescript // Get public asset URL const iconUrl = browser.runtime.getURL('/icon/128.png'); ``` ## Hooks System WXT provides build hooks for customization: ```typescript export default defineConfig({ hooks: { 'build:manifestGenerated': (wxt, manifest) => { // Modify manifest before writing }, 'build:publicAssets': (wxt, assets) => { // Add/modify public assets }, }, }); ```
-
-
LICENSE.txt 8.9 KB
Apache License Version 2.0, January 2004 https://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS -
SKILL.md 8.3 KB
--- name: chrome-extension-wxt description: Build Chrome extensions with the WXT framework and TypeScript, React, Vue, or Svelte. Use when creating browser extensions or cross-browser add-ons. Triggers on "chrome extension", "browser extension", WXT, manifest v3, or wxt.config.ts. metadata: version: "1.1.5" categories: "development" topics: "chrome-extension, wxt, browser-extension, typescript, react" openclaw: homepage: https://github.com/tenequm/skills/tree/main/skills/chrome-extension-wxt emoji: "🧩" --- # Chrome Extension Development with WXT Build modern, cross-browser extensions using WXT - the next-generation framework that supports Chrome, Firefox, Edge, Safari, and all Chromium browsers with a single codebase. ## When to Use This Skill Use this skill when: - Creating a new Chrome/browser extension - Setting up WXT development environment - Building extension features (popup, content scripts, background scripts) - Implementing cross-browser compatibility - Working with Manifest V3 (mandatory standard as of 2025, V2 deprecated) - Integrating React 19, Vue, Svelte, or Solid with extensions ## Quick Start Workflow ### 1. Initialize WXT Project ```bash # Create new project with framework of choice npm create wxt@latest # Or with specific template npm create wxt@latest -- --template react-ts npm create wxt@latest -- --template vue-ts npm create wxt@latest -- --template svelte-ts ``` ### 2. Project Structure WXT uses file-based conventions: ``` project/ ├── entrypoints/ # Auto-discovered entry points │ ├── background.ts # Service worker │ ├── content.ts # Content script │ ├── popup.html # Popup UI │ └── options.html # Options page ├── components/ # Auto-imported UI components ├── utils/ # Auto-imported utilities ├── public/ # Static assets │ └── icon/ # Extension icons ├── wxt.config.ts # Configuration └── package.json ``` ### 3. Development Commands ```bash npm run dev # Start dev server with HMR npm run build # Production build npm run zip # Package for store submission ``` ## Core Entry Points WXT recognizes entry points by filename in `entrypoints/` directory: ### Background Script (Service Worker) ```typescript // entrypoints/background.ts export default defineBackground({ type: 'module', persistent: false, main() { // Listen for extension events browser.action.onClicked.addListener((tab) => { console.log('Extension clicked', tab); }); // Handle messages browser.runtime.onMessage.addListener((message, sender, sendResponse) => { // Handle message sendResponse({ success: true }); return true; // Keep channel open for async }); }, }); ``` ### Content Script ```typescript // entrypoints/content.ts export default defineContentScript({ matches: ['*://*.example.com/*'], runAt: 'document_end', main(ctx) { // Content script logic console.log('Content script loaded'); // Create UI const ui = createShadowRootUi(ctx, { name: 'my-extension-ui', position: 'inline', anchor: 'body', onMount(container) { // Mount React/Vue component const root = ReactDOM.createRoot(container); root.render(<App />); }, }); ui.mount(); }, }); ``` ### Popup UI ```typescript // entrypoints/popup/main.tsx import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App'; ReactDOM.createRoot(document.getElementById('root')!).render( <React.StrictMode> <App /> </React.StrictMode> ); ``` ```html <!-- entrypoints/popup/index.html --> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Extension Popup</title> </head> <body> <div id="root"></div> <script type="module" src="./main.tsx"></script> </body> </html> ``` ## Configuration ### Basic wxt.config.ts ```typescript import { defineConfig } from 'wxt'; export default defineConfig({ // Framework integration modules: ['@wxt-dev/module-react'], // Manifest configuration manifest: { name: 'My Extension', description: 'Extension description', permissions: ['storage', 'activeTab'], host_permissions: ['*://example.com/*'], }, // Browser target browser: 'chrome', // or 'firefox', 'edge', 'safari' }); ``` ## Common Patterns ### Type-Safe Storage ```typescript // utils/storage.ts import { storage } from 'wxt/storage'; export const storageHelper = { async get<T>(key: string): Promise<T | null> { return await storage.getItem<T>(`local:${key}`); }, async set<T>(key: string, value: T): Promise<void> { await storage.setItem(`local:${key}`, value); }, watch<T>(key: string, callback: (newValue: T | null) => void) { return storage.watch<T>(`local:${key}`, callback); }, }; ``` ### Type-Safe Messaging ```typescript // utils/messaging.ts interface Messages { 'get-data': { request: { key: string }; response: { value: any }; }; } export async function sendMessage<K extends keyof Messages>( type: K, payload: Messages[K]['request'] ): Promise<Messages[K]['response']> { return await browser.runtime.sendMessage({ type, payload }); } ``` ### Script Injection ```typescript // Inject script into page context import { injectScript } from 'wxt/client'; await injectScript('/injected.js', { keepInDom: false, }); ``` ## Building & Deployment ### Production Build ```bash # Build for specific browser npm run build -- --browser=chrome npm run build -- --browser=firefox # Create store-ready ZIP npm run zip npm run zip -- --browser=firefox ``` ### Multi-Browser Build ```bash # Build for all browsers npm run zip:all ``` Output: `.output/my-extension-{version}-{browser}.zip` ## Modern Stacks (2025) Popular technology combinations for building Chrome extensions: ### WXT + React + Tailwind + shadcn/ui Most popular stack in 2025. Combines utility-first styling with pre-built accessible components. ```bash npm create wxt@latest -- --template react-ts npm install -D tailwindcss postcss autoprefixer npx tailwindcss init -p npx shadcn@latest init ``` **Best for:** Modern UIs with consistent design system **Example:** https://github.com/imtiger/wxt-react-shadcn-tailwindcss-chrome-extension ### WXT + React + Mantine UI Complete component library with 100+ components and built-in dark mode. ```bash npm create wxt@latest -- --template react-ts npm install @mantine/core @mantine/hooks ``` **Best for:** Feature-rich extensions needing complex components **Example:** https://github.com/ongkay/WXT-Mantine-Tailwind-Browser-Extension ### WXT + React + TypeScript (Minimal) Clean setup for custom designs without UI library dependencies. ```bash npm create wxt@latest -- --template react-ts ``` **Best for:** Simple extensions or highly custom designs ## Advanced Topics For detailed information on advanced topics, see the reference files: - **React Integration**: See `references/react-integration.md` for complete React setup, hooks, state management, and popular UI libraries - **Chrome APIs**: See `references/chrome-api.md` for comprehensive Chrome Extension API reference with examples - **Chrome 140+ Features**: See `references/chrome-140-features.md` for latest Chrome Extension APIs (sidePanel.getLayout(), etc.) - **WXT API**: See `references/wxt-api.md` for complete WXT framework API documentation - **Best Practices**: See `references/best-practices.md` for security, performance, and architecture patterns ## Troubleshooting Common issues and solutions: 1. **Module not found errors**: Ensure modules are installed and properly imported 2. **CSP violations**: Update `content_security_policy` in manifest 3. **Hot reload not working**: Check browser console for errors 4. **Storage not persisting**: Use `storage.local` or `storage.sync` correctly For deeper guidance on avoiding these issues, see [references/best-practices.md](references/best-practices.md). ## Resources ### Official Documentation - WXT Docs: https://wxt.dev - Chrome Extension Docs: https://developer.chrome.com/docs/extensions - Firefox Extension Docs: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons ### Bundled Resources - **references/**: Detailed documentation for advanced features Use these resources as needed when building your extension.
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.