# Plan: Scripting API — `browser.scripting.executeScript`

## Bối cảnh

### Hiện trạng — inject script qua `web_accessible_resources` + `bridgeGetScriptUrl`

Dự án hiện inject script vào page theo pattern:

1. Content script lấy URL của script bằng cách gọi background:
```js
// content script
const { scriptUrl } = await bridgeGetScriptUrl('some/script.js');
```

2. Background handler:
```js
const onBridgeGetScriptUrl = async ({ data }) => ({
  scriptUrl: chrome.runtime.getURL(data.scriptPath),
});
```

3. Content script tạo `<script src="...">` inject vào DOM.

**Vấn đề với pattern này:**
- Phải khai báo script trong `web_accessible_resources` → script URL có thể bị page detect và block
- Không lấy được return value từ script
- Cần round-trip bridge chỉ để lấy URL
- Script inject vào DOM nhưng không biết khi nào chạy xong

---

## `browser.scripting.executeScript` là gì

API của Chrome/Firefox để background inject và **thực thi** script vào tab cụ thể:

```js
// Background inject và nhận kết quả
const results = await browser.scripting.executeScript({
  target: { tabId },
  func: () => document.title,  // inline function hoặc
  files: ['scripts/inject.js'], // file script
});

const returnValue = results[0].result; // lấy return value từ script
```

**Khác với content script thông thường:**

| | Content Script (manifest) | `executeScript` |
|---|---|---|
| Trigger | Tự động khi URL match | Background gọi thủ công |
| Thời điểm | Theo `runAt` trong manifest | Bất kỳ lúc nào |
| Return value | Không có | Có |
| Permission cần | Khai báo trong manifest | `scripting` permission |
| URL pattern | Theo `matches` | Bất kỳ tab nào background đang track |

---

## Use case phù hợp trong dự án

### Use case 1 — Lấy data từ page một lần (thay bridgeGetScriptUrl pattern)

Khi cần đọc một giá trị từ DOM hoặc `window` của page:

```js
// Background — thay vì content script poll liên tục
const results = await browser.scripting.executeScript({
  target: { tabId },
  func: () => {
    // chạy trong context của page
    return window.__NEXT_DATA__?.props?.pageProps?.shopInfo?.shopId;
  },
});

const shopId = results[0].result;
```

**Khi nào dùng**: Cần đọc data từ page một lần duy nhất từ background — không cần inject content script thường xuyên.

### Use case 2 — Inject script vào MAIN world từ background (thay web_accessible_resources pattern)

Hiện tại `shopee-fetch.content.js` (`world: 'MAIN'`) inject để override `window.fetch`. Đây phù hợp.

Nếu cần inject một script khác vào MAIN world từ background:

```js
// entrypoints/intercept-something.js (unlisted script)
export default defineUnlistedScript({
  main() {
    // patch window API, lấy data
    const data = window.somePageVariable;
    return data;
  },
});

// Background gọi khi cần
const results = await browser.scripting.executeScript({
  target: { tabId },
  files: ['intercept-something.js'],
  world: 'MAIN', // chạy trong page context
});
```

### Use case 3 — Check trạng thái tab trước khi gửi bridge message

Thay vì gửi `bridgeTabFetchShopeeUrl` và bắt lỗi "Receiving end does not exist":

```js
// Check content script đã load chưa
const results = await browser.scripting.executeScript({
  target: { tabId },
  func: () => typeof window.__ATSP_LOADED__ !== 'undefined',
}).catch(() => [{ result: false }]);

if (results[0].result) {
  await bridgeTabFetchShopeeUrl(tabId, payload);
}
```

---

## Permission

`scripting` permission cần được thêm vào `manifest()` trong `wxt.config.ts`:

```ts
// wxt.config.ts
manifest: ({ browser, mode }) => ({
  // ...
  permissions: ['storage', 'tabs', 'alarms', 'webRequest', 'notifications', 'cookies', 'scripting'],
  // ...
})
```

Hiện tại **chưa có** `scripting` permission trong manifest — cần thêm trước khi dùng API này.

---

## Khác biệt MV2 vs MV3

| | MV3 (Chrome) | MV2 (Firefox) |
|---|---|---|
| API | `browser.scripting.executeScript` | `browser.tabs.executeScript` (legacy) |
| `world` option | Có (`MAIN` / `ISOLATED`) | Không có |
| WXT handle | Tự động theo build target | — |

Nếu cần cross-browser, kiểm tra:

```js
if (browser.scripting) {
  await browser.scripting.executeScript({ target: { tabId }, func: myFunc });
} else {
  // Firefox MV2 fallback
  await browser.tabs.executeScript(tabId, { code: '(' + myFunc.toString() + ')()' });
}
```

---

## Giới hạn

- **Không dùng closure** — `func` được serialize thành string, biến bên ngoài không truyền được vào:

```js
const myVar = 'hello';

// SAI — myVar undefined trong page context
func: () => console.log(myVar)

// ĐÚNG — truyền qua args
func: (val) => console.log(val),
args: [myVar]
```

- **Chỉ dùng được từ background** — content script không có `browser.scripting`
- **Tab phải tồn tại và đang active** — gọi cho tab đã đóng sẽ throw

---

## Kết luận — Khi nào dùng

Dùng `executeScript` khi:
- Background cần lấy data từ page một lần (không cần content script thường trực)
- Cần return value từ script
- Muốn inject script có điều kiện (theo logic background, không theo URL pattern)

Không dùng để:
- Inject UI/listener lâu dài → dùng content script
- Thay thế bridge messaging → bridge phù hợp hơn cho 2-way communication

---

## Ưu tiên

| Item | Lý do | Effort |
|---|---|---|
| Thêm `scripting` permission vào manifest | Prerequisite cho mọi use case | Rất thấp |
| Dùng `executeScript` để check tab sẵn sàng | Giảm lỗi "Receiving end does not exist" | Thấp |
| Migrate `bridgeGetScriptUrl` → inline executeScript | Bỏ round-trip, bỏ web_accessible_resources dependency | Trung bình |
