7.8 KiB
7.8 KiB
Chrome Extension Best Practices with WXT
Security, performance, and architecture recommendations.
Security
Content Security Policy
Always configure CSP for extension pages:
export default defineConfig({
manifest: {
content_security_policy: {
extension_pages: "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'",
},
},
});
Minimal Permissions
Request only necessary permissions:
// 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:
manifest: {
permissions: ['storage'],
optional_permissions: ['tabs', 'bookmarks'],
}
Input Validation
Always sanitize user input:
function sanitizeInput(input: string): string {
return input
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
Use DOMPurify for HTML content:
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:
// 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:
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:
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:
export default defineConfig({
vite: () => ({
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
utils: ['date-fns', 'lodash-es'],
},
},
},
},
}),
});
Caching Strategy
Cache API responses appropriately:
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:
// 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:
// 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:
// 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
// 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
// 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:
{
"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
# .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