# 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 = `
`;
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.