webext-messenger 0.13.0-8 → 0.15.0-0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,10 @@
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 {};
@@ -0,0 +1,13 @@
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,3 +1,3 @@
1
- export { registerMethods } from "./receiver.js";
2
- export { getContentScriptMethod, getMethod } 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,5 @@
1
- export { registerMethods } from "./receiver.js";
2
- export { getContentScriptMethod, getMethod } from "./sender.js";
1
+ export * from "./receiver.js";
2
+ export * from "./sender.js";
3
+ export * from "./types.js";
4
+ import { initPrivateApi } from "./thisTarget";
5
+ initPrivateApi();
@@ -1,6 +1,9 @@
1
+ import browser from "webextension-polyfill";
1
2
  import { serializeError } from "serialize-error";
2
- import { getContentScriptMethod } from "./sender.js";
3
- import { handlers, isObject, MessengerError, debug, warn, __webextMessenger, } from "./shared.js";
3
+ import { messenger } from "./sender.js";
4
+ import { handlers, isObject, MessengerError, debug, __webextMessenger, } from "./shared.js";
5
+ import { getContextName } from "webext-detect-page";
6
+ import { getActionForMessage, nameThisTarget } from "./thisTarget";
4
7
  export function isMessengerMessage(message) {
5
8
  return (isObject(message) &&
6
9
  typeof message["type"] === "string" &&
@@ -13,22 +16,27 @@ function onMessageListener(message, sender) {
13
16
  // TODO: Add test for this eventuality: ignore unrelated messages
14
17
  return;
15
18
  }
16
- 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;
17
30
  debug(type, "↘️ received", { sender, args });
18
31
  let handleMessage;
19
- if (target) {
20
- if (!browser.tabs) {
21
- throw new MessengerError(`Message ${type} sent to wrong context, it can't be forwarded to ${JSON.stringify(target)}`);
22
- }
32
+ if (action === "forward") {
23
33
  debug(type, "🔀 forwarded", { sender, target });
24
- const publicMethod = getContentScriptMethod(type);
25
- handleMessage = async () => publicMethod(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,17 +54,13 @@ 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}`);
52
61
  }
53
- console.debug(`Messenger: Registered`, type);
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
  }
@@ -1,20 +1,13 @@
1
+ import { PublicMethod, PublicMethodWithTarget, Options, Target, PageTarget } from "./types.js";
1
2
  import { SetReturnType } from "type-fest";
2
- import { PublicMethod, PublicMethodWithTarget, Options } from "./types.js";
3
3
  export declare const errorNonExistingTarget = "Could not establish connection. Receiving end does not exist.";
4
- /**
5
- * Replicates the original method, including its types.
6
- * To be called in the sender’s end.
7
- */
8
- declare function getContentScriptMethod<Type extends keyof MessengerMethods, Method extends MessengerMethods[Type], PublicMethod extends PublicMethodWithTarget<Method>>(type: Type, options: {
4
+ declare function messenger<Type extends keyof MessengerMethods, Method extends MessengerMethods[Type]>(type: Type, options: {
9
5
  isNotification: true;
10
- }): SetReturnType<PublicMethod, void>;
11
- declare function getContentScriptMethod<Type extends keyof MessengerMethods, Method extends MessengerMethods[Type], PublicMethod extends PublicMethodWithTarget<Method>>(type: Type, options?: Options): PublicMethod;
12
- /**
13
- * Replicates the original method, including its types.
14
- * To be called in the sender’s end.
15
- */
16
- declare function getMethod<Type extends keyof MessengerMethods, Method extends MessengerMethods[Type], PublicMethodType extends PublicMethod<Method>>(type: Type, options: {
17
- isNotification: true;
18
- }): SetReturnType<PublicMethodType, void>;
19
- declare function getMethod<Type extends keyof MessengerMethods, Method extends MessengerMethods[Type], PublicMethodType extends PublicMethod<Method>>(type: Type, options?: Options): PublicMethodType;
20
- export { getContentScriptMethod, getMethod };
6
+ }, target: Target | PageTarget, ...args: Parameters<Method>): void;
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
+ declare function getMethod<Type extends keyof MessengerMethods, Method extends MessengerMethods[Type], PublicMethodType extends PublicMethod<Method>>(type: Type, target: Target | PageTarget): PublicMethodType;
9
+ declare function getMethod<Type extends keyof MessengerMethods, Method extends MessengerMethods[Type], PublicMethodWithDynamicTarget extends PublicMethodWithTarget<Method>>(type: Type): PublicMethodWithDynamicTarget;
10
+ declare function getNotifier<Type extends keyof MessengerMethods, Method extends MessengerMethods[Type], PublicMethodType extends SetReturnType<PublicMethod<Method>, void>>(type: Type, target: Target | PageTarget): PublicMethodType;
11
+ declare function getNotifier<Type extends keyof MessengerMethods, Method extends MessengerMethods[Type], PublicMethodWithDynamicTarget extends SetReturnType<PublicMethodWithTarget<Method>, void>>(type: Type): PublicMethodWithDynamicTarget;
12
+ export { messenger, getMethod, getNotifier };
13
+ export declare const backgroundTarget: PageTarget;
@@ -1,3 +1,4 @@
1
+ import browser from "webextension-polyfill";
1
2
  import pRetry from "p-retry";
2
3
  import { isBackgroundPage } from "webext-detect-page";
3
4
  import { deserializeError } from "serialize-error";
@@ -6,12 +7,13 @@ export const errorNonExistingTarget = "Could not establish connection. Receiving
6
7
  function isMessengerResponse(response) {
7
8
  return isObject(response) && response["__webextMessenger"] === true;
8
9
  }
9
- function makeMessage(type, args, target) {
10
+ function makeMessage(type, args, target, options) {
10
11
  return {
11
12
  __webextMessenger,
12
13
  type,
13
14
  args,
14
15
  target,
16
+ options,
15
17
  };
16
18
  }
17
19
  // Do not turn this into an `async` function; Notifications must turn `void`
@@ -29,14 +31,14 @@ async function manageMessage(type, sendMessage) {
29
31
  factor: 1.3,
30
32
  maxRetryTime: 4000,
31
33
  onFailedAttempt(error) {
32
- if (!String(error === null || error === void 0 ? void 0 : error.message).startsWith(errorNonExistingTarget)) {
34
+ if (!String(error.message).startsWith(errorNonExistingTarget)) {
33
35
  throw error;
34
36
  }
35
- debug(type, "will retry");
37
+ debug(type, "will retry. Attempt", error.attemptNumber);
36
38
  },
37
39
  });
38
40
  if (!isMessengerResponse(response)) {
39
- throw new MessengerError(`No handler for ${type} was registered in the receiving end`);
41
+ throw new MessengerError(`No handler registered for ${type} in the receiving end`);
40
42
  }
41
43
  if ("error" in response) {
42
44
  debug(type, "↘️ replied with error", response.error);
@@ -45,30 +47,10 @@ async function manageMessage(type, sendMessage) {
45
47
  debug(type, "↘️ replied successfully", response.value);
46
48
  return response.value;
47
49
  }
48
- function getContentScriptMethod(type, options = {}) {
49
- const publicMethod = (target, ...args) => {
50
- // Contexts without direct Tab access must go through background
51
- if (!browser.tabs) {
52
- return manageConnection(type, options, async () => {
53
- debug(type, "↗️ sending message to runtime");
54
- return browser.runtime.sendMessage(makeMessage(type, args, target));
55
- });
56
- }
57
- // `frameId` must be specified. If missing, the message is sent to every frame
58
- const { tabId, frameId = 0 } = target;
59
- // Message tab directly
60
- return manageConnection(type, options, async () => {
61
- debug(type, "↗️ sending message to tab", tabId, "frame", frameId);
62
- return browser.tabs.sendMessage(tabId, makeMessage(type, args), {
63
- frameId,
64
- });
65
- });
66
- };
67
- return publicMethod;
68
- }
69
- function getMethod(type, options = {}) {
70
- const publicMethod = (...args) => {
71
- if (isBackgroundPage()) {
50
+ function messenger(type, options, target, ...args) {
51
+ // Message goes to extension page
52
+ if ("page" in target) {
53
+ if (target.page === "background" && isBackgroundPage()) {
72
54
  const handler = handlers.get(type);
73
55
  if (handler) {
74
56
  warn(type, "is being handled locally");
@@ -78,10 +60,42 @@ function getMethod(type, options = {}) {
78
60
  }
79
61
  const sendMessage = async () => {
80
62
  debug(type, "↗️ sending message to runtime");
81
- return browser.runtime.sendMessage(makeMessage(type, args));
63
+ return browser.runtime.sendMessage(makeMessage(type, args, target, options));
82
64
  };
83
65
  return manageConnection(type, options, sendMessage);
84
- };
85
- return publicMethod;
66
+ }
67
+ // Contexts without direct Tab access must go through background
68
+ if (!browser.tabs) {
69
+ return manageConnection(type, options, async () => {
70
+ debug(type, "↗️ sending message to runtime");
71
+ return browser.runtime.sendMessage(makeMessage(type, args, target, options));
72
+ });
73
+ }
74
+ // `frameId` must be specified. If missing, the message is sent to every frame
75
+ const { tabId, frameId = 0 } = target;
76
+ // Message tab directly
77
+ return manageConnection(type, options, async () => {
78
+ debug(type, "↗️ sending message to tab", tabId, "frame", frameId);
79
+ return browser.tabs.sendMessage(tabId, makeMessage(type, args, target, options), {
80
+ frameId,
81
+ });
82
+ });
83
+ }
84
+ function getMethod(type, target) {
85
+ if (arguments.length === 1) {
86
+ return messenger.bind(undefined, type, {});
87
+ }
88
+ // @ts-expect-error `bind` types are junk
89
+ return messenger.bind(undefined, type, {}, target);
90
+ }
91
+ function getNotifier(type, target) {
92
+ const options = { isNotification: true };
93
+ if (arguments.length === 1) {
94
+ // @ts-expect-error `bind` types are junk
95
+ return messenger.bind(undefined, type, options);
96
+ }
97
+ // @ts-expect-error `bind` types are junk
98
+ return messenger.bind(undefined, type, options, target);
86
99
  }
87
- export { getContentScriptMethod, getMethod };
100
+ export { messenger, getMethod, getNotifier };
101
+ export const backgroundTarget = { page: "background" };
@@ -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 "./index";
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
+ }
@@ -1,4 +1,4 @@
1
- /// <reference types="firefox-webext-browser" />
1
+ import { Runtime } from "webextension-polyfill";
2
2
  import { Asyncify, ValueOf } from "type-fest";
3
3
  import { ErrorObject } from "serialize-error";
4
4
  declare global {
@@ -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: browser.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;
@@ -48,4 +49,8 @@ export interface Target {
48
49
  tabId: number;
49
50
  frameId?: number;
50
51
  }
52
+ export interface PageTarget {
53
+ tabId?: number;
54
+ page: string;
55
+ }
51
56
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webext-messenger",
3
- "version": "0.13.0-8",
3
+ "version": "0.15.0-0",
4
4
  "description": "Browser Extension component messaging framework",
5
5
  "keywords": [],
6
6
  "repository": "pixiebrix/webext-messenger",
@@ -17,8 +17,8 @@
17
17
  },
18
18
  "scripts": {
19
19
  "build": "tsc",
20
- "demo:watch": "parcel watch --no-cache --no-hmr --detailed-report 0",
21
- "demo:build": "parcel build --no-cache --detailed-report 0",
20
+ "demo:watch": "parcel watch --no-cache --no-hmr",
21
+ "demo:build": "parcel build --no-cache",
22
22
  "prepack": "tsc --sourceMap false",
23
23
  "test": "eslint . && tsc --noEmit",
24
24
  "lint": "eslint .",
@@ -78,23 +78,20 @@
78
78
  "@typescript-eslint/no-unsafe-member-access": "off"
79
79
  }
80
80
  }
81
- ],
82
- "globals": {
83
- "chrome": true
84
- }
81
+ ]
85
82
  },
86
83
  "dependencies": {
87
84
  "p-retry": "^4.6.1",
88
85
  "serialize-error": "^8.1.0",
89
86
  "type-fest": "^2.5.1",
90
- "webext-detect-page": "^3.0.2",
87
+ "webext-detect-page": "^3.1.0",
91
88
  "webextension-polyfill": "^0.8.0"
92
89
  },
93
90
  "devDependencies": {
94
91
  "@parcel/config-webextension": "^2.0.0",
95
92
  "@sindresorhus/tsconfig": "^2.0.0",
96
93
  "@types/chrome": "^0.0.159",
97
- "@types/firefox-webext-browser": "^94.0.0",
94
+ "@types/webextension-polyfill": "^0.8.0",
98
95
  "@typescript-eslint/eslint-plugin": "^5.1.0",
99
96
  "@typescript-eslint/parser": "^5.1.0",
100
97
  "eslint": "^8.1.0",
@@ -107,6 +104,7 @@
107
104
  "npm-run-all": "^4.1.5",
108
105
  "parcel": "^2.0.0",
109
106
  "typescript": "^4.4.4",
107
+ "webext-content-scripts": "^0.10.1",
110
108
  "xo": "^0.45.0"
111
109
  },
112
110
  "targets": {