Skip to content

Commit b21b7ca

Browse files
authored
Migrate Chrome and Edge builds to Manifest V3 (#269)
* Migrate Chrome and Edge builds to Manifest V3 Chrome and Edge now build MV3 by default (Firefox stays MV2): - Drop --mv2 from the chrome/edge build, zip, and start scripts - Branch the manifest on manifestVersion: action replaces page_action, host permissions move to host_permissions, web_accessible_resources uses the MV3 object format, and minimum_chrome_version becomes 88 - Make the background persistent flag MV2-only (MV3 uses a service worker) - Emulate the old page action behavior on MV3 with the action API: disable the toolbar button globally on install/startup and enable it per-tab on deviantart.com; Firefox keeps the real pageAction API - Replace the removed extension.getURL with runtime.getURL - Update the manifest smoke tests to assert the chrome-mv3/edge-mv3 outputs (MV3 shape) alongside the unchanged firefox-mv2 output * Set per-browser transpilation targets for built bundles Vite/esbuild ignore the browserslist field and default to a ~es2020 target, so the firefox build shipped untranspiled optional chaining (?.), nullish coalescing (??), and logical nullish assignment (??=), none of which Firefox 62 (the declared strict_min_version) supports. Set the esbuild target explicitly: firefox62 for the Firefox build and chrome88 for the Chrome/Edge MV3 builds.
1 parent 72223ad commit b21b7ca

6 files changed

Lines changed: 146 additions & 40 deletions

File tree

app/entrypoints/background.js

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,27 @@ import { OnRuntimeMessage } from "scripts/background/messages";
66
import { OnNotificationClicked } from "scripts/background/notifications";
77
import { OnInstalled } from "scripts/background/runtime";
88
import { OnStorageChanged } from "scripts/background/storage";
9-
import { OpenOrShowURL, OnTabUpdate } from "scripts/background/tabs";
9+
import {
10+
InitPageActionEmulation,
11+
OpenOrShowURL,
12+
OnTabUpdate,
13+
} from "scripts/background/tabs";
1014

1115
export default defineBackground({
12-
persistent: true,
16+
// MV3 uses a non-persistent service worker; `persistent` only applies to MV2
17+
persistent: import.meta.env.MANIFEST_VERSION === 2,
1318
main() {
19+
// Firefox (MV2) still has a real page action; Chrome/Edge (MV3) use the
20+
// action API, with per-tab enabling emulating the old page action behavior
21+
const pageAction = browser.pageAction ?? browser.action;
22+
1423
browser.notifications.onClicked.addListener(OnNotificationClicked);
15-
browser.pageAction.onClicked.addListener(() =>
24+
pageAction.onClicked.addListener(() =>
1625
OpenOrShowURL(browser.runtime.getURL("manage.html")),
1726
);
1827
browser.runtime.onInstalled.addListener(OnInstalled);
28+
browser.runtime.onInstalled.addListener(InitPageActionEmulation);
29+
browser.runtime.onStartup.addListener(InitPageActionEmulation);
1930
browser.runtime.onMessage.addListener(OnRuntimeMessage);
2031
browser.storage.onChanged.addListener(OnStorageChanged);
2132
browser.tabs.onUpdated.addListener(OnTabUpdate);

app/scripts/background/runtime.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ const MigrateTagFiltersToKeywordFilters = async () => {
8181

8282
browser.notifications.create(TAG_FILTERS_MIGRATED, {
8383
type: "basic",
84-
iconUrl: browser.extension.getURL("images/icon-64.png"),
84+
iconUrl: browser.runtime.getURL("images/icon-64.png"),
8585
title: browser.i18n.getMessage("ExtensionName"),
8686
message: browser.i18n.getMessage("TagFiltersMigratedNotificationMessage"),
8787
});

app/scripts/background/tabs.js

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,36 @@ export const OpenOrShowURL = async (url) => {
2222
});
2323
};
2424

25+
/**
26+
* Disables the MV3 action button globally so it starts out hidden/disabled
27+
* everywhere, emulating the default state of an MV2 page action; `OnTabUpdate`
28+
* re-enables it per-tab on DeviantArt. No-op on browsers with a real page
29+
* action (Firefox MV2).
30+
*/
31+
export const InitPageActionEmulation = () => {
32+
if (browser.pageAction === undefined) {
33+
browser.action.disable();
34+
}
35+
};
36+
37+
/**
38+
* Shows the page action for a tab (or, on MV3, enables the action button)
39+
* @param {number} tabId the ID of the tab
40+
*/
41+
const ShowPageAction = (tabId) =>
42+
browser.pageAction !== undefined
43+
? browser.pageAction.show(tabId)
44+
: browser.action.enable(tabId);
45+
46+
/**
47+
* Hides the page action for a tab (or, on MV3, disables the action button)
48+
* @param {number} tabId the ID of the tab
49+
*/
50+
const HidePageAction = (tabId) =>
51+
browser.pageAction !== undefined
52+
? browser.pageAction.hide(tabId)
53+
: browser.action.disable(tabId);
54+
2555
/**
2656
* Event handler for tab updates
2757
* @param {number} tabId the ID of the tab that was updated
@@ -30,8 +60,8 @@ export const OpenOrShowURL = async (url) => {
3060
*/
3161
export const OnTabUpdate = (tabId, changeInfo, tab) => {
3262
if (REGEX.test(tab.url)) {
33-
browser.pageAction.show(tabId);
63+
ShowPageAction(tabId);
3464
} else {
35-
browser.pageAction.hide(tabId);
65+
HidePageAction(tabId);
3666
}
3767
};

package.json

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,17 @@
99
"private": true,
1010
"scripts": {
1111
"lint": "eslint ./app/",
12-
"dev": "wxt --mv2",
13-
"build": "wxt build --mv2",
14-
"build:chrome": "wxt build -b chrome --mv2",
15-
"build:edge": "wxt build -b edge --mv2",
12+
"dev": "wxt",
13+
"build": "wxt build",
14+
"build:chrome": "wxt build -b chrome",
15+
"build:edge": "wxt build -b edge",
1616
"build:firefox": "wxt build -b firefox --mv2",
1717
"build:icons": "node scripts/generate-icons",
18-
"zip:chrome": "wxt zip -b chrome --mv2",
19-
"zip:edge": "wxt zip -b edge --mv2",
18+
"zip:chrome": "wxt zip -b chrome",
19+
"zip:edge": "wxt zip -b edge",
2020
"zip:firefox": "wxt zip -b firefox --mv2",
21-
"start:chrome": "wxt -b chrome --mv2",
22-
"start:edge": "wxt -b edge --mv2",
21+
"start:chrome": "wxt -b chrome",
22+
"start:edge": "wxt -b edge",
2323
"start:firefox": "wxt -b firefox --mv2",
2424
"start:firefox:nr": "npm run start:firefox -- --no-reload",
2525
"test": "npm run lint && npm run test:builds && npm run test:packages && npm run test:firefox:lint",

scripts/smoke-manifests.js

Lines changed: 59 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ const fs = require("fs");
22
const path = require("path");
33

44
const root = path.resolve(__dirname, "..");
5+
const hostPermission = "*://*.deviantart.com/*";
56
const requiredPermissions = [
6-
"*://*.deviantart.com/*",
77
"activeTab",
88
"contextMenus",
99
"notifications",
@@ -13,25 +13,28 @@ const requiredPermissions = [
1313

1414
const targets = {
1515
chrome: {
16-
directory: "chrome-mv2",
16+
directory: "chrome-mv3",
17+
manifestVersion: 3,
1718
extraChecks(manifest) {
1819
assert(
19-
manifest.minimum_chrome_version === "49.0",
20-
"Chrome minimum version should be preserved",
20+
manifest.minimum_chrome_version === "88.0",
21+
"Chrome minimum version should be 88 for MV3",
2122
);
2223
},
2324
},
2425
edge: {
25-
directory: "edge-mv2",
26+
directory: "edge-mv3",
27+
manifestVersion: 3,
2628
extraChecks(manifest) {
2729
assert(
28-
manifest.minimum_chrome_version === "79.0",
29-
"Edge minimum version should be preserved",
30+
manifest.minimum_chrome_version === "88.0",
31+
"Edge minimum version should be 88 for MV3",
3032
);
3133
},
3234
},
3335
firefox: {
3436
directory: "firefox-mv2",
37+
manifestVersion: 2,
3538
extraChecks(manifest) {
3639
assert(
3740
manifest.browser_specific_settings?.gecko?.id ===
@@ -55,17 +58,19 @@ const targets = {
5558
},
5659
};
5760

58-
for (const [browser, { directory, extraChecks }] of Object.entries(targets)) {
61+
for (const [
62+
browser,
63+
{ directory, manifestVersion, extraChecks },
64+
] of Object.entries(targets)) {
5965
const outputDir = path.join(root, ".output", directory);
6066
const manifestPath = path.join(outputDir, "manifest.json");
6167
const manifest = readJson(manifestPath);
6268

63-
assert(manifest.manifest_version === 2, `${browser} should remain MV2`);
69+
assert(
70+
manifest.manifest_version === manifestVersion,
71+
`${browser} should be MV${manifestVersion}`,
72+
);
6473
assert(manifest.default_locale === "en", `${browser} locale should exist`);
65-
assert(manifest.background?.scripts?.length, `${browser} needs background`);
66-
assert(manifest.page_action, `${browser} should use page_action`);
67-
assert(!manifest.action, `${browser} should not use MV3 action`);
68-
assert(!manifest.browser_action, `${browser} should not use browser_action`);
6974

7075
for (const permission of requiredPermissions) {
7176
assert(
@@ -74,10 +79,49 @@ for (const [browser, { directory, extraChecks }] of Object.entries(targets)) {
7479
);
7580
}
7681

82+
if (manifestVersion === 3) {
83+
assert(
84+
manifest.background?.service_worker,
85+
`${browser} needs a background service worker`,
86+
);
87+
assert(manifest.action, `${browser} should use MV3 action`);
88+
assert(!manifest.page_action, `${browser} should not use page_action`);
89+
assert(
90+
!manifest.permissions.includes(hostPermission),
91+
`${browser} host match pattern should not be in permissions`,
92+
);
93+
assert(
94+
manifest.host_permissions?.includes(hostPermission),
95+
`${browser} missing host permission: ${hostPermission}`,
96+
);
97+
const warEntries = manifest.web_accessible_resources ?? [];
98+
assert(
99+
warEntries.some(
100+
(entry) =>
101+
entry.resources?.includes("create-filters.html") &&
102+
entry.matches?.includes(hostPermission),
103+
),
104+
`${browser} create-filters page should be web accessible on DeviantArt`,
105+
);
106+
} else {
107+
assert(manifest.background?.scripts?.length, `${browser} needs background`);
108+
assert(manifest.page_action, `${browser} should use page_action`);
109+
assert(!manifest.action, `${browser} should not use MV3 action`);
110+
assert(
111+
manifest.permissions.includes(hostPermission),
112+
`${browser} missing permission: ${hostPermission}`,
113+
);
114+
assert(
115+
manifest.web_accessible_resources?.includes("create-filters.html"),
116+
`${browser} create-filters page should be web accessible`,
117+
);
118+
}
119+
assert(!manifest.browser_action, `${browser} should not use browser_action`);
120+
77121
const contentScript = manifest.content_scripts?.[0];
78122
assert(contentScript, `${browser} needs a content script`);
79123
assert(
80-
contentScript.matches?.includes("*://*.deviantart.com/*"),
124+
contentScript.matches?.includes(hostPermission),
81125
`${browser} content script should match DeviantArt`,
82126
);
83127
assert(
@@ -91,11 +135,6 @@ for (const [browser, { directory, extraChecks }] of Object.entries(targets)) {
91135
assert(contentScript.js?.length, `${browser} content script JS missing`);
92136
assert(contentScript.css?.length, `${browser} content script CSS missing`);
93137

94-
assert(
95-
manifest.web_accessible_resources?.includes("create-filters.html"),
96-
`${browser} create-filters page should be web accessible`,
97-
);
98-
99138
for (const file of [
100139
"manage.html",
101140
"create-filters.html",
@@ -109,7 +148,7 @@ for (const [browser, { directory, extraChecks }] of Object.entries(targets)) {
109148
}
110149

111150
extraChecks(manifest);
112-
console.log(`Validated ${browser} manifest`);
151+
console.log(`Validated ${browser} manifest (MV${manifestVersion})`);
113152
}
114153

115154
function readJson(file) {

wxt.config.ts

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,26 +15,40 @@ const iconPaths = {
1515
export default defineConfig({
1616
srcDir: "app",
1717
modules: ["@wxt-dev/module-react", "@wxt-dev/webextension-polyfill"],
18-
manifest: ({ browser }) => ({
18+
manifest: ({ browser, manifestVersion }) => ({
1919
name: "__MSG_ExtensionName__",
2020
short_name: "__MSG_ExtensionShortName__",
2121
description: "__MSG_ExtensionDescription__",
2222
default_locale: "en",
2323
homepage_url: "https://rthaut.github.io/deviantART-Filter",
2424
icons: iconPaths,
25-
page_action: {
25+
// MV3 has no page_action; the action button is emulated as a page action
26+
// at runtime (disabled globally, enabled per-tab on deviantart.com)
27+
[manifestVersion === 3 ? "action" : "page_action"]: {
2628
default_icon: iconPaths,
2729
default_title: "__MSG_BrowserActionTitle__",
2830
},
2931
permissions: [
30-
"*://*.deviantart.com/*",
3132
"activeTab",
3233
"contextMenus",
3334
"notifications",
3435
"storage",
3536
"tabs",
37+
// MV3 moves host match patterns into host_permissions
38+
...(manifestVersion === 3 ? [] : ["*://*.deviantart.com/*"]),
3639
],
37-
web_accessible_resources: ["create-filters.html"],
40+
...(manifestVersion === 3
41+
? { host_permissions: ["*://*.deviantart.com/*"] }
42+
: {}),
43+
web_accessible_resources:
44+
manifestVersion === 3
45+
? [
46+
{
47+
resources: ["create-filters.html"],
48+
matches: ["*://*.deviantart.com/*"],
49+
},
50+
]
51+
: ["create-filters.html"],
3852
...(browser === "firefox"
3953
? {
4054
browser_specific_settings: {
@@ -48,10 +62,22 @@ export default defineConfig({
4862
},
4963
}
5064
: {
51-
minimum_chrome_version: browser === "edge" ? "79.0" : "49.0",
65+
// MV3 requires Chrome 88+ (Edge 88+ is the matching Chromium)
66+
minimum_chrome_version:
67+
manifestVersion === 3
68+
? "88.0"
69+
: browser === "edge"
70+
? "79.0"
71+
: "49.0",
5272
}),
5373
}),
54-
vite: () => ({
74+
vite: ({ browser }) => ({
75+
build: {
76+
// Vite ignores browserslist; set the transpilation target explicitly so
77+
// built bundles honor each browser's minimum supported version (without
78+
// this, `?.`/`??`/`??=` are emitted untranspiled and break Firefox 62)
79+
target: browser === "firefox" ? "firefox62" : "chrome88",
80+
},
5581
resolve: {
5682
alias: {
5783
scripts: "/app/scripts",

0 commit comments

Comments
 (0)