@wordbricks/playwright-mcp 0.1.25 → 0.1.27
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/lib/browserContextFactory.js +616 -0
- package/lib/browserServerBackend.js +86 -0
- package/lib/config.js +302 -0
- package/lib/context.js +320 -0
- package/lib/extension/cdpRelay.js +352 -0
- package/lib/extension/extensionContextFactory.js +56 -0
- package/lib/frameworkPatterns.js +35 -0
- package/lib/hooks/antiBotDetectionHook.js +178 -0
- package/lib/hooks/core.js +145 -0
- package/lib/hooks/eventConsumer.js +52 -0
- package/lib/hooks/events.js +42 -0
- package/lib/hooks/formatToolCallEvent.js +12 -0
- package/lib/hooks/frameworkStateHook.js +182 -0
- package/lib/hooks/grouping.js +72 -0
- package/lib/hooks/jsonLdDetectionHook.js +182 -0
- package/lib/hooks/networkFilters.js +82 -0
- package/lib/hooks/networkSetup.js +61 -0
- package/lib/hooks/networkTrackingHook.js +67 -0
- package/lib/hooks/pageHeightHook.js +75 -0
- package/lib/hooks/registry.js +41 -0
- package/lib/hooks/requireTabHook.js +26 -0
- package/lib/hooks/schema.js +89 -0
- package/lib/hooks/waitHook.js +33 -0
- package/lib/index.js +41 -0
- package/lib/mcp/inProcessTransport.js +71 -0
- package/lib/mcp/proxyBackend.js +130 -0
- package/lib/mcp/server.js +91 -0
- package/lib/mcp/tool.js +44 -0
- package/lib/mcp/transport.js +188 -0
- package/lib/playwrightTransformer.js +520 -0
- package/lib/program.js +112 -0
- package/lib/response.js +192 -0
- package/lib/sessionLog.js +123 -0
- package/lib/tab.js +251 -0
- package/lib/tools/common.js +55 -0
- package/lib/tools/console.js +33 -0
- package/lib/tools/dialogs.js +50 -0
- package/lib/tools/evaluate.js +62 -0
- package/lib/tools/extractFrameworkState.js +225 -0
- package/lib/tools/files.js +48 -0
- package/lib/tools/form.js +66 -0
- package/lib/tools/getSnapshot.js +36 -0
- package/lib/tools/getVisibleHtml.js +68 -0
- package/lib/tools/install.js +51 -0
- package/lib/tools/keyboard.js +83 -0
- package/lib/tools/mouse.js +97 -0
- package/lib/tools/navigate.js +66 -0
- package/lib/tools/network.js +121 -0
- package/lib/tools/networkDetail.js +238 -0
- package/lib/tools/networkSearch/bodySearch.js +161 -0
- package/lib/tools/networkSearch/grouping.js +37 -0
- package/lib/tools/networkSearch/helpers.js +32 -0
- package/lib/tools/networkSearch/searchHtml.js +76 -0
- package/lib/tools/networkSearch/types.js +1 -0
- package/lib/tools/networkSearch/urlSearch.js +124 -0
- package/lib/tools/networkSearch.js +278 -0
- package/lib/tools/pdf.js +41 -0
- package/lib/tools/repl.js +414 -0
- package/lib/tools/screenshot.js +103 -0
- package/lib/tools/scroll.js +131 -0
- package/lib/tools/snapshot.js +161 -0
- package/lib/tools/tabs.js +62 -0
- package/lib/tools/tool.js +35 -0
- package/lib/tools/utils.js +78 -0
- package/lib/tools/wait.js +60 -0
- package/lib/tools.js +68 -0
- package/lib/utils/adBlockFilter.js +90 -0
- package/lib/utils/codegen.js +55 -0
- package/lib/utils/extensionPath.js +10 -0
- package/lib/utils/fileUtils.js +40 -0
- package/lib/utils/graphql.js +269 -0
- package/lib/utils/guid.js +22 -0
- package/lib/utils/httpServer.js +39 -0
- package/lib/utils/log.js +21 -0
- package/lib/utils/manualPromise.js +111 -0
- package/lib/utils/networkFormat.js +14 -0
- package/lib/utils/package.js +20 -0
- package/lib/utils/result.js +2 -0
- package/lib/utils/sanitizeHtml.js +130 -0
- package/lib/utils/truncate.js +103 -0
- package/lib/utils/withTimeout.js +7 -0
- package/package.json +11 -1
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { filter, join, pipe } from "@fxts/core";
|
|
2
|
+
const getString = (v) => typeof v === "string" ? v : undefined;
|
|
3
|
+
const hasPersistedQuery = (v) => {
|
|
4
|
+
return isPlainObject(v) && "persistedQuery" in v;
|
|
5
|
+
};
|
|
6
|
+
const readGraphQLishBody = (v) => {
|
|
7
|
+
if (!isPlainObject(v))
|
|
8
|
+
return undefined;
|
|
9
|
+
const q = getString(v["query"]);
|
|
10
|
+
const variables = v["variables"];
|
|
11
|
+
const operationName = getString(v["operationName"]);
|
|
12
|
+
const hasPersisted = hasPersistedQuery(v["extensions"]);
|
|
13
|
+
if (!q && !hasPersisted && !operationName)
|
|
14
|
+
return undefined;
|
|
15
|
+
return { query: q, variables, operationName, hasPersisted };
|
|
16
|
+
};
|
|
17
|
+
export const isPlainObject = (value) => {
|
|
18
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
19
|
+
};
|
|
20
|
+
function getHeader(headers, name, altName) {
|
|
21
|
+
if (!headers)
|
|
22
|
+
return undefined;
|
|
23
|
+
const v1 = headers[name];
|
|
24
|
+
if (typeof v1 === "string")
|
|
25
|
+
return v1;
|
|
26
|
+
if (Array.isArray(v1))
|
|
27
|
+
return v1.join(", ");
|
|
28
|
+
if (altName) {
|
|
29
|
+
const v2 = headers[altName];
|
|
30
|
+
if (typeof v2 === "string")
|
|
31
|
+
return v2;
|
|
32
|
+
if (Array.isArray(v2))
|
|
33
|
+
return v2.join(", ");
|
|
34
|
+
}
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
export const safeJSONParse = (text) => {
|
|
38
|
+
if (!text)
|
|
39
|
+
return undefined;
|
|
40
|
+
try {
|
|
41
|
+
return JSON.parse(text);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
const isOperationType = (s) => {
|
|
48
|
+
return s === "query" || s === "mutation" || s === "subscription";
|
|
49
|
+
};
|
|
50
|
+
const parseOperationFromQuery = (query) => {
|
|
51
|
+
if (!query)
|
|
52
|
+
return { type: "unknown" };
|
|
53
|
+
const trimmed = query.trim().replace(/^#.*$/gm, "").trim();
|
|
54
|
+
if (!trimmed)
|
|
55
|
+
return { type: "unknown" };
|
|
56
|
+
if (trimmed.startsWith("{"))
|
|
57
|
+
return { type: "query" };
|
|
58
|
+
const m = /^(query|mutation|subscription)\b\s*([A-Za-z_][A-Za-z0-9_]*)?/i.exec(trimmed);
|
|
59
|
+
if (m) {
|
|
60
|
+
const maybe = m[1].toLowerCase();
|
|
61
|
+
const type = isOperationType(maybe)
|
|
62
|
+
? maybe
|
|
63
|
+
: "unknown";
|
|
64
|
+
const name = m[2] || undefined;
|
|
65
|
+
return { type, name };
|
|
66
|
+
}
|
|
67
|
+
return { type: "unknown" };
|
|
68
|
+
};
|
|
69
|
+
export const parseGraphQLRequestFromHttp = (method, url, headersIn, bodyText) => {
|
|
70
|
+
const contentType = getHeader(headersIn, "content-type", "Content-Type") || "";
|
|
71
|
+
const u = new URL(url, "http://dummy"); // base to satisfy URL if relative
|
|
72
|
+
let query;
|
|
73
|
+
let variables;
|
|
74
|
+
let operationName;
|
|
75
|
+
let isPersistedQuery = false;
|
|
76
|
+
if ((method || "GET").toUpperCase() === "GET") {
|
|
77
|
+
const params = u.searchParams;
|
|
78
|
+
const queryParam = params.get("query");
|
|
79
|
+
const opNameParam = params.get("operationName") ?? undefined;
|
|
80
|
+
const vars = params.get("variables");
|
|
81
|
+
const ext = params.get("extensions");
|
|
82
|
+
const extObj = ext ? safeJSONParse(ext) : undefined;
|
|
83
|
+
const hasPersisted = isPlainObject(extObj) && "persistedQuery" in extObj;
|
|
84
|
+
if (queryParam) {
|
|
85
|
+
// Only treat as GraphQL if the query actually looks like GraphQL
|
|
86
|
+
const parsedOp = parseOperationFromQuery(queryParam);
|
|
87
|
+
if (parsedOp.type !== "unknown") {
|
|
88
|
+
query = queryParam;
|
|
89
|
+
operationName = opNameParam;
|
|
90
|
+
if (vars)
|
|
91
|
+
variables = safeJSONParse(vars);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
else if (hasPersisted) {
|
|
95
|
+
// Accept persisted queries even without a `query` param
|
|
96
|
+
isPersistedQuery = true;
|
|
97
|
+
operationName = opNameParam;
|
|
98
|
+
if (vars)
|
|
99
|
+
variables = safeJSONParse(vars);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
// POST/others
|
|
104
|
+
if (contentType.includes("application/json") ||
|
|
105
|
+
contentType.includes("application/graphql") ||
|
|
106
|
+
contentType.includes("application/x-www-form-urlencoded") ||
|
|
107
|
+
typeof bodyText === "string") {
|
|
108
|
+
const bodyObj = safeJSONParse(bodyText || "");
|
|
109
|
+
const parsed = readGraphQLishBody(bodyObj);
|
|
110
|
+
if (parsed) {
|
|
111
|
+
if (parsed.query) {
|
|
112
|
+
const parsedOp = parseOperationFromQuery(parsed.query);
|
|
113
|
+
if (parsedOp.type !== "unknown") {
|
|
114
|
+
query = parsed.query;
|
|
115
|
+
variables = parsed.variables;
|
|
116
|
+
operationName = parsed.operationName;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
else if (parsed.hasPersisted) {
|
|
120
|
+
isPersistedQuery = true;
|
|
121
|
+
variables = parsed.variables;
|
|
122
|
+
operationName = parsed.operationName;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (!query && !isPersistedQuery)
|
|
128
|
+
return undefined;
|
|
129
|
+
const { type: operationType, name } = parseOperationFromQuery(query);
|
|
130
|
+
if (operationName && !name) {
|
|
131
|
+
// Respect provided operationName when query is anonymous
|
|
132
|
+
return { operationType, operationName, query, variables, isPersistedQuery };
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
operationType,
|
|
136
|
+
operationName: name ?? operationName,
|
|
137
|
+
query,
|
|
138
|
+
variables,
|
|
139
|
+
isPersistedQuery,
|
|
140
|
+
};
|
|
141
|
+
};
|
|
142
|
+
export const summarizeGraphQL = (parsed) => {
|
|
143
|
+
const type = parsed.operationType === "unknown" ? "operation" : parsed.operationType;
|
|
144
|
+
return pipe([parsed.operationName ? `${type} ${parsed.operationName}` : type], filter((v) => typeof v === "string"), join(" "));
|
|
145
|
+
};
|
|
146
|
+
export const extractGraphQLResponseInfo = (bodyText) => {
|
|
147
|
+
const json = safeJSONParse(bodyText || "");
|
|
148
|
+
if (!isPlainObject(json))
|
|
149
|
+
return undefined;
|
|
150
|
+
const errorsVal = json["errors"];
|
|
151
|
+
const dataVal = json["data"];
|
|
152
|
+
const hasGraphQLShape = Array.isArray(errorsVal) || isPlainObject(dataVal);
|
|
153
|
+
if (!hasGraphQLShape)
|
|
154
|
+
return undefined;
|
|
155
|
+
const errors = Array.isArray(errorsVal) ? errorsVal : [];
|
|
156
|
+
const dataKeys = isPlainObject(dataVal) ? Object.keys(dataVal) : [];
|
|
157
|
+
const topMessages = errors.slice(0, 3).map((e) => {
|
|
158
|
+
if (isPlainObject(e)) {
|
|
159
|
+
const m = e["message"];
|
|
160
|
+
if (typeof m === "string")
|
|
161
|
+
return m;
|
|
162
|
+
}
|
|
163
|
+
try {
|
|
164
|
+
return JSON.stringify(e);
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return String(e);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
return {
|
|
171
|
+
hasErrors: errors.length > 0,
|
|
172
|
+
errorCount: errors.length,
|
|
173
|
+
topMessages,
|
|
174
|
+
dataKeys,
|
|
175
|
+
};
|
|
176
|
+
};
|
|
177
|
+
/**
|
|
178
|
+
* Minify a GraphQL operation string by removing comments and unnecessary whitespace,
|
|
179
|
+
* while preserving string literals and required token separators.
|
|
180
|
+
*/
|
|
181
|
+
export const minifyGraphQLQuery = (input) => {
|
|
182
|
+
let out = "";
|
|
183
|
+
let i = 0;
|
|
184
|
+
const len = input.length;
|
|
185
|
+
let inString = null;
|
|
186
|
+
let escaped = false;
|
|
187
|
+
let pendingSpace = false;
|
|
188
|
+
const isIdent = (ch) => /[A-Za-z0-9_]/.test(ch);
|
|
189
|
+
const isWhitespace = (ch) => ch === " " || ch === "\n" || ch === "\r" || ch === "\t" || ch === "\f";
|
|
190
|
+
const isPunct = (ch) => "{}()[]:!@$,=|&.".includes(ch);
|
|
191
|
+
while (i < len) {
|
|
192
|
+
const ch = input[i];
|
|
193
|
+
if (inString) {
|
|
194
|
+
out += ch;
|
|
195
|
+
if (escaped)
|
|
196
|
+
escaped = false;
|
|
197
|
+
else if (ch === "\\")
|
|
198
|
+
escaped = true;
|
|
199
|
+
else if (ch === inString)
|
|
200
|
+
inString = null;
|
|
201
|
+
i++;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
// Not inside string
|
|
205
|
+
if (ch === '"' || ch === "'") {
|
|
206
|
+
// flush pending space
|
|
207
|
+
if (pendingSpace) {
|
|
208
|
+
out += " ";
|
|
209
|
+
pendingSpace = false;
|
|
210
|
+
}
|
|
211
|
+
inString = ch;
|
|
212
|
+
out += ch;
|
|
213
|
+
i++;
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
// Line comment start (# ... end of line)
|
|
217
|
+
if (ch === "#") {
|
|
218
|
+
// Skip until end of line
|
|
219
|
+
while (i < len && input[i] !== "\n")
|
|
220
|
+
i++;
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
// Whitespace handling
|
|
224
|
+
if (isWhitespace(ch)) {
|
|
225
|
+
// Defer deciding about a space until we see the next significant char
|
|
226
|
+
pendingSpace = true;
|
|
227
|
+
i++;
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
// Punctuation: never needs surrounding spaces
|
|
231
|
+
if (isPunct(ch)) {
|
|
232
|
+
out += ch;
|
|
233
|
+
pendingSpace = false;
|
|
234
|
+
i++;
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
// ch is part of an identifier/number or other token
|
|
238
|
+
if (pendingSpace) {
|
|
239
|
+
// Add a space only if the previous output char and current char both look like identifiers
|
|
240
|
+
const prev = out[out.length - 1] || "";
|
|
241
|
+
if (isIdent(prev) && isIdent(ch))
|
|
242
|
+
out += " ";
|
|
243
|
+
pendingSpace = false;
|
|
244
|
+
}
|
|
245
|
+
out += ch;
|
|
246
|
+
i++;
|
|
247
|
+
}
|
|
248
|
+
return out.trim();
|
|
249
|
+
};
|
|
250
|
+
/**
|
|
251
|
+
* Recursively minify GraphQL query strings within a typical GraphQL request body.
|
|
252
|
+
* - If body is an object or array, it returns a new value with any `query` string minimized.
|
|
253
|
+
* - Other fields are preserved as-is.
|
|
254
|
+
*/
|
|
255
|
+
export const minifyGraphQLRequestBody = (body) => {
|
|
256
|
+
if (Array.isArray(body))
|
|
257
|
+
return body.map((item) => minifyGraphQLRequestBody(item));
|
|
258
|
+
if (isPlainObject(body)) {
|
|
259
|
+
const out = {};
|
|
260
|
+
for (const [k, v] of Object.entries(body)) {
|
|
261
|
+
if (k === "query" && typeof v === "string")
|
|
262
|
+
out[k] = minifyGraphQLQuery(v);
|
|
263
|
+
else
|
|
264
|
+
out[k] = minifyGraphQLRequestBody(v);
|
|
265
|
+
}
|
|
266
|
+
return out;
|
|
267
|
+
}
|
|
268
|
+
return body;
|
|
269
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Microsoft Corporation.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
import crypto from "crypto";
|
|
17
|
+
export function createGuid() {
|
|
18
|
+
return crypto.randomBytes(16).toString("hex");
|
|
19
|
+
}
|
|
20
|
+
export function createHash(data) {
|
|
21
|
+
return crypto.createHash("sha256").update(data).digest("hex").slice(0, 7);
|
|
22
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Microsoft Corporation.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
import assert from "assert";
|
|
17
|
+
import http from "http";
|
|
18
|
+
export async function startHttpServer(config) {
|
|
19
|
+
const { host, port } = config;
|
|
20
|
+
const httpServer = http.createServer();
|
|
21
|
+
await new Promise((resolve, reject) => {
|
|
22
|
+
httpServer.on("error", reject);
|
|
23
|
+
httpServer.listen(port, host, () => {
|
|
24
|
+
resolve();
|
|
25
|
+
httpServer.removeListener("error", reject);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
return httpServer;
|
|
29
|
+
}
|
|
30
|
+
export function httpAddressToString(address) {
|
|
31
|
+
assert(address, "Could not bind server socket");
|
|
32
|
+
if (typeof address === "string")
|
|
33
|
+
return address;
|
|
34
|
+
const resolvedPort = address.port;
|
|
35
|
+
let resolvedHost = address.family === "IPv4" ? address.address : `[${address.address}]`;
|
|
36
|
+
if (resolvedHost === "0.0.0.0" || resolvedHost === "[::]")
|
|
37
|
+
resolvedHost = "localhost";
|
|
38
|
+
return `http://${resolvedHost}:${resolvedPort}`;
|
|
39
|
+
}
|
package/lib/utils/log.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Microsoft Corporation.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
import debug from "debug";
|
|
17
|
+
const errorsDebug = debug("pw:mcp:errors");
|
|
18
|
+
export function logUnhandledError(error) {
|
|
19
|
+
errorsDebug(error);
|
|
20
|
+
}
|
|
21
|
+
export const testDebug = debug("pw:mcp:test");
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Microsoft Corporation.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
export class ManualPromise extends Promise {
|
|
17
|
+
_resolve;
|
|
18
|
+
_reject;
|
|
19
|
+
_isDone;
|
|
20
|
+
constructor() {
|
|
21
|
+
let resolve;
|
|
22
|
+
let reject;
|
|
23
|
+
super((f, r) => {
|
|
24
|
+
resolve = f;
|
|
25
|
+
reject = r;
|
|
26
|
+
});
|
|
27
|
+
this._isDone = false;
|
|
28
|
+
this._resolve = resolve;
|
|
29
|
+
this._reject = reject;
|
|
30
|
+
}
|
|
31
|
+
isDone() {
|
|
32
|
+
return this._isDone;
|
|
33
|
+
}
|
|
34
|
+
resolve(t) {
|
|
35
|
+
this._isDone = true;
|
|
36
|
+
this._resolve(t);
|
|
37
|
+
}
|
|
38
|
+
reject(e) {
|
|
39
|
+
this._isDone = true;
|
|
40
|
+
this._reject(e);
|
|
41
|
+
}
|
|
42
|
+
static get [Symbol.species]() {
|
|
43
|
+
return Promise;
|
|
44
|
+
}
|
|
45
|
+
get [Symbol.toStringTag]() {
|
|
46
|
+
return "ManualPromise";
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export class LongStandingScope {
|
|
50
|
+
_terminateError;
|
|
51
|
+
_closeError;
|
|
52
|
+
_terminatePromises = new Map();
|
|
53
|
+
_isClosed = false;
|
|
54
|
+
reject(error) {
|
|
55
|
+
this._isClosed = true;
|
|
56
|
+
this._terminateError = error;
|
|
57
|
+
for (const p of this._terminatePromises.keys())
|
|
58
|
+
p.resolve(error);
|
|
59
|
+
}
|
|
60
|
+
close(error) {
|
|
61
|
+
this._isClosed = true;
|
|
62
|
+
this._closeError = error;
|
|
63
|
+
for (const [p, frames] of this._terminatePromises)
|
|
64
|
+
p.resolve(cloneError(error, frames));
|
|
65
|
+
}
|
|
66
|
+
isClosed() {
|
|
67
|
+
return this._isClosed;
|
|
68
|
+
}
|
|
69
|
+
static async raceMultiple(scopes, promise) {
|
|
70
|
+
return Promise.race(scopes.map((s) => s.race(promise)));
|
|
71
|
+
}
|
|
72
|
+
async race(promise) {
|
|
73
|
+
return this._race(Array.isArray(promise) ? promise : [promise], false);
|
|
74
|
+
}
|
|
75
|
+
async safeRace(promise, defaultValue) {
|
|
76
|
+
return this._race([promise], true, defaultValue);
|
|
77
|
+
}
|
|
78
|
+
async _race(promises, safe, defaultValue) {
|
|
79
|
+
const terminatePromise = new ManualPromise();
|
|
80
|
+
const frames = captureRawStack();
|
|
81
|
+
if (this._terminateError)
|
|
82
|
+
terminatePromise.resolve(this._terminateError);
|
|
83
|
+
if (this._closeError)
|
|
84
|
+
terminatePromise.resolve(cloneError(this._closeError, frames));
|
|
85
|
+
this._terminatePromises.set(terminatePromise, frames);
|
|
86
|
+
try {
|
|
87
|
+
return await Promise.race([
|
|
88
|
+
terminatePromise.then((e) => (safe ? defaultValue : Promise.reject(e))),
|
|
89
|
+
...promises,
|
|
90
|
+
]);
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
this._terminatePromises.delete(terminatePromise);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function cloneError(error, frames) {
|
|
98
|
+
const clone = new Error();
|
|
99
|
+
clone.name = error.name;
|
|
100
|
+
clone.message = error.message;
|
|
101
|
+
clone.stack = [error.name + ":" + error.message, ...frames].join("\n");
|
|
102
|
+
return clone;
|
|
103
|
+
}
|
|
104
|
+
function captureRawStack() {
|
|
105
|
+
const stackTraceLimit = Error.stackTraceLimit;
|
|
106
|
+
Error.stackTraceLimit = 50;
|
|
107
|
+
const error = new Error();
|
|
108
|
+
const stack = error.stack || "";
|
|
109
|
+
Error.stackTraceLimit = stackTraceLimit;
|
|
110
|
+
return stack.split("\n");
|
|
111
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { formatUrlWithTrimmedParams } from "../hooks/networkFilters.js";
|
|
2
|
+
import { parseGraphQLRequestFromHttp, summarizeGraphQL } from "./graphql.js";
|
|
3
|
+
export const formatNetworkSummaryLine = (input, opts) => {
|
|
4
|
+
const method = (input.method || "").toUpperCase();
|
|
5
|
+
const formattedUrl = opts?.trimParams === false
|
|
6
|
+
? input.url
|
|
7
|
+
: formatUrlWithTrimmedParams(input.url);
|
|
8
|
+
const st = input.statusText ? ` ${input.statusText}` : "";
|
|
9
|
+
let line = `${method} ${formattedUrl} → ${input.status}${st}`;
|
|
10
|
+
const gql = parseGraphQLRequestFromHttp(input.method, input.url, input.headers || {}, input.postData ?? undefined);
|
|
11
|
+
if (gql)
|
|
12
|
+
line += ` [GraphQL: ${summarizeGraphQL(gql)}]`;
|
|
13
|
+
return line;
|
|
14
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Microsoft Corporation.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
import fs from "fs";
|
|
17
|
+
import path from "path";
|
|
18
|
+
import url from "url";
|
|
19
|
+
const __filename = url.fileURLToPath(import.meta.url);
|
|
20
|
+
export const packageJSON = JSON.parse(fs.readFileSync(path.join(path.dirname(__filename), "..", "..", "package.json"), "utf8"));
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { pipe, when } from "@fxts/core";
|
|
2
|
+
import * as cheerio from "cheerio";
|
|
3
|
+
import { ElementType } from "domelementtype";
|
|
4
|
+
const I18N_PATTERNS = [
|
|
5
|
+
/window\.__i18n\s*=/i,
|
|
6
|
+
/window\.__translations\s*=/i,
|
|
7
|
+
/window\.i18n\s*=/i,
|
|
8
|
+
/window\._translations\s*=/i,
|
|
9
|
+
/\btranslations\s*:\s*{/i,
|
|
10
|
+
/\blocales\s*:\s*{/i,
|
|
11
|
+
/\bmessages\s*:\s*{/i,
|
|
12
|
+
/\bi18nData\s*=/i,
|
|
13
|
+
/\btranslationData\s*=/i,
|
|
14
|
+
];
|
|
15
|
+
export const isI18nScript = (content) => {
|
|
16
|
+
const text = content || "";
|
|
17
|
+
const hasI18nPattern = I18N_PATTERNS.some((pattern) => pattern.test(text));
|
|
18
|
+
const langCodePattern = /["'](?:[a-z]{2}(?:[_-][A-Z]{2})?|zh-(?:CN|TW|HK)|pt-BR|en-(?:US|GB|CA|AU)|es-(?:ES|MX|AR)|fr-(?:FR|CA)|de-(?:DE|AT|CH))["']\s*:\s*{/g;
|
|
19
|
+
const matches = text.match(langCodePattern) || [];
|
|
20
|
+
const hasLangCodeStructure = matches.length >= 2;
|
|
21
|
+
return hasI18nPattern || hasLangCodeStructure;
|
|
22
|
+
};
|
|
23
|
+
export const removeI18nScripts = ($) => {
|
|
24
|
+
$("script").each((_, el) => {
|
|
25
|
+
const content = $(el).html() || "";
|
|
26
|
+
if (isI18nScript(content))
|
|
27
|
+
$(el).remove();
|
|
28
|
+
});
|
|
29
|
+
return $;
|
|
30
|
+
};
|
|
31
|
+
export const removeScriptTags = ($) => {
|
|
32
|
+
$("script").remove();
|
|
33
|
+
return $;
|
|
34
|
+
};
|
|
35
|
+
export const removeStyleTags = ($) => {
|
|
36
|
+
$("style").remove();
|
|
37
|
+
return $;
|
|
38
|
+
};
|
|
39
|
+
export const NON_ESSENTIAL_RELS = [
|
|
40
|
+
"stylesheet",
|
|
41
|
+
"preload",
|
|
42
|
+
"prefetch",
|
|
43
|
+
"preconnect",
|
|
44
|
+
"dns-prefetch",
|
|
45
|
+
"modulepreload",
|
|
46
|
+
"icon",
|
|
47
|
+
"shortcut icon",
|
|
48
|
+
"apple-touch-icon",
|
|
49
|
+
"apple-touch-icon-precomposed",
|
|
50
|
+
"manifest",
|
|
51
|
+
"pingback",
|
|
52
|
+
"prerender",
|
|
53
|
+
"subresource",
|
|
54
|
+
];
|
|
55
|
+
export const removeNonEssentialLinks = ($) => {
|
|
56
|
+
$("link").each((_, el) => {
|
|
57
|
+
const rel = ($(el).attr("rel") || "").toLowerCase();
|
|
58
|
+
const type = ($(el).attr("type") || "").toLowerCase();
|
|
59
|
+
if (type === "text/css") {
|
|
60
|
+
$(el).remove();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (rel) {
|
|
64
|
+
const relValues = rel.split(/\s+/);
|
|
65
|
+
const hasNonEssential = relValues.some((value) => NON_ESSENTIAL_RELS.includes(value) ||
|
|
66
|
+
(value === "shortcut" && relValues.includes("icon")));
|
|
67
|
+
if (hasNonEssential)
|
|
68
|
+
$(el).remove();
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
return $;
|
|
72
|
+
};
|
|
73
|
+
export const removeMetaTags = ($) => {
|
|
74
|
+
$("meta").remove();
|
|
75
|
+
return $;
|
|
76
|
+
};
|
|
77
|
+
export const removeHtmlComments = ($) => {
|
|
78
|
+
$("*")
|
|
79
|
+
.contents()
|
|
80
|
+
.each((_, node) => {
|
|
81
|
+
if (node.type === ElementType.Comment)
|
|
82
|
+
$(node).remove();
|
|
83
|
+
});
|
|
84
|
+
return $;
|
|
85
|
+
};
|
|
86
|
+
export const SVG_ATTRIBUTES_TO_REMOVE = [
|
|
87
|
+
"xmlns",
|
|
88
|
+
"d",
|
|
89
|
+
"fill",
|
|
90
|
+
"stroke",
|
|
91
|
+
"stroke-width",
|
|
92
|
+
"stroke-linecap",
|
|
93
|
+
"stroke-linejoin",
|
|
94
|
+
"stroke-dasharray",
|
|
95
|
+
"opacity",
|
|
96
|
+
"fill-opacity",
|
|
97
|
+
"stroke-opacity",
|
|
98
|
+
"transform",
|
|
99
|
+
"rotate",
|
|
100
|
+
"scale",
|
|
101
|
+
"translate",
|
|
102
|
+
"filter",
|
|
103
|
+
"mask",
|
|
104
|
+
"clip-path",
|
|
105
|
+
"paint-order",
|
|
106
|
+
"vector-effect",
|
|
107
|
+
"shape-rendering",
|
|
108
|
+
"gradientUnits",
|
|
109
|
+
"gradientTransform",
|
|
110
|
+
"patternUnits",
|
|
111
|
+
"patternTransform",
|
|
112
|
+
"marker-start",
|
|
113
|
+
"marker-mid",
|
|
114
|
+
"marker-end",
|
|
115
|
+
"style",
|
|
116
|
+
];
|
|
117
|
+
export const stripSvgAttributes = ($) => {
|
|
118
|
+
$("svg, svg *").each((_, el) => {
|
|
119
|
+
SVG_ATTRIBUTES_TO_REMOVE.forEach((attr) => {
|
|
120
|
+
$(el).removeAttr(attr);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
return $;
|
|
124
|
+
};
|
|
125
|
+
export const minifyHtml = (html) => html.replace(/>\s+</g, "><").trim();
|
|
126
|
+
export const sanitizeHtml = (html, options) => {
|
|
127
|
+
if (!options.shouldRemoveScripts && !options.shouldRemoveStyles)
|
|
128
|
+
return html;
|
|
129
|
+
return pipe(cheerio.load(html, { xmlMode: false }), when(() => !options.shouldRemoveScripts, removeI18nScripts), when(() => options.shouldRemoveScripts, removeScriptTags), when(() => options.shouldRemoveStyles, removeStyleTags), removeNonEssentialLinks, removeMetaTags, removeHtmlComments, stripSvgAttributes, ($) => minifyHtml($.root().html() || ""));
|
|
130
|
+
};
|