WXT Template
The WXT template provides a modern browser extension development framework with Vue 3, TypeScript, and Vite, perfect for building cross-browser extensions with excellent developer experience.
Technical Stack
- WXT - Browser extension development framework
- Vue 3 - Progressive JavaScript framework
- TypeScript - Type-safe development
- Vite - Fast build tool and dev server
- Vue Router - Client-side routing
- Pinia - State management library
- Vue i18n - Internationalization
- Tailwind CSS - Utility-first CSS framework
Quick Start
1. Create Project
bash
# Initialize project
vup init my-extension-project
cd my-extension-project
# Add WXT template
vup add my-extension
2. Install Dependencies
bash
# Install dependencies
pnpm install
3. Start Development
bash
# Start development server
cd apps/my-extension
pnpm dev
The extension will be available in the browser with hot reload support.
Project Structure
apps/my-extension/
├── src/
│ ├── assets/ # Static assets
│ │ └── images/ # Image files
│ ├── components/ # Vue components
│ │ └── HelloWorld.vue # Demo component
│ ├── composables/ # Vue composables
│ ├── entrypoints/ # Extension entry points
│ │ ├── background.ts # Background script
│ │ ├── content.ts # Content script
│ │ ├── newtab/ # New tab page
│ │ │ ├── newtab.html
│ │ │ └── newtab.vue
│ │ ├── options/ # Options page
│ │ │ ├── options.html
│ │ │ └── options.vue
│ │ └── popup/ # Popup page
│ │ ├── popup.html
│ │ └── popup.vue
│ ├── manifest.ts # Extension manifest
│ └── vue-shim.d.ts # Vue type declarations
├── public/ # Public assets
│ └── icon/ # Extension icons
│ ├── 16.png
│ ├── 32.png
│ ├── 48.png
│ ├── 96.png
│ └── 128.png
├── package.json # Dependencies and scripts
├── tsconfig.json # TypeScript configuration
└── wxt.config.ts # WXT configuration
Core Features
Multiple Entry Points
Background Script
typescript
// entrypoints/background.ts
import { defineBackground } from 'wxt/sandbox';
export default defineBackground(() => {
console.log('Background script loaded');
// Listen for extension installation
chrome.runtime.onInstalled.addListener((details) => {
console.log('Extension installed:', details);
});
// Listen for messages from content scripts
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
console.log('Message received:', message);
sendResponse({ status: 'success' });
});
});
Content Script
typescript
// entrypoints/content.ts
import { defineContentScript } from 'wxt/sandbox';
export default defineContentScript({
matches: ['<all_urls>'],
main() {
console.log('Content script loaded');
// Inject content into the page
const div = document.createElement('div');
div.textContent = 'Hello from WXT!';
div.style.cssText = `
position: fixed;
top: 10px;
right: 10px;
background: #007aff;
color: white;
padding: 10px;
border-radius: 5px;
z-index: 10000;
`;
document.body.appendChild(div);
},
});
Popup Page
vue
<!-- entrypoints/popup/popup.vue -->
<template>
<div class="popup-container">
<h1>{{ title }}</h1>
<p>{{ description }}</p>
<button @click="handleClick" class="btn">
{{ buttonText }}
</button>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const title = ref('WXT Extension');
const description = ref('A modern browser extension');
const buttonText = ref('Click Me');
const handleClick = () => {
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
if (tabs[0]?.id) {
chrome.tabs.sendMessage(tabs[0].id, { action: 'hello' });
}
});
};
</script>
<style scoped>
.popup-container {
@apply w-80 p-4 bg-white;
}
.btn {
@apply w-full px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600;
}
</style>
Options Page
vue
<!-- entrypoints/options/options.vue -->
<template>
<div class="options-container">
<h1>Extension Settings</h1>
<form @submit.prevent="saveSettings">
<div class="form-group">
<label for="theme">Theme:</label>
<select v-model="settings.theme" id="theme">
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
</div>
<div class="form-group">
<label for="notifications">Enable Notifications:</label>
<input
v-model="settings.notifications"
type="checkbox"
id="notifications"
/>
</div>
<button type="submit" class="btn">Save Settings</button>
</form>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
interface Settings {
theme: string;
notifications: boolean;
}
const settings = ref<Settings>({
theme: 'light',
notifications: true,
});
onMounted(() => {
// Load settings from storage
chrome.storage.sync.get(['theme', 'notifications'], (result) => {
settings.value = {
theme: result.theme || 'light',
notifications: result.notifications !== false,
};
});
});
const saveSettings = () => {
chrome.storage.sync.set(settings.value, () => {
console.log('Settings saved');
});
};
</script>
<style scoped>
.options-container {
@apply max-w-2xl mx-auto p-6;
}
.form-group {
@apply mb-4;
}
.form-group label {
@apply block mb-2 font-medium;
}
.form-group select,
.form-group input[type='checkbox'] {
@apply w-full p-2 border rounded;
}
.btn {
@apply px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600;
}
</style>
New Tab Page
vue
<!-- entrypoints/newtab/newtab.vue -->
<template>
<div class="newtab-container">
<div class="hero">
<h1>{{ title }}</h1>
<p>{{ description }}</p>
</div>
<div class="features">
<div v-for="feature in features" :key="feature.title" class="feature">
<h3>{{ feature.title }}</h3>
<p>{{ feature.description }}</p>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const title = ref('WXT Extension');
const description = ref('A modern browser extension with Vue 3');
const features = ref([
{
title: 'Modern Framework',
description: 'Built with Vue 3 and TypeScript',
},
{
title: 'Fast Development',
description: 'Hot reload and instant feedback',
},
{
title: 'Cross-browser',
description: 'Works on Chrome, Firefox, and Edge',
},
]);
</script>
<style scoped>
.newtab-container {
@apply min-h-screen bg-gray-100 p-8;
}
.hero {
@apply text-center mb-12;
}
.hero h1 {
@apply text-4xl font-bold text-gray-900 mb-4;
}
.hero p {
@apply text-xl text-gray-600;
}
.features {
@apply grid grid-cols-1 md:grid-cols-3 gap-8;
}
.feature {
@apply bg-white p-6 rounded-lg shadow-md;
}
.feature h3 {
@apply text-xl font-semibold text-gray-900 mb-2;
}
.feature p {
@apply text-gray-600;
}
</style>
Extension Manifest
typescript
// manifest.ts
import { defineManifest } from 'wxt/sandbox';
export default defineManifest({
manifest_version: 3,
name: 'WXT Extension',
version: '1.0.0',
description: 'A modern browser extension built with WXT',
permissions: ['storage', 'tabs', 'activeTab'],
host_permissions: ['<all_urls>'],
action: {
default_popup: 'popup.html',
default_title: 'WXT Extension',
},
background: {
service_worker: 'background.js',
},
content_scripts: [
{
matches: ['<all_urls>'],
js: ['content.js'],
},
],
web_accessible_resources: [
{
resources: ['icon/*.png'],
matches: ['<all_urls>'],
},
],
icons: {
16: 'icon/16.png',
32: 'icon/32.png',
48: 'icon/48.png',
96: 'icon/96.png',
128: 'icon/128.png',
},
});
Development Tools
WXT Configuration
typescript
// wxt.config.ts
import { defineConfig } from 'wxt';
export default defineConfig({
manifest: {
name: 'WXT Extension',
version: '1.0.0',
description: 'A modern browser extension built with WXT',
},
modules: ['vue'],
vue: {
// Vue-specific configuration
},
runner: {
// Development server configuration
startUrls: ['https://example.com'],
},
build: {
// Build configuration
outDir: '.output',
},
});
TypeScript Configuration
json
// tsconfig.json
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./.output",
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@shared/*": ["../../_shared/*"]
}
},
"include": [
"src/**/*",
"src/**/*.vue",
"src/*.vue",
"./auto-imports.d.ts",
"src/vue-shim.d.ts"
]
}
Available Scripts
json
{
"scripts": {
"dev": "wxt dev",
"build": "wxt build",
"build:firefox": "wxt build --browser firefox",
"build:chrome": "wxt build --browser chrome",
"build:edge": "wxt build --browser edge",
"preview": "wxt preview",
"zip": "wxt zip",
"lint": "eslint src/ --ext .vue,.ts,.js",
"lint:fix": "eslint src/ --ext .vue,.ts,.js --fix",
"format": "prettier --write \"src/**/*.{js,ts,vue,json,css,scss}\"",
"format:check": "prettier --check \"src/**/*.{js,ts,vue,json,css,scss}\""
}
}
Best Practices
Message Passing
typescript
// utils/messaging.ts
export class MessageService {
static async sendMessage<T = any>(message: any, tabId?: number): Promise<T> {
return new Promise((resolve, reject) => {
const target = tabId
? chrome.tabs.sendMessage
: chrome.runtime.sendMessage;
target(tabId || undefined, message, (response) => {
if (chrome.runtime.lastError) {
reject(chrome.runtime.lastError);
} else {
resolve(response);
}
});
});
}
static onMessage<T = any>(
callback: (message: T, sender: chrome.runtime.MessageSender) => void
) {
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
callback(message, sender);
sendResponse({ status: 'success' });
});
}
}
Storage Management
typescript
// utils/storage.ts
export class StorageService {
static async get<T = any>(key: string): Promise<T | null> {
return new Promise((resolve) => {
chrome.storage.sync.get([key], (result) => {
resolve(result[key] || null);
});
});
}
static async set(key: string, value: any): Promise<void> {
return new Promise((resolve) => {
chrome.storage.sync.set({ [key]: value }, () => {
resolve();
});
});
}
static async remove(key: string): Promise<void> {
return new Promise((resolve) => {
chrome.storage.sync.remove([key], () => {
resolve();
});
});
}
}
Error Handling
typescript
// utils/error.ts
export class ExtensionError extends Error {
constructor(
message: string,
public code: string
) {
super(message);
this.name = 'ExtensionError';
}
}
export function handleError(error: unknown): void {
if (error instanceof ExtensionError) {
console.error(`Extension Error [${error.code}]:`, error.message);
} else if (error instanceof Error) {
console.error('Unexpected Error:', error.message);
} else {
console.error('Unknown Error:', error);
}
}
Build and Deployment
Development Build
bash
# Build for development
pnpm build
# The built files will be in the .output directory
Production Build
bash
# Build for specific browser
pnpm build:chrome
pnpm build:firefox
pnpm build:edge
# Create ZIP package
pnpm zip
Browser Installation
- Chrome/Edge: Load unpacked extension from
.output
directory - Firefox: Load temporary add-on from
.output
directory - Production: Upload ZIP file to respective web stores