feat: implement M3 multi-browser support (Firefox, Edge, Safari)

Add browser abstraction layer (compat.ts) that normalizes Chrome/Firefox/
Edge/Safari extension APIs behind a unified promise-based interface.
Replace all direct chrome.* calls with compat layer across service worker,
sync engine, badge, storage, popup, and options modules.

- Browser-specific manifests in manifests/ (Firefox MV3 with gecko settings,
  Edge/Safari variants)
- Multi-target build system: `npm run build` produces all four browser
  builds in build/<browser>/
- Per-browser build scripts: build:chrome, build:firefox, build:edge, build:safari
- Auto-detects browser at runtime for platform-specific device registration
- All 38 existing tests pass, typecheck clean

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
徐枫
2026-03-17 16:58:44 +08:00
parent dc3be4d73f
commit f39ff8c215
14 changed files with 580 additions and 88 deletions

View File

@@ -1,6 +1,9 @@
/**
* Popup script — controls the popup UI interactions.
* Uses the browser-agnostic compat layer.
*/
import { runtime, tabs } from "../lib/compat";
export {};
// --- Elements ---
@@ -40,20 +43,12 @@ const btnPairingCancel = document.getElementById("btn-pairing-cancel") as HTMLBu
// --- Messaging helper ---
function sendMessage(type: string, payload?: unknown): Promise<any> {
return new Promise((resolve, reject) => {
chrome.runtime.sendMessage({ type, payload }, (response) => {
if (chrome.runtime.lastError) {
reject(new Error(chrome.runtime.lastError.message));
return;
}
if (response?.error) {
reject(new Error(response.error));
return;
}
resolve(response);
});
});
async function sendMessage(type: string, payload?: unknown): Promise<any> {
const response = await runtime.sendMessage({ type, payload });
if (response?.error) {
throw new Error(response.error);
}
return response;
}
// --- UI Updates ---
@@ -142,7 +137,7 @@ btnSyncTab.addEventListener("click", async () => {
});
btnAddWhitelist.addEventListener("click", async () => {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
const [tab] = await tabs.query({ active: true, currentWindow: true });
if (!tab?.url) return;
try {
@@ -198,7 +193,7 @@ btnPairingCancel.addEventListener("click", () => {
btnSettings.addEventListener("click", (e) => {
e.preventDefault();
chrome.runtime.openOptionsPage();
runtime.openOptionsPage();
});
// --- Init ---