webext-messenger 0.30.0 → 0.32.0-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.
- package/distribution/logging.js +0 -1
- package/distribution/receiver.d.ts +3 -1
- package/distribution/receiver.js +77 -31
- package/distribution/sender.d.ts +5 -5
- package/distribution/sender.js +13 -7
- package/distribution/targetLogic.js +4 -0
- package/distribution/targetLogic.test.js +6 -2
- package/distribution/types.d.ts +10 -5
- package/package.json +15 -15
package/distribution/logging.js
CHANGED
@@ -1,5 +1,7 @@
|
|
1
|
-
import { type Message, type MessengerMeta } from "./types.js";
|
1
|
+
import { type Message, type ExternalMessage, type MessengerMeta } from "./types.js";
|
2
2
|
export declare function isMessengerMessage(message: unknown): message is Message;
|
3
|
+
export declare function isExternalMessengerMessage(message: unknown): message is ExternalMessage;
|
3
4
|
export declare function registerMethods(methods: Partial<MessengerMethods>): void;
|
5
|
+
export declare function allowExternalUse(...types: Array<keyof MessengerMethods>): void;
|
4
6
|
/** Ensure/document that the current function was called via Messenger */
|
5
7
|
export declare function assertMessengerCall(_this: MessengerMeta): asserts _this is MessengerMeta;
|
package/distribution/receiver.js
CHANGED
@@ -1,19 +1,35 @@
|
|
1
1
|
import { serializeError } from "serialize-error";
|
2
|
-
import { getContextName } from "webext-detect";
|
2
|
+
import { getContextName, isBackground } from "webext-detect";
|
3
3
|
import { messenger } from "./sender.js";
|
4
4
|
import { isObject, MessengerError, __webextMessenger } from "./shared.js";
|
5
5
|
import { log } from "./logging.js";
|
6
6
|
import { getActionForMessage } from "./targetLogic.js";
|
7
7
|
import { didUserRegisterMethods, handlers } from "./handlers.js";
|
8
8
|
import { getTabDataStatus, thisTarget } from "./thisTarget.js";
|
9
|
+
const externalMethods = new Set();
|
9
10
|
export function isMessengerMessage(message) {
|
10
11
|
return (isObject(message) &&
|
11
12
|
typeof message["type"] === "string" &&
|
12
13
|
message["__webextMessenger"] === true &&
|
13
14
|
Array.isArray(message["args"]));
|
14
15
|
}
|
15
|
-
|
16
|
-
|
16
|
+
export function isExternalMessengerMessage(message) {
|
17
|
+
return (isObject(message) &&
|
18
|
+
typeof message["type"] === "string" &&
|
19
|
+
message["__webextMessenger"] === true &&
|
20
|
+
Array.isArray(message["args"]) &&
|
21
|
+
isObject(message["target"]) &&
|
22
|
+
Object.keys(message["target"]).length === 1 && // Ensure it's *only* `extensionId`
|
23
|
+
typeof message["target"]["extensionId"] === "string");
|
24
|
+
}
|
25
|
+
/**
|
26
|
+
* Decides what to do with a message and sends a response (value or error) back to the sender.
|
27
|
+
*
|
28
|
+
* @warn This function cannot return a Promise.
|
29
|
+
* @warn Limit the amount of logic here because errors won't make it to `sendResponse`
|
30
|
+
*/
|
31
|
+
//
|
32
|
+
function onMessageListener(message, sender, sendResponse) {
|
17
33
|
if (!isMessengerMessage(message)) {
|
18
34
|
// TODO: Add test for this eventuality: ignore unrelated messages
|
19
35
|
return;
|
@@ -28,21 +44,10 @@ function onMessageListener(message, sender) {
|
|
28
44
|
});
|
29
45
|
return;
|
30
46
|
}
|
31
|
-
return handleMessage(message, sender, action);
|
32
|
-
}
|
33
|
-
// This function can only be called when the message *will* be handled locally.
|
34
|
-
// Returning "undefined" or throwing an error will still handle it.
|
35
|
-
async function handleMessage(message, sender,
|
36
|
-
// Once messages reach this function they cannot be "ignored", they're already being handled
|
37
|
-
action) {
|
38
47
|
const { type, target, args, options = {} } = message;
|
39
48
|
const { trace = [], seq } = options;
|
40
|
-
trace.push(sender);
|
41
|
-
const meta = { trace };
|
42
|
-
let handleMessage;
|
43
49
|
if (action === "forward") {
|
44
50
|
log.debug(type, seq, "🔀 forwarded", { sender, target });
|
45
|
-
handleMessage = async () => messenger(type, meta, target, ...args);
|
46
51
|
}
|
47
52
|
else {
|
48
53
|
log.debug(type, seq, "↘️ received in", getContextName(), {
|
@@ -50,24 +55,56 @@ action) {
|
|
50
55
|
args,
|
51
56
|
wasForwarded: trace.length > 1,
|
52
57
|
});
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
}
|
60
|
-
|
58
|
+
}
|
59
|
+
// Prepare the response asynchronously because the listener must return `true` synchronously
|
60
|
+
(async () => {
|
61
|
+
try {
|
62
|
+
trace.push(sender);
|
63
|
+
const value = await prepareResponse(message, action, { trace });
|
64
|
+
log.debug(type, seq, "↗️ responding", { value });
|
65
|
+
sendResponse({ __webextMessenger, value });
|
66
|
+
}
|
67
|
+
catch (error) {
|
68
|
+
log.debug(type, seq, "↗️ responding", { error });
|
69
|
+
sendResponse({ __webextMessenger, error: serializeError(error) });
|
61
70
|
}
|
62
|
-
|
71
|
+
})();
|
72
|
+
// This indicates that the message is being handled and a response will be sent asynchronously
|
73
|
+
// TODO: Just return a promise if this is ever implemented https://issues.chromium.org/issues/40753031
|
74
|
+
return true;
|
75
|
+
}
|
76
|
+
// Do not remove.
|
77
|
+
// Early validation to ensure that the message matches the specific allowed target
|
78
|
+
// before letting it flow into the rest of messenger.
|
79
|
+
function onMessageExternalListener(message, sender, sendResponse) {
|
80
|
+
if (isExternalMessengerMessage(message) &&
|
81
|
+
message.target.extensionId === chrome.runtime.id) {
|
82
|
+
return onMessageListener(message, sender, sendResponse);
|
63
83
|
}
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
84
|
+
log.debug("Ignored external message", {
|
85
|
+
message,
|
86
|
+
sender,
|
87
|
+
});
|
88
|
+
}
|
89
|
+
/** Generates the value or error to return to the sender; does not include further messaging logic */
|
90
|
+
async function prepareResponse(message, action, meta) {
|
91
|
+
const { type, target, args } = message;
|
92
|
+
if (action === "forward") {
|
93
|
+
return messenger(type, meta, target, ...args);
|
94
|
+
}
|
95
|
+
const localHandler = handlers.get(type);
|
96
|
+
if (localHandler) {
|
97
|
+
if ("extensionId" in target && !externalMethods.has(type)) {
|
98
|
+
throw new MessengerError(`${type} is not allowed to be called externally in ${getContextName()}`);
|
99
|
+
}
|
100
|
+
return localHandler.apply(meta, args);
|
101
|
+
}
|
102
|
+
if (didUserRegisterMethods()) {
|
103
|
+
throw new MessengerError(`No handler registered for ${type} in ${getContextName()}`);
|
104
|
+
}
|
105
|
+
// TODO: Test the handling of __getTabData in contexts that have no registered methods
|
106
|
+
// https://github.com/pixiebrix/webext-messenger/pull/82
|
107
|
+
throw new MessengerError(`No handlers registered in ${getContextName()}`);
|
71
108
|
}
|
72
109
|
export function registerMethods(methods) {
|
73
110
|
for (const [type, method] of Object.entries(methods)) {
|
@@ -77,7 +114,16 @@ export function registerMethods(methods) {
|
|
77
114
|
log.debug("Registered", type);
|
78
115
|
handlers.set(type, method);
|
79
116
|
}
|
80
|
-
|
117
|
+
chrome.runtime.onMessage.addListener(onMessageListener);
|
118
|
+
// Only handle direct-to-background messages for now
|
119
|
+
if (isBackground()) {
|
120
|
+
chrome.runtime.onMessageExternal.addListener(onMessageExternalListener);
|
121
|
+
}
|
122
|
+
}
|
123
|
+
export function allowExternalUse(...types) {
|
124
|
+
for (const type of types) {
|
125
|
+
externalMethods.add(type);
|
126
|
+
}
|
81
127
|
}
|
82
128
|
/** Ensure/document that the current function was called via Messenger */
|
83
129
|
export function assertMessengerCall(_this) { }
|
package/distribution/sender.d.ts
CHANGED
@@ -1,15 +1,15 @@
|
|
1
|
-
import { type PublicMethod, type PublicMethodWithTarget, type Options, type
|
1
|
+
import { type PublicMethod, type PublicMethodWithTarget, type Options, type AnySpecificTarget, type PageTarget } from "./types.js";
|
2
2
|
import { type Promisable, type SetReturnType } from "type-fest";
|
3
3
|
export declare const errorTargetClosedEarly = "The target was closed before receiving a response";
|
4
4
|
export declare const errorTabDoesntExist = "The tab doesn't exist";
|
5
5
|
export declare const errorTabWasDiscarded = "The tab was discarded";
|
6
6
|
declare function messenger<Type extends keyof MessengerMethods, Method extends MessengerMethods[Type]>(type: Type, options: {
|
7
7
|
isNotification: true;
|
8
|
-
}, target:
|
9
|
-
declare function messenger<Type extends keyof MessengerMethods, Method extends MessengerMethods[Type], ReturnValue extends Promise<ReturnType<Method>>>(type: Type, options: Options, target:
|
10
|
-
declare function getMethod<Type extends keyof MessengerMethods, Method extends MessengerMethods[Type], PublicMethodType extends PublicMethod<Method>>(type: Type, target: Promisable<
|
8
|
+
}, target: AnySpecificTarget, ...args: Parameters<Method>): void;
|
9
|
+
declare function messenger<Type extends keyof MessengerMethods, Method extends MessengerMethods[Type], ReturnValue extends Promise<ReturnType<Method>>>(type: Type, options: Options, target: AnySpecificTarget, ...args: Parameters<Method>): ReturnValue;
|
10
|
+
declare function getMethod<Type extends keyof MessengerMethods, Method extends MessengerMethods[Type], PublicMethodType extends PublicMethod<Method>>(type: Type, target: Promisable<AnySpecificTarget>): PublicMethodType;
|
11
11
|
declare function getMethod<Type extends keyof MessengerMethods, Method extends MessengerMethods[Type], PublicMethodWithDynamicTarget extends PublicMethodWithTarget<Method>>(type: Type): PublicMethodWithDynamicTarget;
|
12
|
-
declare function getNotifier<Type extends keyof MessengerMethods, Method extends MessengerMethods[Type], PublicMethodType extends SetReturnType<PublicMethod<Method>, void>>(type: Type, target: Promisable<
|
12
|
+
declare function getNotifier<Type extends keyof MessengerMethods, Method extends MessengerMethods[Type], PublicMethodType extends SetReturnType<PublicMethod<Method>, void>>(type: Type, target: Promisable<AnySpecificTarget>): PublicMethodType;
|
13
13
|
declare function getNotifier<Type extends keyof MessengerMethods, Method extends MessengerMethods[Type], PublicMethodWithDynamicTarget extends SetReturnType<PublicMethodWithTarget<Method>, void>>(type: Type): PublicMethodWithDynamicTarget;
|
14
14
|
export { messenger, getMethod, getNotifier };
|
15
15
|
export declare const backgroundTarget: PageTarget;
|
package/distribution/sender.js
CHANGED
@@ -6,7 +6,6 @@ import { log } from "./logging.js";
|
|
6
6
|
import { handlers } from "./handlers.js";
|
7
7
|
import { events } from "./events.js";
|
8
8
|
const _errorNonExistingTarget = "Could not establish connection. Receiving end does not exist.";
|
9
|
-
// https://github.com/mozilla/webextension-polyfill/issues/384
|
10
9
|
const _errorTargetClosedEarly = "A listener indicated an asynchronous response by returning true, but the message channel closed before a response was received";
|
11
10
|
export const errorTargetClosedEarly = "The target was closed before receiving a response";
|
12
11
|
export const errorTabDoesntExist = "The tab doesn't exist";
|
@@ -99,9 +98,9 @@ async function manageMessage(type, target, seq, retry, sendMessage) {
|
|
99
98
|
String(error.message).startsWith("No handlers registered in ")))) {
|
100
99
|
throw error;
|
101
100
|
}
|
102
|
-
if (
|
101
|
+
if (chrome.tabs && typeof target.tabId === "number") {
|
103
102
|
try {
|
104
|
-
const tabInfo = await
|
103
|
+
const tabInfo = await chrome.tabs.get(target.tabId);
|
105
104
|
if (tabInfo.discarded) {
|
106
105
|
throw new Error(errorTabWasDiscarded);
|
107
106
|
}
|
@@ -142,6 +141,13 @@ let globalSeq = (Date.now() % 100) * 10_000;
|
|
142
141
|
function messenger(type, options, target, ...args) {
|
143
142
|
options.seq = globalSeq++;
|
144
143
|
const { seq } = options;
|
144
|
+
if ("extensionId" in target) {
|
145
|
+
const sendMessage = async (attemptCount) => {
|
146
|
+
log.debug(type, seq, "↗️ sending message to extension", attemptLog(attemptCount));
|
147
|
+
return chrome.runtime.sendMessage(target.extensionId, makeMessage(type, args, target, options));
|
148
|
+
};
|
149
|
+
return manageConnection(type, options, target, sendMessage);
|
150
|
+
}
|
145
151
|
// Message goes to extension page
|
146
152
|
if ("page" in target) {
|
147
153
|
if (target.page === "background" && isBackground()) {
|
@@ -154,15 +160,15 @@ function messenger(type, options, target, ...args) {
|
|
154
160
|
}
|
155
161
|
const sendMessage = async (attemptCount) => {
|
156
162
|
log.debug(type, seq, "↗️ sending message to runtime", attemptLog(attemptCount));
|
157
|
-
return
|
163
|
+
return chrome.runtime.sendMessage(makeMessage(type, args, target, options));
|
158
164
|
};
|
159
165
|
return manageConnection(type, options, target, sendMessage);
|
160
166
|
}
|
161
167
|
// Contexts without direct Tab access must go through background
|
162
|
-
if (!
|
168
|
+
if (!chrome.tabs) {
|
163
169
|
return manageConnection(type, options, target, async (attemptCount) => {
|
164
170
|
log.debug(type, seq, "↗️ sending message to runtime", attemptLog(attemptCount));
|
165
|
-
return
|
171
|
+
return chrome.runtime.sendMessage(makeMessage(type, args, target, options));
|
166
172
|
});
|
167
173
|
}
|
168
174
|
// `frameId` must be specified. If missing, the message is sent to every frame
|
@@ -170,7 +176,7 @@ function messenger(type, options, target, ...args) {
|
|
170
176
|
// Message tab directly
|
171
177
|
return manageConnection(type, options, target, async (attemptCount) => {
|
172
178
|
log.debug(type, seq, "↗️ sending message to tab", tabId, "frame", frameId, attemptLog(attemptCount));
|
173
|
-
return
|
179
|
+
return chrome.tabs.sendMessage(tabId, makeMessage(type, args, target, options), frameId === "allFrames"
|
174
180
|
? {}
|
175
181
|
: {
|
176
182
|
frameId,
|
@@ -23,6 +23,10 @@ export function compareTargets(to, thisTarget) {
|
|
23
23
|
export function getActionForMessage(from, target, thisTarget) {
|
24
24
|
// Clone object because we're editing it
|
25
25
|
const to = { ...target };
|
26
|
+
if (to.extensionId) {
|
27
|
+
// Only handle external messages in the background page
|
28
|
+
return isBackground() ? "respond" : "ignore";
|
29
|
+
}
|
26
30
|
if (to.page === "any") {
|
27
31
|
return "respond";
|
28
32
|
}
|
@@ -11,11 +11,15 @@ const tab = {
|
|
11
11
|
pinned: false,
|
12
12
|
highlighted: true,
|
13
13
|
incognito: false,
|
14
|
+
discarded: false,
|
15
|
+
frozen: false,
|
16
|
+
selected: true,
|
17
|
+
autoDiscardable: false,
|
18
|
+
groupId: -1,
|
14
19
|
};
|
15
20
|
const senders = {
|
16
|
-
background: { page: "background" },
|
17
21
|
contentScript: { tab },
|
18
|
-
|
22
|
+
// TODO: Test more senders
|
19
23
|
};
|
20
24
|
const targets = {
|
21
25
|
background: { page: "background" },
|
package/distribution/types.d.ts
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
import { type Runtime } from "webextension-polyfill";
|
2
1
|
import { type Asyncify, type ValueOf } from "type-fest";
|
3
2
|
import { type ErrorObject } from "serialize-error";
|
4
3
|
/**
|
@@ -11,7 +10,7 @@ declare global {
|
|
11
10
|
_: Method;
|
12
11
|
}
|
13
12
|
}
|
14
|
-
type WithTarget<Method> = Method extends (...args: infer PreviousArguments) => infer TReturnValue ? (target:
|
13
|
+
type WithTarget<Method> = Method extends (...args: infer PreviousArguments) => infer TReturnValue ? (target: AnySpecificTarget, ...args: PreviousArguments) => TReturnValue : never;
|
15
14
|
type ActuallyOmitThisParameter<T> = T extends (...args: infer A) => infer R ? (...args: A) => R : T;
|
16
15
|
/** Removes the `this` type and ensure it's always Promised */
|
17
16
|
export type PublicMethod<Method extends ValueOf<MessengerMethods>> = Asyncify<ActuallyOmitThisParameter<Method>>;
|
@@ -44,13 +43,14 @@ export interface Options {
|
|
44
43
|
export type Message<LocalArguments extends Arguments = Arguments> = {
|
45
44
|
type: keyof MessengerMethods;
|
46
45
|
args: LocalArguments;
|
47
|
-
target:
|
46
|
+
target: AnySpecificTarget;
|
48
47
|
/** If the message is being sent to an intermediary receiver, also set the options */
|
49
48
|
options?: Options;
|
50
49
|
};
|
51
|
-
export type
|
52
|
-
|
50
|
+
export type ExternalMessage = Message & {
|
51
|
+
target: ExtensionTarget;
|
53
52
|
};
|
53
|
+
export type Sender = chrome.runtime.MessageSender;
|
54
54
|
export type MessengerMessage = Message & {
|
55
55
|
/** Guarantees that a message is meant to be handled by this library */
|
56
56
|
__webextMessenger: true;
|
@@ -59,6 +59,7 @@ export interface AnyTarget {
|
|
59
59
|
tabId?: number | "this";
|
60
60
|
frameId?: number | "allFrames";
|
61
61
|
page?: string;
|
62
|
+
extensionId?: string;
|
62
63
|
}
|
63
64
|
export interface TopLevelFrame {
|
64
65
|
tabId: number;
|
@@ -81,4 +82,8 @@ export interface PageTarget {
|
|
81
82
|
tabId?: number | "this";
|
82
83
|
page: string;
|
83
84
|
}
|
85
|
+
export interface ExtensionTarget {
|
86
|
+
extensionId: string;
|
87
|
+
}
|
88
|
+
export type AnySpecificTarget = Target | PageTarget | ExtensionTarget;
|
84
89
|
export {};
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "webext-messenger",
|
3
|
-
"version": "0.
|
3
|
+
"version": "0.32.0-0",
|
4
4
|
"description": "Browser Extension component messaging framework",
|
5
5
|
"keywords": [],
|
6
6
|
"repository": "pixiebrix/webext-messenger",
|
@@ -16,7 +16,7 @@
|
|
16
16
|
},
|
17
17
|
"scripts": {
|
18
18
|
"build": "tsc",
|
19
|
-
"demo:watch": "parcel
|
19
|
+
"demo:watch": "parcel serve --no-cache --no-hmr",
|
20
20
|
"demo:build": "parcel build --no-cache --no-scope-hoist",
|
21
21
|
"prepack": "tsc --sourceMap false",
|
22
22
|
"test": "run-p test:unit lint build demo:build",
|
@@ -29,9 +29,8 @@
|
|
29
29
|
"p-event": "^6.0.1",
|
30
30
|
"p-retry": "^6.2.1",
|
31
31
|
"serialize-error": "^12.0.0",
|
32
|
-
"type-fest": "^4.
|
33
|
-
"webext-detect": "^5.3.2"
|
34
|
-
"webext-events": "^3.1.1"
|
32
|
+
"type-fest": "^4.36.0",
|
33
|
+
"webext-detect": "^5.3.2"
|
35
34
|
},
|
36
35
|
"@parcel/resolver-default": {
|
37
36
|
"packageExports": true
|
@@ -39,9 +38,8 @@
|
|
39
38
|
"devDependencies": {
|
40
39
|
"@parcel/config-webextension": "^2.11.0",
|
41
40
|
"@sindresorhus/tsconfig": "^7.0.0",
|
42
|
-
"@types/chrome": "^0.0.
|
43
|
-
"@types/tape": "^5.8.
|
44
|
-
"@types/webextension-polyfill": "^0.12.1",
|
41
|
+
"@types/chrome": "^0.0.307",
|
42
|
+
"@types/tape": "^5.8.1",
|
45
43
|
"buffer": "^6.0.3",
|
46
44
|
"eslint": "^8.57.0",
|
47
45
|
"eslint-config-pixiebrix": "^0.41.1",
|
@@ -52,18 +50,19 @@
|
|
52
50
|
"process": "^0.11.10",
|
53
51
|
"stream-browserify": "^3.0.0",
|
54
52
|
"tape": "^5.9.0",
|
55
|
-
"typescript": "^5.
|
56
|
-
"vitest": "^
|
57
|
-
"webext-content-scripts": "^2.7.0",
|
58
|
-
"webextension-polyfill": "^0.12.0"
|
53
|
+
"typescript": "^5.8.2",
|
54
|
+
"vitest": "^3.0.7"
|
59
55
|
},
|
60
56
|
"targets": {
|
61
57
|
"main": false,
|
62
|
-
"
|
58
|
+
"external": {
|
59
|
+
"source": "source/test/external.html"
|
60
|
+
},
|
61
|
+
"extension": {
|
62
|
+
"source": "source/test/manifest.json",
|
63
63
|
"engines": {
|
64
64
|
"browsers": "Chrome 110"
|
65
65
|
},
|
66
|
-
"source": "source/test/manifest.json",
|
67
66
|
"sourceMap": {
|
68
67
|
"inline": true
|
69
68
|
}
|
@@ -75,7 +74,8 @@
|
|
75
74
|
"startUrl": [
|
76
75
|
"https://fregante.github.io/pixiebrix-testing-ground/Will-call-background-methods",
|
77
76
|
"https://fregante.github.io/pixiebrix-testing-ground/Will-call-other-CS-via-background",
|
78
|
-
"https://fregante.github.io/pixiebrix-testing-ground/Will-call-offscreen-methods"
|
77
|
+
"https://fregante.github.io/pixiebrix-testing-ground/Will-call-offscreen-methods",
|
78
|
+
"http://localhost:1234/external.html"
|
79
79
|
]
|
80
80
|
}
|
81
81
|
}
|