webext-messenger 0.19.1 → 0.21.0-0
Sign up to get free protection for your applications and to get access to all the features.
- package/distribution/handlers.d.ts +10 -0
- package/distribution/handlers.js +6 -0
- package/distribution/index.js +1 -5
- package/distribution/receiver.js +10 -7
- package/distribution/sender.js +32 -6
- package/distribution/shared.d.ts +0 -2
- package/distribution/shared.js +4 -2
- package/distribution/thisTarget.d.ts +3 -10
- package/distribution/thisTarget.js +68 -26
- package/distribution/types.d.ts +3 -1
- package/package.json +3 -86
@@ -0,0 +1,10 @@
|
|
1
|
+
import { __getTabData } from "./thisTarget.js";
|
2
|
+
import { Method } from "./types.js";
|
3
|
+
declare global {
|
4
|
+
interface MessengerMethods {
|
5
|
+
__getTabData: typeof __getTabData;
|
6
|
+
}
|
7
|
+
}
|
8
|
+
export declare const privateMethods: (typeof __getTabData)[];
|
9
|
+
export declare const handlers: Map<string, Method>;
|
10
|
+
export declare function didUserRegisterMethods(): boolean;
|
package/distribution/index.js
CHANGED
@@ -1,8 +1,4 @@
|
|
1
|
-
// Imports must use the .js extension because
|
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
|
+
// Imports must use the .js extension because ESM requires it and TS refuses to rewrite .ts to .js
|
6
2
|
export * from "./receiver.js";
|
7
3
|
export * from "./sender.js";
|
8
4
|
export * from "./types.js";
|
package/distribution/receiver.js
CHANGED
@@ -1,8 +1,9 @@
|
|
1
1
|
import { serializeError } from "serialize-error";
|
2
|
+
import { getContextName } from "webext-detect-page";
|
2
3
|
import { messenger } from "./sender.js";
|
3
|
-
import {
|
4
|
-
import {
|
5
|
-
import {
|
4
|
+
import { isObject, MessengerError, debug, __webextMessenger, } from "./shared.js";
|
5
|
+
import { getActionForMessage } from "./thisTarget.js";
|
6
|
+
import { didUserRegisterMethods, handlers } from "./handlers.js";
|
6
7
|
export function isMessengerMessage(message) {
|
7
8
|
return (isObject(message) &&
|
8
9
|
typeof message["type"] === "string" &&
|
@@ -16,7 +17,7 @@ function onMessageListener(message, sender) {
|
|
16
17
|
return;
|
17
18
|
}
|
18
19
|
// Target check must be synchronous (`await` means we're handling the message)
|
19
|
-
const action = getActionForMessage(sender, message
|
20
|
+
const action = getActionForMessage(sender, message);
|
20
21
|
if (action === "ignore") {
|
21
22
|
return;
|
22
23
|
}
|
@@ -44,6 +45,11 @@ action) {
|
|
44
45
|
});
|
45
46
|
const localHandler = handlers.get(type);
|
46
47
|
if (!localHandler) {
|
48
|
+
if (!didUserRegisterMethods()) {
|
49
|
+
// TODO: Test the handling of __getTabData in contexts that have no registered methods
|
50
|
+
// https://github.com/pixiebrix/webext-messenger/pull/82
|
51
|
+
throw new MessengerError(`No handlers registered in ${getContextName()}`);
|
52
|
+
}
|
47
53
|
throw new MessengerError(`No handler registered for ${type} in ${getContextName()}`);
|
48
54
|
}
|
49
55
|
handleMessage = async () => localHandler.apply(meta, args);
|
@@ -57,9 +63,6 @@ action) {
|
|
57
63
|
return { ...response, __webextMessenger };
|
58
64
|
}
|
59
65
|
export function registerMethods(methods) {
|
60
|
-
if (!isBackground()) {
|
61
|
-
void nameThisTarget();
|
62
|
-
}
|
63
66
|
for (const [type, method] of Object.entries(methods)) {
|
64
67
|
if (handlers.has(type)) {
|
65
68
|
throw new MessengerError(`Handler already set for ${type}`);
|
package/distribution/sender.js
CHANGED
@@ -2,7 +2,8 @@ import pRetry from "p-retry";
|
|
2
2
|
import { isBackground } from "webext-detect-page";
|
3
3
|
import { doesTabExist } from "webext-tools";
|
4
4
|
import { deserializeError } from "serialize-error";
|
5
|
-
import { isObject, MessengerError, __webextMessenger,
|
5
|
+
import { isObject, MessengerError, __webextMessenger, debug, warn, } from "./shared.js";
|
6
|
+
import { handlers } from "./handlers.js";
|
6
7
|
const _errorNonExistingTarget = "Could not establish connection. Receiving end does not exist.";
|
7
8
|
// https://github.com/mozilla/webextension-polyfill/issues/384
|
8
9
|
const _errorTargetClosedEarly = "A listener indicated an asynchronous response by returning true, but the message channel closed before a response was received";
|
@@ -32,10 +33,27 @@ function manageConnection(type, options, target, sendMessage) {
|
|
32
33
|
async function manageMessage(type, target, sendMessage) {
|
33
34
|
const response = await pRetry(async () => {
|
34
35
|
const response = await sendMessage();
|
35
|
-
if (
|
36
|
-
|
36
|
+
if (isMessengerResponse(response)) {
|
37
|
+
return response;
|
37
38
|
}
|
38
|
-
|
39
|
+
// If no one answers, `response` will be `undefined`
|
40
|
+
// If the target does not have any `onMessage` listener at all, it will throw
|
41
|
+
// Possible:
|
42
|
+
// - Any target exists and has onMessage handler, but never handled the message
|
43
|
+
// - Extension page exists and has Messenger, but never handled the message (Messenger in Runtime ignores messages when the target isn't found)
|
44
|
+
// Not possible:
|
45
|
+
// - Tab exists and has Messenger, but never handled the message (Messenger in CS always handles messages)
|
46
|
+
// - Any target exists, but Messenger didn't have the specific Type handler (The receiving Messenger will throw an error)
|
47
|
+
// - No targets exist (the browser immediately throws "Could not establish connection. Receiving end does not exist.")
|
48
|
+
if (response === undefined) {
|
49
|
+
if ("page" in target) {
|
50
|
+
throw new MessengerError(`The target ${JSON.stringify(target)} for ${type} was not found`);
|
51
|
+
}
|
52
|
+
throw new MessengerError(`Messenger was not available in the target ${JSON.stringify(target)} for ${type}`);
|
53
|
+
}
|
54
|
+
// Possible:
|
55
|
+
// - Non-Messenger handler responded
|
56
|
+
throw new MessengerError(`Conflict: The message ${type} was handled by a third-party listener`);
|
39
57
|
}, {
|
40
58
|
minTimeout: 100,
|
41
59
|
factor: 1.3,
|
@@ -47,7 +65,10 @@ async function manageMessage(type, target, sendMessage) {
|
|
47
65
|
if (
|
48
66
|
// Don't retry sending to the background page unless it really hasn't loaded yet
|
49
67
|
(target.page !== "background" && error instanceof MessengerError) ||
|
50
|
-
|
68
|
+
// Page or its content script not yet loaded
|
69
|
+
error.message === _errorNonExistingTarget ||
|
70
|
+
// `registerMethods` not yet loaded
|
71
|
+
String(error.message).startsWith("No handlers registered in ")) {
|
51
72
|
if (browser.tabs &&
|
52
73
|
typeof target.tabId === "number" &&
|
53
74
|
!(await doesTabExist(target.tabId))) {
|
@@ -59,6 +80,11 @@ async function manageMessage(type, target, sendMessage) {
|
|
59
80
|
throw error;
|
60
81
|
}
|
61
82
|
},
|
83
|
+
}).catch((error) => {
|
84
|
+
if (error?.message === _errorNonExistingTarget) {
|
85
|
+
throw new MessengerError(`The target ${JSON.stringify(target)} for ${type} was not found`);
|
86
|
+
}
|
87
|
+
throw error;
|
62
88
|
});
|
63
89
|
if ("error" in response) {
|
64
90
|
debug(type, "↘️ replied with error", response.error);
|
@@ -76,7 +102,7 @@ function messenger(type, options, target, ...args) {
|
|
76
102
|
warn(type, "is being handled locally");
|
77
103
|
return handler.apply({ trace: [] }, args);
|
78
104
|
}
|
79
|
-
throw new MessengerError("No handler registered for " + type);
|
105
|
+
throw new MessengerError("No handler registered locally for " + type);
|
80
106
|
}
|
81
107
|
const sendMessage = async () => {
|
82
108
|
debug(type, "↗️ sending message to runtime");
|
package/distribution/shared.d.ts
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
import { JsonObject } from "type-fest";
|
2
|
-
import { Method } from "./types.js";
|
3
2
|
declare type ErrorObject = {
|
4
3
|
name?: string;
|
5
4
|
stack?: string;
|
@@ -11,7 +10,6 @@ export declare function isObject(value: unknown): value is Record<string, unknow
|
|
11
10
|
export declare class MessengerError extends Error {
|
12
11
|
name: string;
|
13
12
|
}
|
14
|
-
export declare const handlers: Map<string, Method>;
|
15
13
|
export declare const debug: (...args: any[]) => void;
|
16
14
|
export declare const warn: (...args: any[]) => void;
|
17
15
|
export declare function isErrorObject(error: unknown): error is ErrorObject;
|
package/distribution/shared.js
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
import { errorConstructors } from "serialize-error";
|
1
2
|
const logging = (() => {
|
2
3
|
try {
|
3
4
|
// @ts-expect-error it would break Webpack
|
@@ -25,13 +26,14 @@ export class MessengerError extends Error {
|
|
25
26
|
});
|
26
27
|
}
|
27
28
|
}
|
28
|
-
|
29
|
+
// @ts-expect-error Wrong `errorConstructors` types
|
30
|
+
errorConstructors.set("MessengerError", MessengerError);
|
29
31
|
// .bind preserves the call location in the console
|
30
32
|
export const debug = logging ? console.debug.bind(console, "Messenger:") : noop;
|
31
33
|
export const warn = logging ? console.warn.bind(console, "Messenger:") : noop;
|
32
34
|
export function isErrorObject(error) {
|
33
35
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- This is a type guard function and it uses ?.
|
34
|
-
return typeof
|
36
|
+
return typeof error?.message === "string";
|
35
37
|
}
|
36
38
|
export async function delay(milliseconds) {
|
37
39
|
return new Promise((resolve) => {
|
@@ -1,11 +1,4 @@
|
|
1
|
-
import { AnyTarget, MessengerMeta, Sender } from "./types.js";
|
2
|
-
export declare function getActionForMessage(from: Sender,
|
3
|
-
export declare function
|
4
|
-
declare function __getTabData(this: MessengerMeta): AnyTarget;
|
5
|
-
declare global {
|
6
|
-
interface MessengerMethods {
|
7
|
-
__getTabData: typeof __getTabData;
|
8
|
-
}
|
9
|
-
}
|
1
|
+
import { AnyTarget, Message, MessengerMeta, Sender } from "./types.js";
|
2
|
+
export declare function getActionForMessage(from: Sender, message: Message): "respond" | "forward" | "ignore";
|
3
|
+
export declare function __getTabData(this: MessengerMeta): AnyTarget;
|
10
4
|
export declare function initPrivateApi(): void;
|
11
|
-
export {};
|
@@ -1,11 +1,44 @@
|
|
1
1
|
import { isBackground, isContentScript, isExtensionContext, } from "webext-detect-page";
|
2
2
|
import { messenger } from "./sender.js";
|
3
3
|
import { registerMethods } from "./receiver.js";
|
4
|
-
import { debug } from "./shared.js";
|
4
|
+
import { debug, MessengerError } from "./shared.js";
|
5
|
+
/**
|
6
|
+
* @file This file exists because `runtime.sendMessage` acts as a broadcast to
|
7
|
+
* all open extension pages, so the receiver needs a way to figure out if the
|
8
|
+
* message was intended for them.
|
9
|
+
*
|
10
|
+
* If the requested target only includes a `page` (URL), then it can be determined
|
11
|
+
* immediately. If the target also specifies a tab, like `{tabId: 1, page: '/sidebar.html'}`,
|
12
|
+
* then the receiving target needs to fetch the tab information via `__getTabData`.
|
13
|
+
*
|
14
|
+
* `__getTabData` is called automatically when `webext-messenger` is imported in
|
15
|
+
* a context that requires this logic (most extension:// pages).
|
16
|
+
*
|
17
|
+
* If a broadcast message with `tabId` target is received before `__getTabData` is "received",
|
18
|
+
* the message will be ignored and it can be retried. If `__getTabData` somehow fails,
|
19
|
+
* the target will forever ignore any messages that require the `tabId`. In that case,
|
20
|
+
* an error would be thrown once and will be visible in the console, uncaught.
|
21
|
+
*
|
22
|
+
* Content scripts do not use this logic at all at the moment because they're
|
23
|
+
* always targeted via `tabId/frameId` combo and `tabs.sendMessage`.
|
24
|
+
*/
|
5
25
|
// Soft warning: Race conditions are possible.
|
6
26
|
// This CANNOT be awaited because waiting for it means "I will handle the message."
|
7
27
|
// If a message is received before this is ready, it will just have to be ignored.
|
8
|
-
|
28
|
+
const thisTarget = isBackground()
|
29
|
+
? { page: "background" }
|
30
|
+
: {
|
31
|
+
get page() {
|
32
|
+
return location.pathname + location.search;
|
33
|
+
},
|
34
|
+
};
|
35
|
+
let tabDataStatus =
|
36
|
+
// The background page doesn't have a tab
|
37
|
+
isBackground() ||
|
38
|
+
// Content scripts don't use named targets yet
|
39
|
+
isContentScript()
|
40
|
+
? "not-needed"
|
41
|
+
: "needed";
|
9
42
|
function compareTargets(to, thisTarget) {
|
10
43
|
for (const [key, value] of Object.entries(to)) {
|
11
44
|
if (thisTarget[key] === value) {
|
@@ -27,9 +60,10 @@ function compareTargets(to, thisTarget) {
|
|
27
60
|
}
|
28
61
|
return true;
|
29
62
|
}
|
30
|
-
|
31
|
-
) {
|
32
|
-
|
63
|
+
// TODO: Test this in Jest, outside the browser
|
64
|
+
export function getActionForMessage(from, message) {
|
65
|
+
// Clone object because we're editing it
|
66
|
+
const to = { ...message.target };
|
33
67
|
if (to.page === "any") {
|
34
68
|
return "respond";
|
35
69
|
}
|
@@ -42,41 +76,49 @@ export function getActionForMessage(from, { ...to } // Clone object because we'r
|
|
42
76
|
if (!to.page) {
|
43
77
|
return "forward";
|
44
78
|
}
|
45
|
-
if (!thisTarget) {
|
46
|
-
console.warn("A message was received before this context was ready");
|
47
|
-
// If this *was* the target, then probably no one else answered
|
48
|
-
return "ignore";
|
49
|
-
}
|
50
79
|
// Set "this" tab to the current tabId
|
51
|
-
if (to.tabId === "this" && thisTarget.tabId ===
|
80
|
+
if (to.tabId === "this" && thisTarget.tabId === from.tab?.id) {
|
52
81
|
to.tabId = thisTarget.tabId;
|
53
82
|
}
|
54
83
|
// Every `target` key must match `thisTarget`
|
55
84
|
const isThisTarget = compareTargets(to, thisTarget);
|
56
85
|
if (!isThisTarget) {
|
57
|
-
debug("
|
86
|
+
debug(message.type, "🤫 ignored due to target mismatch", {
|
87
|
+
requestedTarget: to,
|
88
|
+
thisTarget,
|
89
|
+
tabDataStatus,
|
90
|
+
});
|
58
91
|
}
|
59
92
|
return isThisTarget ? "respond" : "ignore";
|
60
93
|
}
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
thisTarget
|
94
|
+
async function storeTabData() {
|
95
|
+
if (tabDataStatus !== "needed") {
|
96
|
+
return;
|
97
|
+
}
|
98
|
+
try {
|
99
|
+
tabDataStatus = "pending";
|
100
|
+
Object.assign(thisTarget, {
|
101
|
+
...(await messenger("__getTabData", {}, { page: "any" })),
|
102
|
+
});
|
103
|
+
tabDataStatus = "received";
|
104
|
+
}
|
105
|
+
catch (error) {
|
106
|
+
tabDataStatus = "error";
|
107
|
+
throw new MessengerError("Tab registration failed. This page won’t be able to receive messages that require tab information",
|
108
|
+
// @ts-expect-error TODO: update lib to accept Error#cause
|
109
|
+
{ cause: error });
|
68
110
|
}
|
69
111
|
}
|
70
|
-
function __getTabData() {
|
71
|
-
|
72
|
-
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 };
|
112
|
+
export function __getTabData() {
|
113
|
+
return { tabId: this.trace[0]?.tab?.id, frameId: this.trace[0]?.frameId };
|
73
114
|
}
|
74
115
|
export function initPrivateApi() {
|
75
|
-
if (isBackground()) {
|
76
|
-
thisTarget = { page: "background" };
|
77
|
-
}
|
78
116
|
if (isExtensionContext()) {
|
79
|
-
//
|
117
|
+
// Only `runtime` pages can handle this message but I can't remove it because its listener
|
118
|
+
// also serves the purpose of throwing a specific error when no methods have been registered.
|
119
|
+
// https://github.com/pixiebrix/webext-messenger/pull/80
|
80
120
|
registerMethods({ __getTabData });
|
121
|
+
// `getTabInformation` includes per-context exclusion logic
|
122
|
+
void storeTabData();
|
81
123
|
}
|
82
124
|
}
|
package/distribution/types.d.ts
CHANGED
@@ -40,7 +40,9 @@ export declare type Message<LocalArguments extends Arguments = Arguments> = {
|
|
40
40
|
/** If the message is being sent to an intermediary receiver, also set the options */
|
41
41
|
options?: Options;
|
42
42
|
};
|
43
|
-
export declare type Sender = Runtime.MessageSender
|
43
|
+
export declare type Sender = Runtime.MessageSender & {
|
44
|
+
origin?: string;
|
45
|
+
};
|
44
46
|
export declare type MessengerMessage = Message & {
|
45
47
|
/** Guarantees that a message is meant to be handled by this library */
|
46
48
|
__webextMessenger: true;
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "webext-messenger",
|
3
|
-
"version": "0.
|
3
|
+
"version": "0.21.0-0",
|
4
4
|
"description": "Browser Extension component messaging framework",
|
5
5
|
"keywords": [],
|
6
6
|
"repository": "pixiebrix/webext-messenger",
|
@@ -18,85 +18,6 @@
|
|
18
18
|
"fix": "eslint . --fix",
|
19
19
|
"watch": "tsc --watch"
|
20
20
|
},
|
21
|
-
"eslintConfig": {
|
22
|
-
"env": {
|
23
|
-
"browser": true
|
24
|
-
},
|
25
|
-
"parserOptions": {
|
26
|
-
"project": "tsconfig.json"
|
27
|
-
},
|
28
|
-
"plugins": [
|
29
|
-
"import"
|
30
|
-
],
|
31
|
-
"extends": [
|
32
|
-
"plugin:@typescript-eslint/recommended",
|
33
|
-
"xo",
|
34
|
-
"xo-typescript",
|
35
|
-
"prettier",
|
36
|
-
"plugin:import/recommended",
|
37
|
-
"plugin:import/typescript",
|
38
|
-
"plugin:unicorn/recommended"
|
39
|
-
],
|
40
|
-
"rules": {
|
41
|
-
"no-restricted-imports": [
|
42
|
-
"error",
|
43
|
-
{
|
44
|
-
"paths": [
|
45
|
-
{
|
46
|
-
"name": "./index",
|
47
|
-
"message": "The index file is only used to re-export internal files. Use direct imports instead."
|
48
|
-
}
|
49
|
-
]
|
50
|
-
}
|
51
|
-
],
|
52
|
-
"import/extensions": [
|
53
|
-
"error",
|
54
|
-
"always"
|
55
|
-
],
|
56
|
-
"import/no-unresolved": "off",
|
57
|
-
"unicorn/filename-case": [
|
58
|
-
"error",
|
59
|
-
{
|
60
|
-
"case": "camelCase"
|
61
|
-
}
|
62
|
-
],
|
63
|
-
"unicorn/no-useless-undefined": [
|
64
|
-
"error",
|
65
|
-
{
|
66
|
-
"checkArguments": false
|
67
|
-
}
|
68
|
-
],
|
69
|
-
"unicorn/prevent-abbreviations": [
|
70
|
-
"error",
|
71
|
-
{
|
72
|
-
"allowList": {
|
73
|
-
"args": true
|
74
|
-
}
|
75
|
-
}
|
76
|
-
]
|
77
|
-
},
|
78
|
-
"overrides": [
|
79
|
-
{
|
80
|
-
"files": [
|
81
|
-
"*.test.ts",
|
82
|
-
"testingApi.ts"
|
83
|
-
],
|
84
|
-
"rules": {
|
85
|
-
"@typescript-eslint/no-explicit-any": "off",
|
86
|
-
"@typescript-eslint/no-non-null-assertion": "off",
|
87
|
-
"@typescript-eslint/no-unsafe-member-access": "off"
|
88
|
-
}
|
89
|
-
},
|
90
|
-
{
|
91
|
-
"files": [
|
92
|
-
"source/test/**/*"
|
93
|
-
],
|
94
|
-
"rules": {
|
95
|
-
"import/extensions": "off"
|
96
|
-
}
|
97
|
-
}
|
98
|
-
]
|
99
|
-
},
|
100
21
|
"dependencies": {
|
101
22
|
"p-retry": "^5.1.1",
|
102
23
|
"serialize-error": "^11.0.0",
|
@@ -131,12 +52,8 @@
|
|
131
52
|
"webextension-polyfill": "^0.9.0"
|
132
53
|
},
|
133
54
|
"alias": {
|
134
|
-
"./this-stuff-is-just-for-local-parcel-tests": "
|
135
|
-
"./source
|
136
|
-
"./source/receiver.js": "./source/receiver.ts",
|
137
|
-
"./source/types.js": "./source/types.ts",
|
138
|
-
"./source/shared.js": "./source/shared.ts",
|
139
|
-
"./source/thisTarget.js": "./source/thisTarget.ts"
|
55
|
+
"./this-stuff-is-just-for-local-parcel-tests": "https://github.com/parcel-bundler/parcel/issues/4936",
|
56
|
+
"./source/**/*.js": "./source/$1/$2.ts"
|
140
57
|
},
|
141
58
|
"targets": {
|
142
59
|
"main": false,
|