Skip to content

Commit 0f27f77

Browse files
authored
Avoid duplicate injection on MV3 worker restart (#73)
1 parent f1561a1 commit 0f27f77

File tree

8 files changed

+89
-31
lines changed

8 files changed

+89
-31
lines changed

package-lock.json

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
"content-scripts-register-polyfill": "^4.0.2",
7878
"webext-content-scripts": "^2.6.1",
7979
"webext-detect-page": "^5.0.1",
80+
"webext-events": "^3.0.0",
8081
"webext-patterns": "^1.4.0",
8182
"webext-permissions": "^3.1.3",
8283
"webext-polyfill-kinda": "^1.0.2"

readme.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ navigator.importScripts('webext-dynamic-content-scripts.js');
4444
```json
4545
// example manifest.json
4646
{
47-
"permissions": ["scripting"],
47+
"permissions": ["scripting", "storage"],
4848
"optional_host_permissions": ["*://*/*"],
4949
"background": {
5050
"service_worker": "background.worker.js"

source/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
import {init} from './lib.js';
22

3-
void init();
3+
init();

source/inject-to-existing-tabs.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import {getTabsByUrl, injectContentScript} from 'webext-content-scripts';
22

33
type ManifestContentScripts = NonNullable<chrome.runtime.Manifest['content_scripts']>;
44

5+
// May not be needed in the future in Firefox
6+
// https://bugzilla.mozilla.org/show_bug.cgi?id=1458947
57
export async function injectToExistingTabs(
68
origins: string[],
79
scripts: ManifestContentScripts,

source/lib.test.ts

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,15 @@ import {
33
describe, it, vi, beforeEach, expect,
44
} from 'vitest';
55
import {queryAdditionalPermissions} from 'webext-permissions';
6+
import {onExtensionStart} from 'webext-events';
67
import {init} from './lib.js';
78
import {injectToExistingTabs} from './inject-to-existing-tabs.js';
89
import {registerContentScript} from './register-content-script-shim.js';
910

11+
type AsyncFunction = () => void | Promise<void>;
12+
1013
vi.mock('webext-permissions');
14+
vi.mock('webext-events');
1115
vi.mock('./register-content-script-shim.js');
1216
vi.mock('./inject-to-existing-tabs.js');
1317

@@ -21,6 +25,7 @@ const baseManifest: chrome.runtime.Manifest = {
2125
matches: ['https://content-script.example.com/*'],
2226
},
2327
],
28+
permissions: ['storage'],
2429
host_permissions: ['https://permission-only.example.com/*'],
2530
optional_host_permissions: ['*://*/*'],
2631
};
@@ -34,6 +39,17 @@ const queryAdditionalPermissionsMock = vi.mocked(queryAdditionalPermissions);
3439
const injectToExistingTabsMock = vi.mocked(injectToExistingTabs);
3540
const registerContentScriptMock = vi.mocked(registerContentScript);
3641

42+
const callbacks = new Set<AsyncFunction>();
43+
44+
vi.mocked(onExtensionStart.addListener).mockImplementation((callback: AsyncFunction) => {
45+
callbacks.add(callback);
46+
});
47+
48+
async function simulateExtensionStart() {
49+
await Promise.all(Array.from(callbacks).map(async callback => callback()));
50+
callbacks.clear();
51+
}
52+
3753
beforeEach(() => {
3854
registerContentScriptMock.mockClear();
3955
injectToExistingTabsMock.mockClear();
@@ -43,7 +59,9 @@ beforeEach(() => {
4359

4460
describe('init', () => {
4561
it('it should register the listeners and start checking permissions', async () => {
46-
await init();
62+
init();
63+
await simulateExtensionStart();
64+
4765
expect(queryAdditionalPermissionsMock).toHaveBeenCalled();
4866
expect(injectToExistingTabsMock).toHaveBeenCalledWith(
4967
additionalPermissions.origins,
@@ -59,13 +77,16 @@ describe('init', () => {
5977
const manifest = structuredClone(baseManifest);
6078
delete manifest.content_scripts;
6179
chrome.runtime.getManifest.mockReturnValue(manifest);
62-
await expect(init()).rejects.toMatchInlineSnapshot('[Error: webext-dynamic-content-scripts tried to register scripts on the new host permissions, but no content scripts were found in the manifest.]');
80+
init();
81+
await expect(simulateExtensionStart).rejects
82+
.toThrowErrorMatchingInlineSnapshot('[Error: webext-dynamic-content-scripts tried to register scripts on the new host permissions, but no content scripts were found in the manifest.]');
6383
});
6484
});
6585

6686
describe('init - registerContentScript', () => {
6787
it('should register the manifest scripts on new permissions', async () => {
68-
await init();
88+
init();
89+
await simulateExtensionStart();
6990
expect(registerContentScriptMock).toMatchSnapshot();
7091
});
7192

@@ -78,7 +99,8 @@ describe('init - registerContentScript', () => {
7899
permissions: [],
79100
});
80101

81-
await init();
102+
init();
103+
await simulateExtensionStart();
82104
expect(registerContentScriptMock).toMatchSnapshot();
83105
});
84106

@@ -90,7 +112,8 @@ describe('init - registerContentScript', () => {
90112
});
91113
chrome.runtime.getManifest.mockReturnValue(manifest);
92114

93-
await init();
115+
init();
116+
await simulateExtensionStart();
94117
expect(registerContentScriptMock).toMatchSnapshot();
95118
});
96119
});

source/lib.ts

Lines changed: 39 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {queryAdditionalPermissions} from 'webext-permissions';
2+
import {onExtensionStart} from 'webext-events';
23
import {excludeDuplicateFiles} from './deduplicator.js';
34
import {injectToExistingTabs} from './inject-to-existing-tabs.js';
45
import {registerContentScript} from './register-content-script-shim.js';
@@ -13,25 +14,28 @@ function makePathRelative(file: string): string {
1314
return new URL(file, location.origin).pathname;
1415
}
1516

16-
// Automatically register the content scripts on the new origins
17-
async function registerOnOrigins({
18-
origins: newOrigins,
19-
}: chrome.permissions.Permissions): Promise<void> {
20-
if (!newOrigins?.length) {
21-
return;
22-
}
23-
17+
function getContentScripts() {
2418
const {content_scripts: rawManifest, manifest_version: manifestVersion} = chrome.runtime.getManifest();
2519

2620
if (!rawManifest) {
2721
throw new Error('webext-dynamic-content-scripts tried to register scripts on the new host permissions, but no content scripts were found in the manifest.');
2822
}
2923

30-
const cleanManifest = excludeDuplicateFiles(rawManifest, {warn: manifestVersion === 2});
24+
return excludeDuplicateFiles(rawManifest, {warn: manifestVersion === 2});
25+
}
26+
27+
// Automatically register the content scripts on the new origins
28+
async function registerOnOrigins(
29+
origins: string[],
30+
contentScripts: ReturnType<typeof getContentScripts>,
31+
): Promise<void> {
32+
if (origins.length === 0) {
33+
return;
34+
}
3135

3236
// Register one at a time to allow removing one at a time as well
33-
for (const origin of newOrigins) {
34-
for (const config of cleanManifest) {
37+
for (const origin of origins) {
38+
for (const config of contentScripts) {
3539
const registeredScript = registerContentScript({
3640
// Always convert paths here because we don't know whether Firefox MV3 will accept full URLs
3741
js: config.js?.map(file => makePathRelative(file)),
@@ -44,14 +48,10 @@ async function registerOnOrigins({
4448
registeredScripts.set(origin, registeredScript);
4549
}
4650
}
47-
48-
// May not be needed in the future in Firefox
49-
// https://bugzilla.mozilla.org/show_bug.cgi?id=1458947
50-
void injectToExistingTabs(newOrigins, cleanManifest);
5151
}
5252

53-
function handleNewPermissions(permissions: chrome.permissions.Permissions) {
54-
void registerOnOrigins(permissions);
53+
async function handleNewPermissions({origins}: chrome.permissions.Permissions) {
54+
await enableOnOrigins(origins);
5555
}
5656

5757
async function handledDroppedPermissions({origins}: chrome.permissions.Permissions) {
@@ -68,12 +68,28 @@ async function handledDroppedPermissions({origins}: chrome.permissions.Permissio
6868
}
6969
}
7070

71-
export async function init() {
71+
async function enableOnOrigins(origins: string[] | undefined) {
72+
if (!origins?.length) {
73+
return;
74+
}
75+
76+
const contentScripts = getContentScripts();
77+
await Promise.all([
78+
injectToExistingTabs(origins, contentScripts),
79+
registerOnOrigins(origins, contentScripts),
80+
]);
81+
}
82+
83+
async function registerExistingOrigins() {
84+
const {origins} = await queryAdditionalPermissions({
85+
strictOrigins: false,
86+
});
87+
88+
await enableOnOrigins(origins);
89+
}
90+
91+
export function init() {
7292
chrome.permissions.onRemoved.addListener(handledDroppedPermissions);
7393
chrome.permissions.onAdded.addListener(handleNewPermissions);
74-
await registerOnOrigins(
75-
await queryAdditionalPermissions({
76-
strictOrigins: false,
77-
}),
78-
);
94+
onExtensionStart.addListener(registerExistingOrigins);
7995
}

test/demo-extension/mv3/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "webext-dynamic-content-scripts-mv3",
33
"version": "0.0.0",
44
"manifest_version": 3,
5-
"permissions": ["webNavigation", "scripting", "contextMenus", "activeTab"],
5+
"permissions": ["webNavigation", "scripting", "contextMenus", "activeTab", "storage"],
66
"host_permissions": [
77
"https://dynamic-ephiframe.vercel.app/*",
88
"https://accepted-ephiframe.vercel.app/*"

0 commit comments

Comments
 (0)