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.
@@ -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 "./thisTarget.js";
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) { }
@@ -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) * 10000;
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 TopLevelFrame, type Message, type MessengerMeta, type Sender, type FrameTarget } from "./types.js";
2
- export declare function getActionForMessage(from: Sender, message: Message): "respond" | "forward" | "ignore";
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, isContentScript, isExtensionContext, } from "webext-detect-page";
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 { log } from "./logging.js";
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 compareTargets(to, thisTarget) {
45
- for (const [key, value] of Object.entries(to)) {
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?.href})`;
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.26.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.js",
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": "eslint . && tsc --noEmit",
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.2",
30
- "type-fest": "^4.10.2",
31
- "webext-detect-page": "^5.0.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.259",
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.56.0",
44
- "eslint-config-pixiebrix": "^0.34.1",
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.4",
52
- "typescript": "^5.3.3",
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.10.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 considertions for this library](https://github.com/pixiebrix/webext-messenger/issues/1)
20
+ - [Initial considerations for this library](https://github.com/pixiebrix/webext-messenger/issues/1)
21
21
 
22
22
 
23
23
  ## npm publishing