Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
"devDependencies": {
"@parcel/config-webextension": "^2.8.2",
"@sindresorhus/tsconfig": "^3.0.1",
"@types/chrome": "^0.0.206",
"@types/chrome": "^0.0.208",
"@types/firefox-webext-browser": "^94.0.1",
"@types/jest": "^29.2.5",
"jest": "^29.3.1",
Expand Down
128 changes: 128 additions & 0 deletions source/__snapshots__/lib.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// Vitest Snapshot v1

exports[`init - registerContentScript > should register multiple manifest scripts on new permissions 1`] = `
[MockFunction registerContentScript] {
"calls": [
[
{
"allFrames": undefined,
"css": undefined,
"excludeMatches": [
"https://content-script.example.com/*",
],
"js": [
"/script.js",
],
"matches": [
"https://granted.example.com/*",
],
"runAt": undefined,
},
],
[
{
"allFrames": undefined,
"css": undefined,
"excludeMatches": [
"https://content-script-extra.example.com/*",
],
"js": [
"/otherScript.js",
],
"matches": [
"https://granted.example.com/*",
],
"runAt": undefined,
},
],
],
"results": [
{
"type": "return",
"value": undefined,
},
{
"type": "return",
"value": undefined,
},
],
}
`;

exports[`init - registerContentScript > should register the manifest scripts on multiple new permissions 1`] = `
[MockFunction registerContentScript] {
"calls": [
[
{
"allFrames": undefined,
"css": undefined,
"excludeMatches": [
"https://content-script.example.com/*",
],
"js": [
"/script.js",
],
"matches": [
"https://granted.example.com/*",
],
"runAt": undefined,
},
],
[
{
"allFrames": undefined,
"css": undefined,
"excludeMatches": [
"https://content-script.example.com/*",
],
"js": [
"/script.js",
],
"matches": [
"https://granted-more.example.com/*",
],
"runAt": undefined,
},
],
],
"results": [
{
"type": "return",
"value": undefined,
},
{
"type": "return",
"value": undefined,
},
],
}
`;

exports[`init - registerContentScript > should register the manifest scripts on new permissions 1`] = `
[MockFunction registerContentScript] {
"calls": [
[
{
"allFrames": undefined,
"css": undefined,
"excludeMatches": [
"https://content-script.example.com/*",
],
"js": [
"/script.js",
],
"matches": [
"https://granted.example.com/*",
],
"runAt": undefined,
},
],
],
"results": [
{
"type": "return",
"value": undefined,
},
],
}
`;
19 changes: 19 additions & 0 deletions source/inject-to-existing-tabs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {injectContentScript} from 'webext-content-scripts';

export function injectToExistingTabs(
origins: string[],
scripts: ManifestContentScripts) {
if (origins.length === 0) {
return;
}

chrome.tabs.query({
url: origins,
}, tabs => {
for (const tab of tabs) {
if (tab.id) {
void injectContentScript(tab.id, scripts);
}
}
});
}
94 changes: 94 additions & 0 deletions source/lib.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import {chrome} from 'jest-chrome';
import {describe, it, vi, beforeEach, expect} from 'vitest';
import {getAdditionalPermissions} from 'webext-additional-permissions';
import {init} from './lib.js';
import {injectToExistingTabs} from './inject-to-existing-tabs.js';
import {registerContentScript} from './register-content-script-shim.js';

vi.mock('webext-additional-permissions');
vi.mock('./register-content-script-shim.js');
vi.mock('./inject-to-existing-tabs.js');

const baseManifest: chrome.runtime.Manifest = {
name: 'required',
manifest_version: 3,
version: '0.0.0',
content_scripts: [
{
js: ['script.js'],
matches: ['https://content-script.example.com/*'],
},
],
host_permissions: ['https://permission-only.example.com/*'],
optional_host_permissions: ['*://*/*'],
};

const additionalPermissions: Required<chrome.permissions.Permissions> = {
origins: ['https://granted.example.com/*'],
permissions: [],
};

const getAdditionalPermissionsMock = vi.mocked(getAdditionalPermissions);
const injectToExistingTabsMock = vi.mocked(injectToExistingTabs);
const registerContentScriptMock = vi.mocked(registerContentScript);

beforeEach(() => {
registerContentScriptMock.mockClear();
injectToExistingTabsMock.mockClear();
getAdditionalPermissionsMock.mockResolvedValue(additionalPermissions);
chrome.runtime.getManifest.mockReturnValue(baseManifest);
});

describe('init', () => {
it('it should register the listeners and start checking permissions', async () => {
await init();
expect(getAdditionalPermissionsMock).toHaveBeenCalled();
expect(injectToExistingTabsMock).toHaveBeenCalledWith(
additionalPermissions.origins,
baseManifest.content_scripts,
);

// TODO: https://github.com/extend-chrome/jest-chrome/issues/20
// expect(chrome.permissions.onAdded.addListener).toHaveBeenCalledOnce();
// expect(chrome.permissions.onRemoved.addListener).toHaveBeenCalledOnce();
});

it('it should throw if no content scripts exist at all', async () => {
const manifest = structuredClone(baseManifest);
delete manifest.content_scripts;
chrome.runtime.getManifest.mockReturnValue(manifest);
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.]');
});
});

describe('init - registerContentScript', () => {
it('should register the manifest scripts on new permissions', async () => {
await init();
expect(registerContentScriptMock).toMatchSnapshot();
});

it('should register the manifest scripts on multiple new permissions', async () => {
getAdditionalPermissionsMock.mockResolvedValue({
origins: [
'https://granted.example.com/*',
'https://granted-more.example.com/*',
],
permissions: [],
});

await init();
expect(registerContentScriptMock).toMatchSnapshot();
});

it('should register multiple manifest scripts on new permissions', async () => {
const manifest = structuredClone(baseManifest);
manifest.content_scripts!.push({
js: ['otherScript.js'],
matches: ['https://content-script-extra.example.com/*'],
});
chrome.runtime.getManifest.mockReturnValue(manifest);

await init();
expect(registerContentScriptMock).toMatchSnapshot();
});
});
62 changes: 3 additions & 59 deletions source/lib.ts
Original file line number Diff line number Diff line change
@@ -1,73 +1,17 @@
import registerContentScriptPonyfill from 'content-scripts-register-polyfill/ponyfill.js';
import {getAdditionalPermissions} from 'webext-additional-permissions';
import {injectContentScript} from 'webext-content-scripts';
import {injectToExistingTabs} from './inject-to-existing-tabs.js';
import {registerContentScript} from './register-content-script-shim.js';

const registeredScripts = new Map<
string,
Promise<browser.contentScripts.RegisteredContentScript>
>();

const chromeRegister = globalThis?.chrome?.scripting?.registerContentScripts;
const firefoxRegister = globalThis?.browser?.contentScripts?.register;

async function registerContentScript(
contentScript: ChromeContentScript,
): Promise<browser.contentScripts.RegisteredContentScript> {
if (chromeRegister) {
const id = 'webext-dynamic-content-script-' + JSON.stringify(contentScript);
try {
await chromeRegister([{
id,
...contentScript,
}]);
} catch (error) {
if (!(error as Error)?.message.startsWith('Duplicate script ID')) {
throw error;
}
}

return {
unregister: async () => chrome.scripting.unregisterContentScripts([id]),
};
}

const firefoxContentScript = {
...contentScript,
js: contentScript.js?.map(file => ({file})),
css: contentScript.css?.map(file => ({file})),
};

if (firefoxRegister) {
return firefoxRegister(firefoxContentScript);
}

return registerContentScriptPonyfill(firefoxContentScript);
}

// In Firefox, paths in the manifest are converted to full URLs under `moz-extension://` but browser.contentScripts expects exclusively relative paths
function makePathRelative(file: string): string {
return new URL(file, location.origin).pathname;
}

function injectToExistingTabs(
origins: string[],
scripts: ManifestContentScripts,
) {
if (origins.length === 0) {
return;
}

chrome.tabs.query({
url: origins,
}, tabs => {
for (const tab of tabs) {
if (tab.id) {
void injectContentScript(tab.id, scripts);
}
}
});
}

// Automatically register the content scripts on the new origins
async function registerOnOrigins({
origins: newOrigins,
Expand Down Expand Up @@ -122,7 +66,7 @@ async function handledDroppedPermissions({origins}: chrome.permissions.Permissio
export async function init() {
chrome.permissions.onRemoved.addListener(handledDroppedPermissions);
chrome.permissions.onAdded.addListener(handleNewPermissions);
void registerOnOrigins(
await registerOnOrigins(
await getAdditionalPermissions({
strictOrigins: false,
}),
Expand Down
Loading