webext-messenger 0.14.0 → 0.15.0-2

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,3 @@
1
- export { registerMethods } from "./receiver.js";
2
- export { messenger, getMethod, getNotifier, backgroundTarget, } from "./sender.js";
3
- export { MessengerMeta, Target } from "./types.js";
1
+ export * from "./receiver.js";
2
+ export * from "./sender.js";
3
+ export * from "./types.js";
@@ -1,2 +1,10 @@
1
- export { registerMethods } from "./receiver.js";
2
- export { messenger, getMethod, getNotifier, backgroundTarget, } from "./sender.js";
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
+ // 🥲
6
+ export * from "./receiver.js";
7
+ export * from "./sender.js";
8
+ export * from "./types.js";
9
+ import { initPrivateApi } from "./thisTarget.js";
10
+ initPrivateApi();
@@ -1,7 +1,9 @@
1
1
  import browser from "webextension-polyfill";
2
2
  import { serializeError } from "serialize-error";
3
3
  import { messenger } from "./sender.js";
4
- import { handlers, isObject, MessengerError, debug, warn, __webextMessenger, } from "./shared.js";
4
+ import { handlers, isObject, MessengerError, debug, __webextMessenger, } from "./shared.js";
5
+ import { getContextName } from "webext-detect-page";
6
+ import { getActionForMessage, nameThisTarget } from "./thisTarget.js";
5
7
  export function isMessengerMessage(message) {
6
8
  return (isObject(message) &&
7
9
  typeof message["type"] === "string" &&
@@ -14,21 +16,27 @@ function onMessageListener(message, sender) {
14
16
  // TODO: Add test for this eventuality: ignore unrelated messages
15
17
  return;
16
18
  }
17
- const { type, target, args } = message;
19
+ // Target check must be synchronous (`await` means we're handing the message)
20
+ const action = getActionForMessage(message.target);
21
+ if (action === "ignore") {
22
+ return;
23
+ }
24
+ return handleMessage(message, sender, action);
25
+ }
26
+ // This function can only be called when the message *will* be handled locally.
27
+ // Returning "undefined" or throwing an error will still handle it.
28
+ async function handleMessage(message, sender, action) {
29
+ const { type, target, args, options: { trace } = {} } = message;
18
30
  debug(type, "↘️ received", { sender, args });
19
31
  let handleMessage;
20
- if (target) {
21
- if (!browser.tabs) {
22
- throw new MessengerError(`Message ${type} sent to wrong context, it can't be forwarded to ${JSON.stringify(target)}`);
23
- }
32
+ if (action === "forward") {
24
33
  debug(type, "🔀 forwarded", { sender, target });
25
- handleMessage = async () => messenger(type, {}, target, ...args);
34
+ handleMessage = async () => messenger(type, { trace }, target, ...args);
26
35
  }
27
36
  else {
28
37
  const localHandler = handlers.get(type);
29
38
  if (!localHandler) {
30
- warn(type, "⚠️ ignored, can't be handled here");
31
- return;
39
+ throw new MessengerError(`No handler registered for ${type} in ${getContextName()}`);
32
40
  }
33
41
  debug(type, "➡️ will be handled here");
34
42
  const meta = { trace: [sender] };
@@ -46,6 +54,7 @@ function onMessageListener(message, sender) {
46
54
  });
47
55
  }
48
56
  export function registerMethods(methods) {
57
+ void nameThisTarget();
49
58
  for (const [type, method] of Object.entries(methods)) {
50
59
  if (handlers.has(type)) {
51
60
  throw new MessengerError(`Handler already set for ${type}`);
@@ -53,10 +62,5 @@ export function registerMethods(methods) {
53
62
  console.debug("Messenger: Registered", type);
54
63
  handlers.set(type, method);
55
64
  }
56
- if ("browser" in globalThis) {
57
- browser.runtime.onMessage.addListener(onMessageListener);
58
- }
59
- else {
60
- throw new Error("`webext-messenger` requires `webextension");
61
- }
65
+ browser.runtime.onMessage.addListener(onMessageListener);
62
66
  }
@@ -4,7 +4,7 @@ export declare const errorNonExistingTarget = "Could not establish connection. R
4
4
  declare function messenger<Type extends keyof MessengerMethods, Method extends MessengerMethods[Type]>(type: Type, options: {
5
5
  isNotification: true;
6
6
  }, target: Target | PageTarget, ...args: Parameters<Method>): void;
7
- declare function messenger<Type extends keyof MessengerMethods, Method extends MessengerMethods[Type], ReturnValue extends ReturnType<Method>>(type: Type, options: Options, target: Target | PageTarget, ...args: Parameters<Method>): ReturnValue;
7
+ declare function messenger<Type extends keyof MessengerMethods, Method extends MessengerMethods[Type], ReturnValue extends Promise<ReturnType<Method>>>(type: Type, options: Options, target: Target | PageTarget, ...args: Parameters<Method>): ReturnValue;
8
8
  declare function getMethod<Type extends keyof MessengerMethods, Method extends MessengerMethods[Type], PublicMethodType extends PublicMethod<Method>>(type: Type, target: Target | PageTarget): PublicMethodType;
9
9
  declare function getMethod<Type extends keyof MessengerMethods, Method extends MessengerMethods[Type], PublicMethodWithDynamicTarget extends PublicMethodWithTarget<Method>>(type: Type): PublicMethodWithDynamicTarget;
10
10
  declare function getNotifier<Type extends keyof MessengerMethods, Method extends MessengerMethods[Type], PublicMethodType extends SetReturnType<PublicMethod<Method>, void>>(type: Type, target: Target | PageTarget): PublicMethodType;
@@ -7,12 +7,13 @@ export const errorNonExistingTarget = "Could not establish connection. Receiving
7
7
  function isMessengerResponse(response) {
8
8
  return isObject(response) && response["__webextMessenger"] === true;
9
9
  }
10
- function makeMessage(type, args, target) {
10
+ function makeMessage(type, args, target, options) {
11
11
  return {
12
12
  __webextMessenger,
13
13
  type,
14
14
  args,
15
15
  target,
16
+ options,
16
17
  };
17
18
  }
18
19
  // Do not turn this into an `async` function; Notifications must turn `void`
@@ -25,20 +26,26 @@ function manageConnection(type, options, sendMessage) {
25
26
  });
26
27
  }
27
28
  async function manageMessage(type, sendMessage) {
28
- 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
+ }, {
29
36
  minTimeout: 100,
30
37
  factor: 1.3,
31
38
  maxRetryTime: 4000,
32
39
  onFailedAttempt(error) {
33
- if (!String(error === null || error === void 0 ? void 0 : 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 {
34
45
  throw error;
35
46
  }
36
- debug(type, "will retry");
37
47
  },
38
48
  });
39
- if (!isMessengerResponse(response)) {
40
- throw new MessengerError(`No handler for ${type} was registered in the receiving end`);
41
- }
42
49
  if ("error" in response) {
43
50
  debug(type, "↘️ replied with error", response.error);
44
51
  throw deserializeError(response.error);
@@ -47,6 +54,7 @@ async function manageMessage(type, sendMessage) {
47
54
  return response.value;
48
55
  }
49
56
  function messenger(type, options, target, ...args) {
57
+ // Message goes to extension page
50
58
  if ("page" in target) {
51
59
  if (target.page === "background" && isBackgroundPage()) {
52
60
  const handler = handlers.get(type);
@@ -58,7 +66,7 @@ function messenger(type, options, target, ...args) {
58
66
  }
59
67
  const sendMessage = async () => {
60
68
  debug(type, "↗️ sending message to runtime");
61
- return browser.runtime.sendMessage(makeMessage(type, args));
69
+ return browser.runtime.sendMessage(makeMessage(type, args, target, options));
62
70
  };
63
71
  return manageConnection(type, options, sendMessage);
64
72
  }
@@ -66,7 +74,7 @@ function messenger(type, options, target, ...args) {
66
74
  if (!browser.tabs) {
67
75
  return manageConnection(type, options, async () => {
68
76
  debug(type, "↗️ sending message to runtime");
69
- return browser.runtime.sendMessage(makeMessage(type, args, target));
77
+ return browser.runtime.sendMessage(makeMessage(type, args, target, options));
70
78
  });
71
79
  }
72
80
  // `frameId` must be specified. If missing, the message is sent to every frame
@@ -74,7 +82,7 @@ function messenger(type, options, target, ...args) {
74
82
  // Message tab directly
75
83
  return manageConnection(type, options, async () => {
76
84
  debug(type, "↗️ sending message to tab", tabId, "frame", frameId);
77
- return browser.tabs.sendMessage(tabId, makeMessage(type, args), {
85
+ return browser.tabs.sendMessage(tabId, makeMessage(type, args, target, options), {
78
86
  frameId,
79
87
  });
80
88
  });
@@ -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
+ }
@@ -0,0 +1,12 @@
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";
4
+ export declare function nameThisTarget(): Promise<void>;
5
+ declare function __getTabData(this: MessengerMeta): AnyTarget;
6
+ declare global {
7
+ interface MessengerMethods {
8
+ __getTabData: typeof __getTabData;
9
+ }
10
+ }
11
+ export declare function initPrivateApi(): void;
12
+ export {};
@@ -0,0 +1,53 @@
1
+ import { isBackgroundPage, isContentScript } from "webext-detect-page";
2
+ import { messenger } from "./sender.js";
3
+ import { registerMethods } from "./receiver.js";
4
+ import { debug } from "./shared.js";
5
+ // Soft warning: Race conditions are possible.
6
+ // This CANNOT be awaited because waiting for it means "I will handle the message."
7
+ // If a message is received before this is ready, it will just have to be ignored.
8
+ let thisTarget;
9
+ //
10
+ export function getActionForMessage(target) {
11
+ if (target.page === "any") {
12
+ return "respond";
13
+ }
14
+ // Content scripts only receive messages that are meant for them. In the future
15
+ // they'll also forward them, but that still means they need to be handled here.
16
+ if (isContentScript()) {
17
+ return "respond";
18
+ }
19
+ // We're in an extension page, but the target is not one.
20
+ if (!("page" in target)) {
21
+ return "forward";
22
+ }
23
+ if (!thisTarget) {
24
+ console.warn("A message was received before this context was ready");
25
+ // If this *was* the target, then probably no one else answered
26
+ return "ignore";
27
+ }
28
+ // Every `target` key must match `thisTarget`
29
+ const isThisTarget = Object.entries(target).every(
30
+ // @ts-expect-error Optional properties
31
+ ([key, value]) => thisTarget[key] === value);
32
+ if (!isThisTarget) {
33
+ debug("The message’s target is", target, "but this is", thisTarget);
34
+ }
35
+ return isThisTarget ? "respond" : "ignore";
36
+ }
37
+ export async function nameThisTarget() {
38
+ // Same as above: CS receives messages correctly
39
+ if (!thisTarget && !isContentScript()) {
40
+ thisTarget = await messenger("__getTabData", {}, { page: "any" });
41
+ thisTarget.page = location.pathname;
42
+ }
43
+ }
44
+ function __getTabData() {
45
+ var _a, _b, _c;
46
+ 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
+ }
48
+ export function initPrivateApi() {
49
+ if (isBackgroundPage()) {
50
+ thisTarget = { page: "background" };
51
+ registerMethods({ __getTabData });
52
+ }
53
+ }
@@ -6,13 +6,13 @@ declare global {
6
6
  _: Method;
7
7
  }
8
8
  }
9
- declare type WithTarget<Method> = Method extends (...args: infer PreviousArguments) => infer TReturnValue ? (target: Target, ...args: PreviousArguments) => TReturnValue : never;
9
+ declare type WithTarget<Method> = Method extends (...args: infer PreviousArguments) => infer TReturnValue ? (target: Target | PageTarget, ...args: PreviousArguments) => TReturnValue : never;
10
10
  declare type ActuallyOmitThisParameter<T> = T extends (...args: infer A) => infer R ? (...args: A) => R : T;
11
11
  /** Removes the `this` type and ensure it's always Promised */
12
12
  export declare type PublicMethod<Method extends ValueOf<MessengerMethods>> = Asyncify<ActuallyOmitThisParameter<Method>>;
13
13
  export declare type PublicMethodWithTarget<Method extends ValueOf<MessengerMethods>> = WithTarget<PublicMethod<Method>>;
14
14
  export interface MessengerMeta {
15
- trace: Runtime.MessageSender[];
15
+ trace: Sender[];
16
16
  }
17
17
  declare type RawMessengerResponse = {
18
18
  value: unknown;
@@ -31,15 +31,16 @@ export interface Options {
31
31
  * @default false
32
32
  */
33
33
  isNotification?: boolean;
34
+ trace?: Sender[];
34
35
  }
35
36
  export declare type Message<LocalArguments extends Arguments = Arguments> = {
36
37
  type: keyof MessengerMethods;
37
38
  args: LocalArguments;
38
- /** If the message is being sent to an intermediary receiver, also set the target */
39
- target?: Target;
39
+ target: Target | PageTarget;
40
40
  /** If the message is being sent to an intermediary receiver, also set the options */
41
- options?: Target;
41
+ options?: Options;
42
42
  };
43
+ export declare type Sender = Runtime.MessageSender;
43
44
  export declare type MessengerMessage = Message & {
44
45
  /** Guarantees that a message is meant to be handled by this library */
45
46
  __webextMessenger: true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webext-messenger",
3
- "version": "0.14.0",
3
+ "version": "0.15.0-2",
4
4
  "description": "Browser Extension component messaging framework",
5
5
  "keywords": [],
6
6
  "repository": "pixiebrix/webext-messenger",
@@ -13,12 +13,13 @@
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",
20
- "demo:watch": "parcel watch --no-cache --no-hmr --detailed-report 0",
21
- "demo:build": "parcel build --no-cache --detailed-report 0",
21
+ "demo:watch": "parcel watch --no-cache --no-hmr",
22
+ "demo:build": "parcel build --no-cache",
22
23
  "prepack": "tsc --sourceMap false",
23
24
  "test": "eslint . && tsc --noEmit",
24
25
  "lint": "eslint .",
@@ -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,34 +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",
87
- "webext-detect-page": "^3.0.2",
108
+ "p-retry": "^5.0.0",
109
+ "serialize-error": "^9.0.0",
110
+ "type-fest": "^2.6.0",
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
- "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"
108
133
  },
109
134
  "targets": {
110
135
  "main": false,
@@ -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
- }