feat(extension): add Firefox WebExtension support
Testing / remote-protocol-compat (0.9.5) (push) Successful in 48s
Testing / remote-protocol-compat (0.9.3) (push) Successful in 47s
Build & Publish Package / publish (push) Successful in 46s
Package Extension / package-extension (push) Successful in 59s
Testing / test (push) Failing after 50s
Testing / remote-protocol-compat (0.9.5) (push) Successful in 48s
Testing / remote-protocol-compat (0.9.3) (push) Successful in 47s
Build & Publish Package / publish (push) Successful in 46s
Package Extension / package-extension (push) Successful in 59s
Testing / test (push) Failing after 50s
- Add a neutral WebExtension API adapter that uses Firefox browser.* or Chromium chrome.* without mutating globals. - Switch extension runtime code to the adapter and add Firefox-specific typings for tabs, windows, tab groups, storage, scripting, and native messaging ports. - Fix Firefox temporary add-on instructions to load the packaged manifest with background.scripts instead of the Chromium service worker manifest. - Detect Firefox in clients.list via runtime.getBrowserInfo and keep Chromium user-agent fallback support. - Make navigate.open wait briefly for Firefox to replace initial about:blank with the requested URL. - Add JS coverage for API selection, clients.list browser detection, and Firefox navigate.open URL polling. - Bump package and extension version to 0.15.2.
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import type { TabGroupColor } from '../types';
|
||||
import { webExtApi as api } from '../browser-api';
|
||||
// Tab-group resolution and normalization helpers.
|
||||
import { queryTabGroups } from './tab-groups';
|
||||
|
||||
@@ -10,7 +12,7 @@ export async function resolveGroupId(nameOrId: string | number): Promise<number>
|
||||
return match.id;
|
||||
}
|
||||
|
||||
export function normalizeGroupColor(color: string | undefined): chrome.tabGroups.Color {
|
||||
export function normalizeGroupColor(color: string | undefined): TabGroupColor {
|
||||
const allowed = new Set(["grey", "blue", "red", "yellow", "green", "pink", "purple", "cyan", "orange"]);
|
||||
return (allowed.has(color as string) ? color : "grey") as chrome.tabGroups.Color;
|
||||
return (allowed.has(color as string) ? color : "grey") as TabGroupColor;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
// chrome.scripting.executeScript wrapper with transient-error retry.
|
||||
import { webExtApi as api } from '../browser-api';
|
||||
import type { ScriptInjection, ScriptInjectionResult } from '../types';
|
||||
// api.scripting.executeScript wrapper with transient-error retry.
|
||||
import { isTransientScriptError } from './errors';
|
||||
import { sleep } from './throttle';
|
||||
import type { Serializable } from '../types';
|
||||
|
||||
export async function executeScript<Args extends Serializable[], Result>(
|
||||
options: chrome.scripting.ScriptInjection<Args, Result>,
|
||||
options: ScriptInjection<Args>,
|
||||
retries = 3,
|
||||
): Promise<chrome.scripting.InjectionResult<chrome.scripting.Awaited<Result>>[]> {
|
||||
): Promise<ScriptInjectionResult<Result>[]> {
|
||||
for (let i = 0; i < retries; i++) {
|
||||
try {
|
||||
return await chrome.scripting.executeScript(options);
|
||||
return await api.scripting.executeScript(options);
|
||||
} catch (e) {
|
||||
if (i < retries - 1 && isTransientScriptError(e)) {
|
||||
await sleep(300);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// chrome.storage.local accessors for profile alias, window aliases, and sessions.
|
||||
import { webExtApi as api } from '../browser-api';
|
||||
// api.storage.local accessors for profile alias, window aliases, and sessions.
|
||||
import type { SessionTab, StoredSession } from '../types';
|
||||
|
||||
export async function getProfileAlias(): Promise<string> {
|
||||
const { profileAlias } = await chrome.storage.local.get<{ profileAlias?: string }>("profileAlias");
|
||||
const { profileAlias } = await api.storage.local.get<{ profileAlias?: string }>("profileAlias");
|
||||
return profileAlias || "default";
|
||||
}
|
||||
|
||||
@@ -20,11 +21,11 @@ export function getSessionTabs(session: StoredSession | undefined | null): Sessi
|
||||
}
|
||||
|
||||
export async function getAliases(): Promise<Record<string, string>> {
|
||||
const { windowAliases } = await chrome.storage.local.get<{ windowAliases?: Record<string, string> }>("windowAliases");
|
||||
const { windowAliases } = await api.storage.local.get<{ windowAliases?: Record<string, string> }>("windowAliases");
|
||||
return windowAliases || {};
|
||||
}
|
||||
|
||||
export async function getSessions(): Promise<Record<string, StoredSession>> {
|
||||
const { sessions } = await chrome.storage.local.get<{ sessions?: Record<string, StoredSession> }>("sessions");
|
||||
const { sessions } = await api.storage.local.get<{ sessions?: Record<string, StoredSession> }>("sessions");
|
||||
return sessions || {};
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { TabGroupQueryInfo, TabGroup, TabGroupUpdateProperties, TabGroupMoveProperties, TabGroupOptions, BrowserEvent } from '../types';
|
||||
import { webExtApi as api } from '../browser-api';
|
||||
// Optional tab-group API accessors. Firefox currently does not implement the
|
||||
// Chromium tabGroups/tabs.group APIs, so keep runtime checks in one place and
|
||||
// use bracket access to avoid Firefox package validation flagging static API
|
||||
@@ -5,43 +7,43 @@
|
||||
|
||||
const TAB_GROUPS_UNSUPPORTED = "Tab groups are not supported by this browser";
|
||||
|
||||
function tabGroupsApi(): typeof chrome.tabGroups {
|
||||
const api = chrome["tabGroups" as keyof typeof chrome] as typeof chrome.tabGroups | undefined;
|
||||
if (!api) throw new Error(TAB_GROUPS_UNSUPPORTED);
|
||||
return api;
|
||||
function tabGroupsApi(): typeof api.tabGroups {
|
||||
const tabGroups = api["tabGroups" as keyof typeof api] as typeof api.tabGroups | undefined;
|
||||
if (!tabGroups) throw new Error(TAB_GROUPS_UNSUPPORTED);
|
||||
return tabGroups;
|
||||
}
|
||||
|
||||
function tabsGroupApi(): typeof chrome.tabs.group {
|
||||
const fn = chrome.tabs["group" as keyof typeof chrome.tabs] as typeof chrome.tabs.group | undefined;
|
||||
function tabsGroupApi(): typeof api.tabs.group {
|
||||
const fn = api.tabs["group" as keyof typeof api.tabs] as typeof api.tabs.group | undefined;
|
||||
if (!fn) throw new Error(TAB_GROUPS_UNSUPPORTED);
|
||||
return fn.bind(chrome.tabs);
|
||||
return fn.bind(api.tabs);
|
||||
}
|
||||
|
||||
function tabsUngroupApi(): typeof chrome.tabs.ungroup {
|
||||
const fn = chrome.tabs["ungroup" as keyof typeof chrome.tabs] as typeof chrome.tabs.ungroup | undefined;
|
||||
function tabsUngroupApi(): typeof api.tabs.ungroup {
|
||||
const fn = api.tabs["ungroup" as keyof typeof api.tabs] as typeof api.tabs.ungroup | undefined;
|
||||
if (!fn) throw new Error(TAB_GROUPS_UNSUPPORTED);
|
||||
return fn.bind(chrome.tabs);
|
||||
return fn.bind(api.tabs);
|
||||
}
|
||||
|
||||
export async function queryTabGroups(queryInfo: chrome.tabGroups.QueryInfo = {}): Promise<chrome.tabGroups.TabGroup[]> {
|
||||
const api = chrome["tabGroups" as keyof typeof chrome] as typeof chrome.tabGroups | undefined;
|
||||
if (!api) return [];
|
||||
return api.query(queryInfo);
|
||||
export async function queryTabGroups(queryInfo: TabGroupQueryInfo = {}): Promise<TabGroup[]> {
|
||||
const tabGroups = api["tabGroups" as keyof typeof api] as typeof api.tabGroups | undefined;
|
||||
if (!tabGroups) return [];
|
||||
return tabGroups.query(queryInfo);
|
||||
}
|
||||
|
||||
export async function getTabGroup(groupId: number): Promise<chrome.tabGroups.TabGroup> {
|
||||
export async function getTabGroup(groupId: number): Promise<TabGroup> {
|
||||
return tabGroupsApi().get(groupId);
|
||||
}
|
||||
|
||||
export async function updateTabGroup(groupId: number, updateProperties: chrome.tabGroups.UpdateProperties): Promise<chrome.tabGroups.TabGroup> {
|
||||
export async function updateTabGroup(groupId: number, updateProperties: TabGroupUpdateProperties): Promise<TabGroup> {
|
||||
return tabGroupsApi().update(groupId, updateProperties);
|
||||
}
|
||||
|
||||
export async function moveTabGroup(groupId: number, moveProperties: chrome.tabGroups.MoveProperties): Promise<chrome.tabGroups.TabGroup> {
|
||||
export async function moveTabGroup(groupId: number, moveProperties: TabGroupMoveProperties): Promise<TabGroup> {
|
||||
return tabGroupsApi().move(groupId, moveProperties);
|
||||
}
|
||||
|
||||
export async function groupTabs(createProperties: chrome.tabs.GroupOptions): Promise<number> {
|
||||
export async function groupTabs(createProperties: TabGroupOptions): Promise<number> {
|
||||
return tabsGroupApi()(createProperties);
|
||||
}
|
||||
|
||||
@@ -49,7 +51,7 @@ export async function ungroupTabs(tabIds: [number, ...number[]]): Promise<void>
|
||||
return tabsUngroupApi()(tabIds);
|
||||
}
|
||||
|
||||
export function tabGroupsOnUpdated(): chrome.events.Event<(group: chrome.tabGroups.TabGroup) => void> | undefined {
|
||||
const api = chrome["tabGroups" as keyof typeof chrome] as typeof chrome.tabGroups | undefined;
|
||||
return api?.onUpdated;
|
||||
export function tabGroupsOnUpdated(): BrowserEvent<(group: TabGroup) => void> | undefined {
|
||||
const tabGroups = api["tabGroups" as keyof typeof api] as typeof api.tabGroups | undefined;
|
||||
return tabGroups?.onUpdated;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { webExtApi as api } from '../browser-api';
|
||||
import type { Tab } from '../types';
|
||||
// Tab-related shared helpers: info shaping, scriptable-url checks, active-tab
|
||||
// resolution, and HTML fetching.
|
||||
import { isBrowserErrorUrl, isErrorPageScriptError } from './errors';
|
||||
@@ -5,8 +7,8 @@ import { executeScript } from './scripting';
|
||||
import type { TabBlock } from '../types';
|
||||
|
||||
/**
|
||||
* Narrow a plain id array to the non-empty-tuple shape that chrome.tabs.group /
|
||||
* chrome.tabs.ungroup declare. The runtime happily accepts any array (including
|
||||
* Narrow a plain id array to the non-empty-tuple shape that api.tabs.group /
|
||||
* api.tabs.ungroup declare. The runtime happily accepts any array (including
|
||||
* a single element); the published @types/chrome just over-constrain the param
|
||||
* to `[number, ...number[]]`. Callers guarantee non-emptiness before calling.
|
||||
*/
|
||||
@@ -14,7 +16,7 @@ export function asTabIds(ids: number[]): [number, ...number[]] {
|
||||
return ids as [number, ...number[]];
|
||||
}
|
||||
|
||||
export function tabInfo(t: chrome.tabs.Tab) {
|
||||
export function tabInfo(t: Tab) {
|
||||
return {
|
||||
id: t.id,
|
||||
windowId: t.windowId,
|
||||
@@ -36,16 +38,16 @@ export function isScriptableUrl(url: string | undefined | null): boolean {
|
||||
}
|
||||
|
||||
export async function getActiveTab() {
|
||||
const activeTabs = await chrome.tabs.query({ active: true });
|
||||
const activeTabs = await api.tabs.query({ active: true });
|
||||
if (!activeTabs.length) throw new Error("No active tab found");
|
||||
|
||||
const windows = await chrome.windows.getAll({ populate: false });
|
||||
const windows = await api.windows.getAll({ populate: false });
|
||||
const focusedWindowIds = new Set(windows.filter(window => window.focused).map(window => window.id));
|
||||
|
||||
const chooseTab = (predicate: (tab: chrome.tabs.Tab) => boolean) => activeTabs.find(predicate);
|
||||
const byFocusAndScriptable = (tab: chrome.tabs.Tab) => focusedWindowIds.has(tab.windowId) && isScriptableUrl(tab.url || tab.pendingUrl || "");
|
||||
const byScriptable = (tab: chrome.tabs.Tab) => isScriptableUrl(tab.url || tab.pendingUrl || "");
|
||||
const byFocus = (tab: chrome.tabs.Tab) => focusedWindowIds.has(tab.windowId);
|
||||
const chooseTab = (predicate: (tab: Tab) => boolean) => activeTabs.find(predicate);
|
||||
const byFocusAndScriptable = (tab: Tab) => focusedWindowIds.has(tab.windowId) && isScriptableUrl(tab.url || tab.pendingUrl || "");
|
||||
const byScriptable = (tab: Tab) => isScriptableUrl(tab.url || tab.pendingUrl || "");
|
||||
const byFocus = (tab: Tab) => focusedWindowIds.has(tab.windowId);
|
||||
|
||||
return chooseTab(byFocusAndScriptable)
|
||||
|| chooseTab(byScriptable)
|
||||
@@ -54,8 +56,8 @@ export async function getActiveTab() {
|
||||
}
|
||||
|
||||
/** Resolve the target tab (explicit id or the active tab) and its current URL. */
|
||||
export async function resolveTabUrl(tabId?: number | null): Promise<{ tab: chrome.tabs.Tab; url: string }> {
|
||||
const tab = tabId ? await chrome.tabs.get(tabId) : await getActiveTab();
|
||||
export async function resolveTabUrl(tabId?: number | null): Promise<{ tab: Tab; url: string }> {
|
||||
const tab = tabId ? await api.tabs.get(tabId) : await getActiveTab();
|
||||
return { tab, url: tab.url || tab.pendingUrl || "" };
|
||||
}
|
||||
|
||||
@@ -70,11 +72,11 @@ export function assertScriptableUrl(url: string, action: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
export async function resolveTabForDirectAction(tabId: number | undefined | null, actionName: string): Promise<chrome.tabs.Tab> {
|
||||
export async function resolveTabForDirectAction(tabId: number | undefined | null, actionName: string): Promise<Tab> {
|
||||
if (tabId != null) {
|
||||
return chrome.tabs.get(tabId);
|
||||
return api.tabs.get(tabId);
|
||||
}
|
||||
const allTabs = await chrome.tabs.query({});
|
||||
const allTabs = await api.tabs.query({});
|
||||
if (allTabs.length !== 1) {
|
||||
throw new Error(
|
||||
`Refusing to ${actionName} without explicit tab ID when ${allTabs.length} tabs are open`
|
||||
@@ -83,7 +85,7 @@ export async function resolveTabForDirectAction(tabId: number | undefined | null
|
||||
return allTabs[0];
|
||||
}
|
||||
|
||||
export function buildTabBlocks(tabs: chrome.tabs.Tab[]): TabBlock[] {
|
||||
export function buildTabBlocks(tabs: Tab[]): TabBlock[] {
|
||||
const blocks: TabBlock[] = [];
|
||||
for (const tab of tabs) {
|
||||
const normalizedGroupId = tab.groupId >= 0 ? tab.groupId : null;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { webExtApi as api } from '../browser-api';
|
||||
// Large-operation throttling, performance profile, and job-progress helpers.
|
||||
import type { Job, JobProgressUpdate } from '../types';
|
||||
|
||||
@@ -16,7 +17,7 @@ function debugLargeOperation(message: string) {
|
||||
}
|
||||
|
||||
export async function hasAudibleTabs() {
|
||||
const audibleTabs = await chrome.tabs.query({ audible: true });
|
||||
const audibleTabs = await api.tabs.query({ audible: true });
|
||||
return audibleTabs.some(tab => !(tab.mutedInfo && tab.mutedInfo.muted));
|
||||
}
|
||||
|
||||
@@ -36,14 +37,14 @@ export async function runLargeOperation<T>(name: string, fn: () => Promise<T>):
|
||||
}
|
||||
|
||||
export async function getPerformanceProfile() {
|
||||
const { performanceProfile } = await chrome.storage.local.get<{ performanceProfile?: string }>("performanceProfile");
|
||||
const { performanceProfile } = await api.storage.local.get<{ performanceProfile?: string }>("performanceProfile");
|
||||
return performanceProfile || "auto";
|
||||
}
|
||||
|
||||
export async function setPerformanceProfile(profile: string) {
|
||||
const allowed = new Set(["auto", "normal", "gentle", "ultra"]);
|
||||
const performanceProfile = allowed.has(profile) ? profile : "auto";
|
||||
await chrome.storage.local.set({ performanceProfile });
|
||||
await api.storage.local.set({ performanceProfile });
|
||||
return { performanceProfile };
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user