WXT là một framework thế hệ mới dành cho việc phát triển Web Extension (tiện ích mở rộng trình duyệt). Được lấy cảm hứng từ Nuxt (framework Vue.js nổi tiếng), WXT mang đến trải nghiệm phát triển (DX) vượt trội: tự động sinh manifest.json từ cấu trúc file, HMR siêu nhanh khi phát triển, hỗ trợ TypeScript mặc định, auto-imports kiểu Nuxt, và khả năng build cho mọi trình duyệt từ cùng một codebase.
Trước WXT, phát triển extension trình duyệt là quá trình thủ công, đau đầu: bạn phải viết manifest.json bằng tay, tự cấu hình bundler, không có HMR (phải reload extension thủ công mỗi lần thay đổi), và phải duy trì codebase riêng cho MV2/MV3 hoặc Chrome/Firefox. WXT giải quyết tất cả những vấn đề này.
Thông tin dự án:
- Repository: github.com/wxt-dev/wxt
- Website: wxt.dev
- Giấy phép: MIT (miễn phí 100%, mã nguồn mở)
- Ngôn ngữ: TypeScript (99.2%)
- Package:
wxttrên npm - Phiên bản hiện tại: v0.20.17 (13 tháng 2, 2026)
- Stars: ~9,200+ | Forks: ~459+ | Contributors: 216+
- Được sử dụng bởi: 2,600+ dự án
- Tác giả: Aaron Klinker (@aklinker1)
- Ngày tạo: Tháng 7, 2023
- Bundler: Vite
- Discord: discord.gg/ZFsZqGery9
graph TB
CENTER["⚡ WXT"]
subgraph dx ["Trải nghiệm Phát triển"]
D1["HMR siêu nhanh"]
D2["Auto-imports kiểu Nuxt"]
D3["TypeScript mặc định"]
end
subgraph build ["Hệ thống Build"]
B1["Manifest tự sinh từ file"]
B2["MV2 + MV3 từ cùng code"]
B3["Zip + Publish tự động"]
end
subgraph browsers ["Trình duyệt"]
BR1["Chrome · Firefox · Edge"]
BR2["Safari · mọi Chromium"]
end
subgraph oss ["Mã nguồn mở"]
O1["MIT · 9k+ Stars · 216+ Contributors"]
end
CENTER --- dx
CENTER --- build
CENTER --- browsers
CENTER --- oss
style CENTER fill:#6c5ce7,color:#fff,stroke:#a29bfe,stroke-width:3px
style dx fill:#1e272e,color:#dfe6e9,stroke:#0984e3,stroke-width:2px
style build fill:#1e272e,color:#dfe6e9,stroke:#00b894,stroke-width:2px
style browsers fill:#1e272e,color:#dfe6e9,stroke:#e17055,stroke-width:2px
style oss fill:#1e272e,color:#dfe6e9,stroke:#fdcb6e,stroke-width:2px
style D1 fill:#0984e3,color:#fff,stroke:#74b9ff
style D2 fill:#0984e3,color:#fff,stroke:#74b9ff
style D3 fill:#0984e3,color:#fff,stroke:#74b9ff
style B1 fill:#00b894,color:#fff,stroke:#55efc4
style B2 fill:#00b894,color:#fff,stroke:#55efc4
style B3 fill:#00b894,color:#fff,stroke:#55efc4
style BR1 fill:#e17055,color:#fff,stroke:#fab1a0
style BR2 fill:#e17055,color:#fff,stroke:#fab1a0
style O1 fill:#fdcb6e,color:#2d3436,stroke:#f9ca24
Phát triển web extension truyền thống gặp rất nhiều vấn đề mà WXT giải quyết triệt để:
| Vấn đề | Cách truyền thống | WXT |
|---|---|---|
| Manifest | Viết manifest.json thủ công, dễ sai |
Tự động sinh từ cấu trúc file |
| Dev mode | Reload thủ công mỗi lần thay đổi | HMR cho UI, fast reload cho scripts |
| MV2/MV3 | Duy trì 2 manifest riêng | Cùng codebase, WXT tự chuyển đổi |
| Multi-browser | Xử lý khác biệt Chrome/Firefox thủ công | Build cho mọi browser bằng flag -b |
| TypeScript | Tự cấu hình, thiếu type cho API | Hỗ trợ sẵn, types tự sinh |
| Build & Bundle | Tự setup Webpack/Rollup/Vite | Vite tích hợp sẵn, zero-config |
| Publish | Upload ZIP thủ công lên từng store | wxt submit tự động qua CLI |
| CSS trong Content Script | Thêm vào manifest thủ công | Import CSS → tự thêm vào manifest |
| Auto-imports | Không có | Kiểu Nuxt, tất cả API sẵn sàng |
graph LR
subgraph before ["Phát triển Truyền thống"]
T1["📝 Viết manifest.json thủ công"]
T2["🔄 Reload extension bằng tay"]
T3["📦 Tự setup bundler"]
T4["😰 MV2 ≠ MV3"]
end
subgraph after ["Với WXT"]
W1["⚡ Manifest tự sinh"]
W2["🔥 HMR tức thì"]
W3["📦 Vite zero-config"]
W4["✅ Cùng code → MV2 + MV3"]
end
before -->|"Chuyển sang"| after
style before fill:#1e272e,color:#dfe6e9,stroke:#e17055,stroke-width:2px
style after fill:#1e272e,color:#dfe6e9,stroke:#00b894,stroke-width:2px
style T1 fill:#e17055,color:#fff,stroke:#fab1a0
style T2 fill:#e17055,color:#fff,stroke:#fab1a0
style T3 fill:#e17055,color:#fff,stroke:#fab1a0
style T4 fill:#e17055,color:#fff,stroke:#fab1a0
style W1 fill:#00b894,color:#fff,stroke:#55efc4
style W2 fill:#00b894,color:#fff,stroke:#55efc4
style W3 fill:#00b894,color:#fff,stroke:#55efc4
style W4 fill:#00b894,color:#fff,stroke:#55efc4
- Node.js 16+ (khuyến nghị 20+)
- Package manager: npm, pnpm, yarn, hoặc bun
- Kiến thức cơ bản về web extension – WXT không thay đổi cách bạn dùng Extension APIs, chỉ đơn giản hóa quá trình build và phát triển
Chưa biết gì về extension? Hãy làm theo Chrome Hello World tutorial trước, rồi quay lại đây.
# pnpm (khuyến nghị)
pnpm dlx wxt@latest init
# npm
npx wxt@latest init
# bun
bunx wxt@latest init
# yarn – dùng npx rồi chọn Yarn khi được hỏi
npx wxt@latest initLệnh init sẽ hỏi bạn chọn:
- Tên dự án
- Frontend framework: Vanilla, Vue, React, Svelte, hoặc Solid
- Package manager: npm, pnpm, yarn, bun
# 1. Tạo dự án
mkdir my-extension && cd my-extension
pnpm init
# 2. Cài WXT
pnpm i -D wxt
# 3. Tạo entrypoint đầu tiên
mkdir entrypoints
cat > entrypoints/background.ts << 'EOF'
export default defineBackground(() => {
console.log('Hello from background!');
});
EOF
# 4. Thêm scripts vào package.jsonThêm scripts vào package.json:
{
"scripts": {
"dev": "wxt",
"dev:firefox": "wxt -b firefox",
"build": "wxt build",
"build:firefox": "wxt build -b firefox",
"zip": "wxt zip",
"zip:firefox": "wxt zip -b firefox",
"postinstall": "wxt prepare"
}
}pnpm dev # Mở Chrome với extension đã cài
pnpm dev:firefox # Mở Firefox với extension đã càiWXT sẽ tự động mở trình duyệt với extension đã được cài đặt. Mọi thay đổi sẽ được phản ánh tức thì nhờ HMR.
graph TD
subgraph setup ["Thiết lập dự án"]
S1["pnpm dlx wxt@latest init"]
S2["Chọn framework + package manager"]
S3["pnpm dev"]
end
S1 --> S2 --> S3
S3 --> RESULT["🎉 Trình duyệt mở với extension sẵn sàng"]
style setup fill:#1e272e,color:#dfe6e9,stroke:#0984e3,stroke-width:2px
style S1 fill:#0984e3,color:#fff,stroke:#74b9ff
style S2 fill:#0984e3,color:#fff,stroke:#74b9ff
style S3 fill:#0984e3,color:#fff,stroke:#74b9ff
style RESULT fill:#00b894,color:#fff,stroke:#55efc4,stroke-width:2px
WXT tuân theo cấu trúc thư mục nghiêm ngặt, được thiết kế để tự động sinh manifest:
📂 my-extension/
📁 .output/ ← Build artifacts
📁 .wxt/ ← Generated types & config
📁 assets/ ← CSS, images (được xử lý bởi Vite)
📁 components/ ← UI components (auto-imported)
📁 composables/ ← Composables cho Vue (auto-imported)
📁 entrypoints/ ← ⭐ Các entrypoint → sinh manifest
📁 hooks/ ← Hooks cho React/Solid (auto-imported)
📁 modules/ ← Local WXT modules
📁 public/ ← Static files (copy nguyên vẹn)
📁 utils/ ← Utilities (auto-imported)
📄 .env ← Biến môi trường
📄 .env.publish ← Biến môi trường cho publishing
📄 app.config.ts ← Runtime config
📄 package.json
📄 tsconfig.json
📄 web-ext.config.ts ← Cấu hình browser startup
📄 wxt.config.ts ← ⭐ Config chính của WXT
Đây là thư mục quan trọng nhất. Tên file trong entrypoints/ quyết định loại entrypoint và WXT sẽ tự động sinh manifest tương ứng.
| File/Thư mục | Loại Entrypoint | Output |
|---|---|---|
background.ts |
Background script/service worker | /background.js |
popup.html hoặc popup/index.html |
Popup UI | /popup.html |
options.html |
Options page | /options.html |
content.ts |
Content script | /content-scripts/content.js |
{name}.content.ts |
Content script có tên | /content-scripts/{name}.js |
sidepanel.html |
Side panel | /sidepanel.html |
newtab.html |
New tab override | /newtab.html |
devtools.html |
DevTools page | /devtools.html |
bookmarks.html |
Bookmarks override | /bookmarks.html |
history.html |
History override | /history.html |
{name}.sandbox.html |
Sandboxed page (Chromium only) | /{name}.html |
{name}.html |
Unlisted page | /{name}.html |
{name}.ts |
Unlisted script | /{name}.js |
{name}.css |
Unlisted CSS | /{name}.css |
Khi entrypoint phức tạp, dùng thư mục với index file:
📂 entrypoints/
📂 popup/
📄 index.html ← Entrypoint chính
📄 main.ts
📄 App.vue
📄 style.css
📂 background/
📄 index.ts ← Entrypoint chính
📄 alarms.ts
📄 messaging.ts
📂 youtube.content/
📄 index.ts ← Entrypoint chính
📄 style.css
Quan trọng: KHÔNG đặt file liên quan trực tiếp trong
entrypoints/– WXT sẽ coi chúng là entrypoint mới. Luôn dùng thư mục khi cần nhiều file.
Nếu bạn thích tách source code khỏi config:
// wxt.config.ts
export default defineConfig({
srcDir: 'src',
});📂 my-extension/
📁 .output/
📁 .wxt/
📁 modules/
📁 public/
📂 src/
📁 assets/
📁 components/
📁 entrypoints/
📁 utils/
📄 wxt.config.ts
📄 package.json
Entrypoint là bất kỳ file nào trong entrypoints/ mà WXT sử dụng để sinh manifest.json. Thay vì viết manifest thủ công, bạn khai báo options ngay trong entrypoint.
Background Script:
// entrypoints/background.ts
export default defineBackground(() => {
console.log('Extension installed!');
browser.runtime.onInstalled.addListener(() => {
console.log('First install or update');
});
});
// Với options
export default defineBackground({
persistent: false, // MV2: non-persistent background
type: 'module', // MV3: module type
main() {
// Code chạy ở đây
},
});Content Script:
// entrypoints/github.content.ts
export default defineContentScript({
matches: ['*://*.github.com/*'],
runAt: 'document_idle',
main(ctx) {
console.log('Running on GitHub!');
// ctx giúp xử lý context invalidation
ctx.setTimeout(() => {
console.log('Still valid!');
}, 5000);
},
});Popup (HTML):
<!-- entrypoints/popup.html -->
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My Extension Popup</title>
<!-- Tùy chỉnh manifest options qua meta tags -->
<meta name="manifest.type" content="browser_action" />
</head>
<body>
<div id="app"></div>
<script type="module" src="./main.ts"></script>
</body>
</html>WXT sinh manifest.json từ nhiều nguồn:
graph TD
subgraph sources ["Nguồn sinh Manifest"]
S1["wxt.config.ts → manifest option"]
S2["entrypoints/ → tự phát hiện"]
S3["WXT Modules → hook modify"]
S4["public/icon-*.png → auto icons"]
end
S1 --> MANIFEST["📄 .output/{target}/manifest.json"]
S2 --> MANIFEST
S3 --> MANIFEST
S4 --> MANIFEST
style sources fill:#1e272e,color:#dfe6e9,stroke:#0984e3,stroke-width:2px
style S1 fill:#0984e3,color:#fff,stroke:#74b9ff
style S2 fill:#00b894,color:#fff,stroke:#55efc4
style S3 fill:#6c5ce7,color:#fff,stroke:#a29bfe
style S4 fill:#fdcb6e,color:#2d3436,stroke:#f9ca24
style MANIFEST fill:#e17055,color:#fff,stroke:#fab1a0,stroke-width:2px
Tự động chuyển đổi MV2 ↔ MV3:
Khi bạn viết config theo format MV3, WXT tự động chuyển sang MV2 nếu build target là MV2:
// wxt.config.ts – Luôn viết theo MV3
export default defineConfig({
manifest: {
action: {
default_title: 'My Extension',
},
web_accessible_resources: [
{
matches: ['*://*.google.com/*'],
resources: ['icon/*.png'],
},
],
},
});WXT sẽ tự chuyển action → browser_action và web_accessible_resources format khi build MV2.
Manifest động theo target:
export default defineConfig({
manifest: ({ browser, manifestVersion, mode, command }) => ({
permissions: [
'storage',
...(browser === 'firefox' ? ['tabs'] : []),
],
host_permissions:
manifestVersion === 3
? ['https://api.example.com/*']
: undefined,
}),
});Auto-discovery icons:
Đặt file icon trong public/ theo format:
public/
├─ icon-16.png
├─ icon-48.png
├─ icon-96.png
└─ icon-128.png
WXT tự phát hiện và thêm vào manifest. Không cần khai báo thủ công.
WXT sử dụng unimport (cùng engine với Nuxt) để auto-import:
- WXT APIs:
defineBackground,defineContentScript,defineConfig,createShadowRootUi,createIntegratedUi,createIframeUi,injectScript,MatchPattern... - Browser API:
browser(polyfill tương thích cả Chrome và Firefox) - Thư mục dự án:
components/,composables/,hooks/,utils/
// Không cần import! Tất cả đều available
export default defineContentScript({
matches: ['<all_urls>'],
main(ctx) {
// browser, defineContentScript, createShadowRootUi... đều auto-imported
const ui = createIntegratedUi(ctx, { /* ... */ });
},
});Tắt auto-imports nếu không thích:
export default defineConfig({
imports: false, // Tắt hoàn toàn
});
// Sau đó import thủ công từ #imports
import { createShadowRootUi, ContentScriptContext } from '#imports';ESLint integration:
export default defineConfig({
imports: {
eslintrc: {
enabled: 9, // ESLint 9 hoặc 8
},
},
});Khi extension bị update/uninstall/disable, content scripts vẫn chạy nhưng extension context bị invalidated. WXT cung cấp ContentScriptContext để xử lý:
export default defineContentScript({
matches: ['<all_urls>'],
main(ctx) {
// Thay vì dùng window.setTimeout, dùng ctx
ctx.setTimeout(() => {
console.log('This wont run if context invalidated');
}, 5000);
ctx.setInterval(() => {
console.log('Safe interval');
}, 1000);
ctx.addEventListener(window, 'scroll', () => {
console.log('Safe event listener');
});
// Kiểm tra thủ công
if (ctx.isValid) {
// Context còn hợp lệ
}
},
});WXT cung cấp 3 phương thức inject UI vào trang web:
| Phương thức | CSS cách ly | Events cách ly | HMR | Dùng context trang |
|---|---|---|---|---|
Integrated (createIntegratedUi) |
❌ | ❌ | ❌ | ✅ |
Shadow Root (createShadowRootUi) |
✅ | ✅ (tùy chọn) | ❌ | ✅ |
IFrame (createIframeUi) |
✅ | ✅ | ✅ | ❌ |
Ví dụ Shadow Root UI với Vue:
// entrypoints/overlay.content/index.ts
import './style.css';
import { createApp } from 'vue';
import App from './App.vue';
export default defineContentScript({
matches: ['<all_urls>'],
cssInjectionMode: 'ui', // Quan trọng cho Shadow Root
async main(ctx) {
const ui = await createShadowRootUi(ctx, {
name: 'my-overlay',
position: 'inline',
anchor: 'body',
onMount: (container) => {
const app = createApp(App);
app.mount(container);
return app;
},
onRemove: (app) => {
app?.unmount();
},
});
ui.mount();
},
});Ví dụ React Shadow Root UI:
// entrypoints/overlay.content/index.tsx
import './style.css';
import ReactDOM from 'react-dom/client';
import App from './App.tsx';
export default defineContentScript({
matches: ['<all_urls>'],
cssInjectionMode: 'ui',
async main(ctx) {
const ui = await createShadowRootUi(ctx, {
name: 'my-overlay',
position: 'inline',
anchor: 'body',
onMount: (container) => {
const wrapper = document.createElement('div');
container.append(wrapper);
const root = ReactDOM.createRoot(wrapper);
root.render(<App />);
return root;
},
onRemove: (root) => {
root?.unmount();
},
});
ui.mount();
},
});Auto-mount cho dynamic elements:
const ui = createIntegratedUi(ctx, {
position: 'inline',
anchor: '#dynamic-element', // Tự mount khi element xuất hiện
onMount: (container) => {
container.textContent = 'Injected!';
},
});
ui.autoMount(); // Tự mount/unmount theo sự xuất hiện của anchorWXT cung cấp wrapper wxt/utils/storage xung quanh browser storage API:
import { storage } from 'wxt/utils/storage';
// Định nghĩa storage item với type-safety
const counter = storage.defineItem<number>('local:counter', {
fallback: 0,
});
// Đọc
const value = await counter.getValue();
// Ghi
await counter.setValue(42);
// Watch thay đổi (reactive)
const unwatch = counter.watch((newValue) => {
console.log('Counter changed:', newValue);
});Mặc định, content scripts chạy trong isolated world (chỉ share DOM). Để chạy trong main world (có quyền truy cập JS context của trang):
// entrypoints/inject.ts – Unlisted script chạy trong main world
export default defineUnlistedScript(() => {
console.log('Running in main world!');
console.log(window.myAppData); // Truy cập được biến của trang
});
// entrypoints/example.content.ts – Content script inject vào main world
export default defineContentScript({
matches: ['*://*/*'],
async main() {
await injectScript('/inject.js', { keepInDom: true });
console.log('Script injected into main world!');
},
});Nhớ thêm script vào
web_accessible_resourcestrongwxt.config.ts.
WXT hỗ trợ mọi framework có Vite plugin. Các framework phổ biến có module chính thức:
# React
pnpm i @wxt-dev/module-react react react-dom
# Vue
pnpm i @wxt-dev/module-vue vue
# Svelte
pnpm i @wxt-dev/module-svelte svelte
# Solid
pnpm i @wxt-dev/module-solid solid-js// wxt.config.ts
export default defineConfig({
modules: ['@wxt-dev/module-react'], // hoặc module-vue, module-svelte, module-solid
});// wxt.config.ts
import { defineConfig } from 'wxt';
import react from '@vitejs/plugin-react';
export default defineConfig({
vite: () => ({
plugins: [react()],
}),
});Extension thường có nhiều UI (popup, options, content script...), mỗi UI cần app instance riêng:
📂 src/
📂 assets/
📄 tailwind.css ← CSS shared
📂 components/
📄 Button.tsx ← Components shared
📂 entrypoints/
📂 popup/
📄 index.html
📄 App.tsx
📄 main.tsx ← Tạo app instance cho popup
📂 options/
📄 index.html
📄 App.tsx
📄 main.tsx ← Tạo app instance cho options
📂 sidebar.content/
📄 index.tsx
📄 App.tsx ← UI inject vào trang
Extension HTML files là static (chrome-extension://{id}/popup.html), không thể thay đổi path. Router phải dùng hash mode:
// React Router
import { createHashRouter } from 'react-router-dom';
const router = createHashRouter([/* routes */]);
// Vue Router
import { createRouter, createWebHashHistory } from 'vue-router';
const router = createRouter({
history: createWebHashHistory(),
routes: [/* routes */],
});URL sẽ là: popup.html#/, popup.html#/settings, v.v.
import { defineConfig } from 'wxt';
export default defineConfig({
// Source directory
srcDir: 'src', // default: "."
outDir: 'dist', // default: ".output"
entrypointsDir: 'entries', // default: "entrypoints"
publicDir: 'static', // default: "public"
modulesDir: 'wxt-modules', // default: "modules"
// Manifest
manifest: {
name: 'My Extension',
description: 'A cool extension',
permissions: ['storage', 'tabs'],
host_permissions: ['https://api.example.com/*'],
action: {
default_title: 'Click me',
},
},
// Modules
modules: [
'@wxt-dev/module-react',
'@wxt-dev/auto-icons',
],
// Auto-imports
imports: {
// Xem https://www.npmjs.com/package/unimport#configurations
},
// Vite config
vite: () => ({
plugins: [],
css: {
modules: { /* ... */ },
},
}),
// Zip/Publish config
zip: {
name: 'my-extension',
// Firefox sources zip
includeSources: ['.env'],
downloadPackages: ['@mycompany/private-pkg'],
},
});| Browser | Flag | Manifest Version |
|---|---|---|
| Chrome | -b chrome (mặc định) |
MV3 |
| Firefox | -b firefox |
MV2 |
| Edge | -b edge |
MV3 |
| Safari | -b safari |
MV2 |
| Chrome MV2 | -b chrome --mv2 |
MV2 |
| Firefox MV3 | -b firefox --mv3 |
MV3 |
Tạo file .env:
# .env
WXT_API_URL=https://api.example.com
VITE_PUBLIC_KEY=pk_123Truy cập trong code:
// Chỉ ở build time (wxt.config.ts, modules)
import.meta.env.WXT_API_URL
// Ở runtime (entrypoints)
import.meta.env.VITE_PUBLIC_KEY// Chỉ build cho Chrome
export default defineContentScript({
matches: ['<all_urls>'],
include: ['chrome'], // Chỉ Chrome builds
exclude: ['firefox'], // Loại Firefox builds
main() { /* ... */ },
});| Lệnh | Mô tả |
|---|---|
wxt / wxt dev |
Chạy dev mode, mở browser với extension |
wxt dev -b firefox |
Dev mode cho Firefox |
wxt build |
Build production cho Chrome |
wxt build -b firefox |
Build production cho Firefox |
wxt build --mv2 |
Build MV2 cho Chrome |
wxt zip |
Build + tạo ZIP cho Chrome Web Store |
wxt zip -b firefox |
Build + tạo ZIP + sources ZIP cho Firefox |
wxt zip -b edge |
Build + tạo ZIP cho Edge Addons |
wxt prepare |
Sinh types và config (dùng cho postinstall) |
wxt init |
Bootstrap dự án mới |
wxt submit init |
Setup secrets cho automated publishing |
wxt submit |
Submit extension lên stores |
wxt submit --dry-run |
Test submit không thực sự upload |
# Build tất cả ZIPs
wxt zip
wxt zip -b firefox
# Submit lên 3 stores cùng lúc
wxt submit \
--chrome-zip .output/my-ext-0.1.0-chrome.zip \
--firefox-zip .output/my-ext-0.1.0-firefox.zip \
--firefox-sources-zip .output/my-ext-0.1.0-sources.zip \
--edge-zip .output/my-ext-0.1.0-chrome.zipContent scripts chỉ chạy khi trang full reload. Với SPA như YouTube (dùng HTML5 history), cần xử lý đặc biệt:
const watchPattern = new MatchPattern('*://*.youtube.com/watch*');
export default defineContentScript({
matches: ['*://*.youtube.com/*'], // Match rộng hơn
main(ctx) {
// Lắng nghe thay đổi URL (WXT cung cấp event này)
ctx.addEventListener(window, 'wxt:locationchange', ({ newUrl }) => {
if (watchPattern.includes(newUrl)) {
mountWatchUI(ctx);
}
});
},
});| Store | Hỗ trợ | ZIP Command |
|---|---|---|
| Chrome Web Store | ✅ Đầy đủ | wxt zip |
| Firefox Addon Store | ✅ Đầy đủ | wxt zip -b firefox (tạo cả sources zip) |
| Edge Addons | ✅ Đầy đủ | wxt zip -b edge hoặc dùng Chrome ZIP |
| Safari | 🚧 Chưa tự động | Build rồi dùng safari-web-extension-converter |
name: Release
on:
workflow_dispatch:
jobs:
submit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Zip extensions
run: |
pnpm zip
pnpm zip:firefox
- name: Submit to stores
run: |
pnpm wxt submit \
--chrome-zip .output/*-chrome.zip \
--firefox-zip .output/*-firefox.zip \
--firefox-sources-zip .output/*-sources.zip
env:
CHROME_EXTENSION_ID: ${{ secrets.CHROME_EXTENSION_ID }}
CHROME_CLIENT_ID: ${{ secrets.CHROME_CLIENT_ID }}
CHROME_CLIENT_SECRET: ${{ secrets.CHROME_CLIENT_SECRET }}
CHROME_REFRESH_TOKEN: ${{ secrets.CHROME_REFRESH_TOKEN }}
FIREFOX_EXTENSION_ID: ${{ secrets.FIREFOX_EXTENSION_ID }}
FIREFOX_JWT_ISSUER: ${{ secrets.FIREFOX_JWT_ISSUER }}
FIREFOX_JWT_SECRET: ${{ secrets.FIREFOX_JWT_SECRET }}Module system cho phép tái sử dụng build-time và runtime code giữa nhiều extension.
| Module | Chức năng |
|---|---|
@wxt-dev/module-react |
Tích hợp React |
@wxt-dev/module-vue |
Tích hợp Vue |
@wxt-dev/module-svelte |
Tích hợp Svelte |
@wxt-dev/module-solid |
Tích hợp Solid |
@wxt-dev/auto-icons |
Tự sinh icons ở nhiều kích thước |
@wxt-dev/i18n |
Internationalization helper |
// modules/my-module.ts
import { defineWxtModule } from 'wxt/modules';
export default defineWxtModule({
setup(wxt) {
// Hook vào build lifecycle
wxt.hook('build:manifestGenerated', (_, manifest) => {
manifest.content_scripts ??= [];
manifest.content_scripts.push({
css: ['injected-style.css'],
matches: ['*://*/*'],
});
});
// Thay đổi config
wxt.hook('config:resolved', () => {
wxt.config.outDir = 'dist';
});
// Thêm public assets
wxt.hook('build:publicAssets', (_, assets) => {
assets.push({
relativeDest: 'generated-data.json',
contents: JSON.stringify({ key: 'value' }),
});
});
},
});Xây dựng extension thêm tính năng cho YouTube: hiển thị thống kê video, nút tải thumbnail, và tùy chỉnh giao diện.
pnpm dlx wxt@latest init youtube-enhancer
# Chọn: React, pnpm
cd youtube-enhancer
pnpm install// wxt.config.ts
export default defineConfig({
modules: ['@wxt-dev/module-react'],
manifest: {
name: 'YouTube Enhancer',
description: 'Enhance your YouTube experience',
permissions: ['storage'],
host_permissions: ['*://*.youtube.com/*'],
},
});// entrypoints/background.ts
export default defineBackground(() => {
browser.runtime.onInstalled.addListener(({ reason }) => {
if (reason === 'install') {
browser.tabs.create({
url: browser.runtime.getURL('/welcome.html'),
});
}
});
});// entrypoints/youtube.content/index.tsx
import './style.css';
import ReactDOM from 'react-dom/client';
import VideoStats from './VideoStats';
export default defineContentScript({
matches: ['*://*.youtube.com/*'],
cssInjectionMode: 'ui',
async main(ctx) {
const watchPattern = new MatchPattern('*://*.youtube.com/watch*');
// Xử lý SPA navigation
ctx.addEventListener(window, 'wxt:locationchange', ({ newUrl }) => {
if (watchPattern.includes(newUrl)) {
mountStatsUI(ctx);
}
});
// Mount lần đầu nếu đang ở trang watch
if (watchPattern.includes(window.location.href)) {
mountStatsUI(ctx);
}
},
});
async function mountStatsUI(ctx) {
const ui = await createShadowRootUi(ctx, {
name: 'yt-stats',
position: 'inline',
anchor: '#above-the-fold',
onMount: (container) => {
const wrapper = document.createElement('div');
container.append(wrapper);
const root = ReactDOM.createRoot(wrapper);
root.render(<VideoStats />);
return root;
},
onRemove: (root) => root?.unmount(),
});
ui.mount();
}// entrypoints/popup/App.tsx
import { useState, useEffect } from 'react';
import { storage } from 'wxt/utils/storage';
const enabled = storage.defineItem<boolean>('local:enabled', {
fallback: true,
});
export default function App() {
const [isEnabled, setIsEnabled] = useState(true);
useEffect(() => {
enabled.getValue().then(setIsEnabled);
return enabled.watch(setIsEnabled);
}, []);
const toggle = async () => {
await enabled.setValue(!isEnabled);
};
return (
<div style={{ width: 300, padding: 16 }}>
<h1>YouTube Enhancer</h1>
<button onClick={toggle}>
{isEnabled ? '✅ Enabled' : '❌ Disabled'}
</button>
</div>
);
}# Dev
pnpm dev
# Build production
pnpm build
pnpm build:firefox
# Tạo ZIP
pnpm zip
pnpm zip:firefox
# Submit
pnpm wxt submit \
--chrome-zip .output/youtube-enhancer-*-chrome.zip \
--firefox-zip .output/youtube-enhancer-*-firefox.zip \
--firefox-sources-zip .output/youtube-enhancer-*-sources.zipgraph LR
DEV["pnpm dev"]
BUILD["pnpm build"]
ZIP["pnpm zip"]
SUBMIT["wxt submit"]
STORES["Chrome · Firefox · Edge"]
DEV -->|"Phát triển"| BUILD
BUILD -->|"Production"| ZIP
ZIP -->|"Upload"| SUBMIT
SUBMIT -->|"Tự động"| STORES
style DEV fill:#0984e3,color:#fff,stroke:#74b9ff
style BUILD fill:#00b894,color:#fff,stroke:#55efc4
style ZIP fill:#6c5ce7,color:#fff,stroke:#a29bfe
style SUBMIT fill:#e17055,color:#fff,stroke:#fab1a0
style STORES fill:#fdcb6e,color:#2d3436,stroke:#f9ca24
| Tính năng | WXT | Plasmo | CRXJS |
|---|---|---|---|
| Đang bảo trì | ✅ Tích cực | 🟡 Maintenance mode | 🟡 Chậm |
| Mọi trình duyệt | ✅ | ✅ | 🟡 Chủ yếu Chrome |
| MV2 + MV3 | ✅ Cả hai | ✅ Cả hai | 🟡 Chọn một |
| Tạo ZIP | ✅ | ✅ | ❌ |
| Sources ZIP (Firefox) | ✅ | ❌ | ❌ |
| TypeScript | ✅ | ✅ | ✅ |
| File-based entrypoints | ✅ | ✅ | ❌ |
| Inline entrypoint config | ✅ | ✅ | ❌ |
| Auto-imports | ✅ | ❌ | ❌ |
| Module system | ✅ | ❌ | ❌ |
| Mọi framework | ✅ (qua Vite plugin) | 🟡 React/Vue/Svelte | ✅ |
| Automated publishing | ✅ | ✅ | ❌ |
| Remote code bundling | ✅ | ✅ | ❌ |
| HMR cho UI | ✅ | 🟡 React only | ✅ |
| Mở browser tự động | ✅ | ❌ | ❌ |
| Built-in Storage wrapper | ✅ | ✅ | ❌ |
| Content Script UI helpers | ✅ | ✅ | ❌ |
| I18n helper | ✅ | ❌ | ❌ |
| Bundle analysis | ✅ | ❌ | ❌ |
graph TB
subgraph wxt_col ["WXT"]
W1["Active development · 9k+ stars"]
W2["Mọi browser · MV2+MV3"]
W3["Nuxt-like DX · Auto-imports"]
W4["Module system · ZIP + Submit"]
end
subgraph plasmo_col ["Plasmo"]
P1["Maintenance mode"]
P2["React-centric"]
P3["Framework-specific entrypoints"]
P4["Built-in messaging"]
end
subgraph crxjs_col ["CRXJS"]
C1["Vite plugin · Minimal"]
C2["Chủ yếu Chrome"]
C3["ESM content scripts"]
C4["Ít tính năng wrapper"]
end
style wxt_col fill:#1e272e,color:#dfe6e9,stroke:#00b894,stroke-width:2px
style plasmo_col fill:#1e272e,color:#dfe6e9,stroke:#0984e3,stroke-width:2px
style crxjs_col fill:#1e272e,color:#dfe6e9,stroke:#e17055,stroke-width:2px
style W1 fill:#00b894,color:#fff,stroke:#55efc4
style W2 fill:#00b894,color:#fff,stroke:#55efc4
style W3 fill:#00b894,color:#fff,stroke:#55efc4
style W4 fill:#00b894,color:#fff,stroke:#55efc4
style P1 fill:#0984e3,color:#fff,stroke:#74b9ff
style P2 fill:#0984e3,color:#fff,stroke:#74b9ff
style P3 fill:#0984e3,color:#fff,stroke:#74b9ff
style P4 fill:#0984e3,color:#fff,stroke:#74b9ff
style C1 fill:#e17055,color:#fff,stroke:#fab1a0
style C2 fill:#e17055,color:#fff,stroke:#fab1a0
style C3 fill:#e17055,color:#fff,stroke:#fab1a0
style C4 fill:#e17055,color:#fff,stroke:#fab1a0
| Loại thay đổi | Hành vi |
|---|---|
| UI pages (popup, options, sidepanel...) | ⚡ HMR – cập nhật tức thì, không mất state |
| Content Scripts | 🔄 Fast reload – re-inject script mới |
| Background Script | 🔄 Reload toàn bộ extension |
| HTML files | 🔄 Reload trang |
wxt.config.ts |
🔄 Restart dev server |
WXT tự mở browser với extension đã cài. Cấu hình thêm:
// web-ext.config.ts
import { defineRunnerConfig } from 'wxt';
export default defineRunnerConfig({
startUrls: ['https://www.google.com'], // Mở URL khi start
chromiumArgs: ['--auto-open-devtools-for-tabs'],
chromiumProfile: '/path/to/profile',
firefoxProfile: '/path/to/profile',
});| Vấn đề | Giải pháp |
|---|---|
| Types không nhận ra auto-imports | Chạy wxt prepare hoặc thêm "postinstall": "wxt prepare" |
| ESLint báo lỗi auto-imports | Cấu hình imports.eslintrc.enabled rồi import .wxt/eslint-auto-imports.mjs |
| Extension context invalidated | Dùng ctx.setTimeout(), ctx.setInterval() thay vì window.* |
| Content script không chạy trên SPA | Dùng wxt:locationchange event + MatchPattern |
| CSS bị ảnh hưởng bởi trang | Dùng createShadowRootUi với cssInjectionMode: 'ui' |
| Firefox sources ZIP build khác | Kiểm tra .env files, dùng zip.includeSources |
MV2 injectScript chạy async |
Đây là hạn chế MV2, script không sync với run_at |
Build lỗi với file trong entrypoints/ |
Không đặt file phụ trực tiếp trong entrypoints/, dùng thư mục |
| Router không hoạt động | Phải dùng hash mode: createWebHashHistory() hoặc createHashRouter() |
| Icons không tự phát hiện | Đặt trong public/ theo format icon-{size}.png |
| Hạn chế | Chi tiết |
|---|---|
| Chưa hỗ trợ ESM Content Scripts | WIP, tiến triển chậm (theo dõi #357) |
| Safari chưa tự động | Cần dùng safari-web-extension-converter thủ công sau khi build |
| Không có built-in Messaging wrapper | Phải dùng browser API trực tiếp hoặc NPM package |
| HMR không hỗ trợ Shadow Root UI | Shadow Root và Integrated UI phải reload, chỉ IFrame có HMR |
| Phụ thuộc Vite | Không dùng được bundler khác (Webpack, esbuild, ...) |
| Learning curve cho người mới | Cần hiểu extension basics trước; WXT thêm abstraction layer |
| Deeply nested entrypoints | Entrypoints chỉ hỗ trợ tối đa 1 cấp sâu |
| Pre-1.0 | API có thể thay đổi giữa các phiên bản minor |
Những extension phổ biến được build bằng WXT:
| Extension | Người dùng | Rating |
|---|---|---|
| BetterCampus (BetterCanvas) | 2,000,000 | ⭐ 4.9 |
| Eye Dropper | 1,000,000 | ⭐ 4.3 |
| Jetwriter AI | 600,000 | ⭐ 4.6 |
| Record & Transcribe for Google Meet | 400,000 | ⭐ 4.6 |
| PreMiD | 400,000 | ⭐ 3.7 |
| StayFree | 200,000 | ⭐ 4.7 |
| YouTube Auto HD + FPS | 200,000 | ⭐ 4.3 |
| Web to PDF | 100,000 | ⭐ 4.8 |
Tổng cộng hơn 5 triệu người dùng đang sử dụng extension được xây dựng bằng WXT.
graph TB
subgraph structure ["Cấu trúc"]
S1["1. Dùng srcDir: 'src' cho dự án lớn"]
S2["2. Mỗi entrypoint phức tạp → dùng thư mục"]
S3["3. Components/utils shared → thư mục riêng"]
end
subgraph dev ["Phát triển"]
D1["4. Luôn chạy wxt prepare sau install"]
D2["5. Dùng ctx.* thay window.* trong content scripts"]
D3["6. Viết manifest theo MV3, WXT tự convert"]
end
subgraph publish ["Publish"]
P1["7. Test sources ZIP trước khi submit Firefox"]
P2["8. Dùng GitHub Action cho CI/CD"]
P3["9. Chạy --dry-run trước submit thật"]
end
subgraph avoid ["Tránh"]
A1["10. Không đặt file phụ trong entrypoints/"]
end
style structure fill:#1e272e,color:#dfe6e9,stroke:#00b894,stroke-width:2px
style dev fill:#1e272e,color:#dfe6e9,stroke:#0984e3,stroke-width:2px
style publish fill:#1e272e,color:#dfe6e9,stroke:#6c5ce7,stroke-width:2px
style avoid fill:#1e272e,color:#dfe6e9,stroke:#e17055,stroke-width:2px
style S1 fill:#00b894,color:#fff,stroke:#55efc4
style S2 fill:#00b894,color:#fff,stroke:#55efc4
style S3 fill:#00b894,color:#fff,stroke:#55efc4
style D1 fill:#0984e3,color:#fff,stroke:#74b9ff
style D2 fill:#0984e3,color:#fff,stroke:#74b9ff
style D3 fill:#0984e3,color:#fff,stroke:#74b9ff
style P1 fill:#6c5ce7,color:#fff,stroke:#a29bfe
style P2 fill:#6c5ce7,color:#fff,stroke:#a29bfe
style P3 fill:#6c5ce7,color:#fff,stroke:#a29bfe
style A1 fill:#e17055,color:#fff,stroke:#fab1a0
- Dùng
srcDir: 'src'cho dự án lớn – tách source code khỏi config files - Mỗi entrypoint phức tạp → dùng thư mục với
indexfile thay vì file đơn - Components/utils shared → đặt trong
components/,utils/để auto-import - Luôn chạy
wxt preparesau install – đảm bảo types chính xác - Dùng
ctx.*thaywindow.*trong content scripts – xử lý context invalidation - Viết manifest theo MV3 format – WXT tự convert sang MV2 khi cần
- Test sources ZIP trước khi submit Firefox – đảm bảo build giống nhau
- Dùng GitHub Action cho CI/CD publish tự động
- Chạy
--dry-runtrước khi submit thật – tránh lỗi credentials - Không đặt file phụ trong
entrypoints/– WXT sẽ coi chúng là entrypoint mới
- WXT Documentation – Tài liệu đầy đủ
- Installation Guide – Bắt đầu nhanh
- Project Structure – Cấu trúc dự án
- Entrypoints – Tham chiếu entrypoints
- Content Scripts – Hướng dẫn content scripts
- Configuration – API config
- Examples – Thư viện ví dụ
- Changelog – Lịch sử thay đổi
- Chrome Extension Docs – Tài liệu Chrome chính thức
- Mozilla WebExtensions – Tài liệu Firefox chính thức
- Chrome Extension Getting Started – Hello World tutorial
- Discord Server – Hỗ trợ trực tiếp
- GitHub Discussions – Thảo luận
- GitHub Issues – Báo lỗi
- Examples Repository – Ví dụ mẫu
- Compare Page – So sánh với Plasmo/CRXJS
- Migration Guide – Chuyển từ dự án cũ
| Package | Chức năng |
|---|---|
@wxt-dev/module-react |
Module React chính thức |
@wxt-dev/module-vue |
Module Vue chính thức |
@wxt-dev/module-svelte |
Module Svelte chính thức |
@wxt-dev/module-solid |
Module Solid chính thức |
@wxt-dev/auto-icons |
Tự sinh icons từ 1 file source |
@wxt-dev/i18n |
Internationalization helper |
webext-bridge |
Messaging giữa các contexts |
webext-storage |
Type-safe storage wrapper |
WXT đại diện cho thế hệ mới của công cụ phát triển web extension – mang triết lý DX-first từ thế giới web framework (Nuxt, Next.js) vào một lĩnh vực vốn đầy rào cản kỹ thuật. Insight cốt lõi: phát triển extension không nên khó hơn phát triển web app.
Với 9,200+ stars, 2,600+ dự án sử dụng, và hàng triệu end-user đang dùng extension được build bằng WXT, framework này đã chứng minh giá trị trong production. So với Plasmo (đang trong maintenance mode) và CRXJS (phát triển chậm), WXT là lựa chọn tốt nhất hiện tại cho bất kỳ ai muốn phát triển web extension một cách chuyên nghiệp.
graph LR
P1["📝 Viết manifest thủ công"]
P2["📦 Webpack/Rollup configs"]
P3["⚡ WXT Framework"]
P4["🚀 Extension Production-ready"]
P1 -->|"Quá phức tạp"| P2
P2 -->|"Giải pháp"| P3
P3 -->|"Phát triển nhanh"| P4
style P1 fill:#e17055,color:#fff,stroke:#fab1a0
style P2 fill:#fdcb6e,color:#2d3436,stroke:#f9ca24
style P3 fill:#00b894,color:#fff,stroke:#55efc4
style P4 fill:#0984e3,color:#fff,stroke:#74b9ff