Initial commit
This commit is contained in:
12
.claude-plugin/plugin.json
Normal file
12
.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "chrome-extension-wxt",
|
||||||
|
"description": "Skill: Build Chrome extensions with WXT framework",
|
||||||
|
"version": "0.0.0-2025.11.28",
|
||||||
|
"author": {
|
||||||
|
"name": "Misha Kolesnik",
|
||||||
|
"email": "misha@kolesnik.io"
|
||||||
|
},
|
||||||
|
"skills": [
|
||||||
|
"./skills/skill"
|
||||||
|
]
|
||||||
|
}
|
||||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# chrome-extension-wxt
|
||||||
|
|
||||||
|
Skill: Build Chrome extensions with WXT framework
|
||||||
64
plugin.lock.json
Normal file
64
plugin.lock.json
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
{
|
||||||
|
"$schema": "internal://schemas/plugin.lock.v1.json",
|
||||||
|
"pluginId": "gh:tenequm/claude-plugins:chrome-extension-wxt",
|
||||||
|
"normalized": {
|
||||||
|
"repo": null,
|
||||||
|
"ref": "refs/tags/v20251128.0",
|
||||||
|
"commit": "272061d27658e70d70a09192292b477a3c23e3f7",
|
||||||
|
"treeHash": "4ad808461d8cc467ef9aa37e2ccba49fa646b9003b661af74efa9a123697efaf",
|
||||||
|
"generatedAt": "2025-11-28T10:28:37.260757Z",
|
||||||
|
"toolVersion": "publish_plugins.py@0.2.0"
|
||||||
|
},
|
||||||
|
"origin": {
|
||||||
|
"remote": "git@github.com:zhongweili/42plugin-data.git",
|
||||||
|
"branch": "master",
|
||||||
|
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
|
||||||
|
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
|
||||||
|
},
|
||||||
|
"manifest": {
|
||||||
|
"name": "chrome-extension-wxt",
|
||||||
|
"description": "Skill: Build Chrome extensions with WXT framework"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"path": "README.md",
|
||||||
|
"sha256": "12e1b406625bc8eaadf0550c57d19dabeec260dc690b1edd12caac75f7ed1036"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": ".claude-plugin/plugin.json",
|
||||||
|
"sha256": "e699866ee2b6a7bd29a85f815538722ceaed46677412545536acd5b603d20e5e"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/skill/SKILL.md",
|
||||||
|
"sha256": "b4673cacf3dd11831121a608233e39563655510c610a0212d03529dc9b4e0847"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/skill/references/chrome-api.md",
|
||||||
|
"sha256": "6e679d0e9992be920d365c172666b81e06483a1569fac1107c83a1c0c5b14ecd"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/skill/references/wxt-api.md",
|
||||||
|
"sha256": "6728d10a8a983d90d4eaf80cd17b510e7551dfc7f8684d1ce0e2e512a2f0eebf"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/skill/references/best-practices.md",
|
||||||
|
"sha256": "0ff82d5a327d840c01e0cfe46cbe06a266ad2d29117152b7f0a47ffbc443363b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/skill/references/react-integration.md",
|
||||||
|
"sha256": "6c315c9f097cc4a84cb03586f484be5d44abad1f3ac0bf3e217987c6d30e33b8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/skill/references/chrome-140-features.md",
|
||||||
|
"sha256": "7dee9fb90f8feffba297ad17e2631be4bd641b093f0088e47b62afa37a540217"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dirSha256": "4ad808461d8cc467ef9aa37e2ccba49fa646b9003b661af74efa9a123697efaf"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"scannedAt": null,
|
||||||
|
"scannerVersion": null,
|
||||||
|
"flags": []
|
||||||
|
}
|
||||||
|
}
|
||||||
320
skills/skill/SKILL.md
Normal file
320
skills/skill/SKILL.md
Normal file
@@ -0,0 +1,320 @@
|
|||||||
|
---
|
||||||
|
name: chrome-extension-wxt
|
||||||
|
description: 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", "manifest v3", or file patterns like wxt.config.ts.
|
||||||
|
---
|
||||||
|
|
||||||
|
# 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 detailed troubleshooting, see `references/troubleshooting.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
|
||||||
|
|
||||||
|
- **scripts/**: Helper utilities for common extension tasks
|
||||||
|
- **references/**: Detailed documentation for advanced features
|
||||||
|
- **assets/**: Starter templates and example components
|
||||||
|
|
||||||
|
Use these resources as needed when building your extension.
|
||||||
402
skills/skill/references/best-practices.md
Normal file
402
skills/skill/references/best-practices.md
Normal 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, '<')
|
||||||
|
.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
|
||||||
|
```
|
||||||
284
skills/skill/references/chrome-140-features.md
Normal file
284
skills/skill/references/chrome-140-features.md
Normal 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
|
||||||
885
skills/skill/references/chrome-api.md
Normal file
885
skills/skill/references/chrome-api.md
Normal 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
|
||||||
991
skills/skill/references/react-integration.md
Normal file
991
skills/skill/references/react-integration.md
Normal 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);
|
||||||
|
}, []);
|
||||||
|
```
|
||||||
322
skills/skill/references/wxt-api.md
Normal file
322
skills/skill/references/wxt-api.md
Normal 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
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user