webext-messenger 0.26.0 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/distribution/receiver.js +9 -5
- package/distribution/sender.js +1 -1
- package/distribution/targetLogic.d.ts +3 -0
- package/distribution/targetLogic.js +50 -0
- package/distribution/targetLogic.test.d.ts +1 -0
- package/distribution/targetLogic.test.js +61 -0
- package/distribution/thisTarget.d.ts +25 -2
- package/distribution/thisTarget.js +13 -58
- package/package.json +15 -12
- package/readme.md +1 -1
package/distribution/receiver.js
CHANGED
@@ -3,8 +3,9 @@ import { getContextName } from "webext-detect-page";
|
|
3
3
|
import { messenger } from "./sender.js";
|
4
4
|
import { isObject, MessengerError, __webextMessenger } from "./shared.js";
|
5
5
|
import { log } from "./logging.js";
|
6
|
-
import { getActionForMessage } from "./
|
6
|
+
import { getActionForMessage } from "./targetLogic.js";
|
7
7
|
import { didUserRegisterMethods, handlers } from "./handlers.js";
|
8
|
+
import { getTabDataStatus, thisTarget } from "./thisTarget.js";
|
8
9
|
export function isMessengerMessage(message) {
|
9
10
|
return (isObject(message) &&
|
10
11
|
typeof message["type"] === "string" &&
|
@@ -18,8 +19,13 @@ function onMessageListener(message, sender) {
|
|
18
19
|
return;
|
19
20
|
}
|
20
21
|
// Target check must be synchronous (`await` means we're handling the message)
|
21
|
-
const action = getActionForMessage(sender, message);
|
22
|
+
const action = getActionForMessage(sender, message.target, thisTarget);
|
22
23
|
if (action === "ignore") {
|
24
|
+
log.debug(message.type, "🤫 ignored due to target mismatch", {
|
25
|
+
requestedTarget: message.target,
|
26
|
+
thisTarget,
|
27
|
+
tabDataStatus: getTabDataStatus(),
|
28
|
+
});
|
23
29
|
return;
|
24
30
|
}
|
25
31
|
return handleMessage(message, sender, action);
|
@@ -74,6 +80,4 @@ export function registerMethods(methods) {
|
|
74
80
|
browser.runtime.onMessage.addListener(onMessageListener);
|
75
81
|
}
|
76
82
|
/** Ensure/document that the current function was called via Messenger */
|
77
|
-
export function assertMessengerCall(_this
|
78
|
-
// eslint-disable-next-line @typescript-eslint/no-empty-function -- TypeScript already does this, it's a documentation-only call
|
79
|
-
) { }
|
83
|
+
export function assertMessengerCall(_this) { }
|
package/distribution/sender.js
CHANGED
@@ -135,7 +135,7 @@ async function manageMessage(type, target, seq, retry, sendMessage) {
|
|
135
135
|
// Example log when seen in the background page:
|
136
136
|
// Tab 1 sends: 33000, 33001, 33002
|
137
137
|
// Tab 2 sends: 12000, 12001, 12002
|
138
|
-
let globalSeq = (Date.now() % 100) *
|
138
|
+
let globalSeq = (Date.now() % 100) * 10_000;
|
139
139
|
function messenger(type, options, target, ...args) {
|
140
140
|
options.seq = globalSeq++;
|
141
141
|
const { seq } = options;
|
@@ -0,0 +1,3 @@
|
|
1
|
+
import { type AnyTarget, type Sender } from "./types.js";
|
2
|
+
export declare function compareTargets(to: AnyTarget, thisTarget: AnyTarget): boolean;
|
3
|
+
export declare function getActionForMessage(from: Sender, target: AnyTarget, thisTarget: AnyTarget): "respond" | "forward" | "ignore";
|
@@ -0,0 +1,50 @@
|
|
1
|
+
import { isBackground, isContentScript } from "webext-detect-page";
|
2
|
+
export function compareTargets(to, thisTarget) {
|
3
|
+
for (const [key, value] of Object.entries(to)) {
|
4
|
+
if (thisTarget[key] === value) {
|
5
|
+
continue;
|
6
|
+
}
|
7
|
+
if (key !== "page") {
|
8
|
+
return false;
|
9
|
+
}
|
10
|
+
const toUrl = new URL(to.page, location.origin);
|
11
|
+
const thisUrl = new URL(thisTarget.page, location.origin);
|
12
|
+
if (toUrl.pathname !== thisUrl.pathname) {
|
13
|
+
return false;
|
14
|
+
}
|
15
|
+
for (const [parameterKey, parameterValue] of toUrl.searchParams) {
|
16
|
+
if (thisUrl.searchParams.get(parameterKey) !== parameterValue) {
|
17
|
+
return false;
|
18
|
+
}
|
19
|
+
}
|
20
|
+
}
|
21
|
+
return true;
|
22
|
+
}
|
23
|
+
export function getActionForMessage(from, target, thisTarget) {
|
24
|
+
// Clone object because we're editing it
|
25
|
+
const to = { ...target };
|
26
|
+
if (to.page === "any") {
|
27
|
+
return "respond";
|
28
|
+
}
|
29
|
+
// Content scripts only receive messages that are meant for them. In the future
|
30
|
+
// they'll also forward them, but that still means they need to be handled here.
|
31
|
+
if (isContentScript()) {
|
32
|
+
return "respond";
|
33
|
+
}
|
34
|
+
// We're in an extension page, but the target is not one.
|
35
|
+
if (!to.page) {
|
36
|
+
// Only the background page can forward messages at the moment
|
37
|
+
// https://github.com/pixiebrix/webext-messenger/issues/219#issuecomment-2011000477
|
38
|
+
// If this condition is changed, it might also need to ensure that messages are not
|
39
|
+
// forwarded to itself, e.g. when using the tabs API in an iframed extension page.
|
40
|
+
// https://github.com/pixiebrix/webext-messenger/issues/221#issuecomment-2010165690
|
41
|
+
return isBackground() ? "forward" : "ignore";
|
42
|
+
}
|
43
|
+
// Set "this" tab to the current tabId
|
44
|
+
if (to.tabId === "this" && thisTarget.tabId === from.tab?.id) {
|
45
|
+
to.tabId = thisTarget.tabId;
|
46
|
+
}
|
47
|
+
// Every `target` key must match `thisTarget`
|
48
|
+
const isThisTarget = compareTargets(to, thisTarget);
|
49
|
+
return isThisTarget ? "respond" : "ignore";
|
50
|
+
}
|
@@ -0,0 +1 @@
|
|
1
|
+
export {};
|
@@ -0,0 +1,61 @@
|
|
1
|
+
import { assert, describe, test, vi } from "vitest";
|
2
|
+
import { getActionForMessage } from "./targetLogic.js";
|
3
|
+
import { isContentScript, isBackground } from "webext-detect-page";
|
4
|
+
vi.mock("webext-detect-page");
|
5
|
+
const tab = {
|
6
|
+
id: 1,
|
7
|
+
url: "http://example.com",
|
8
|
+
windowId: 1,
|
9
|
+
active: true,
|
10
|
+
index: 0,
|
11
|
+
pinned: false,
|
12
|
+
highlighted: true,
|
13
|
+
incognito: false,
|
14
|
+
};
|
15
|
+
const senders = {
|
16
|
+
background: { page: "background" },
|
17
|
+
contentScript: { tab },
|
18
|
+
somePage: { page: "/page.html" },
|
19
|
+
};
|
20
|
+
const targets = {
|
21
|
+
background: { page: "background" },
|
22
|
+
somePage: { page: "/page.html" },
|
23
|
+
anyPage: { page: "any" },
|
24
|
+
thisTab: { tabId: "this" },
|
25
|
+
};
|
26
|
+
const thisTarget = {
|
27
|
+
background: { page: "background" },
|
28
|
+
somePage: { page: "/page.html" },
|
29
|
+
tab: { tabId: 1, frameId: 0 },
|
30
|
+
frame: { tabId: 1, frameId: 1 },
|
31
|
+
};
|
32
|
+
describe("getActionForMessage", async () => {
|
33
|
+
test.each([
|
34
|
+
// Sender Target Receiver Expected
|
35
|
+
["contentScript", "background", "background", "respond"],
|
36
|
+
["contentScript", "background", "somePage", "ignore"],
|
37
|
+
["contentScript", "background", "tab", "respond"], // Wrong, but won't happen
|
38
|
+
["contentScript", "background", "frame", "respond"], // Wrong, but won't happen
|
39
|
+
["contentScript", "anyPage", "background", "respond"],
|
40
|
+
["contentScript", "anyPage", "somePage", "respond"],
|
41
|
+
["contentScript", "anyPage", "tab", "respond"],
|
42
|
+
["contentScript", "anyPage", "frame", "respond"],
|
43
|
+
["contentScript", "somePage", "background", "ignore"],
|
44
|
+
["contentScript", "somePage", "somePage", "respond"],
|
45
|
+
["contentScript", "somePage", "tab", "respond"], // Wrong, but won't happen
|
46
|
+
["contentScript", "somePage", "frame", "respond"], // Wrong, but won't happen
|
47
|
+
["contentScript", "thisTab", "background", "forward"],
|
48
|
+
["contentScript", "thisTab", "somePage", "ignore"],
|
49
|
+
["contentScript", "thisTab", "tab", "respond"], // Won't happen, content scripts cannot target tabs
|
50
|
+
["contentScript", "thisTab", "frame", "respond"], // Won't happen, content scripts cannot target tabs
|
51
|
+
])("from %s to %s, receiver %s should %s", async (from, to, receiver, expected) => {
|
52
|
+
const isCs = receiver === "tab" || receiver === "frame";
|
53
|
+
vi.mocked(isContentScript).mockReturnValueOnce(isCs);
|
54
|
+
vi.mocked(isBackground).mockReturnValueOnce(receiver === "background");
|
55
|
+
vi.stubGlobal("location", {
|
56
|
+
origin: isCs ? "http://example.com" : "chrome-extension://extension-id",
|
57
|
+
});
|
58
|
+
const result = getActionForMessage(senders[from], targets[to], thisTarget[receiver]);
|
59
|
+
assert(result === expected, `"${receiver}" got message for "${to}" and decided to ${result.toUpperCase()} instead of ${expected.toUpperCase()}`);
|
60
|
+
});
|
61
|
+
});
|
@@ -1,6 +1,29 @@
|
|
1
|
-
import { type AnyTarget, type
|
2
|
-
|
1
|
+
import { type AnyTarget, type KnownTarget, type TopLevelFrame, type MessengerMeta, type FrameTarget } from "./types.js";
|
2
|
+
/**
|
3
|
+
* @file This file exists because `runtime.sendMessage` acts as a broadcast to
|
4
|
+
* all open extension pages, so the receiver needs a way to figure out if the
|
5
|
+
* message was intended for them.
|
6
|
+
*
|
7
|
+
* If the requested target only includes a `page` (URL), then it can be determined
|
8
|
+
* immediately. If the target also specifies a tab, like `{tabId: 1, page: '/sidebar.html'}`,
|
9
|
+
* then the receiving target needs to fetch the tab information via `__getTabData`.
|
10
|
+
*
|
11
|
+
* `__getTabData` is called automatically when `webext-messenger` is imported in
|
12
|
+
* a context that requires this logic (most extension:// pages).
|
13
|
+
*
|
14
|
+
* If a broadcast message with `tabId` target is received before `__getTabData` is "received",
|
15
|
+
* the message will be ignored and it can be retried. If `__getTabData` somehow fails,
|
16
|
+
* the target will forever ignore any messages that require the `tabId`. In that case,
|
17
|
+
* an error would be thrown once and will be visible in the console, uncaught.
|
18
|
+
*
|
19
|
+
* Content scripts do not use this logic at all at the moment because they're
|
20
|
+
* always targeted via `tabId/frameId` combo and `tabs.sendMessage`.
|
21
|
+
*/
|
22
|
+
export declare const thisTarget: KnownTarget;
|
23
|
+
declare let tabDataStatus: "needed" | "pending" | "received" | "not-needed" | "error";
|
24
|
+
export declare function getTabDataStatus(): typeof tabDataStatus;
|
3
25
|
export declare function __getTabData(this: MessengerMeta): AnyTarget;
|
4
26
|
export declare function getThisFrame(): Promise<FrameTarget>;
|
5
27
|
export declare function getTopLevelFrame(): Promise<TopLevelFrame>;
|
6
28
|
export declare function initPrivateApi(): void;
|
29
|
+
export {};
|
@@ -1,8 +1,8 @@
|
|
1
|
-
import { getContextName, isBackground,
|
1
|
+
import { getContextName, isBackground, isExtensionContext, } from "webext-detect-page";
|
2
2
|
import { messenger } from "./sender.js";
|
3
3
|
import { registerMethods } from "./receiver.js";
|
4
4
|
import { MessengerError, once } from "./shared.js";
|
5
|
-
import {
|
5
|
+
import { pEvent } from "p-event";
|
6
6
|
/**
|
7
7
|
* @file This file exists because `runtime.sendMessage` acts as a broadcast to
|
8
8
|
* all open extension pages, so the receiver needs a way to figure out if the
|
@@ -26,7 +26,7 @@ import { log } from "./logging.js";
|
|
26
26
|
// Soft warning: Race conditions are possible.
|
27
27
|
// This CANNOT be awaited because waiting for it means "I will handle the message."
|
28
28
|
// If a message is received before this is ready, it will just have to be ignored.
|
29
|
-
const thisTarget = isBackground()
|
29
|
+
export const thisTarget = isBackground()
|
30
30
|
? { page: "background" }
|
31
31
|
: {
|
32
32
|
get page() {
|
@@ -41,67 +41,21 @@ const thisTarget = isBackground()
|
|
41
41
|
let tabDataStatus =
|
42
42
|
// The background page doesn't have a tab
|
43
43
|
isBackground() ? "not-needed" : "needed";
|
44
|
-
function
|
45
|
-
|
46
|
-
if (thisTarget[key] === value) {
|
47
|
-
continue;
|
48
|
-
}
|
49
|
-
if (key !== "page") {
|
50
|
-
return false;
|
51
|
-
}
|
52
|
-
const toUrl = new URL(to.page, location.origin);
|
53
|
-
const thisUrl = new URL(thisTarget.page, location.origin);
|
54
|
-
if (toUrl.pathname !== thisUrl.pathname) {
|
55
|
-
return false;
|
56
|
-
}
|
57
|
-
for (const [parameterKey, parameterValue] of toUrl.searchParams) {
|
58
|
-
if (thisUrl.searchParams.get(parameterKey) !== parameterValue) {
|
59
|
-
return false;
|
60
|
-
}
|
61
|
-
}
|
62
|
-
}
|
63
|
-
return true;
|
64
|
-
}
|
65
|
-
// TODO: Test this in Jest, outside the browser
|
66
|
-
export function getActionForMessage(from, message) {
|
67
|
-
// Clone object because we're editing it
|
68
|
-
const to = { ...message.target };
|
69
|
-
if (to.page === "any") {
|
70
|
-
return "respond";
|
71
|
-
}
|
72
|
-
// Content scripts only receive messages that are meant for them. In the future
|
73
|
-
// they'll also forward them, but that still means they need to be handled here.
|
74
|
-
if (isContentScript()) {
|
75
|
-
return "respond";
|
76
|
-
}
|
77
|
-
// We're in an extension page, but the target is not one.
|
78
|
-
if (!to.page) {
|
79
|
-
return "forward";
|
80
|
-
}
|
81
|
-
// Set "this" tab to the current tabId
|
82
|
-
if (to.tabId === "this" && thisTarget.tabId === from.tab?.id) {
|
83
|
-
to.tabId = thisTarget.tabId;
|
84
|
-
}
|
85
|
-
// Every `target` key must match `thisTarget`
|
86
|
-
const isThisTarget = compareTargets(to, thisTarget);
|
87
|
-
if (!isThisTarget) {
|
88
|
-
log.debug(message.type, "🤫 ignored due to target mismatch", {
|
89
|
-
requestedTarget: to,
|
90
|
-
thisTarget,
|
91
|
-
tabDataStatus,
|
92
|
-
});
|
93
|
-
}
|
94
|
-
return isThisTarget ? "respond" : "ignore";
|
44
|
+
export function getTabDataStatus() {
|
45
|
+
return tabDataStatus;
|
95
46
|
}
|
96
47
|
const storeTabData = once(async () => {
|
97
48
|
if (tabDataStatus !== "needed") {
|
98
49
|
return;
|
99
50
|
}
|
51
|
+
// If the page is prerendering, wait for the change to be able to get the tab data so the frameId is correct
|
52
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/Document/prerenderingchange_event
|
53
|
+
if ("prerendering" in document && Boolean(document.prerendering)) {
|
54
|
+
await pEvent(document, 'prerenderingchange');
|
55
|
+
}
|
100
56
|
try {
|
101
57
|
tabDataStatus = "pending";
|
102
|
-
Object.assign(thisTarget, {
|
103
|
-
...(await messenger("__getTabData", {}, { page: "any" })),
|
104
|
-
});
|
58
|
+
Object.assign(thisTarget, await messenger("__getTabData", {}, { page: "any" }));
|
105
59
|
tabDataStatus = "received";
|
106
60
|
}
|
107
61
|
catch (error) {
|
@@ -119,7 +73,8 @@ export async function getThisFrame() {
|
|
119
73
|
if (typeof tabId !== "number" || typeof frameId !== "number") {
|
120
74
|
let moreInfo = "(error retrieving context information)";
|
121
75
|
try {
|
122
|
-
moreInfo = `(context: ${getContextName()}, url: ${globalThis.location
|
76
|
+
moreInfo = `(context: ${getContextName()}, url: ${globalThis.location
|
77
|
+
?.href})`;
|
123
78
|
}
|
124
79
|
catch { }
|
125
80
|
throw new TypeError(`This target is not in a frame ${moreInfo}`);
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "webext-messenger",
|
3
|
-
"version": "0.
|
3
|
+
"version": "0.28.0",
|
4
4
|
"description": "Browser Extension component messaging framework",
|
5
5
|
"keywords": [],
|
6
6
|
"repository": "pixiebrix/webext-messenger",
|
@@ -9,7 +9,7 @@
|
|
9
9
|
"type": "module",
|
10
10
|
"exports": {
|
11
11
|
".": {
|
12
|
-
"types": "./distribution/index.
|
12
|
+
"types": "./distribution/index.d.ts",
|
13
13
|
"default": "./distribution/index.js"
|
14
14
|
},
|
15
15
|
"./*": "./distribution/*"
|
@@ -19,16 +19,18 @@
|
|
19
19
|
"demo:watch": "parcel watch --no-cache --no-hmr",
|
20
20
|
"demo:build": "parcel build --no-cache --no-scope-hoist",
|
21
21
|
"prepack": "tsc --sourceMap false",
|
22
|
-
"test": "
|
22
|
+
"test": "run-p test:unit lint build demo:build",
|
23
|
+
"test:unit": "vitest run",
|
23
24
|
"lint": "eslint .",
|
24
25
|
"fix": "eslint . --fix",
|
25
26
|
"watch": "tsc --watch"
|
26
27
|
},
|
27
28
|
"dependencies": {
|
29
|
+
"p-event": "^6.0.1",
|
28
30
|
"p-retry": "^6.2.0",
|
29
|
-
"serialize-error": "^11.0.
|
30
|
-
"type-fest": "^4.
|
31
|
-
"webext-detect-page": "^5.0.
|
31
|
+
"serialize-error": "^11.0.3",
|
32
|
+
"type-fest": "^4.18.3",
|
33
|
+
"webext-detect-page": "^5.0.1"
|
32
34
|
},
|
33
35
|
"@parcel/resolver-default": {
|
34
36
|
"packageExports": true
|
@@ -36,22 +38,23 @@
|
|
36
38
|
"devDependencies": {
|
37
39
|
"@parcel/config-webextension": "^2.11.0",
|
38
40
|
"@sindresorhus/tsconfig": "^5.0.0",
|
39
|
-
"@types/chrome": "^0.0.
|
41
|
+
"@types/chrome": "^0.0.268",
|
40
42
|
"@types/tape": "^5.6.4",
|
41
43
|
"@types/webextension-polyfill": "^0.10.7",
|
42
44
|
"buffer": "^6.0.3",
|
43
|
-
"eslint": "^8.
|
44
|
-
"eslint-config-pixiebrix": "^0.
|
45
|
+
"eslint": "^8.57.0",
|
46
|
+
"eslint-config-pixiebrix": "^0.39.0",
|
45
47
|
"events": "^3.3.0",
|
46
48
|
"npm-run-all": "^4.1.5",
|
47
49
|
"parcel": "^2.11.0",
|
48
50
|
"path-browserify": "^1.0.1",
|
49
51
|
"process": "^0.11.10",
|
50
52
|
"stream-browserify": "^3.0.0",
|
51
|
-
"tape": "^5.7.
|
52
|
-
"typescript": "^5.
|
53
|
+
"tape": "^5.7.5",
|
54
|
+
"typescript": "^5.4.5",
|
55
|
+
"vitest": "^1.6.0",
|
53
56
|
"webext-content-scripts": "^2.6.1",
|
54
|
-
"webextension-polyfill": "^0.
|
57
|
+
"webextension-polyfill": "^0.12.0"
|
55
58
|
},
|
56
59
|
"targets": {
|
57
60
|
"main": false,
|
package/readme.md
CHANGED
@@ -17,7 +17,7 @@ import messenger from "webext-messenger";
|
|
17
17
|
|
18
18
|
## Context
|
19
19
|
|
20
|
-
- [Initial
|
20
|
+
- [Initial considerations for this library](https://github.com/pixiebrix/webext-messenger/issues/1)
|
21
21
|
|
22
22
|
|
23
23
|
## npm publishing
|