commit 21a29589abaffcece49c47a7c6b60a60a7fd7643 Author: Zhongwei Li Date: Sun Nov 30 09:01:07 2025 +0800 Initial commit diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..c8b7e95 --- /dev/null +++ b/.claude-plugin/plugin.json @@ -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" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..a76d9d5 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# chrome-extension-wxt + +Skill: Build Chrome extensions with WXT framework diff --git a/plugin.lock.json b/plugin.lock.json new file mode 100644 index 0000000..3d2a64f --- /dev/null +++ b/plugin.lock.json @@ -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": [] + } +} \ No newline at end of file diff --git a/skills/skill/SKILL.md b/skills/skill/SKILL.md new file mode 100644 index 0000000..d3d81ba --- /dev/null +++ b/skills/skill/SKILL.md @@ -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(); + }, + }); + + 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( + + + +); +``` + +```html + + + + + + Extension Popup + + +
+ + + +``` + +## 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(key: string): Promise { + return await storage.getItem(`local:${key}`); + }, + + async set(key: string, value: T): Promise { + await storage.setItem(`local:${key}`, value); + }, + + watch(key: string, callback: (newValue: T | null) => void) { + return storage.watch(`local:${key}`, callback); + }, +}; +``` + +### Type-Safe Messaging + +```typescript +// utils/messaging.ts +interface Messages { + 'get-data': { + request: { key: string }; + response: { value: any }; + }; +} + +export async function sendMessage( + type: K, + payload: Messages[K]['request'] +): Promise { + 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. diff --git a/skills/skill/references/best-practices.md b/skills/skill/references/best-practices.md new file mode 100644 index 0000000..d5a8694 --- /dev/null +++ b/skills/skill/references/best-practices.md @@ -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: ['', '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, '''); +} +``` + +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 }; + response: { success: boolean }; + }; +} + +// utils/messaging.ts +export async function sendMessage( + type: K, + payload: MessageMap[K]['request'] +): Promise { + 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; + updateSettings: (updates: Record) => void; +} + +export const useStore = create()( + 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 +``` diff --git a/skills/skill/references/chrome-140-features.md b/skills/skill/references/chrome-140-features.md new file mode 100644 index 0000000..845af60 --- /dev/null +++ b/skills/skill/references/chrome-140-features.md @@ -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 ( +
+

Panel is positioned on the {panelSide}

+ {/* Adjust UI layout based on panel side */} +
+ ); +} +``` + +##### 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 ( +
+
+

Side Panel Content

+
+
+

Current side: {side}

+
+
+ ); +} +``` + +#### 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 ; + + return ( +
+ {/* Content positioned based on panel location */} +
+ ); +} +``` + +#### 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 diff --git a/skills/skill/references/chrome-api.md b/skills/skill/references/chrome-api.md new file mode 100644 index 0000000..9abf9f9 --- /dev/null +++ b/skills/skill/references/chrome-api.md @@ -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: [''] }, + ['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 diff --git a/skills/skill/references/react-integration.md b/skills/skill/references/react-integration.md new file mode 100644 index 0000000..ae6c79c --- /dev/null +++ b/skills/skill/references/react-integration.md @@ -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 + + + + + + Extension Popup + + +
+ + + +``` + +**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( + + + +); +``` + +**entrypoints/popup/App.tsx:** +```typescript +import { useState, useEffect } from 'react'; + +export default function App() { + const [data, setData] = useState(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
Loading...
; + } + + return ( +
+

My Extension

+
+ {/* Your UI here */} +
+
+ ); +} +``` + +### Options Page with React + +**entrypoints/options/index.html:** +```html + + + + + Extension Options + + +
+ + + +``` + +**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 ( +
+

Extension Settings

+ +
+ +
+ +
+ +
+ +
+ +
+ + +
+ ); +} +``` + +### 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(); + + 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 ( +
+ + + {visible && ( +
+

Extension Panel

+

This is injected into the page!

+
+ )} +
+ ); +} +``` + +## React Hooks for Extensions + +### useStorage Hook + +```typescript +// hooks/useStorage.ts +import { useState, useEffect } from 'react'; + +export function useStorage(key: string, defaultValue: T) { + const [value, setValue] = useState(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
Loading...
; + + return ( + + ); +} +``` + +### useMessage Hook + +```typescript +// hooks/useMessage.ts +import { useEffect, useCallback } from 'react'; + +type MessageHandler = ( + message: T, + sender: browser.Runtime.MessageSender +) => any | Promise; + +export function useMessage( + type: string, + handler: MessageHandler +) { + 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
Component listening for messages
; +} +``` + +### useTabs Hook + +```typescript +// hooks/useTabs.ts +import { useState, useEffect } from 'react'; + +export function useTabs() { + const [tabs, setTabs] = useState([]); + + 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 ( +
    + {tabs.map((tab) => ( +
  • + {tab.title} - {tab.url} +
  • + ))} +
+ ); +} +``` + +## 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; +} + +const ExtensionContext = createContext(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 ( + + {children} + + ); +} + +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 ( + + + + ); +} +``` + +### 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()( + 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 ( + + ); +} +``` + +## 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 {} + +const Button = React.forwardRef( + ({ className, ...props }, ref) => { + return ( + + ); +} +``` + +### 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 ; +} +``` + +## Performance Optimization + +### Code Splitting + +```typescript +// Lazy load heavy components +import { lazy, Suspense } from 'react'; + +const HeavyComponent = lazy(() => import('./HeavyComponent')); + +function App() { + return ( + Loading...}> + + + ); +} +``` + +### React.memo for Expensive Components + +```typescript +import { memo } from 'react'; + +const ExpensiveComponent = memo(({ data }: { data: any }) => { + // Expensive rendering logic + return
{/* rendered content */}
; +}); +``` + +### 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 ( + + {sortedData.map((item) => ( + handleClick(item.id)}> + + + ))} +
{item.name}
+ ); +} +``` + +## Common Patterns + +### Loading States + +```typescript +function DataLoader() { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + fetchData() + .then(setData) + .catch(setError) + .finally(() => setLoading(false)); + }, []); + + if (loading) return ; + if (error) return ; + if (!data) return ; + + return ; +} +``` + +### 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 ( +
+ setFormData({ ...formData, name: e.target.value })} + /> + setFormData({ ...formData, email: e.target.value })} + /> + + +
+ ); +} +``` + +### Modal/Dialog Patterns + +```typescript +function ConfirmDialog({ isOpen, onConfirm, onCancel }: any) { + if (!isOpen) return null; + + return ( +
+
e.stopPropagation()}> +

Are you sure?

+
+ + +
+
+
+ ); +} +``` + +## 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(); + expect(screen.getByText('Click me')).toBeInTheDocument(); + }); + + it('calls onClick when clicked', () => { + const handleClick = vi.fn(); + render(); + + 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); +}, []); +``` diff --git a/skills/skill/references/wxt-api.md b/skills/skill/references/wxt-api.md new file mode 100644 index 0000000..798dc2b --- /dev/null +++ b/skills/skill/references/wxt-api.md @@ -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('local:key'); + +// Set item +await storage.setItem('local:key', value); + +// Remove item +await storage.removeItem('local:key'); + +// Watch for changes +const unwatch = storage.watch('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( + 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 + }, + }, +}); +```