Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 09:01:07 +08:00
commit 21a29589ab
9 changed files with 3283 additions and 0 deletions

View File

@@ -0,0 +1,402 @@
# 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, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#x27;');
}
```
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
```

View File

@@ -0,0 +1,284 @@
# 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

View File

@@ -0,0 +1,885 @@
# 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

View File

@@ -0,0 +1,991 @@
# 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);
}, []);
```

View File

@@ -0,0 +1,322 @@
# 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
},
},
});
```