diff --git a/app/entrypoints/background.js b/app/entrypoints/background.js index c49954f..bf7a50c 100644 --- a/app/entrypoints/background.js +++ b/app/entrypoints/background.js @@ -6,16 +6,27 @@ import { OnRuntimeMessage } from "scripts/background/messages"; import { OnNotificationClicked } from "scripts/background/notifications"; import { OnInstalled } from "scripts/background/runtime"; import { OnStorageChanged } from "scripts/background/storage"; -import { OpenOrShowURL, OnTabUpdate } from "scripts/background/tabs"; +import { + InitPageActionEmulation, + OpenOrShowURL, + OnTabUpdate, +} from "scripts/background/tabs"; export default defineBackground({ - persistent: true, + // MV3 uses a non-persistent service worker; `persistent` only applies to MV2 + persistent: import.meta.env.MANIFEST_VERSION === 2, main() { + // Firefox (MV2) still has a real page action; Chrome/Edge (MV3) use the + // action API, with per-tab enabling emulating the old page action behavior + const pageAction = browser.pageAction ?? browser.action; + browser.notifications.onClicked.addListener(OnNotificationClicked); - browser.pageAction.onClicked.addListener(() => + pageAction.onClicked.addListener(() => OpenOrShowURL(browser.runtime.getURL("manage.html")), ); browser.runtime.onInstalled.addListener(OnInstalled); + browser.runtime.onInstalled.addListener(InitPageActionEmulation); + browser.runtime.onStartup.addListener(InitPageActionEmulation); browser.runtime.onMessage.addListener(OnRuntimeMessage); browser.storage.onChanged.addListener(OnStorageChanged); browser.tabs.onUpdated.addListener(OnTabUpdate); diff --git a/app/scripts/background/runtime.js b/app/scripts/background/runtime.js index f64a570..73fde11 100644 --- a/app/scripts/background/runtime.js +++ b/app/scripts/background/runtime.js @@ -81,7 +81,7 @@ const MigrateTagFiltersToKeywordFilters = async () => { browser.notifications.create(TAG_FILTERS_MIGRATED, { type: "basic", - iconUrl: browser.extension.getURL("images/icon-64.png"), + iconUrl: browser.runtime.getURL("images/icon-64.png"), title: browser.i18n.getMessage("ExtensionName"), message: browser.i18n.getMessage("TagFiltersMigratedNotificationMessage"), }); diff --git a/app/scripts/background/tabs.js b/app/scripts/background/tabs.js index 0296509..018955b 100644 --- a/app/scripts/background/tabs.js +++ b/app/scripts/background/tabs.js @@ -22,6 +22,36 @@ export const OpenOrShowURL = async (url) => { }); }; +/** + * Disables the MV3 action button globally so it starts out hidden/disabled + * everywhere, emulating the default state of an MV2 page action; `OnTabUpdate` + * re-enables it per-tab on DeviantArt. No-op on browsers with a real page + * action (Firefox MV2). + */ +export const InitPageActionEmulation = () => { + if (browser.pageAction === undefined) { + browser.action.disable(); + } +}; + +/** + * Shows the page action for a tab (or, on MV3, enables the action button) + * @param {number} tabId the ID of the tab + */ +const ShowPageAction = (tabId) => + browser.pageAction !== undefined + ? browser.pageAction.show(tabId) + : browser.action.enable(tabId); + +/** + * Hides the page action for a tab (or, on MV3, disables the action button) + * @param {number} tabId the ID of the tab + */ +const HidePageAction = (tabId) => + browser.pageAction !== undefined + ? browser.pageAction.hide(tabId) + : browser.action.disable(tabId); + /** * Event handler for tab updates * @param {number} tabId the ID of the tab that was updated @@ -30,8 +60,8 @@ export const OpenOrShowURL = async (url) => { */ export const OnTabUpdate = (tabId, changeInfo, tab) => { if (REGEX.test(tab.url)) { - browser.pageAction.show(tabId); + ShowPageAction(tabId); } else { - browser.pageAction.hide(tabId); + HidePageAction(tabId); } }; diff --git a/package.json b/package.json index cdf2f57..77e8f77 100644 --- a/package.json +++ b/package.json @@ -9,17 +9,17 @@ "private": true, "scripts": { "lint": "eslint ./app/", - "dev": "wxt --mv2", - "build": "wxt build --mv2", - "build:chrome": "wxt build -b chrome --mv2", - "build:edge": "wxt build -b edge --mv2", + "dev": "wxt", + "build": "wxt build", + "build:chrome": "wxt build -b chrome", + "build:edge": "wxt build -b edge", "build:firefox": "wxt build -b firefox --mv2", "build:icons": "node scripts/generate-icons", - "zip:chrome": "wxt zip -b chrome --mv2", - "zip:edge": "wxt zip -b edge --mv2", + "zip:chrome": "wxt zip -b chrome", + "zip:edge": "wxt zip -b edge", "zip:firefox": "wxt zip -b firefox --mv2", - "start:chrome": "wxt -b chrome --mv2", - "start:edge": "wxt -b edge --mv2", + "start:chrome": "wxt -b chrome", + "start:edge": "wxt -b edge", "start:firefox": "wxt -b firefox --mv2", "start:firefox:nr": "npm run start:firefox -- --no-reload", "test": "npm run lint && npm run test:builds && npm run test:packages && npm run test:firefox:lint", diff --git a/scripts/smoke-manifests.js b/scripts/smoke-manifests.js index 2e1f05a..50463dd 100644 --- a/scripts/smoke-manifests.js +++ b/scripts/smoke-manifests.js @@ -2,8 +2,8 @@ const fs = require("fs"); const path = require("path"); const root = path.resolve(__dirname, ".."); +const hostPermission = "*://*.deviantart.com/*"; const requiredPermissions = [ - "*://*.deviantart.com/*", "activeTab", "contextMenus", "notifications", @@ -13,25 +13,28 @@ const requiredPermissions = [ const targets = { chrome: { - directory: "chrome-mv2", + directory: "chrome-mv3", + manifestVersion: 3, extraChecks(manifest) { assert( - manifest.minimum_chrome_version === "49.0", - "Chrome minimum version should be preserved", + manifest.minimum_chrome_version === "88.0", + "Chrome minimum version should be 88 for MV3", ); }, }, edge: { - directory: "edge-mv2", + directory: "edge-mv3", + manifestVersion: 3, extraChecks(manifest) { assert( - manifest.minimum_chrome_version === "79.0", - "Edge minimum version should be preserved", + manifest.minimum_chrome_version === "88.0", + "Edge minimum version should be 88 for MV3", ); }, }, firefox: { directory: "firefox-mv2", + manifestVersion: 2, extraChecks(manifest) { assert( manifest.browser_specific_settings?.gecko?.id === @@ -55,17 +58,19 @@ const targets = { }, }; -for (const [browser, { directory, extraChecks }] of Object.entries(targets)) { +for (const [ + browser, + { directory, manifestVersion, extraChecks }, +] of Object.entries(targets)) { const outputDir = path.join(root, ".output", directory); const manifestPath = path.join(outputDir, "manifest.json"); const manifest = readJson(manifestPath); - assert(manifest.manifest_version === 2, `${browser} should remain MV2`); + assert( + manifest.manifest_version === manifestVersion, + `${browser} should be MV${manifestVersion}`, + ); assert(manifest.default_locale === "en", `${browser} locale should exist`); - assert(manifest.background?.scripts?.length, `${browser} needs background`); - assert(manifest.page_action, `${browser} should use page_action`); - assert(!manifest.action, `${browser} should not use MV3 action`); - assert(!manifest.browser_action, `${browser} should not use browser_action`); for (const permission of requiredPermissions) { assert( @@ -74,10 +79,49 @@ for (const [browser, { directory, extraChecks }] of Object.entries(targets)) { ); } + if (manifestVersion === 3) { + assert( + manifest.background?.service_worker, + `${browser} needs a background service worker`, + ); + assert(manifest.action, `${browser} should use MV3 action`); + assert(!manifest.page_action, `${browser} should not use page_action`); + assert( + !manifest.permissions.includes(hostPermission), + `${browser} host match pattern should not be in permissions`, + ); + assert( + manifest.host_permissions?.includes(hostPermission), + `${browser} missing host permission: ${hostPermission}`, + ); + const warEntries = manifest.web_accessible_resources ?? []; + assert( + warEntries.some( + (entry) => + entry.resources?.includes("create-filters.html") && + entry.matches?.includes(hostPermission), + ), + `${browser} create-filters page should be web accessible on DeviantArt`, + ); + } else { + assert(manifest.background?.scripts?.length, `${browser} needs background`); + assert(manifest.page_action, `${browser} should use page_action`); + assert(!manifest.action, `${browser} should not use MV3 action`); + assert( + manifest.permissions.includes(hostPermission), + `${browser} missing permission: ${hostPermission}`, + ); + assert( + manifest.web_accessible_resources?.includes("create-filters.html"), + `${browser} create-filters page should be web accessible`, + ); + } + assert(!manifest.browser_action, `${browser} should not use browser_action`); + const contentScript = manifest.content_scripts?.[0]; assert(contentScript, `${browser} needs a content script`); assert( - contentScript.matches?.includes("*://*.deviantart.com/*"), + contentScript.matches?.includes(hostPermission), `${browser} content script should match DeviantArt`, ); assert( @@ -91,11 +135,6 @@ for (const [browser, { directory, extraChecks }] of Object.entries(targets)) { assert(contentScript.js?.length, `${browser} content script JS missing`); assert(contentScript.css?.length, `${browser} content script CSS missing`); - assert( - manifest.web_accessible_resources?.includes("create-filters.html"), - `${browser} create-filters page should be web accessible`, - ); - for (const file of [ "manage.html", "create-filters.html", @@ -109,7 +148,7 @@ for (const [browser, { directory, extraChecks }] of Object.entries(targets)) { } extraChecks(manifest); - console.log(`Validated ${browser} manifest`); + console.log(`Validated ${browser} manifest (MV${manifestVersion})`); } function readJson(file) { diff --git a/wxt.config.ts b/wxt.config.ts index 35bff55..15ad85f 100644 --- a/wxt.config.ts +++ b/wxt.config.ts @@ -15,26 +15,40 @@ const iconPaths = { export default defineConfig({ srcDir: "app", modules: ["@wxt-dev/module-react", "@wxt-dev/webextension-polyfill"], - manifest: ({ browser }) => ({ + manifest: ({ browser, manifestVersion }) => ({ name: "__MSG_ExtensionName__", short_name: "__MSG_ExtensionShortName__", description: "__MSG_ExtensionDescription__", default_locale: "en", homepage_url: "https://rthaut.github.io/deviantART-Filter", icons: iconPaths, - page_action: { + // MV3 has no page_action; the action button is emulated as a page action + // at runtime (disabled globally, enabled per-tab on deviantart.com) + [manifestVersion === 3 ? "action" : "page_action"]: { default_icon: iconPaths, default_title: "__MSG_BrowserActionTitle__", }, permissions: [ - "*://*.deviantart.com/*", "activeTab", "contextMenus", "notifications", "storage", "tabs", + // MV3 moves host match patterns into host_permissions + ...(manifestVersion === 3 ? [] : ["*://*.deviantart.com/*"]), ], - web_accessible_resources: ["create-filters.html"], + ...(manifestVersion === 3 + ? { host_permissions: ["*://*.deviantart.com/*"] } + : {}), + web_accessible_resources: + manifestVersion === 3 + ? [ + { + resources: ["create-filters.html"], + matches: ["*://*.deviantart.com/*"], + }, + ] + : ["create-filters.html"], ...(browser === "firefox" ? { browser_specific_settings: { @@ -48,10 +62,22 @@ export default defineConfig({ }, } : { - minimum_chrome_version: browser === "edge" ? "79.0" : "49.0", + // MV3 requires Chrome 88+ (Edge 88+ is the matching Chromium) + minimum_chrome_version: + manifestVersion === 3 + ? "88.0" + : browser === "edge" + ? "79.0" + : "49.0", }), }), - vite: () => ({ + vite: ({ browser }) => ({ + build: { + // Vite ignores browserslist; set the transpilation target explicitly so + // built bundles honor each browser's minimum supported version (without + // this, `?.`/`??`/`??=` are emitted untranspiled and break Firefox 62) + target: browser === "firefox" ? "firefox62" : "chrome88", + }, resolve: { alias: { scripts: "/app/scripts",