# WXT - Next-gen Web Extension Framework WXT is a modern framework for building cross-browser web extensions, often described as "Nuxt for Web Extensions." It provides a file-based entrypoint system, automatic manifest generation, hot module replacement during development, and built-in support for TypeScript, auto-imports, and all major frontend frameworks including Vue, React, Svelte, and Solid. The framework supports both Manifest V2 and V3, enabling developers to build extensions for Chrome, Firefox, Edge, Safari, and other browsers from a single codebase. WXT handles the complexity of cross-browser compatibility, CSS injection, content script lifecycle management, and extension publishing while providing an excellent developer experience with features like dev mode HMR, bundle analysis, and automated store submissions. ## CLI Commands ### Initialize a New Project Bootstrap a new WXT project with your preferred frontend framework and package manager. ```bash # Using npm npx wxt@latest init my-extension # Using pnpm pnpm dlx wxt@latest init my-extension # Using bun bunx wxt@latest init my-extension ``` ### Development Mode Start the development server with hot module replacement. WXT automatically opens a browser window with the extension installed. ```bash # Default (Chrome) pnpm dev # Firefox pnpm dev:firefox # or wxt -b firefox # With specific manifest version wxt --mv2 wxt --mv3 ``` ### Build for Production Build the extension for production deployment. ```bash # Build for Chrome (default) wxt build # Build for Firefox wxt build -b firefox # Build for Edge wxt build -b edge # Build with specific manifest version wxt build --mv2 wxt build --mv3 ``` ### Create ZIP for Store Submission Generate distribution ZIP files for browser extension stores. ```bash # Chrome Web Store wxt zip # Firefox Addon Store (creates extension + sources ZIPs) wxt zip -b firefox # Edge Addons wxt zip -b edge ``` ### Automated Store Submission Submit extensions to browser stores automatically using the WXT CLI. ```bash # Initialize submission credentials wxt submit init # Submit to stores (dry run first) wxt submit --dry-run \ --chrome-zip .output/my-extension-1.0.0-chrome.zip \ --firefox-zip .output/my-extension-1.0.0-firefox.zip \ --firefox-sources-zip .output/my-extension-1.0.0-sources.zip \ --edge-zip .output/my-extension-1.0.0-chrome.zip # Actual submission wxt submit \ --chrome-zip .output/my-extension-1.0.0-chrome.zip \ --firefox-zip .output/my-extension-1.0.0-firefox.zip \ --firefox-sources-zip .output/my-extension-1.0.0-sources.zip ``` ## Configuration (wxt.config.ts) ### Basic Configuration Define your extension's configuration in `wxt.config.ts` at the project root. ```typescript // wxt.config.ts import { defineConfig } from 'wxt'; export default defineConfig({ // Source directory (default: project root) srcDir: 'src', // Output directory (default: '.output') outDir: 'dist', // Entrypoints directory entrypointsDir: 'src/entrypoints', // Target browser (chrome, firefox, edge, safari) browser: 'chrome', // Manifest version manifestVersion: 3, // Build mode mode: 'production', // Debug logging debug: false, // Manifest configuration manifest: { name: 'My Extension', description: 'A WXT-powered browser extension', permissions: ['storage', 'tabs', 'activeTab'], host_permissions: ['https://*.example.com/*'], }, }); ``` ### Dynamic Manifest Configuration Generate manifest properties dynamically based on build context. ```typescript // wxt.config.ts import { defineConfig } from 'wxt'; export default defineConfig({ manifest: ({ browser, manifestVersion, mode, command }) => ({ name: mode === 'development' ? 'My Extension (DEV)' : 'My Extension', permissions: ['storage'], host_permissions: manifestVersion === 3 ? ['https://*.google.com/*'] : ['*://*.google.com/*'], action: { default_title: 'Click me', }, web_accessible_resources: [ { matches: ['*://*.google.com/*'], resources: ['injected.js', 'icon/*.png'], }, ], }), }); ``` ### Auto-imports Configuration Configure automatic imports for composables, utilities, and third-party libraries. ```typescript // wxt.config.ts import { defineConfig } from 'wxt'; export default defineConfig({ imports: { // Add directories to auto-import from dirs: ['utils', 'composables'], // Add specific imports presets: ['vue'], // Custom imports imports: [ { name: 'default', as: 'axios', from: 'axios' }, { name: 'useStore', from: '@/stores/main' }, ], }, }); ``` ### ZIP Configuration for Firefox Configure source code bundling for Firefox Addon Store submissions. ```typescript // wxt.config.ts import { defineConfig } from 'wxt'; export default defineConfig({ zip: { // Custom ZIP filename template artifactTemplate: '{{name}}-{{version}}-{{browser}}.zip', // Force sources ZIP for all browsers zipSources: true, // Sources ZIP filename template sourcesTemplate: '{{name}}-{{version}}-sources.zip', // Exclude files from sources ZIP excludeSources: ['**/*.test.ts', '**/__tests__/**'], // Include additional files in sources includeSources: ['.env.example', 'README.md'], // Download private packages for Firefox review downloadPackages: ['@mycompany/private-package'], }, }); ``` ## Entrypoints ### Background Script Define the background service worker (MV3) or background page (MV2). ```typescript // entrypoints/background.ts export default defineBackground(() => { console.log('Background script loaded!', { id: browser.runtime.id }); // Listen for extension installation browser.runtime.onInstalled.addListener(({ reason }) => { if (reason === 'install') { console.log('Extension installed'); browser.tabs.create({ url: 'https://example.com/welcome' }); } }); // Handle messages from content scripts browser.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message.type === 'GET_DATA') { fetchData().then(sendResponse); return true; // Keep channel open for async response } }); // Set up alarms browser.alarms.create('periodicTask', { periodInMinutes: 30 }); browser.alarms.onAlarm.addListener((alarm) => { if (alarm.name === 'periodicTask') { performPeriodicTask(); } }); }); async function fetchData() { const response = await fetch('https://api.example.com/data'); return response.json(); } function performPeriodicTask() { console.log('Periodic task executed'); } ``` ### Background Script with Options Configure background script behavior with manifest options. ```typescript // entrypoints/background.ts export default defineBackground({ // MV2: persistent background page persistent: true, // MV3: ES module type type: 'module', // Only include for specific browsers include: ['chrome', 'firefox'], exclude: ['safari'], main() { console.log('Background initialized'); browser.action.onClicked.addListener((tab) => { browser.scripting.executeScript({ target: { tabId: tab.id! }, files: ['content-scripts/injected.js'], }); }); }, }); ``` ### Content Script Create content scripts that run on matched pages. ```typescript // entrypoints/content.ts export default defineContentScript({ matches: ['*://*.youtube.com/*', '*://*.twitter.com/*'], excludeMatches: ['*://admin.youtube.com/*'], runAt: 'document_idle', allFrames: false, main(ctx) { console.log('Content script loaded on:', window.location.href); // Use context for invalidation-aware operations ctx.setTimeout(() => { console.log('Delayed operation'); }, 1000); ctx.addEventListener(window, 'click', (event) => { console.log('Click detected:', event.target); }); // Check if context is still valid if (ctx.isValid) { initializeFeatures(); } }, }); function initializeFeatures() { // Feature implementation } ``` ### Named Content Script Create multiple content scripts with unique names. ```typescript // entrypoints/youtube.content.ts export default defineContentScript({ matches: ['*://*.youtube.com/watch*'], runAt: 'document_start', main(ctx) { console.log('YouTube video page content script'); // Wait for video element const observer = new MutationObserver((mutations, obs) => { const video = document.querySelector('video'); if (video) { obs.disconnect(); setupVideoControls(video); } }); observer.observe(document.body, { childList: true, subtree: true }); }, }); function setupVideoControls(video: HTMLVideoElement) { // Video control implementation } ``` ### Popup HTML Page Create the extension popup using HTML entrypoints. ```html My Extension Popup
``` ```typescript // entrypoints/popup/main.ts import './style.css'; document.querySelector('#app')!.innerHTML = ` `; document.querySelector('#actionBtn')!.addEventListener('click', async () => { const [tab] = await browser.tabs.query({ active: true, currentWindow: true }); const response = await browser.tabs.sendMessage(tab.id!, { type: 'GET_PAGE_INFO' }); document.querySelector('#status')!.textContent = JSON.stringify(response); }); ``` ### Options Page Create an options/settings page for your extension. ```html Extension Options
``` ### Side Panel Create a side panel (Chrome) or sidebar (Firefox). ```html Extension Side Panel
``` ### Unlisted Script Create scripts that are not listed in the manifest but can be loaded at runtime. ```typescript // entrypoints/injected.ts export default defineUnlistedScript(() => { console.log('Injected script running in main world'); // Access page's JavaScript context const originalFetch = window.fetch; window.fetch = async (...args) => { console.log('Fetch intercepted:', args[0]); return originalFetch.apply(window, args); }; }); ``` ### Unlisted Page Create HTML pages accessible at runtime but not in the manifest. ```html Welcome

Welcome to My Extension!

``` ```typescript // Open unlisted page from background script browser.tabs.create({ url: browser.runtime.getURL('/welcome.html'), }); ``` ## Content Script UI ### Integrated UI Inject UI directly into the page DOM (affected by page CSS). ```typescript // entrypoints/overlay.content.ts export default defineContentScript({ matches: ['*://*/*'], main(ctx) { const ui = createIntegratedUi(ctx, { position: 'inline', anchor: 'body', append: 'first', onMount(container) { const app = document.createElement('div'); app.innerHTML = `

Extension Widget

`; container.append(app); app.querySelector('#close-btn')!.addEventListener('click', () => { ui.remove(); }); return app; }, onRemove(app) { app?.remove(); }, }); ui.mount(); }, }); ``` ### Shadow Root UI Inject UI with style isolation using Shadow DOM. ```typescript // entrypoints/widget.content/index.ts import './style.css'; export default defineContentScript({ matches: ['*://*/*'], cssInjectionMode: 'ui', async main(ctx) { const ui = await createShadowRootUi(ctx, { name: 'my-extension-widget', position: 'inline', anchor: 'body', append: 'last', // Isolate click events from the page isolateEvents: true, onMount(container) { container.innerHTML = `

Isolated Widget

This UI is isolated from page styles

`; container.querySelector('.action-btn')!.addEventListener('click', () => { console.log('Button clicked'); }); }, }); ui.mount(); }, }); ``` ```css /* entrypoints/widget.content/style.css */ .widget { position: fixed; bottom: 20px; right: 20px; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border-radius: 12px; font-family: system-ui, sans-serif; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); z-index: 2147483647; } .action-btn { background: white; color: #667eea; border: none; padding: 8px 16px; border-radius: 6px; cursor: pointer; } ``` ### Shadow Root UI with React Integrate React components into shadow root content script UI. ```tsx // entrypoints/react-widget.content/index.tsx import './style.css'; import ReactDOM from 'react-dom/client'; import App from './App'; export default defineContentScript({ matches: ['*://*/*'], cssInjectionMode: 'ui', async main(ctx) { const ui = await createShadowRootUi(ctx, { name: 'react-extension-widget', position: 'inline', anchor: 'body', onMount(container) { const wrapper = document.createElement('div'); container.append(wrapper); const root = ReactDOM.createRoot(wrapper); root.render(); return root; }, onRemove(root) { root?.unmount(); }, }); ui.mount(); }, }); ``` ### Shadow Root UI with Vue Integrate Vue components into shadow root content script UI. ```typescript // entrypoints/vue-widget.content/index.ts import './style.css'; import { createApp } from 'vue'; import App from './App.vue'; export default defineContentScript({ matches: ['*://*/*'], cssInjectionMode: 'ui', async main(ctx) { const ui = await createShadowRootUi(ctx, { name: 'vue-extension-widget', position: 'inline', anchor: 'body', onMount(container) { const app = createApp(App); app.mount(container); return app; }, onRemove(app) { app?.unmount(); }, }); ui.mount(); }, }); ``` ### IFrame UI Create isolated UI using an iframe with HMR support. ```typescript // entrypoints/iframe-widget.content.ts export default defineContentScript({ matches: ['*://*/*'], main(ctx) { const ui = createIframeUi(ctx, { page: '/widget-frame.html', position: 'inline', anchor: 'body', onMount(wrapper, iframe) { iframe.style.cssText = ` position: fixed; bottom: 20px; right: 20px; width: 350px; height: 400px; border: none; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.2); z-index: 2147483647; `; }, }); ui.mount(); }, }); ``` ```html Widget Frame

IFrame Widget

This runs in an isolated context with HMR support!

``` ### Auto-Mount to Dynamic Elements Automatically mount/unmount UI when target elements appear or disappear. ```typescript // entrypoints/dynamic.content.ts export default defineContentScript({ matches: ['*://*.twitter.com/*'], main(ctx) { const ui = createIntegratedUi(ctx, { position: 'inline', anchor: '[data-testid="tweet"]', onMount(container) { const button = document.createElement('button'); button.textContent = 'Custom Action'; button.style.cssText = 'margin-left: 8px; padding: 4px 8px;'; container.append(button); return button; }, onRemove(button) { button?.remove(); }, }); // Automatically mount when anchor appears, unmount when removed ui.autoMount(); }, }); ``` ## Main World Script Injection ### Inject Script into Main World Inject scripts into the page's main execution context to access page JavaScript. ```typescript // entrypoints/main-world.ts export default defineUnlistedScript(() => { console.log('Running in main world!'); // Access page's global variables console.log('Page title:', document.title); // Intercept page functions const originalPush = Array.prototype.push; Array.prototype.push = function(...args) { console.log('Array.push called with:', args); return originalPush.apply(this, args); }; }); ``` ```typescript // entrypoints/injector.content.ts export default defineContentScript({ matches: ['*://*/*'], runAt: 'document_start', async main() { await injectScript('/main-world.js', { keepInDom: true, }); console.log('Main world script injected'); }, }); ``` ```typescript // wxt.config.ts - Add to web accessible resources export default defineConfig({ manifest: { web_accessible_resources: [ { resources: ['main-world.js'], matches: ['*://*/*'], }, ], }, }); ``` ### Bidirectional Communication with Injected Script Communicate between content script and injected main world script. ```typescript // entrypoints/main-world-comm.ts export default defineUnlistedScript(() => { const script = document.currentScript; // Receive messages from content script script?.addEventListener('from-content-script', (event) => { if (event instanceof CustomEvent) { console.log('Received from content script:', event.detail); // Send response back script.dispatchEvent(new CustomEvent('from-main-world', { detail: { response: 'Hello from main world!' }, })); } }); }); ``` ```typescript // entrypoints/communicator.content.ts export default defineContentScript({ matches: ['*://*/*'], async main() { const { script } = await injectScript('/main-world-comm.js', { modifyScript(script) { // Listen for responses before script loads script.addEventListener('from-main-world', (event) => { if (event instanceof CustomEvent) { console.log('Response from main world:', event.detail); } }); }, }); // Send message after script is loaded script.dispatchEvent(new CustomEvent('from-content-script', { detail: { message: 'Hello from content script!' }, })); }, }); ``` ## Storage API ### Basic Storage Operations Use WXT's storage wrapper for simplified extension storage operations. ```typescript // utils/storage-example.ts import { storage } from '#imports'; // Get a value (must include storage area prefix) const username = await storage.getItem('local:username'); // Set a value await storage.setItem('local:username', 'john_doe'); // Remove a value await storage.removeItem('local:username'); // Get multiple values const items = await storage.getItems([ 'local:username', 'local:settings', 'sync:preferences', ]); // Set multiple values await storage.setItems([ { key: 'local:username', value: 'john_doe' }, { key: 'local:lastLogin', value: Date.now() }, ]); // Watch for changes const unwatch = storage.watch('local:username', (newValue, oldValue) => { console.log('Username changed:', { old: oldValue, new: newValue }); }); // Stop watching unwatch(); ``` ### Defined Storage Items Create type-safe, reusable storage items with default values and migrations. ```typescript // utils/storage.ts import { storage } from '#imports'; // Simple storage item with fallback export const theme = storage.defineItem<'light' | 'dark'>('local:theme', { fallback: 'light', }); // Storage item with initialization export const userId = storage.defineItem('local:userId', { init: () => crypto.randomUUID(), }); export const installDate = storage.defineItem('local:installDate', { init: () => Date.now(), }); // Complex storage item interface UserSettings { notifications: boolean; autoSave: boolean; language: string; } export const userSettings = storage.defineItem('sync:userSettings', { fallback: { notifications: true, autoSave: true, language: 'en', }, }); // Usage in other files async function example() { // Type-safe getValue const currentTheme = await theme.getValue(); // 'light' | 'dark' // Type-safe setValue await theme.setValue('dark'); // Watch for changes const unwatch = userSettings.watch((newSettings) => { console.log('Settings changed:', newSettings); }); // Get/set partial values const settings = await userSettings.getValue(); await userSettings.setValue({ ...settings, notifications: false }); } ``` ### Storage Item Versioning and Migrations Handle storage schema changes with versioned migrations. ```typescript // utils/storage.ts import { storage } from '#imports'; // Version 1: Simple string array type IgnoredSitesV1 = string[]; // Version 2: Object with more properties interface IgnoredSiteV2 { id: string; url: string; addedAt: number; } // Version 3: Add enabled flag interface IgnoredSiteV3 { id: string; url: string; addedAt: number; enabled: boolean; } export const ignoredSites = storage.defineItem('local:ignoredSites', { fallback: [], version: 3, migrations: { // Migrate from v1 to v2 2: (sites: IgnoredSitesV1): IgnoredSiteV2[] => { return sites.map((url) => ({ id: crypto.randomUUID(), url, addedAt: Date.now(), })); }, // Migrate from v2 to v3 3: (sites: IgnoredSiteV2[]): IgnoredSiteV3[] => { return sites.map((site) => ({ ...site, enabled: true, })); }, }, }); // Usage - migrations run automatically on first access const sites = await ignoredSites.getValue(); ``` ### Storage Metadata Store and retrieve metadata associated with storage keys. ```typescript // utils/storage-meta.ts import { storage } from '#imports'; // Set value with metadata await storage.setItem('local:preferences', { darkMode: true }); await storage.setMeta('local:preferences', { lastModified: Date.now(), modifiedBy: 'user', }); // Get metadata const meta = await storage.getMeta<{ lastModified: number; modifiedBy: string }>( 'local:preferences' ); console.log('Last modified:', new Date(meta.lastModified)); // Remove specific metadata properties await storage.removeMeta('local:preferences', 'modifiedBy'); // Remove all metadata await storage.removeMeta('local:preferences'); ``` ## Build Hooks ### Manifest Generation Hook Modify the generated manifest before it's written to disk. ```typescript // wxt.config.ts import { defineConfig } from 'wxt'; export default defineConfig({ hooks: { 'build:manifestGenerated': (wxt, manifest) => { // Add development indicator to name if (wxt.config.mode === 'development') { manifest.name += ' (DEV)'; } // Add version suffix if (wxt.config.browser === 'firefox') { manifest.browser_specific_settings = { gecko: { id: 'my-extension@example.com', strict_min_version: '109.0', }, }; } // Dynamically add permissions based on feature flags if (process.env.ENABLE_NOTIFICATIONS === 'true') { manifest.permissions = [...(manifest.permissions || []), 'notifications']; } }, }, }); ``` ### Public Assets Hook Add generated files to the extension output. ```typescript // wxt.config.ts import { defineConfig } from 'wxt'; export default defineConfig({ hooks: { 'build:publicAssets': (wxt, assets) => { // Generate a config file assets.push({ relativeDest: 'config.json', contents: JSON.stringify({ version: wxt.config.version, buildTime: new Date().toISOString(), mode: wxt.config.mode, }, null, 2), }); // Generate a CSS file assets.push({ relativeDest: 'generated-styles.css', contents: ` :root { --primary-color: ${process.env.PRIMARY_COLOR || '#007bff'}; } `, }); }, }, }); ``` ### Entrypoints Hook Modify or add entrypoints during the build process. ```typescript // wxt.config.ts import { defineConfig } from 'wxt'; export default defineConfig({ hooks: { 'entrypoints:resolved': (wxt, entrypoints) => { // Log all discovered entrypoints console.log('Discovered entrypoints:', entrypoints.map(e => e.name)); // Modify content script matches based on environment for (const entry of entrypoints) { if (entry.type === 'content-script' && wxt.config.mode === 'development') { entry.options.matches = ['http://localhost/*', ...entry.options.matches]; } } }, }, }); ``` ## WXT Modules ### Creating a Custom Module Build reusable modules to extend WXT functionality. ```typescript // modules/analytics.ts import { defineWxtModule } from 'wxt/modules'; export interface AnalyticsOptions { trackingId: string; debug?: boolean; } declare module 'wxt' { interface InlineConfig { analytics?: AnalyticsOptions; } } export default defineWxtModule({ name: 'analytics', configKey: 'analytics', setup(wxt, options) { if (!options?.trackingId) { wxt.logger.warn('Analytics: No tracking ID provided'); return; } // Generate analytics initialization code wxt.hook('build:publicAssets', (_, assets) => { assets.push({ relativeDest: 'analytics-config.json', contents: JSON.stringify({ trackingId: options.trackingId, debug: options.debug ?? false, }), }); }); // Add analytics script to manifest wxt.hook('build:manifestGenerated', (_, manifest) => { manifest.web_accessible_resources ??= []; manifest.web_accessible_resources.push({ matches: ['*://*/*'], resources: ['analytics-config.json'], }); }); wxt.logger.success(`Analytics module configured with ID: ${options.trackingId}`); }, }); ``` ```typescript // wxt.config.ts - Using the module import { defineConfig } from 'wxt'; export default defineConfig({ modules: ['./modules/analytics'], analytics: { trackingId: 'UA-XXXXX-Y', debug: true, }, }); ``` ### Module with Auto-imports Create a module that adds auto-imports for generated utilities. ```typescript // modules/api-client.ts import { defineWxtModule, addAlias } from 'wxt/modules'; import { resolve } from 'node:path'; export default defineWxtModule({ name: 'api-client', // Add auto-imports for the generated module imports: [ { from: '#api', name: 'api' }, { from: '#api', name: 'fetchData' }, { from: '#api', name: 'postData' }, ], setup(wxt) { const modulePath = resolve(wxt.config.wxtDir, 'api/index.ts'); // Add import alias addAlias(wxt, '#api', modulePath); // Generate the module code wxt.hook('prepare:types', async (_, entries) => { entries.push({ path: modulePath, text: ` const BASE_URL = '${process.env.API_URL || 'https://api.example.com'}'; export async function fetchData(endpoint: string): Promise { const response = await fetch(\`\${BASE_URL}\${endpoint}\`); return response.json(); } export async function postData(endpoint: string, data: unknown): Promise { const response = await fetch(\`\${BASE_URL}\${endpoint}\`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); return response.json(); } export const api = { fetchData, postData }; `, }); }); }, }); ``` ## Unit Testing with Vitest ### Vitest Configuration Set up Vitest for testing WXT extensions. ```typescript // vitest.config.ts import { defineConfig } from 'vitest/config'; import { WxtVitest } from 'wxt/testing/vitest-plugin'; export default defineConfig({ plugins: [WxtVitest()], test: { environment: 'happy-dom', coverage: { provider: 'v8', reporter: ['text', 'json', 'html'], }, }, }); ``` ### Testing Storage Write tests using the fake browser implementation. ```typescript // utils/__tests__/storage.test.ts import { describe, it, expect, beforeEach } from 'vitest'; import { fakeBrowser } from 'wxt/testing/fake-browser'; import { storage } from '#imports'; interface Account { username: string; email: string; } const accountStorage = storage.defineItem('local:account'); async function isLoggedIn(): Promise { const account = await accountStorage.getValue(); return account !== null; } async function getUsername(): Promise { const account = await accountStorage.getValue(); return account?.username ?? null; } describe('Account Storage', () => { beforeEach(() => { fakeBrowser.reset(); }); it('should return true when account exists', async () => { await accountStorage.setValue({ username: 'testuser', email: 'test@example.com', }); expect(await isLoggedIn()).toBe(true); }); it('should return false when account does not exist', async () => { expect(await isLoggedIn()).toBe(false); }); it('should return username when logged in', async () => { await accountStorage.setValue({ username: 'john_doe', email: 'john@example.com', }); expect(await getUsername()).toBe('john_doe'); }); it('should return null when not logged in', async () => { expect(await getUsername()).toBeNull(); }); }); ``` ### Testing with Mocked WXT APIs Mock WXT utilities for isolated testing. ```typescript // utils/__tests__/injection.test.ts import { describe, it, expect, vi, beforeEach } from 'vitest'; // Mock the injectScript utility vi.mock('wxt/utils/inject-script', () => ({ injectScript: vi.fn().mockResolvedValue({ script: document.createElement('script') }), })); import { injectScript } from 'wxt/utils/inject-script'; describe('Script Injection', () => { beforeEach(() => { vi.clearAllMocks(); }); it('should call injectScript with correct path', async () => { await injectScript('/my-script.js', { keepInDom: true }); expect(injectScript).toHaveBeenCalledWith('/my-script.js', { keepInDom: true }); }); it('should handle injection options', async () => { const mockModify = vi.fn(); await injectScript('/my-script.js', { modifyScript: mockModify, }); expect(injectScript).toHaveBeenCalledWith('/my-script.js', { modifyScript: mockModify, }); }); }); ``` ## SPA Navigation Handling ### Handle Single Page Application Navigation Detect URL changes in SPAs and run content script logic accordingly. ```typescript // entrypoints/spa-handler.content.ts import { MatchPattern } from 'wxt/utils/match-patterns'; const videoPagePattern = new MatchPattern('*://*.youtube.com/watch*'); const channelPagePattern = new MatchPattern('*://*.youtube.com/channel/*'); export default defineContentScript({ matches: ['*://*.youtube.com/*'], main(ctx) { // Handle initial load handleNavigation(window.location.href); // Listen for SPA navigation events ctx.addEventListener(window, 'wxt:locationchange', ({ newUrl }) => { handleNavigation(newUrl); }); }, }); function handleNavigation(url: string) { if (videoPagePattern.includes(url)) { initializeVideoFeatures(); } else if (channelPagePattern.includes(url)) { initializeChannelFeatures(); } else { cleanupFeatures(); } } function initializeVideoFeatures() { console.log('Video page features initialized'); } function initializeChannelFeatures() { console.log('Channel page features initialized'); } function cleanupFeatures() { console.log('Features cleaned up'); } ``` --- WXT is primarily used for building production-grade browser extensions that need to work across multiple browsers and manifest versions. Its main use cases include content manipulation extensions (ad blockers, page enhancers), productivity tools (tab managers, bookmarking), developer tools, and any extension requiring complex UI injection via content scripts. The framework integrates seamlessly with modern frontend frameworks through its module system, making it ideal for teams already using Vue, React, or Svelte. The file-based entrypoint structure, automatic manifest generation, and built-in dev server with HMR provide an experience similar to modern web frameworks while handling browser-extension-specific concerns like cross-context communication, isolated CSS injection, and automated store publishing workflows.