# WXT — Hướng dẫn sử dụng và phát triển

> Tài liệu này mô tả cách WXT hoạt động trong dự án AutoShopee Kit, dựa trên cấu hình thực tế trong `wxt.config.ts` và `entrypoints/`.

---

## 1. WXT là gì

WXT là build framework cho browser extension, thay thế Vue CLI + Webpack. Vai trò của nó:

- **Sinh `manifest.json`** từ config + file-based entrypoints
- **Bundle code** qua Vite (nhanh hơn Webpack 5-10x)
- **HMR** cho popup/options trong dev mode
- **Auto-reload** content scripts và background khi file thay đổi
- **Multi-browser**: Chrome MV3 và Firefox MV2 từ cùng một codebase

---

## 2. Cấu trúc dự án với WXT

```
AutoShopee Kit/
├── entrypoints/                          ← WXT scan thư mục này để tạo manifest
│   ├── popup/
│   │   ├── index.html
│   │   └── main.js
│   ├── options/
│   │   ├── index.html
│   │   └── main.js
│   ├── background.js
│   ├── all-listener.content.js           ← inject vào <all_urls>
│   ├── all-ping.content.js               ← service worker keep-alive (<all_urls>)
│   ├── file-pdf.content.js               ← PDF viewer (*://*/*)
│   ├── shopee-seller.content.js          ← banhang.shopee.vn/*
│   ├── shopee-seller-ads.content.js      ← .../marketing/pas*
│   ├── shopee-seller-chat.content.js     ← .../webchat/*
│   ├── shopee-seller-marketing.content.js← .../marketing*
│   ├── shopee-seller-order.content.js    ← .../sale/order/*
│   ├── shopee-seller-print.content.js    ← .../awbprint* (runAt: document_end)
│   ├── shopee-seller-product.content.js  ← .../product/*
│   ├── shopee-client.content.js          ← shopee.vn/*
│   ├── shopee-fetch.content.js           ← shopee.vn/* (world: MAIN)
│   ├── lazada.content.js
│   ├── tiktok.content.js
│   └── tiki.content.js
├── src/
│   ├── app/                              ← srcDir (alias @)
│   ├── content/                          ← alias @content
│   ├── background/                       ← alias @background
│   ├── pdfjs/                            ← PDF.js static files
│   └── shims/                            ← Node built-in shims
├── public/                               ← static assets (copy nguyên xi vào dist)
└── wxt.config.ts
```

WXT **không đọc** `src/` trực tiếp. Tất cả phải đi qua `entrypoints/`.

---

## 3. `wxt.config.ts` — Giải thích chi tiết

```ts
export default defineConfig({
  srcDir: 'src/app',   // @ alias trỏ vào đây
  outDir: 'dist',      // output build
  // entrypointsDir: mặc định là 'entrypoints/' ở root — không cần khai báo
```

### 3.1 Aliases

```ts
alias: {
  '@content':    'src/content',
  '@background': 'src/background',
  '@type':       'src/app/types',
},
```

`@` (không có suffix) được WXT tự động gán = `srcDir` = `src/app/`. **Không** cần khai báo.

| Alias | Trỏ đến | Dùng cho |
|---|---|---|
| `@` | `src/app/` | Vue components, store, router, api |
| `@content` | `src/content/` | Content script logic |
| `@background` | `src/background/` | Background logic |
| `@type` | `src/app/types/` | JSDoc type definitions |

### 3.2 Vite config

```ts
vite: () => ({
  plugins: [
    { name: 'global-polyfill', ... },  // fix lỗi 'global is not defined' từ Node packages
    vue2(),
  ],
  define: { global: 'globalThis' },
  resolve: {
    alias: [
      { find: 'vue-input-tag', replacement: 'node_modules/vue-input-tag/src/components/index.js' },  // dùng source thay vì dist
      { find: 'pouchdb', replacement: 'node_modules/pouchdb-browser/lib/index.js' },                 // dùng browser build
      { find: /^os$/, replacement: 'src/shims/os.js' },                                              // shim Node built-in
      { find: /^moment$/, replacement: 'node_modules/moment/moment.js' },                            // force ESM build
    ],
  },
})
```

**Lưu ý**: `global-polyfill` plugin là custom plugin xử lý packages Node.js (PouchDB, Socket.io) dùng `global` thay vì `globalThis`. Không xóa.

**`src/shims/os.js`**: Một số packages (PouchDB, Socket.io) import Node.js built-in `os`. Browser không có `os` module — WXT/Vite sẽ báo lỗi. Shim này export các hàm `os` trả về giá trị dummy phù hợp môi trường browser (`platform() → 'browser'`, `tmpdir() → '/tmp'`, v.v.).

### 3.3 Manifest function

```ts
manifest: ({ browser, mode }) => {
  const isDevMode = mode === 'development';
  // ...
}
```

`browser` = `'chrome'` | `'firefox'`. `mode` = `'development'` | `'production'`.

Dùng để:
- Dev mode → icon khác (`dev_icon_*.png`), tên khác (`Dev`), thêm `localhost` vào `host_permissions`
- Firefox → thêm `browser_specific_settings.gecko.id`
- Chrome → thêm `incognito: 'split'`

### 3.4 Permissions — mục đích từng permission

| Permission | Dùng cho |
|---|---|
| `storage` | `chrome.storage.local` / `.sync` — lưu settings, state persist (Vuex, Pinia) |
| `tabs` | `chrome.tabs.query/create/reload/sendMessage` — detect tab hiện tại, mở tab mới, gửi message đến content script |
| `alarms` | `chrome.alarms` — periodic tasks: kiểm tra flash sale scheduler, sync shop data (defined tại `src/background/shopee/index.js`) |
| `webRequest` | `chrome.webRequest` — intercept/monitor network request (wrapper tại `src/app/browser/api.js`) |
| `notifications` | `chrome.notifications.create` — hiển thị notification hệ thống (wrapper tại `src/app/browser/api.js`) |
| `cookies` | `chrome.cookies.get/set/getAll/remove` — đọc session cookie Shopee để authenticate API calls (dùng tại `src/app/browser/cookies.js`, `src/app/api/shopee/`) |

---

## 4. Entrypoints — Quy tắc đặt tên

WXT xác định **loại entrypoint** từ tên file/thư mục:

| Pattern | Loại | Ví dụ trong dự án |
|---|---|---|
| `entrypoints/popup/` | Popup (HTML page) | `popup/index.html` + `popup/main.js` |
| `entrypoints/options/` | Options page | `options/index.html` + `options/main.js` |
| `entrypoints/background.js` | Service Worker / Background | `background.js` |
| `entrypoints/*.content.js` | Content Script | `shopee-seller.content.js` |
| `entrypoints/*.content.css` | Content Script (CSS only) | — |

**Tên content script** → tên trong manifest: `shopee-seller.content.js` → `content-scripts/shopee-seller.js`.

**Ngoại lệ — logic inline** (`all-ping.content.js`): Một số entrypoint chứa logic trực tiếp thay vì dynamic import, khi code đơn giản và không cần share với module khác:

```js
// entrypoints/all-ping.content.js — logic inline
export default defineContentScript({
  matches: ['<all_urls>'],
  main() {
    const serviceWorkerKeepAlive = () => {
      const port = chrome.runtime.connect({ name: 'keepAlive' });
      port.postMessage({ ping: true, from: 'Content Script' });
    };
    chrome.runtime.sendMessage({ type: 'wakeUp' });
    serviceWorkerKeepAlive();
    setInterval(serviceWorkerKeepAlive, 5000);
  },
});
```

**Lý do tồn tại của `all-ping.content.js`**: Chrome MV3 service worker tự terminate sau ~30 giây không có activity. `all-ping` inject vào tất cả tabs, mở port kết nối đến background mỗi 5 giây — port connection giữ service worker alive. Không có file này, background sẽ bị terminate giữa chừng và mất state.

---

## 5. Auto-imports

WXT tự động import các helper function — **không cần `import` thủ công** trong entrypoints:

```js
// Các hàm này available toàn cục trong entrypoints/
defineBackground()
defineContentScript()
defineUnlistedScript()
browser           // webextension-polyfill
```

Đó là lý do `entrypoints/background.js` không có dòng import nào:

```js
// background.js — không cần import gì
export default defineBackground(() => {
  import('@background/index.js');
});
```

---

## 6. Các loại Entrypoint

### 6.1 Background

```js
// entrypoints/background.js
export default defineBackground(() => {
  import('@background/index.js');
});
```

**Với options:**
```js
export default defineBackground({
  persistent: false,       // MV3: service worker (default false)
  type: 'module',
  main() {
    import('@background/index.js');
  },
});
```

WXT tự xử lý MV2/MV3: Chrome MV3 → `service_worker`, Firefox MV2 → `scripts: [...]`.

---

### 6.2 Content Script — Pattern chuẩn của dự án

Tất cả content script trong dự án dùng pattern **thin wrapper**:

```js
// entrypoints/shopee-seller.content.js
export default defineContentScript({
  matches: ['https://banhang.shopee.vn/*'],
  main() {
    import('@content/shopee/seller/loader.js');  // load logic gốc
  },
});
```

Logic thực sự nằm trong `src/content/`, không nằm trong entrypoint. Entrypoint chỉ là metadata + dynamic import.

**Các options của `defineContentScript`:**

```js
defineContentScript({
  matches: ['https://banhang.shopee.vn/*'],   // URL patterns — bắt buộc
  runAt: 'document_idle',                      // 'document_start' | 'document_end' | 'document_idle' (default)
  world: 'ISOLATED',                           // 'ISOLATED' (default) | 'MAIN'
  allFrames: false,
  matchAboutBlank: false,
  main(ctx) { ... },                           // ctx: ContentScriptContext (xem phần 9)
})
```

**`world: 'MAIN'`** — chạy trong page context, truy cập được `window` của trang. Dùng cho `shopee-fetch.content.js` vì cần intercept `fetch`:

```js
// entrypoints/shopee-fetch.content.js
export default defineContentScript({
  matches: ['https://shopee.vn/*', 'https://banhang.shopee.vn/*'],
  world: 'MAIN',   // ← chạy trong page context
  main() {
    import('@content/shopee/script/fetch.js');
  },
});
```

**`runAt: 'document_end'`** — chạy sau khi DOM parse xong (trước khi tài nguyên load). Dùng cho print:

```js
// entrypoints/shopee-seller-print.content.js
export default defineContentScript({
  matches: ['https://banhang.shopee.vn/awbprint*'],
  runAt: 'document_end',
  main() {
    import('@content/shopee/seller/print.js');
  },
});
```

---

### 6.3 Popup / Options

Là HTML page thông thường. WXT tự sinh `action.default_popup` trong manifest.

```
entrypoints/popup/
├── index.html    ← HTML template với <div id="app">
└── main.js       ← khởi tạo Vue app
```

`popup/main.js` là file Vue thông thường, import store/router/plugins bình thường.

---

## 7. Thêm Entrypoint mới

### Thêm Content Script mới

1. Tạo file `entrypoints/ten-feature.content.js`:

```js
export default defineContentScript({
  matches: ['https://example.com/*'],
  main() {
    import('@content/example/loader.js');
  },
});
```

2. Tạo logic trong `src/content/example/loader.js`

3. WXT tự động thêm vào `manifest.json` khi build. **Không cần sửa `wxt.config.ts`.**

### Thêm Content Script chạy ở MAIN world

```js
export default defineContentScript({
  matches: ['https://example.com/*'],
  world: 'MAIN',
  runAt: 'document_start',  // inject sớm trước khi page JS chạy
  main() {
    import('@content/example/intercept.js');
  },
});
```

---

## 8. Commands

```bash
npm run dev              # Dev Chrome MV3, mở Chrome, HMR popup/options, auto-reload content scripts
npm run dev:firefox      # Dev Firefox MV2
npm run dev:tool         # Dev + Vue DevTools
npm run dev:seller       # Dev chỉ popup + shopee-seller + shopee-fetch (nhanh hơn)
npm run build:chrome     # Production build Chrome
npm run build:firefox    # Production build Firefox
npm run zip:chrome       # Build + tạo file .zip để upload Chrome Web Store
npm run zip:firefox      # Build + tạo file .zip để upload Firefox AMO
npm run lint             # ESLint check
npm run format           # Prettier + ESLint fix
```

### `wxt prepare` — Tạo type declarations

`npm install` tự chạy `postinstall` → `wxt prepare`. Lệnh này tạo thư mục `.wxt/` chứa:

- `tsconfig.json` — paths cho aliases (`@`, `@content`, `@background`, `@type`)
- `types/` — type declarations cho `defineBackground`, `defineContentScript`, `browser`, auto-imports
- `imports.d.ts` — global auto-import shims

**Khi nào cần chạy lại thủ công**: Sau khi thêm alias mới vào `wxt.config.ts`, hoặc IDE không nhận type của WXT helpers.

```bash
npx wxt prepare
```

### Build chỉ một số entrypoints (tăng tốc dev)

```bash
# Chỉ build popup
wxt --filter-entrypoints popup

# Build popup + shopee-seller
wxt --filter-entrypoints popup,shopee-seller,shopee-seller-product
```

Thêm vào `package.json` nếu thường xuyên dùng:

```json
"dev:seller": "wxt --filter-entrypoints popup,shopee-seller,shopee-seller-product,shopee-fetch"
```

### Phân tích bundle

```bash
ANALYZE=true npm run build:chrome
```

Config `analysis` đã có sẵn trong `wxt.config.ts`:
```ts
analysis: {
  enabled: process.env.ANALYZE === 'true',
  open: true,
}
```

---

## 9. Content Script Context (`ctx`)

WXT inject `ctx` vào `main(ctx)` để handle **extension context invalidation** — lỗi thường xảy ra khi extension reload trong khi content script đang chạy.

**Vấn đề hiện tại**: Các content script của dự án không nhận `ctx` vì dùng dynamic import:

```js
// Hiện tại — ctx không được dùng
main() {
  import('@content/shopee/seller/loader.js');
}

// Đúng hơn — truyền ctx vào loader
async main(ctx) {
  const { init } = await import('@content/shopee/seller/loader.js');
  init(ctx);
}
```

**API của `ctx`:**

```js
async main(ctx) {
  // Dùng thay cho window.addEventListener — tự cleanup khi extension reload
  ctx.addEventListener(window, 'message', handler);
  ctx.addEventListener(document, 'click', handler);

  // Dùng thay cho setTimeout/setInterval
  ctx.setTimeout(() => { ... }, 1000);
  ctx.setInterval(() => { ... }, 5000);

  // Kiểm tra context còn valid không
  if (ctx.isInvalid) return;

  // AbortSignal để cancel fetch khi context bị hủy
  const res = await fetch(url, { signal: ctx.signal });
}
```

---

## 10. SPA Navigation

Shopee, Lazada, TikTok đều là SPA — URL thay đổi mà không reload page. WXT cung cấp event `wxt:locationchange` (chỉ trong content script chạy ở `ISOLATED` world):

```js
// Trong src/content/shopee/seller/loader.js
window.addEventListener('wxt:locationchange', ({ newUrl, oldUrl }) => {
  console.log('Route changed:', oldUrl, '→', newUrl);
  reinitFeature(newUrl);
});
```

Dự án hiện tại **không** patch `history.pushState` thủ công — có thể dùng `wxt:locationchange` trực tiếp khi cần detect route change.

---

## 11. Output structure

```
dist/
├── chrome-mv3-dev/         ← npm run dev
├── chrome-mv3/             ← npm run build:chrome
├── firefox-mv2/            ← npm run build:firefox
└── *.zip                   ← npm run zip:*
```

Mỗi thư mục là extension hoàn chỉnh, có thể load trực tiếp vào Chrome/Firefox.

---

## 12. Xử lý browser differences trong code

```js
// Trong entrypoint hoặc src/
if (import.meta.env.BROWSER === 'firefox') {
  // Firefox-specific code
}

if (import.meta.env.MANIFEST_VERSION === 3) {
  // MV3-specific code
}

// Mode
if (import.meta.env.MODE === 'development') {
  // Dev-only code
}
```

---

## 13. Env variables

File `.env` và `.env.prod`, prefix `VITE_`:

```bash
# .env
VITE_API_URL=https://api.autoshopee.com
```

Truy cập trong code:
```js
import.meta.env.VITE_API_URL
```

Tất cả env vars trong dự án đi qua `src/app/config/env.js` — không import `import.meta.env` trực tiếp ở nơi khác.

---

## 14. Troubleshooting

### `global is not defined`
Package Node.js dùng `global` thay vì `globalThis`. Đã xử lý bằng `global-polyfill` plugin trong `wxt.config.ts`. Nếu gặp với package mới, kiểm tra xem plugin có cover không.

### `require is not defined`
WXT dùng ESM-only. File nào còn `require()` trong `src/` sẽ lỗi. Fix: chuyển sang `import`.

### Extension không load sau khi dev reload
Content script bị stuck ở context cũ. Reload tab trước khi test lại.

### Content script không chạy trên URL mới
Shopee SPA không reload page → content script đã inject không chạy lại. Cần dùng `wxt:locationchange` hoặc MutationObserver.

### Build lỗi `Cannot find module '@content/...'`
Kiểm tra alias `@content` trong `wxt.config.ts` có trỏ đúng `src/content/` không.

---

## 15. CI/CD Pipeline (Drone)

File `.drone.yml` — trigger khi push vào branch `dev`.

```
push → dev
  ├── restore-node-cache
  ├── install (npm install + wxt prepare)
  ├── zip-chrome  ──┐
  ├── zip-firefox ──┤ (parallel)
  └── zip-s3 ← depends on cả hai zip
```

**`zip-s3`** upload lên S3 với config từ Drone secrets:
- `OWN_S3_ENDPOINT`, `OWN_S3_PORT`, `OWN_S3_ACCESS_KEY`, `OWN_S3_SECRET_KEY`
- Bucket: `autoshopee-kit`

**Lưu ý**: Pipeline không có bước lint/test — lỗi ESLint không block deploy.

---

## 16. HMR và reload behavior

| Thay đổi ở | Dev behavior |
|---|---|
| `entrypoints/popup/**` | HMR — hot reload, không reload extension |
| `entrypoints/options/**` | HMR — hot reload, không reload extension |
| `entrypoints/background.js` | Auto-reload background (extension reload nhẹ) |
| `entrypoints/*.content.js` | Auto-reload content script trong tab đang mở |
| `src/app/**` (Vue/store) | HMR nếu được dùng bởi popup/options; reload nếu bởi content script |
| `src/content/**` | Auto-reload content script trong tab |
| `src/background/**` | Auto-reload background |
| `wxt.config.ts` | Cần restart `npm run dev` thủ công |

**Lưu ý**: Sau khi extension reload (background thay đổi), các tab đang mở sẽ có content script cũ — cần reload tab để lấy version mới.

---

## 17. Web Accessible Resources

WXT **không tự động** thêm `web_accessible_resources` — phải khai báo thủ công trong `manifest()` của `wxt.config.ts`.

Dự án hiện tại khai báo:

```ts
web_accessible_resources: [
  {
    resources: ['pdfjs/pdf.min.js', 'pdfjs/pdf.js', 'pdfjs/pdf.worker.min.js', 'pdfjs/pdf.worker.js'],
    matches: ['*://*/*'],
  },
],
```

WXT copy thư mục `public/` vào output nguyên xi. Các file static khác cần được copy thủ công vào `public/` trước khi build.

**PDF.js**: các file nằm ở `src/pdfjs/` — cần copy sang `public/pdfjs/` trước khi build để manifest `web_accessible_resources` nhận đúng path. Hiện tại chưa có bước copy tự động trong build pipeline.

**Khi cần thêm resource mới** (ví dụ: inject script vào page):

1. Đặt file vào `public/<path>/file.js`
2. Khai báo trong `manifest()`:

```ts
web_accessible_resources: [
  // ... existing ...
  {
    resources: ['<path>/file.js'],
    matches: ['https://banhang.shopee.vn/*'],
  },
],
```

3. Truy cập từ content script bằng `browser.runtime.getURL('<path>/file.js')`.

---

## 18. Known Issues & Fix Guide

### Issue 1 — pdfjs không tồn tại trong build output

**Triệu chứng**: Extension lỗi khi gọi `browser.runtime.getURL('pdfjs/pdf.js')` — trả về URL trỏ đến file không tồn tại.

**Root cause**:
- `manifest()` khai báo `web_accessible_resources` trỏ đến `pdfjs/*.js`
- Files thực tế nằm ở `src/pdfjs/` — không phải `public/`
- WXT chỉ copy `public/` vào dist, không đụng đến `src/pdfjs/`
- Kết quả: `dist/chrome-mv3/pdfjs/` **không tồn tại**

**Fix — thêm `vite-plugin-static-copy` vào `wxt.config.ts`**:

```bash
npm install vite-plugin-static-copy --save-dev
```

```ts
// wxt.config.ts
import { viteStaticCopy } from 'vite-plugin-static-copy';

vite: () => ({
  plugins: [
    // ... existing plugins ...
    viteStaticCopy({
      targets: [
        { src: 'src/pdfjs/*', dest: 'pdfjs' },
      ],
    }),
  ],
})
```

**Fix thay thế — không cần thêm dependency**: Di chuyển toàn bộ `src/pdfjs/` vào `public/pdfjs/`. WXT tự copy `public/` vào dist. Cần update import path trong `src/app/lib/pdf/worker.js`.

---

## 19. Extension APIs — `browser` unified object

WXT cung cấp `browser` object thống nhất qua `wxt/browser`, merge `chrome` (Chromium) và `browser` (Firefox) thành một interface nhất quán.

```js
import { browser } from 'wxt/browser';

// Auto-imported trong entrypoints — không cần import thủ công
browser.action.onClicked.addListener(() => { ... });
browser.runtime.onSuspend?.addListener(() => { ... }); // optional chaining cho API không chắc tồn tại
```

**Lưu ý quan trọng**: Chỉ dùng `browser` **bên trong** hàm `main()` của entrypoint, không dùng ở top-level module — WXT import entrypoints trong môi trường Node.js (không có browser APIs).

```js
// SAI — lỗi khi WXT import file này
import { browser } from 'wxt/browser';
browser.storage.local.get('key'); // ← top-level, crash ngay

// ĐÚNG
export default defineContentScript({
  main() {
    browser.storage.local.get('key'); // ← trong main(), OK
  },
});
```

**Feature detection** — một số API chỉ có ở MV3 hoặc một browser cụ thể:

```js
// Dùng optional chaining để check trước khi dùng
browser.runtime.onSuspend?.addListener(() => { ... });
```

---

## 20. Storage — WXT built-in vs KV Store hiện tại

Dự án hiện dùng custom **KV Store** (`src/app/plugins/kv/`) — gọi API server bên ngoài (`KV_API`). WXT cũng cung cấp `@wxt-dev/storage` wrapper cho `chrome.storage` nội bộ extension.

### Phân biệt hai loại storage

| | KV Store (hiện tại) | `@wxt-dev/storage` |
|---|---|---|
| **Lưu ở đâu** | Server bên ngoài (`KV_API`) | `chrome.storage.local/sync` |
| **TTL / expiry** | Hỗ trợ (server-side) | Không có native |
| **Encryption** | Hỗ trợ (crypt key) | Không |
| **Lock/Unlock** | Hỗ trợ | Không |
| **Cross-user global** | Hỗ trợ (`isGlobal`) | Không (chỉ local device) |
| **Watch changes** | Không | Có (reactive) |
| **Versioning/Migration** | Không | Có |
| **Offline** | Không (cần network) | Có |

**KV Store không thể thay thế bằng `@wxt-dev/storage`** vì KV Store là server-side với TTL, lock, encryption, và cross-device sync.

### Các nơi dùng `chrome.storage` trực tiếp (phù hợp dùng `@wxt-dev/storage`)

`vuex-persistedstate` và `pinia-plugin-persistedstate` đang persist state vào `chrome.storage.local`. Nếu muốn typed access và watch, có thể wrap bằng `@wxt-dev/storage`:

```js
import { storage } from 'wxt/storage';

// Định nghĩa typed storage item với versioning
export const userSettingStorage = storage.defineItem('local:userSetting', {
  fallback: {},
  version: 1,
  migrations: {
    2: (old) => ({ ...old, newField: 'default' }),
  },
});

// Watch thay đổi (ví dụ: sync giữa background và popup)
userSettingStorage.watch((newVal, oldVal) => {
  console.log('Setting changed:', newVal);
});
```

---

## 21. Messaging — webext-bridge (hiện tại) và alternatives

Dự án đang dùng **`webext-bridge`** — đây là lựa chọn WXT cũng khuyến nghị cho messaging.

### Pattern hiện tại

```js
// Content script → Background
import { sendMessage } from 'webext-bridge/content-script';
const result = await sendMessage('EVENT_GET_USER_INFO', payload);

// Background → Content script
import { sendMessage } from 'webext-bridge/background';
await sendMessage('EVENT_TAB_FETCH_URL', payload, { context: 'content-script', tabId });

// Background lắng nghe
import { onMessage } from 'webext-bridge/background';
onMessage('EVENT_GET_USER_INFO', async (msg) => { ... });
```

### Giới hạn của `webext-bridge`

`webext-bridge` **không hỗ trợ** messaging giữa content script `world: 'MAIN'` và background — vì MAIN world không có `chrome.runtime`. Hiện tại `shopee-fetch.content.js` (MAIN world) dùng `window.postMessage` để truyền dữ liệu sang ISOLATED world trước, rồi ISOLATED world mới relay qua bridge.

### Các alternatives WXT đề xuất (nếu cần migrate)

| Library | Ưu điểm | Dùng khi |
|---|---|---|
| `webext-bridge` (hiện tại) | Đơn giản, đã battle-tested | Giữ nguyên |
| `@webext-core/messaging` | Type-safe hơn | Muốn TypeScript strict |
| `@webext-core/proxy-service` | Gọi function cross-context như local | Background service cần call từ content |

---

## 22. Scripting API — `browser.scripting.executeScript`

Dùng để inject script động vào tab từ background, khác với content script (inject tự động theo URL pattern).

```js
// Background inject script vào tab cụ thể
await browser.scripting.executeScript({
  target: { tabId },
  files: ['scripts/inject.js'],  // unlisted script
});

// Hoặc inline function (không cần file)
await browser.scripting.executeScript({
  target: { tabId },
  func: () => {
    return document.title; // return value truyền về background
  },
});
```

**Định nghĩa unlisted script** (dùng với `files`):

```js
// entrypoints/inject-something.js (không có .content.js suffix)
export default defineUnlistedScript({
  main() {
    // chạy khi được inject bằng executeScript
    return document.querySelector('#data')?.textContent;
  },
});
```

**Khi nào dùng so với content script**:

| | Content Script | `executeScript` |
|---|---|---|
| Trigger | Tự động khi URL match | Background gọi thủ công |
| Timing | Theo manifest | Bất kỳ lúc nào |
| Return value | Không | Có |
| Dùng khi | Inject UI/listener thường xuyên | Lấy data từ page một lần |

---

### Issue 2 — Content scripts không dùng `ctx` (memory leak khi extension reload)

**Triệu chứng**: Sau khi extension reload trong dev mode, tab cũ xuất hiện lỗi "Extension context invalidated". Event listeners và timers từ lần inject trước vẫn đang chạy.

**Root cause**: Tất cả 15 content scripts không pass `ctx` vào loader:

```js
// Pattern hiện tại — ctx bị bỏ qua
main() {
  import('@content/shopee/seller/loader.js');
}
```

Khi extension reload, WXT cố cancel các async operation qua `ctx`, nhưng loader không nhận được nên không cleanup được.

**Loaders bị ảnh hưởng**:

| Content Script | Loader | Vấn đề cụ thể |
|---|---|---|
| `shopee-seller.content.js` | `src/content/shopee/seller/loader.js` | `document.addEventListener`, `setTimeout` |
| `shopee-client.content.js` | `src/content/shopee/client/loader.js` | `document.addEventListener`, `window.onscroll`, `setTimeout`, `setInterval` |
| `all-listener.content.js` | `src/content/all/listener.js` | `onMessage`, `chrome.runtime.onMessage.addListener` |
| `all-ping.content.js` | inline | `setInterval(serviceWorkerKeepAlive, 5000)` |
| `lazada.content.js` | `src/content/lazada/loader.js` | `setTimeout`, async fetch |
| `tiki.content.js` | `src/content/tiki/loader.js` | `setTimeout` |

**Fix — pattern chuẩn**:

Bước 1 — sửa entrypoint:
```js
// trước
main() {
  import('@content/shopee/seller/loader.js');
}

// sau
async main(ctx) {
  const { init } = await import('@content/shopee/seller/loader.js');
  init(ctx);
}
```

Bước 2 — sửa loader export `init(ctx)` và dùng `ctx.*` thay cho native API:
```js
// src/content/shopee/seller/loader.js — trước
document.addEventListener('readystatechange', () => {
  setTimeout(() => { addBtnLoader(); }, 2000);
});
setTimeout(() => { listenerFetch(); }, 500);

// sau
export function init(ctx) {
  ctx.addEventListener(document, 'readystatechange', () => {
    ctx.setTimeout(() => { addBtnLoader(); }, 2000);
  });
  ctx.setTimeout(() => { listenerFetch(); }, 500);
}
```

`all-ping.content.js` (inline logic — chỉ sửa entrypoint):
```js
main(ctx) {
  const serviceWorkerKeepAlive = () => {
    const port = chrome.runtime.connect({ name: 'keepAlive' });
    port.postMessage({ ping: true, from: 'Content Script' });
  };
  chrome.runtime.sendMessage({ type: 'wakeUp' });
  serviceWorkerKeepAlive();
  ctx.setInterval(serviceWorkerKeepAlive, 5000);  // thay setInterval
}
```

**Lưu ý**: `shopee-fetch.content.js` chạy ở `world: 'MAIN'` — `ctx` không khả dụng trong MAIN world. Không áp dụng fix này cho entrypoint đó.
