wxt-framework-patterns

WXT Framework Patterns

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "wxt-framework-patterns" with this command: npx skills add arustydev/ai/arustydev-ai-wxt-framework-patterns

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' &#x26;&#x26; {
  browser_specific_settings: {
    gecko: {
      id: 'my-extension@example.com',
      strict_min_version: '109.0',
      data_collection_permissions: {
        required: [],
        optional: ['technicalAndInteraction'],
      },
    },
  },
}),

// Chrome-specific
...(browser === 'chrome' &#x26;&#x26; {
  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

  • WXT Documentation

  • MDN: browser_specific_settings

  • Firefox MV3 Migration Guide

  • Apple: Privacy Manifest Files

  • Apple: Safari Web Extensions

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

pkgmgr-homebrew-formula-dev

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

cross-browser-compatibility

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

content-structure-patterns

No summary provided by upstream source.

Repository SourceNeeds Review