Skip to content

Instantly share code, notes, and snippets.

@thuongtin
Last active February 13, 2026 13:27
Show Gist options
  • Select an option

  • Save thuongtin/c9800b5d9b8b1704cb6d9abdadc3a5a2 to your computer and use it in GitHub Desktop.

Select an option

Save thuongtin/c9800b5d9b8b1704cb6d9abdadc3a5a2 to your computer and use it in GitHub Desktop.
WXT – Hướng dẫn từ cơ bản đến thành thạo

WXT – Hướng dẫn từ cơ bản đến thành thạo

Giới thiệu tổng quan

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: wxt trê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
Loading

Tại sao cần WXT? Phát triển Extension truyền thống vs WXT

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
Loading

Cài đặt

Yêu cầu chuẩn bị

  • 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.

Bootstrap dự án mới (Khuyến nghị)

# 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 init

Lệ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

Cài đặt thủ công (Từ đầu)

# 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.json

Thê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"
  }
}

Chạy Dev Mode

pnpm dev          # Mở Chrome với extension đã cài
pnpm dev:firefox  # Mở Firefox với extension đã cài

WXT 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
Loading

Cấu trúc dự án

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

Thư mục entrypoints/ – Trái tim 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

Dùng thư mục thay vì file đơn

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.

Thêm thư mục src/

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

Khái niệm cốt lõi

1. Entrypoints – Tự sinh Manifest

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>

2. Manifest tự sinh

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
Loading

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 actionbrowser_actionweb_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.

3. Auto-imports

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
    },
  },
});

4. Content Script Context

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ệ
    }
  },
});

5. Content Script UI

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 anchor

6. Storage API

WXT 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);
});

7. Isolated World vs Main World

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_resources trong wxt.config.ts.


Frontend Frameworks

WXT hỗ trợ mọi framework có Vite plugin. Các framework phổ biến có module chính thức:

Cài đặt Framework

# 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
});

Framework khác – Dùng Vite Plugin

// wxt.config.ts
import { defineConfig } from 'wxt';
import react from '@vitejs/plugin-react';

export default defineConfig({
  vite: () => ({
    plugins: [react()],
  }),
});

Cấu trúc đề xuất cho Multi-UI

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

Router – Hash Mode bắt buộc

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.


Cấu hình WXT

wxt.config.ts – Config chính

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'],
  },
});

Target Browsers

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

Biến môi trường

Tạo file .env:

# .env
WXT_API_URL=https://api.example.com
VITE_PUBLIC_KEY=pk_123

Truy 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

Include/Exclude Entrypoints theo Browser

// Chỉ build cho Chrome
export default defineContentScript({
  matches: ['<all_urls>'],
  include: ['chrome'],   // Chỉ Chrome builds
  exclude: ['firefox'],  // Loại Firefox builds
  main() { /* ... */ },
});

Tham chiếu lệnh CLI

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

Ví dụ Submit tự động

# 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.zip

Xử lý SPA (Single Page Applications)

Content 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);
      }
    });
  },
});

Publishing Extension

Stores được hỗ trợ

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

GitHub Action tự động

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 }}

WXT Modules

Module system cho phép tái sử dụng build-time và runtime code giữa nhiều extension.

Modules có sẵn

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

Viết Module riêng

// 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' }),
      });
    });
  },
});

Ví dụ thực tế: Xây dựng YouTube Enhancer Extension

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.

Bước 1: Khởi tạo dự án

pnpm dlx wxt@latest init youtube-enhancer
# Chọn: React, pnpm
cd youtube-enhancer
pnpm install

Bước 2: Cấu hình

// 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/*'],
  },
});

Bước 3: Background Script

// entrypoints/background.ts
export default defineBackground(() => {
  browser.runtime.onInstalled.addListener(({ reason }) => {
    if (reason === 'install') {
      browser.tabs.create({
        url: browser.runtime.getURL('/welcome.html'),
      });
    }
  });
});

Bước 4: Content Script cho YouTube

// 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();
}

Bước 5: Popup

// 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>
  );
}

Bước 6: Build & Publish

# 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.zip
graph 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
Loading

So sánh với công cụ khác

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
Loading

Dev Mode chi tiết

Hot Module Replacement (HMR)

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

Browser Startup tự động

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',
});

Troubleshooting

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ế và phê bình trung thực

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

Ai đang dùng WXT?

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.


Best Practices

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
Loading
  1. Dùng srcDir: 'src' cho dự án lớn – tách source code khỏi config files
  2. Mỗi entrypoint phức tạp → dùng thư mục với index file thay vì file đơn
  3. Components/utils shared → đặt trong components/, utils/ để auto-import
  4. Luôn chạy wxt prepare sau install – đảm bảo types chính xác
  5. Dùng ctx.* thay window.* trong content scripts – xử lý context invalidation
  6. Viết manifest theo MV3 format – WXT tự convert sang MV2 khi cần
  7. Test sources ZIP trước khi submit Firefox – đảm bảo build giống nhau
  8. Dùng GitHub Action cho CI/CD publish tự động
  9. Chạy --dry-run trước khi submit thật – tránh lỗi credentials
  10. Không đặt file phụ trong entrypoints/ – WXT sẽ coi chúng là entrypoint mới

Tài nguyên học tập

Tài liệu chính thức

Extension APIs (cần thiết khi phát triển)

Cộng đồng

NPM Packages hữu ích

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

Tổng kết

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
Loading
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment