webext-messenger 0.15.0-0 → 0.15.0-4

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,10 @@
1
+ // Imports must use the .js extension because of ESM requires it and TS refuses to rewrite .ts to .js
2
+ // This works in TS even if the .js doesn't exist, but it breaks Parcel (the tests builder)
3
+ // For this reason, there's an `alias` field in package.json to redirect these imports.
4
+ // If you see "@parcel/resolver-default: Cannot load file './yourNewFile.js'" you need to add it to the `alias` list
5
+ // 🥲
1
6
  export * from "./receiver.js";
2
7
  export * from "./sender.js";
3
8
  export * from "./types.js";
4
- import { initPrivateApi } from "./thisTarget";
9
+ import { initPrivateApi } from "./thisTarget.js";
5
10
  initPrivateApi();
@@ -3,7 +3,7 @@ import { serializeError } from "serialize-error";
3
3
  import { messenger } from "./sender.js";
4
4
  import { handlers, isObject, MessengerError, debug, __webextMessenger, } from "./shared.js";
5
5
  import { getContextName } from "webext-detect-page";
6
- import { getActionForMessage, nameThisTarget } from "./thisTarget";
6
+ import { getActionForMessage, nameThisTarget } from "./thisTarget.js";
7
7
  export function isMessengerMessage(message) {
8
8
  return (isObject(message) &&
9
9
  typeof message["type"] === "string" &&
@@ -17,7 +17,7 @@ function onMessageListener(message, sender) {
17
17
  return;
18
18
  }
19
19
  // Target check must be synchronous (`await` means we're handing the message)
20
- const action = getActionForMessage(message.target);
20
+ const action = getActionForMessage(sender, message.target);
21
21
  if (action === "ignore") {
22
22
  return;
23
23
  }
@@ -26,20 +26,26 @@ function manageConnection(type, options, sendMessage) {
26
26
  });
27
27
  }
28
28
  async function manageMessage(type, sendMessage) {
29
- const response = await pRetry(sendMessage, {
29
+ const response = await pRetry(async () => {
30
+ const response = await sendMessage();
31
+ if (!isMessengerResponse(response)) {
32
+ throw new MessengerError(`No handler registered for ${type} in the receiving end`);
33
+ }
34
+ return response;
35
+ }, {
30
36
  minTimeout: 100,
31
37
  factor: 1.3,
32
38
  maxRetryTime: 4000,
33
39
  onFailedAttempt(error) {
34
- if (!String(error.message).startsWith(errorNonExistingTarget)) {
40
+ if (error instanceof MessengerError ||
41
+ String(error.message).startsWith(errorNonExistingTarget)) {
42
+ debug(type, "will retry. Attempt", error.attemptNumber);
43
+ }
44
+ else {
35
45
  throw error;
36
46
  }
37
- debug(type, "will retry. Attempt", error.attemptNumber);
38
47
  },
39
48
  });
40
- if (!isMessengerResponse(response)) {
41
- throw new MessengerError(`No handler registered for ${type} in the receiving end`);
42
- }
43
49
  if ("error" in response) {
44
50
  debug(type, "↘️ replied with error", response.error);
45
51
  throw deserializeError(response.error);
@@ -1,4 +1,11 @@
1
+ import { JsonObject } from "type-fest";
1
2
  import { Method } from "./types.js";
3
+ declare type ErrorObject = {
4
+ name?: string;
5
+ stack?: string;
6
+ message?: string;
7
+ code?: string;
8
+ } & JsonObject;
2
9
  export declare const __webextMessenger = true;
3
10
  export declare function isObject(value: unknown): value is Record<string, unknown>;
4
11
  export declare class MessengerError extends Error {
@@ -7,3 +14,6 @@ export declare class MessengerError extends Error {
7
14
  export declare const handlers: Map<string, Method>;
8
15
  export declare const debug: (...args: any[]) => void;
9
16
  export declare const warn: (...args: any[]) => void;
17
+ export declare function isErrorObject(error: unknown): error is ErrorObject;
18
+ export declare function delay(milliseconds: number): Promise<void>;
19
+ export {};
@@ -17,3 +17,13 @@ export const handlers = new Map();
17
17
  // .bind preserves the call location in the console
18
18
  export const debug = console.debug.bind(console, "Messenger:");
19
19
  export const warn = console.warn.bind(console, "Messenger:");
20
+ export function isErrorObject(error) {
21
+ var _a;
22
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- This is a type guard function and it uses ?.
23
+ return typeof ((_a = error) === null || _a === void 0 ? void 0 : _a.message) === "string";
24
+ }
25
+ export async function delay(milliseconds) {
26
+ return new Promise((resolve) => {
27
+ setTimeout(resolve, milliseconds);
28
+ });
29
+ }
@@ -1,6 +1,10 @@
1
- import { Target, PageTarget, MessengerMeta } from "./types.js";
2
- declare type AnyTarget = Partial<Target & PageTarget>;
3
- export declare function getActionForMessage(target: AnyTarget): "respond" | "forward" | "ignore";
1
+ import { MessengerMeta, Sender } from "./types.js";
2
+ interface AnyTarget {
3
+ tabId?: number | "this";
4
+ frameId?: number;
5
+ page?: string;
6
+ }
7
+ export declare function getActionForMessage(from: Sender, { ...to }: AnyTarget): "respond" | "forward" | "ignore";
4
8
  export declare function nameThisTarget(): Promise<void>;
5
9
  declare function __getTabData(this: MessengerMeta): AnyTarget;
6
10
  declare global {
@@ -1,14 +1,15 @@
1
- import { isBackgroundPage, isContentScript } from "webext-detect-page";
2
- import { messenger } from "./index";
1
+ import { isBackgroundPage, isContentScript, isExtensionContext, } from "webext-detect-page";
2
+ import { messenger } from "./sender.js";
3
3
  import { registerMethods } from "./receiver.js";
4
4
  import { debug } from "./shared.js";
5
5
  // Soft warning: Race conditions are possible.
6
6
  // This CANNOT be awaited because waiting for it means "I will handle the message."
7
7
  // If a message is received before this is ready, it will just have to be ignored.
8
8
  let thisTarget;
9
- //
10
- export function getActionForMessage(target) {
11
- if (target.page === "any") {
9
+ export function getActionForMessage(from, { ...to } // Clone object because we're editing it
10
+ ) {
11
+ var _a;
12
+ if (to.page === "any") {
12
13
  return "respond";
13
14
  }
14
15
  // Content scripts only receive messages that are meant for them. In the future
@@ -17,7 +18,7 @@ export function getActionForMessage(target) {
17
18
  return "respond";
18
19
  }
19
20
  // We're in an extension page, but the target is not one.
20
- if (!("page" in target)) {
21
+ if (!to.page) {
21
22
  return "forward";
22
23
  }
23
24
  if (!thisTarget) {
@@ -25,18 +26,24 @@ export function getActionForMessage(target) {
25
26
  // If this *was* the target, then probably no one else answered
26
27
  return "ignore";
27
28
  }
29
+ // If requests "this" tab, then set it to allow the next condition
30
+ if (to.tabId === "this" && thisTarget.tabId === ((_a = from.tab) === null || _a === void 0 ? void 0 : _a.id)) {
31
+ to.tabId = thisTarget.tabId;
32
+ }
28
33
  // Every `target` key must match `thisTarget`
29
- const isThisTarget = Object.entries(target).every(
34
+ const isThisTarget = Object.entries(to).every(
30
35
  // @ts-expect-error Optional properties
31
36
  ([key, value]) => thisTarget[key] === value);
32
37
  if (!isThisTarget) {
33
- debug("The message’s target is", target, "but this is", thisTarget);
38
+ debug("The message’s target is", to, "but this is", thisTarget);
34
39
  }
35
40
  return isThisTarget ? "respond" : "ignore";
36
41
  }
42
+ let nameRequested = false;
37
43
  export async function nameThisTarget() {
38
44
  // Same as above: CS receives messages correctly
39
- if (!thisTarget && !isContentScript()) {
45
+ if (!nameRequested && !thisTarget && !isContentScript()) {
46
+ nameRequested = true;
40
47
  thisTarget = await messenger("__getTabData", {}, { page: "any" });
41
48
  thisTarget.page = location.pathname;
42
49
  }
@@ -46,8 +53,11 @@ function __getTabData() {
46
53
  return { tabId: (_b = (_a = this.trace[0]) === null || _a === void 0 ? void 0 : _a.tab) === null || _b === void 0 ? void 0 : _b.id, frameId: (_c = this.trace[0]) === null || _c === void 0 ? void 0 : _c.frameId };
47
54
  }
48
55
  export function initPrivateApi() {
56
+ if (isExtensionContext()) {
57
+ // Any context can handler this message
58
+ registerMethods({ __getTabData });
59
+ }
49
60
  if (isBackgroundPage()) {
50
61
  thisTarget = { page: "background" };
51
- registerMethods({ __getTabData });
52
62
  }
53
63
  }
@@ -50,7 +50,7 @@ export interface Target {
50
50
  frameId?: number;
51
51
  }
52
52
  export interface PageTarget {
53
- tabId?: number;
53
+ tabId?: number | "this";
54
54
  page: string;
55
55
  }
56
56
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webext-messenger",
3
- "version": "0.15.0-0",
3
+ "version": "0.15.0-4",
4
4
  "description": "Browser Extension component messaging framework",
5
5
  "keywords": [],
6
6
  "repository": "pixiebrix/webext-messenger",
@@ -13,7 +13,8 @@
13
13
  "./source/sender.js": "./source/sender.ts",
14
14
  "./source/receiver.js": "./source/receiver.ts",
15
15
  "./source/types.js": "./source/types.ts",
16
- "./source/shared.js": "./source/shared.ts"
16
+ "./source/shared.js": "./source/shared.ts",
17
+ "./source/thisTarget.js": "./source/thisTarget.ts"
17
18
  },
18
19
  "scripts": {
19
20
  "build": "tsc",
@@ -45,6 +46,21 @@
45
46
  "plugin:unicorn/recommended"
46
47
  ],
47
48
  "rules": {
49
+ "no-restricted-imports": [
50
+ "error",
51
+ {
52
+ "paths": [
53
+ {
54
+ "name": "./index",
55
+ "message": "The index file is only used to re-export internal files. Use direct imports instead."
56
+ }
57
+ ]
58
+ }
59
+ ],
60
+ "import/extensions": [
61
+ "error",
62
+ "always"
63
+ ],
48
64
  "import/no-unresolved": "off",
49
65
  "unicorn/filename-case": [
50
66
  "error",
@@ -77,35 +93,43 @@
77
93
  "@typescript-eslint/no-explicit-any": "off",
78
94
  "@typescript-eslint/no-unsafe-member-access": "off"
79
95
  }
96
+ },
97
+ {
98
+ "files": [
99
+ "source/test/**/*"
100
+ ],
101
+ "rules": {
102
+ "import/extensions": "off"
103
+ }
80
104
  }
81
105
  ]
82
106
  },
83
107
  "dependencies": {
84
- "p-retry": "^4.6.1",
85
- "serialize-error": "^8.1.0",
86
- "type-fest": "^2.5.1",
108
+ "p-retry": "^5.0.0",
109
+ "serialize-error": "^9.0.0",
110
+ "type-fest": "^2.6.0",
87
111
  "webext-detect-page": "^3.1.0",
88
112
  "webextension-polyfill": "^0.8.0"
89
113
  },
90
114
  "devDependencies": {
91
- "@parcel/config-webextension": "^2.0.0",
115
+ "@parcel/config-webextension": "^2.0.1",
92
116
  "@sindresorhus/tsconfig": "^2.0.0",
93
- "@types/chrome": "^0.0.159",
94
- "@types/webextension-polyfill": "^0.8.0",
95
- "@typescript-eslint/eslint-plugin": "^5.1.0",
96
- "@typescript-eslint/parser": "^5.1.0",
97
- "eslint": "^8.1.0",
117
+ "@types/chrome": "^0.0.164",
118
+ "@types/tape": "^4.13.2",
119
+ "@types/webextension-polyfill": "^0.8.2",
120
+ "@typescript-eslint/eslint-plugin": "^5.4.0",
121
+ "@typescript-eslint/parser": "^5.4.0",
122
+ "eslint": "^8.3.0",
98
123
  "eslint-config-prettier": "^8.3.0",
99
124
  "eslint-config-xo": "^0.39.0",
100
- "eslint-config-xo-typescript": "^0.45.2",
101
- "eslint-plugin-import": "^2.25.2",
102
- "eslint-plugin-unicorn": "^37.0.1",
103
- "fresh-tape": "^5.3.1",
125
+ "eslint-config-xo-typescript": "^0.47.1",
126
+ "eslint-plugin-import": "^2.25.3",
127
+ "eslint-plugin-unicorn": "^39.0.0",
104
128
  "npm-run-all": "^4.1.5",
105
- "parcel": "^2.0.0",
106
- "typescript": "^4.4.4",
107
- "webext-content-scripts": "^0.10.1",
108
- "xo": "^0.45.0"
129
+ "parcel": "^2.0.1",
130
+ "tape": "^5.3.2",
131
+ "typescript": "^4.5.2",
132
+ "webext-content-scripts": "^0.10.1"
109
133
  },
110
134
  "targets": {
111
135
  "main": false,
@@ -1,10 +0,0 @@
1
- import { Target, MessengerMeta } from "./types";
2
- declare function __getTabData(this: MessengerMeta): Target;
3
- declare global {
4
- interface MessengerMethods {
5
- __getTabData: typeof __getTabData;
6
- }
7
- }
8
- export declare const getTabData: () => Promise<Target>;
9
- export declare function initPrivateApi(): void;
10
- export {};
@@ -1,13 +0,0 @@
1
- import { getMethod } from "./sender";
2
- import { registerMethods } from "./receiver";
3
- import { isBackgroundPage } from "webext-detect-page";
4
- function __getTabData() {
5
- return { tabId: this.trace[0].tab.id, frameId: this.trace[0].frameId };
6
- }
7
- // First page to respond wins. Any context has this piece of information.
8
- export const getTabData = getMethod("__getTabData", { page: "any" });
9
- export function initPrivateApi() {
10
- if (isBackgroundPage()) {
11
- registerMethods({ __getTabData });
12
- }
13
- }
@@ -1,14 +0,0 @@
1
- /// <reference types="firefox-webext-browser" />
2
- import { NamedTarget, Target, MessengerMeta } from "./types.js";
3
- declare global {
4
- interface MessengerMethods {
5
- __webextMessengerTargetRegistration: typeof _registerTarget;
6
- }
7
- }
8
- export declare function resolveNamedTarget(target: NamedTarget, sender?: browser.runtime.MessageSender): Target;
9
- export declare const targets: Map<string, Target>;
10
- /** Register the current context so that it can be targeted with a name */
11
- export declare const registerTarget: (name: string) => Promise<void>;
12
- declare function _registerTarget(this: MessengerMeta, name: string): Promise<void>;
13
- export declare function initTargets(): void;
14
- export {};
@@ -1,40 +0,0 @@
1
- import { isBackgroundPage } from "webext-detect-page";
2
- import { errorNonExistingTarget, getMethod } from "./sender.js";
3
- import { registerMethods } from "./receiver.js";
4
- export function resolveNamedTarget(target, sender) {
5
- var _a;
6
- if (!isBackgroundPage()) {
7
- throw new Error("Named targets can only be resolved in the background page");
8
- }
9
- const { name, tabId = (_a = sender === null || sender === void 0 ? void 0 : sender.tab) === null || _a === void 0 ? void 0 : _a.id, // If not specified, try to use the sender’s
10
- } = target;
11
- if (typeof tabId === "undefined") {
12
- throw new TypeError(`${errorNonExistingTarget} The tab ID was not specified nor it was automatically determinable.`);
13
- }
14
- const resolvedTarget = targets.get(`${tabId}%${name}`);
15
- if (!resolvedTarget) {
16
- throw new Error(`${errorNonExistingTarget} Target named ${name} not registered for tab ${tabId}.`);
17
- }
18
- return resolvedTarget;
19
- }
20
- // TODO: Remove targets after tab closes to avoid "memory leaks"
21
- export const targets = new Map();
22
- /** Register the current context so that it can be targeted with a name */
23
- export const registerTarget = getMethod("__webextMessengerTargetRegistration");
24
- async function _registerTarget(name) {
25
- const sender = this.trace[0];
26
- const tabId = sender.tab.id;
27
- const { frameId } = sender;
28
- targets.set(`${tabId}%${name}`, {
29
- tabId,
30
- frameId,
31
- });
32
- console.debug(`Messenger: Target "${name}" registered for tab ${tabId}`);
33
- }
34
- export function initTargets() {
35
- if (isBackgroundPage()) {
36
- registerMethods({
37
- __webextMessengerTargetRegistration: _registerTarget,
38
- });
39
- }
40
- }