skills/arustydev/ai/wxt-framework-patterns

wxt-framework-patterns

SKILL.md

WXT Framework Patterns

Comprehensive guide for building cross-browser extensions with WXT, including security hardening, Firefox/Safari specifics, and production patterns.

Overview

WXT is the leading framework for browser extension development, offering:

  • Cross-browser support: Chrome, Firefox, Edge, Safari
  • Manifest agnostic: MV2 and MV3 from single codebase
  • File-based entrypoints: Auto-generated manifest
  • Vite-powered: Fast HMR for all script types
  • Framework agnostic: React, Vue, Svelte, Solid, vanilla

This skill covers:

  • Project structure and entrypoint patterns
  • Configuration and manifest generation
  • Security hardening rules (49 rules)
  • Firefox-specific patterns
  • Safari-specific patterns
  • Testing and debugging

This skill does NOT cover:

  • General JavaScript/TypeScript patterns
  • Specific UI framework implementations
  • Store submission process (see store-submission skill)

Quick Reference

CLI Commands

Command Purpose
wxt Start dev mode with HMR
wxt build Production build
wxt build -b firefox Firefox-specific build
wxt zip Package for distribution
wxt prepare Generate TypeScript types
wxt clean Clean output directories
wxt submit Publish to stores

Entrypoint Types

Type File Manifest Key
Background entrypoints/background.ts background.service_worker
Content Script entrypoints/content.ts content_scripts
Popup entrypoints/popup/ action.default_popup
Options entrypoints/options/ options_page
Side Panel entrypoints/sidepanel/ side_panel
Unlisted entrypoints/*.ts Not in manifest

Project Structure

my-extension/
├── entrypoints/
│   ├── background.ts           # Service worker
│   ├── content.ts              # Content script
│   ├── content/                # Multi-file content script
│   │   ├── index.ts
│   │   └── styles.css
│   ├── popup/
│   │   ├── index.html
│   │   ├── main.ts
│   │   └── App.vue
│   ├── options/
│   │   └── index.html
│   └── sidepanel/
│       └── index.html
├── public/
│   └── icon/
│       ├── 16.png
│       ├── 32.png
│       ├── 48.png
│       └── 128.png
├── utils/                      # Shared utilities
├── wxt.config.ts               # WXT configuration
├── tsconfig.json
└── package.json

Entrypoint Patterns

Background Script (Service Worker)

// entrypoints/background.ts
export default defineBackground(() => {
  console.log('Extension loaded', { id: browser.runtime.id });

  // Handle messages from content scripts
  browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
    if (message.type === 'getData') {
      handleGetData(message.payload).then(sendResponse);
      return true; // Keep channel open for async response
    }
  });

  // Use alarms for recurring tasks (MV3 service worker friendly)
  browser.alarms.create('sync', { periodInMinutes: 5 });
  browser.alarms.onAlarm.addListener((alarm) => {
    if (alarm.name === 'sync') {
      performSync();
    }
  });
});

Content Script

// entrypoints/content.ts
export default defineContentScript({
  matches: ['*://*.example.com/*'],
  runAt: 'document_idle',

  main(ctx) {
    console.log('Content script loaded');

    // Use context for lifecycle management
    ctx.onInvalidated(() => {
      console.log('Extension updated/disabled');
      cleanup();
    });

    // Create isolated UI
    const ui = createShadowRootUi(ctx, {
      name: 'my-extension-ui',
      position: 'inline',
      anchor: '#target-element',
      onMount(container) {
        // Mount your UI framework here
        return mount(App, { target: container });
      },
      onRemove(app) {
        app.$destroy();
      },
    });

    ui.mount();
  },
});

Content Script with Main World Access

// entrypoints/content.ts
export default defineContentScript({
  matches: ['*://*.example.com/*'],
  world: 'MAIN', // Access page's JavaScript context

  main() {
    // Can access page's window object
    window.myExtensionApi = {
      getData: () => { /* ... */ }
    };
  },
});

Popup with Framework

<!-- entrypoints/popup/index.html -->
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
  <div id="app"></div>
  <script type="module" src="./main.ts"></script>
</body>
</html>
// entrypoints/popup/main.ts
import { createApp } from 'vue';
import App from './App.vue';
import './style.css';

createApp(App).mount('#app');

Configuration

Basic Configuration

// wxt.config.ts
import { defineConfig } from 'wxt';

export default defineConfig({
  srcDir: 'src',
  entrypointsDir: 'src/entrypoints',
  outDir: 'dist',

  manifest: {
    name: 'My Extension',
    description: 'Extension description',
    version: '1.0.0',
    permissions: ['storage', 'activeTab'],
    host_permissions: ['*://*.example.com/*'],
  },
});

Cross-Browser Configuration

// wxt.config.ts
import { defineConfig } from 'wxt';

export default defineConfig({
  manifest: ({ browser }) => ({
    name: 'My Extension',
    description: 'Cross-browser extension',

    // Browser-specific settings
    ...(browser === 'firefox' && {
      browser_specific_settings: {
        gecko: {
          id: 'my-extension@example.com',
          strict_min_version: '109.0',
          data_collection_permissions: {
            required: [],
            optional: ['technicalAndInteraction'],
          },
        },
      },
    }),

    // Chrome-specific
    ...(browser === 'chrome' && {
      minimum_chrome_version: '116',
    }),
  }),
});

Per-Browser Entrypoint Options

// entrypoints/background.ts
export default defineBackground({
  // Different behavior per browser
  persistent: {
    firefox: true,  // Use persistent background in Firefox
    chrome: false,  // Service worker in Chrome
  },

  main() {
    // ...
  },
});

Security Hardening Rules

Manifest Security (Rules 1-10)

# Rule Rationale
1 Minimize permissions Request only what's needed
2 Use optional_permissions Request sensitive permissions at runtime
3 Scope host_permissions Narrow to specific domains, never <all_urls>
4 Set minimum_chrome_version Ensure security features are available
5 Avoid externally_connectable wildcards Limit which sites can message extension
6 Set strict CSP No unsafe-eval, no external scripts
7 Use web_accessible_resources sparingly Fingerprinting risk
8 Never expose source maps Hide implementation details
9 Remove debug permissions in production e.g., management, debugger
10 Validate manifest with wxt build --analyze Catch permission bloat

Content Script Security (Rules 11-20)

# Rule Rationale
11 Use Shadow DOM for injected UI Style isolation, DOM encapsulation
12 Never use innerHTML with untrusted data XSS prevention
13 Validate all messages from page Don't trust window.postMessage
14 Use ContentScriptContext for cleanup Prevent memory leaks
15 Avoid storing sensitive data in DOM Page scripts can read it
16 Use document_idle over document_start Less intrusive, more stable
17 Scope CSS selectors narrowly Avoid page conflicts
18 Never inject into banking/payment pages High-risk surfaces
19 Use MutationObserver over polling Performance
20 Validate URL before injecting Prevent injection on wrong pages

Background Script Security (Rules 21-30)

# Rule Rationale
21 Persist state to chrome.storage Service worker terminates
22 Use chrome.alarms over setInterval Survives worker restart
23 Validate all incoming messages Don't trust content scripts
24 Never store secrets in code Use secure storage
25 Use HTTPS for all fetch requests Data in transit security
26 Implement rate limiting Prevent abuse
27 Log security events Audit trail
28 Handle extension update gracefully Reconnect content scripts
29 Use webRequest carefully Performance impact
30 Avoid long-running operations Service worker termination

Storage Security (Rules 31-40)

# Rule Rationale
31 Use storage.local for sensitive data Not synced to cloud
32 Encrypt sensitive values Defense in depth
33 Implement storage quotas Prevent unbounded growth
34 Validate data before storing Type safety
35 Use versioned schema migrations Data integrity
36 Clear storage on uninstall User privacy
37 Don't store PII without consent GDPR/CCPA compliance
38 Use storage.session for temporary data Auto-cleared
39 Implement backup/restore Data recovery
40 Audit storage access Security logging

Communication Security (Rules 41-49)

# Rule Rationale
41 Use runtime.sendMessage over postMessage Type-safe, scoped
42 Validate sender in message handlers Prevent spoofing
43 Never pass functions in messages Serialization issues
44 Chunk large data transfers Memory efficiency
45 Use typed message protocols Maintainability
46 Implement request timeouts Prevent hanging
47 Handle disconnection gracefully Tab closed, extension disabled
48 Don't expose internal APIs externally Use separate handlers
49 Log and monitor message patterns Detect anomalies

Firefox-Specific Patterns

Required Gecko Settings

// wxt.config.ts
manifest: {
  browser_specific_settings: {
    gecko: {
      // Required for AMO submission
      id: 'my-extension@example.com',

      // Version constraints
      strict_min_version: '109.0',

      // Data collection (required since Nov 2025)
      data_collection_permissions: {
        required: [],
        optional: ['technicalAndInteraction'],
      },
    },

    // Firefox for Android
    gecko_android: {
      strict_min_version: '120.0',
    },
  },
}

Firefox MV3 Differences

Feature Chrome MV3 Firefox MV3
Background Service worker only Event page supported
Persistent No Optional with persistent: true
browser API Promisified polyfill needed Native promises
DNR Full support Partial support
Side Panel Supported Not supported

Firefox-Specific Build

# Build for Firefox only
wxt build -b firefox

# Build MV2 for Firefox (if needed)
wxt build -b firefox --mv2

Handling Firefox Differences

// utils/browser-detect.ts
export const isFirefox = navigator.userAgent.includes('Firefox');

// entrypoints/background.ts
export default defineBackground({
  persistent: isFirefox, // Keep background alive in Firefox

  main() {
    if (isFirefox) {
      // Firefox-specific initialization
    }
  },
});

Safari-Specific Patterns

Xcode Project Requirements

Safari extensions require an Xcode host app:

# Convert existing extension to Safari
xcrun safari-web-extension-converter /path/to/extension \
  --project-location /path/to/output \
  --app-name "My Extension" \
  --bundle-identifier com.example.myextension

Privacy Manifest (Required)

Every Safari extension host app needs PrivacyInfo.xcprivacy:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>NSPrivacyTracking</key>
  <false/>
  <key>NSPrivacyTrackingDomains</key>
  <array/>
  <key>NSPrivacyCollectedDataTypes</key>
  <array/>
  <key>NSPrivacyAccessedAPITypes</key>
  <array/>
</dict>
</plist>

Safari Limitations

Feature Status Workaround
Side Panel Not supported Use popup
declarativeNetRequest Limited Use webRequest
offscreen API Not supported Use content script
Persistent background Not supported State persistence
chrome.scripting.executeScript Limited Declare in manifest

Safari Build Workflow

# 1. Build extension
wxt build -b safari

# 2. Convert to Xcode project
xcrun safari-web-extension-converter dist/safari-mv3 \
  --project-location safari-app

# 3. Open in Xcode
open safari-app/MyExtension.xcodeproj

# 4. Add PrivacyInfo.xcprivacy to host app target

# 5. Archive and submit to App Store

TestFlight Distribution

As of 2025, Safari extensions can be submitted as ZIP files to App Store Connect for TestFlight testing without needing Xcode locally.

Storage Patterns

Using WXT Storage Utility

// utils/storage.ts
import { storage } from 'wxt/storage';

// Define typed storage items
export const userSettings = storage.defineItem<{
  theme: 'light' | 'dark';
  notifications: boolean;
}>('local:settings', {
  defaultValue: {
    theme: 'light',
    notifications: true,
  },
});

export const sessionData = storage.defineItem<string[]>(
  'session:recentTabs',
  { defaultValue: [] }
);

// Usage
const settings = await userSettings.getValue();
await userSettings.setValue({ ...settings, theme: 'dark' });

// Watch for changes
userSettings.watch((newValue, oldValue) => {
  console.log('Settings changed:', newValue);
});

Storage Migrations

// utils/storage.ts
import { storage } from 'wxt/storage';

export const userPrefs = storage.defineItem('local:prefs', {
  defaultValue: { version: 2, theme: 'system' },

  migrations: [
    // v1 -> v2: renamed 'darkMode' to 'theme'
    {
      version: 2,
      migrate(oldValue: { darkMode?: boolean }) {
        return {
          version: 2,
          theme: oldValue.darkMode ? 'dark' : 'light',
        };
      },
    },
  ],
});

Testing Patterns

Unit Testing with Vitest

// tests/background.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { fakeBrowser } from 'wxt/testing';

describe('background script', () => {
  beforeEach(() => {
    fakeBrowser.reset();
  });

  it('handles getData message', async () => {
    // Setup fake response
    fakeBrowser.storage.local.get.mockResolvedValue({ data: 'test' });

    // Import and run background script
    await import('../entrypoints/background');

    // Simulate message
    const [listener] = fakeBrowser.runtime.onMessage.addListener.mock.calls[0];
    const response = await new Promise((resolve) => {
      listener({ type: 'getData' }, {}, resolve);
    });

    expect(response).toEqual({ data: 'test' });
  });
});

E2E Testing

// tests/e2e/extension.test.ts
import { test, expect, chromium } from '@playwright/test';
import path from 'path';

test('popup shows correct UI', async () => {
  const extensionPath = path.join(__dirname, '../../dist/chrome-mv3');

  const context = await chromium.launchPersistentContext('', {
    headless: false,
    args: [
      `--disable-extensions-except=${extensionPath}`,
      `--load-extension=${extensionPath}`,
    ],
  });

  // Get extension ID
  const [background] = context.serviceWorkers();
  const extensionId = background.url().split('/')[2];

  // Open popup
  const popup = await context.newPage();
  await popup.goto(`chrome-extension://${extensionId}/popup.html`);

  await expect(popup.locator('h1')).toHaveText('My Extension');
});

Production Checklist

Before Build

  • Remove console.log statements
  • Set production environment variables
  • Verify all permissions are necessary
  • Test on all target browsers
  • Run security audit (npm audit)
  • Check bundle size (wxt build --analyze)

Manifest Validation

  • Extension name and description are accurate
  • Icons in all required sizes (16, 32, 48, 128)
  • Version follows semver
  • Gecko ID set for Firefox
  • Privacy manifest for Safari
  • CSP is strict (no unsafe-eval)

Cross-Browser Build

# Build all browsers
wxt build -b chrome
wxt build -b firefox
wxt build -b safari
wxt build -b edge

# Package for submission
wxt zip -b chrome
wxt zip -b firefox

References

Weekly Installs
9
Repository
arustydev/ai
GitHub Stars
6
First Seen
Feb 21, 2026
Installed on
opencode9
antigravity9
claude-code9
github-copilot9
codex9
zencoder9