@wordbricks/playwright-mcp 0.1.19 → 0.1.22
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/README.md +54 -44
- package/cli-wrapper.js +15 -14
- package/cli.js +1 -1
- package/config.d.ts +11 -6
- package/index.d.ts +7 -5
- package/index.js +1 -1
- package/lib/browserContextFactory.js +131 -58
- package/lib/browserServerBackend.js +14 -12
- package/lib/config.js +60 -46
- package/lib/context.js +41 -39
- package/lib/extension/cdpRelay.js +67 -61
- package/lib/extension/extensionContextFactory.js +10 -10
- package/lib/frameworkPatterns.js +21 -21
- package/lib/hooks/antiBotDetectionHook.js +178 -0
- package/lib/hooks/core.js +11 -10
- package/lib/hooks/eventConsumer.js +29 -16
- package/lib/hooks/events.js +3 -3
- package/lib/hooks/formatToolCallEvent.js +3 -7
- package/lib/hooks/frameworkStateHook.js +40 -40
- package/lib/hooks/grouping.js +3 -3
- package/lib/hooks/jsonLdDetectionHook.js +44 -37
- package/lib/hooks/networkFilters.js +24 -15
- package/lib/hooks/networkSetup.js +11 -6
- package/lib/hooks/networkTrackingHook.js +31 -19
- package/lib/hooks/pageHeightHook.js +9 -9
- package/lib/hooks/registry.js +18 -16
- package/lib/hooks/requireTabHook.js +3 -3
- package/lib/hooks/schema.js +44 -32
- package/lib/hooks/waitHook.js +7 -7
- package/lib/index.js +12 -10
- package/lib/mcp/inProcessTransport.js +3 -4
- package/lib/mcp/proxyBackend.js +43 -28
- package/lib/mcp/server.js +24 -19
- package/lib/mcp/tool.js +14 -8
- package/lib/mcp/transport.js +60 -53
- package/lib/playwrightTransformer.js +129 -106
- package/lib/program.js +54 -52
- package/lib/response.js +36 -30
- package/lib/sessionLog.js +19 -17
- package/lib/tab.js +41 -39
- package/lib/tools/common.js +19 -19
- package/lib/tools/console.js +11 -11
- package/lib/tools/dialogs.js +18 -15
- package/lib/tools/evaluate.js +26 -17
- package/lib/tools/extractFrameworkState.js +48 -37
- package/lib/tools/files.js +17 -14
- package/lib/tools/form.js +32 -23
- package/lib/tools/getSnapshot.js +14 -15
- package/lib/tools/getVisibleHtml.js +33 -17
- package/lib/tools/install.js +20 -20
- package/lib/tools/keyboard.js +29 -24
- package/lib/tools/mouse.js +29 -31
- package/lib/tools/navigate.js +19 -23
- package/lib/tools/network.js +12 -14
- package/lib/tools/networkDetail.js +68 -61
- package/lib/tools/networkSearch/bodySearch.js +46 -32
- package/lib/tools/networkSearch/grouping.js +15 -6
- package/lib/tools/networkSearch/helpers.js +4 -4
- package/lib/tools/networkSearch/searchHtml.js +25 -16
- package/lib/tools/networkSearch/urlSearch.js +56 -14
- package/lib/tools/networkSearch.js +65 -35
- package/lib/tools/pdf.js +13 -12
- package/lib/tools/repl.js +66 -54
- package/lib/tools/screenshot.js +57 -33
- package/lib/tools/scroll.js +29 -24
- package/lib/tools/snapshot.js +66 -49
- package/lib/tools/tabs.js +22 -19
- package/lib/tools/tool.js +5 -3
- package/lib/tools/utils.js +17 -13
- package/lib/tools/wait.js +24 -19
- package/lib/tools.js +21 -20
- package/lib/utils/adBlockFilter.js +29 -26
- package/lib/utils/codegen.js +20 -16
- package/lib/utils/extensionPath.js +4 -4
- package/lib/utils/fileUtils.js +17 -13
- package/lib/utils/graphql.js +69 -58
- package/lib/utils/guid.js +3 -3
- package/lib/utils/httpServer.js +9 -9
- package/lib/utils/log.js +3 -3
- package/lib/utils/manualPromise.js +7 -7
- package/lib/utils/networkFormat.js +7 -5
- package/lib/utils/package.js +4 -4
- package/lib/utils/sanitizeHtml.js +66 -34
- package/lib/utils/truncate.js +25 -25
- package/lib/utils/withTimeout.js +1 -1
- package/package.json +34 -57
- package/src/index.ts +27 -17
- package/LICENSE +0 -202
package/lib/utils/fileUtils.js
CHANGED
|
@@ -13,24 +13,28 @@
|
|
|
13
13
|
* See the License for the specific language governing permissions and
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
|
-
import os from
|
|
17
|
-
import path from
|
|
16
|
+
import os from "node:os";
|
|
17
|
+
import path from "node:path";
|
|
18
18
|
export function cacheDir() {
|
|
19
19
|
let cacheDirectory;
|
|
20
|
-
if (process.platform ===
|
|
21
|
-
cacheDirectory =
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
if (process.platform === "linux")
|
|
21
|
+
cacheDirectory =
|
|
22
|
+
process.env.XDG_CACHE_HOME || path.join(os.homedir(), ".cache");
|
|
23
|
+
else if (process.platform === "darwin")
|
|
24
|
+
cacheDirectory = path.join(os.homedir(), "Library", "Caches");
|
|
25
|
+
else if (process.platform === "win32")
|
|
26
|
+
cacheDirectory =
|
|
27
|
+
process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local");
|
|
26
28
|
else
|
|
27
|
-
throw new Error(
|
|
28
|
-
return path.join(cacheDirectory,
|
|
29
|
+
throw new Error("Unsupported platform: " + process.platform);
|
|
30
|
+
return path.join(cacheDirectory, "ms-playwright");
|
|
29
31
|
}
|
|
30
32
|
export function sanitizeForFilePath(s) {
|
|
31
|
-
const sanitize = (s) => s.replace(/[\x00-\x2C\x2E-\x2F\x3A-\x40\x5B-\x60\x7B-\x7F]+/g,
|
|
32
|
-
const separator = s.lastIndexOf(
|
|
33
|
+
const sanitize = (s) => s.replace(/[\x00-\x2C\x2E-\x2F\x3A-\x40\x5B-\x60\x7B-\x7F]+/g, "-");
|
|
34
|
+
const separator = s.lastIndexOf(".");
|
|
33
35
|
if (separator === -1)
|
|
34
36
|
return sanitize(s);
|
|
35
|
-
return sanitize(s.substring(0, separator)) +
|
|
37
|
+
return (sanitize(s.substring(0, separator)) +
|
|
38
|
+
"." +
|
|
39
|
+
sanitize(s.substring(separator + 1)));
|
|
36
40
|
}
|
package/lib/utils/graphql.js
CHANGED
|
@@ -1,36 +1,36 @@
|
|
|
1
|
-
import {
|
|
2
|
-
const getString = (v) => typeof v ===
|
|
1
|
+
import { filter, join, pipe } from "@fxts/core";
|
|
2
|
+
const getString = (v) => typeof v === "string" ? v : undefined;
|
|
3
3
|
const hasPersistedQuery = (v) => {
|
|
4
|
-
return isPlainObject(v) &&
|
|
4
|
+
return isPlainObject(v) && "persistedQuery" in v;
|
|
5
5
|
};
|
|
6
6
|
const readGraphQLishBody = (v) => {
|
|
7
7
|
if (!isPlainObject(v))
|
|
8
8
|
return undefined;
|
|
9
|
-
const q = getString(v[
|
|
10
|
-
const variables = v[
|
|
11
|
-
const operationName = getString(v[
|
|
12
|
-
const hasPersisted = hasPersistedQuery(v[
|
|
9
|
+
const q = getString(v["query"]);
|
|
10
|
+
const variables = v["variables"];
|
|
11
|
+
const operationName = getString(v["operationName"]);
|
|
12
|
+
const hasPersisted = hasPersistedQuery(v["extensions"]);
|
|
13
13
|
if (!q && !hasPersisted && !operationName)
|
|
14
14
|
return undefined;
|
|
15
15
|
return { query: q, variables, operationName, hasPersisted };
|
|
16
16
|
};
|
|
17
17
|
export const isPlainObject = (value) => {
|
|
18
|
-
return !!value && typeof value ===
|
|
18
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
19
19
|
};
|
|
20
20
|
function getHeader(headers, name, altName) {
|
|
21
21
|
if (!headers)
|
|
22
22
|
return undefined;
|
|
23
23
|
const v1 = headers[name];
|
|
24
|
-
if (typeof v1 ===
|
|
24
|
+
if (typeof v1 === "string")
|
|
25
25
|
return v1;
|
|
26
26
|
if (Array.isArray(v1))
|
|
27
|
-
return v1.join(
|
|
27
|
+
return v1.join(", ");
|
|
28
28
|
if (altName) {
|
|
29
29
|
const v2 = headers[altName];
|
|
30
|
-
if (typeof v2 ===
|
|
30
|
+
if (typeof v2 === "string")
|
|
31
31
|
return v2;
|
|
32
32
|
if (Array.isArray(v2))
|
|
33
|
-
return v2.join(
|
|
33
|
+
return v2.join(", ");
|
|
34
34
|
}
|
|
35
35
|
return undefined;
|
|
36
36
|
}
|
|
@@ -45,44 +45,46 @@ export const safeJSONParse = (text) => {
|
|
|
45
45
|
}
|
|
46
46
|
};
|
|
47
47
|
const isOperationType = (s) => {
|
|
48
|
-
return s ===
|
|
48
|
+
return s === "query" || s === "mutation" || s === "subscription";
|
|
49
49
|
};
|
|
50
50
|
const parseOperationFromQuery = (query) => {
|
|
51
51
|
if (!query)
|
|
52
|
-
return { type:
|
|
53
|
-
const trimmed = query.trim().replace(/^#.*$/gm,
|
|
52
|
+
return { type: "unknown" };
|
|
53
|
+
const trimmed = query.trim().replace(/^#.*$/gm, "").trim();
|
|
54
54
|
if (!trimmed)
|
|
55
|
-
return { type:
|
|
56
|
-
if (trimmed.startsWith(
|
|
57
|
-
return { type:
|
|
55
|
+
return { type: "unknown" };
|
|
56
|
+
if (trimmed.startsWith("{"))
|
|
57
|
+
return { type: "query" };
|
|
58
58
|
const m = /^(query|mutation|subscription)\b\s*([A-Za-z_][A-Za-z0-9_]*)?/i.exec(trimmed);
|
|
59
59
|
if (m) {
|
|
60
60
|
const maybe = m[1].toLowerCase();
|
|
61
|
-
const type = isOperationType(maybe)
|
|
61
|
+
const type = isOperationType(maybe)
|
|
62
|
+
? maybe
|
|
63
|
+
: "unknown";
|
|
62
64
|
const name = m[2] || undefined;
|
|
63
65
|
return { type, name };
|
|
64
66
|
}
|
|
65
|
-
return { type:
|
|
67
|
+
return { type: "unknown" };
|
|
66
68
|
};
|
|
67
69
|
export const parseGraphQLRequestFromHttp = (method, url, headersIn, bodyText) => {
|
|
68
|
-
const contentType = getHeader(headersIn,
|
|
69
|
-
const u = new URL(url,
|
|
70
|
+
const contentType = getHeader(headersIn, "content-type", "Content-Type") || "";
|
|
71
|
+
const u = new URL(url, "http://dummy"); // base to satisfy URL if relative
|
|
70
72
|
let query;
|
|
71
73
|
let variables;
|
|
72
74
|
let operationName;
|
|
73
75
|
let isPersistedQuery = false;
|
|
74
|
-
if ((method ||
|
|
76
|
+
if ((method || "GET").toUpperCase() === "GET") {
|
|
75
77
|
const params = u.searchParams;
|
|
76
|
-
const queryParam = params.get(
|
|
77
|
-
const opNameParam = params.get(
|
|
78
|
-
const vars = params.get(
|
|
79
|
-
const ext = params.get(
|
|
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");
|
|
80
82
|
const extObj = ext ? safeJSONParse(ext) : undefined;
|
|
81
|
-
const hasPersisted = isPlainObject(extObj) &&
|
|
83
|
+
const hasPersisted = isPlainObject(extObj) && "persistedQuery" in extObj;
|
|
82
84
|
if (queryParam) {
|
|
83
85
|
// Only treat as GraphQL if the query actually looks like GraphQL
|
|
84
86
|
const parsedOp = parseOperationFromQuery(queryParam);
|
|
85
|
-
if (parsedOp.type !==
|
|
87
|
+
if (parsedOp.type !== "unknown") {
|
|
86
88
|
query = queryParam;
|
|
87
89
|
operationName = opNameParam;
|
|
88
90
|
if (vars)
|
|
@@ -99,16 +101,16 @@ export const parseGraphQLRequestFromHttp = (method, url, headersIn, bodyText) =>
|
|
|
99
101
|
}
|
|
100
102
|
else {
|
|
101
103
|
// POST/others
|
|
102
|
-
if (contentType.includes(
|
|
103
|
-
contentType.includes(
|
|
104
|
-
contentType.includes(
|
|
105
|
-
typeof bodyText ===
|
|
106
|
-
const bodyObj = safeJSONParse(bodyText ||
|
|
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 || "");
|
|
107
109
|
const parsed = readGraphQLishBody(bodyObj);
|
|
108
110
|
if (parsed) {
|
|
109
111
|
if (parsed.query) {
|
|
110
112
|
const parsedOp = parseOperationFromQuery(parsed.query);
|
|
111
|
-
if (parsedOp.type !==
|
|
113
|
+
if (parsedOp.type !== "unknown") {
|
|
112
114
|
query = parsed.query;
|
|
113
115
|
variables = parsed.variables;
|
|
114
116
|
operationName = parsed.operationName;
|
|
@@ -129,20 +131,24 @@ export const parseGraphQLRequestFromHttp = (method, url, headersIn, bodyText) =>
|
|
|
129
131
|
// Respect provided operationName when query is anonymous
|
|
130
132
|
return { operationType, operationName, query, variables, isPersistedQuery };
|
|
131
133
|
}
|
|
132
|
-
return {
|
|
134
|
+
return {
|
|
135
|
+
operationType,
|
|
136
|
+
operationName: name ?? operationName,
|
|
137
|
+
query,
|
|
138
|
+
variables,
|
|
139
|
+
isPersistedQuery,
|
|
140
|
+
};
|
|
133
141
|
};
|
|
134
142
|
export const summarizeGraphQL = (parsed) => {
|
|
135
|
-
const type = parsed.operationType ===
|
|
136
|
-
return pipe([
|
|
137
|
-
parsed.operationName ? `${type} ${parsed.operationName}` : type,
|
|
138
|
-
], filter((v) => typeof v === 'string'), join(' '));
|
|
143
|
+
const type = parsed.operationType === "unknown" ? "operation" : parsed.operationType;
|
|
144
|
+
return pipe([parsed.operationName ? `${type} ${parsed.operationName}` : type], filter((v) => typeof v === "string"), join(" "));
|
|
139
145
|
};
|
|
140
146
|
export const extractGraphQLResponseInfo = (bodyText) => {
|
|
141
|
-
const json = safeJSONParse(bodyText ||
|
|
147
|
+
const json = safeJSONParse(bodyText || "");
|
|
142
148
|
if (!isPlainObject(json))
|
|
143
149
|
return undefined;
|
|
144
|
-
const errorsVal = json[
|
|
145
|
-
const dataVal = json[
|
|
150
|
+
const errorsVal = json["errors"];
|
|
151
|
+
const dataVal = json["data"];
|
|
146
152
|
const hasGraphQLShape = Array.isArray(errorsVal) || isPlainObject(dataVal);
|
|
147
153
|
if (!hasGraphQLShape)
|
|
148
154
|
return undefined;
|
|
@@ -150,8 +156,8 @@ export const extractGraphQLResponseInfo = (bodyText) => {
|
|
|
150
156
|
const dataKeys = isPlainObject(dataVal) ? Object.keys(dataVal) : [];
|
|
151
157
|
const topMessages = errors.slice(0, 3).map((e) => {
|
|
152
158
|
if (isPlainObject(e)) {
|
|
153
|
-
const m = e[
|
|
154
|
-
if (typeof m ===
|
|
159
|
+
const m = e["message"];
|
|
160
|
+
if (typeof m === "string")
|
|
155
161
|
return m;
|
|
156
162
|
}
|
|
157
163
|
try {
|
|
@@ -161,29 +167,34 @@ export const extractGraphQLResponseInfo = (bodyText) => {
|
|
|
161
167
|
return String(e);
|
|
162
168
|
}
|
|
163
169
|
});
|
|
164
|
-
return {
|
|
170
|
+
return {
|
|
171
|
+
hasErrors: errors.length > 0,
|
|
172
|
+
errorCount: errors.length,
|
|
173
|
+
topMessages,
|
|
174
|
+
dataKeys,
|
|
175
|
+
};
|
|
165
176
|
};
|
|
166
177
|
/**
|
|
167
178
|
* Minify a GraphQL operation string by removing comments and unnecessary whitespace,
|
|
168
179
|
* while preserving string literals and required token separators.
|
|
169
180
|
*/
|
|
170
181
|
export const minifyGraphQLQuery = (input) => {
|
|
171
|
-
let out =
|
|
182
|
+
let out = "";
|
|
172
183
|
let i = 0;
|
|
173
184
|
const len = input.length;
|
|
174
185
|
let inString = null;
|
|
175
186
|
let escaped = false;
|
|
176
187
|
let pendingSpace = false;
|
|
177
188
|
const isIdent = (ch) => /[A-Za-z0-9_]/.test(ch);
|
|
178
|
-
const isWhitespace = (ch) => ch ===
|
|
179
|
-
const isPunct = (ch) =>
|
|
189
|
+
const isWhitespace = (ch) => ch === " " || ch === "\n" || ch === "\r" || ch === "\t" || ch === "\f";
|
|
190
|
+
const isPunct = (ch) => "{}()[]:!@$,=|&.".includes(ch);
|
|
180
191
|
while (i < len) {
|
|
181
192
|
const ch = input[i];
|
|
182
193
|
if (inString) {
|
|
183
194
|
out += ch;
|
|
184
195
|
if (escaped)
|
|
185
196
|
escaped = false;
|
|
186
|
-
else if (ch ===
|
|
197
|
+
else if (ch === "\\")
|
|
187
198
|
escaped = true;
|
|
188
199
|
else if (ch === inString)
|
|
189
200
|
inString = null;
|
|
@@ -191,10 +202,10 @@ export const minifyGraphQLQuery = (input) => {
|
|
|
191
202
|
continue;
|
|
192
203
|
}
|
|
193
204
|
// Not inside string
|
|
194
|
-
if (ch === '"' || ch === '
|
|
205
|
+
if (ch === '"' || ch === "'") {
|
|
195
206
|
// flush pending space
|
|
196
207
|
if (pendingSpace) {
|
|
197
|
-
out +=
|
|
208
|
+
out += " ";
|
|
198
209
|
pendingSpace = false;
|
|
199
210
|
}
|
|
200
211
|
inString = ch;
|
|
@@ -203,9 +214,9 @@ export const minifyGraphQLQuery = (input) => {
|
|
|
203
214
|
continue;
|
|
204
215
|
}
|
|
205
216
|
// Line comment start (# ... end of line)
|
|
206
|
-
if (ch ===
|
|
217
|
+
if (ch === "#") {
|
|
207
218
|
// Skip until end of line
|
|
208
|
-
while (i < len && input[i] !==
|
|
219
|
+
while (i < len && input[i] !== "\n")
|
|
209
220
|
i++;
|
|
210
221
|
continue;
|
|
211
222
|
}
|
|
@@ -226,9 +237,9 @@ export const minifyGraphQLQuery = (input) => {
|
|
|
226
237
|
// ch is part of an identifier/number or other token
|
|
227
238
|
if (pendingSpace) {
|
|
228
239
|
// Add a space only if the previous output char and current char both look like identifiers
|
|
229
|
-
const prev = out[out.length - 1] ||
|
|
240
|
+
const prev = out[out.length - 1] || "";
|
|
230
241
|
if (isIdent(prev) && isIdent(ch))
|
|
231
|
-
out +=
|
|
242
|
+
out += " ";
|
|
232
243
|
pendingSpace = false;
|
|
233
244
|
}
|
|
234
245
|
out += ch;
|
|
@@ -243,11 +254,11 @@ export const minifyGraphQLQuery = (input) => {
|
|
|
243
254
|
*/
|
|
244
255
|
export const minifyGraphQLRequestBody = (body) => {
|
|
245
256
|
if (Array.isArray(body))
|
|
246
|
-
return body.map(item => minifyGraphQLRequestBody(item));
|
|
257
|
+
return body.map((item) => minifyGraphQLRequestBody(item));
|
|
247
258
|
if (isPlainObject(body)) {
|
|
248
259
|
const out = {};
|
|
249
260
|
for (const [k, v] of Object.entries(body)) {
|
|
250
|
-
if (k ===
|
|
261
|
+
if (k === "query" && typeof v === "string")
|
|
251
262
|
out[k] = minifyGraphQLQuery(v);
|
|
252
263
|
else
|
|
253
264
|
out[k] = minifyGraphQLRequestBody(v);
|
package/lib/utils/guid.js
CHANGED
|
@@ -13,10 +13,10 @@
|
|
|
13
13
|
* See the License for the specific language governing permissions and
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
|
-
import crypto from
|
|
16
|
+
import crypto from "crypto";
|
|
17
17
|
export function createGuid() {
|
|
18
|
-
return crypto.randomBytes(16).toString(
|
|
18
|
+
return crypto.randomBytes(16).toString("hex");
|
|
19
19
|
}
|
|
20
20
|
export function createHash(data) {
|
|
21
|
-
return crypto.createHash(
|
|
21
|
+
return crypto.createHash("sha256").update(data).digest("hex").slice(0, 7);
|
|
22
22
|
}
|
package/lib/utils/httpServer.js
CHANGED
|
@@ -13,27 +13,27 @@
|
|
|
13
13
|
* See the License for the specific language governing permissions and
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
|
-
import assert from
|
|
17
|
-
import http from
|
|
16
|
+
import assert from "assert";
|
|
17
|
+
import http from "http";
|
|
18
18
|
export async function startHttpServer(config) {
|
|
19
19
|
const { host, port } = config;
|
|
20
20
|
const httpServer = http.createServer();
|
|
21
21
|
await new Promise((resolve, reject) => {
|
|
22
|
-
httpServer.on(
|
|
22
|
+
httpServer.on("error", reject);
|
|
23
23
|
httpServer.listen(port, host, () => {
|
|
24
24
|
resolve();
|
|
25
|
-
httpServer.removeListener(
|
|
25
|
+
httpServer.removeListener("error", reject);
|
|
26
26
|
});
|
|
27
27
|
});
|
|
28
28
|
return httpServer;
|
|
29
29
|
}
|
|
30
30
|
export function httpAddressToString(address) {
|
|
31
|
-
assert(address,
|
|
32
|
-
if (typeof address ===
|
|
31
|
+
assert(address, "Could not bind server socket");
|
|
32
|
+
if (typeof address === "string")
|
|
33
33
|
return address;
|
|
34
34
|
const resolvedPort = address.port;
|
|
35
|
-
let resolvedHost = address.family ===
|
|
36
|
-
if (resolvedHost ===
|
|
37
|
-
resolvedHost =
|
|
35
|
+
let resolvedHost = address.family === "IPv4" ? address.address : `[${address.address}]`;
|
|
36
|
+
if (resolvedHost === "0.0.0.0" || resolvedHost === "[::]")
|
|
37
|
+
resolvedHost = "localhost";
|
|
38
38
|
return `http://${resolvedHost}:${resolvedPort}`;
|
|
39
39
|
}
|
package/lib/utils/log.js
CHANGED
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
* See the License for the specific language governing permissions and
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
|
-
import debug from
|
|
17
|
-
const errorsDebug = debug(
|
|
16
|
+
import debug from "debug";
|
|
17
|
+
const errorsDebug = debug("pw:mcp:errors");
|
|
18
18
|
export function logUnhandledError(error) {
|
|
19
19
|
errorsDebug(error);
|
|
20
20
|
}
|
|
21
|
-
export const testDebug = debug(
|
|
21
|
+
export const testDebug = debug("pw:mcp:test");
|
|
@@ -43,7 +43,7 @@ export class ManualPromise extends Promise {
|
|
|
43
43
|
return Promise;
|
|
44
44
|
}
|
|
45
45
|
get [Symbol.toStringTag]() {
|
|
46
|
-
return
|
|
46
|
+
return "ManualPromise";
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
export class LongStandingScope {
|
|
@@ -67,7 +67,7 @@ export class LongStandingScope {
|
|
|
67
67
|
return this._isClosed;
|
|
68
68
|
}
|
|
69
69
|
static async raceMultiple(scopes, promise) {
|
|
70
|
-
return Promise.race(scopes.map(s => s.race(promise)));
|
|
70
|
+
return Promise.race(scopes.map((s) => s.race(promise)));
|
|
71
71
|
}
|
|
72
72
|
async race(promise) {
|
|
73
73
|
return this._race(Array.isArray(promise) ? promise : [promise], false);
|
|
@@ -85,8 +85,8 @@ export class LongStandingScope {
|
|
|
85
85
|
this._terminatePromises.set(terminatePromise, frames);
|
|
86
86
|
try {
|
|
87
87
|
return await Promise.race([
|
|
88
|
-
terminatePromise.then(e => safe ? defaultValue : Promise.reject(e)),
|
|
89
|
-
...promises
|
|
88
|
+
terminatePromise.then((e) => (safe ? defaultValue : Promise.reject(e))),
|
|
89
|
+
...promises,
|
|
90
90
|
]);
|
|
91
91
|
}
|
|
92
92
|
finally {
|
|
@@ -98,14 +98,14 @@ function cloneError(error, frames) {
|
|
|
98
98
|
const clone = new Error();
|
|
99
99
|
clone.name = error.name;
|
|
100
100
|
clone.message = error.message;
|
|
101
|
-
clone.stack = [error.name +
|
|
101
|
+
clone.stack = [error.name + ":" + error.message, ...frames].join("\n");
|
|
102
102
|
return clone;
|
|
103
103
|
}
|
|
104
104
|
function captureRawStack() {
|
|
105
105
|
const stackTraceLimit = Error.stackTraceLimit;
|
|
106
106
|
Error.stackTraceLimit = 50;
|
|
107
107
|
const error = new Error();
|
|
108
|
-
const stack = error.stack ||
|
|
108
|
+
const stack = error.stack || "";
|
|
109
109
|
Error.stackTraceLimit = stackTraceLimit;
|
|
110
|
-
return stack.split(
|
|
110
|
+
return stack.split("\n");
|
|
111
111
|
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { formatUrlWithTrimmedParams } from "../hooks/networkFilters.js";
|
|
2
|
+
import { parseGraphQLRequestFromHttp, summarizeGraphQL } from "./graphql.js";
|
|
3
3
|
export const formatNetworkSummaryLine = (input, opts) => {
|
|
4
|
-
const method = (input.method ||
|
|
5
|
-
const formattedUrl = opts?.trimParams === false
|
|
6
|
-
|
|
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}` : "";
|
|
7
9
|
let line = `${method} ${formattedUrl} → ${input.status}${st}`;
|
|
8
10
|
const gql = parseGraphQLRequestFromHttp(input.method, input.url, input.headers || {}, input.postData ?? undefined);
|
|
9
11
|
if (gql)
|
package/lib/utils/package.js
CHANGED
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* See the License for the specific language governing permissions and
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
|
-
import fs from
|
|
17
|
-
import path from
|
|
18
|
-
import url from
|
|
16
|
+
import fs from "fs";
|
|
17
|
+
import path from "path";
|
|
18
|
+
import url from "url";
|
|
19
19
|
const __filename = url.fileURLToPath(import.meta.url);
|
|
20
|
-
export const packageJSON = JSON.parse(fs.readFileSync(path.join(path.dirname(__filename),
|
|
20
|
+
export const packageJSON = JSON.parse(fs.readFileSync(path.join(path.dirname(__filename), "..", "..", "package.json"), "utf8"));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
1
|
+
import { pipe, when } from "@fxts/core";
|
|
2
|
+
import * as cheerio from "cheerio";
|
|
3
|
+
import { ElementType } from "domelementtype";
|
|
4
4
|
const I18N_PATTERNS = [
|
|
5
5
|
/window\.__i18n\s*=/i,
|
|
6
6
|
/window\.__translations\s*=/i,
|
|
@@ -13,46 +13,57 @@ const I18N_PATTERNS = [
|
|
|
13
13
|
/\btranslationData\s*=/i,
|
|
14
14
|
];
|
|
15
15
|
export const isI18nScript = (content) => {
|
|
16
|
-
const text = content ||
|
|
17
|
-
const hasI18nPattern = I18N_PATTERNS.some(pattern => pattern.test(text));
|
|
16
|
+
const text = content || "";
|
|
17
|
+
const hasI18nPattern = I18N_PATTERNS.some((pattern) => pattern.test(text));
|
|
18
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
19
|
const matches = text.match(langCodePattern) || [];
|
|
20
20
|
const hasLangCodeStructure = matches.length >= 2;
|
|
21
21
|
return hasI18nPattern || hasLangCodeStructure;
|
|
22
22
|
};
|
|
23
23
|
export const removeI18nScripts = ($) => {
|
|
24
|
-
$(
|
|
25
|
-
const content = $(el).html() ||
|
|
24
|
+
$("script").each((_, el) => {
|
|
25
|
+
const content = $(el).html() || "";
|
|
26
26
|
if (isI18nScript(content))
|
|
27
27
|
$(el).remove();
|
|
28
28
|
});
|
|
29
29
|
return $;
|
|
30
30
|
};
|
|
31
31
|
export const removeScriptTags = ($) => {
|
|
32
|
-
$(
|
|
32
|
+
$("script").remove();
|
|
33
33
|
return $;
|
|
34
34
|
};
|
|
35
35
|
export const removeStyleTags = ($) => {
|
|
36
|
-
$(
|
|
36
|
+
$("style").remove();
|
|
37
37
|
return $;
|
|
38
38
|
};
|
|
39
39
|
export const NON_ESSENTIAL_RELS = [
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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",
|
|
44
54
|
];
|
|
45
55
|
export const removeNonEssentialLinks = ($) => {
|
|
46
|
-
$(
|
|
47
|
-
const rel = ($(el).attr(
|
|
48
|
-
const type = ($(el).attr(
|
|
49
|
-
if (type ===
|
|
56
|
+
$("link").each((_, el) => {
|
|
57
|
+
const rel = ($(el).attr("rel") || "").toLowerCase();
|
|
58
|
+
const type = ($(el).attr("type") || "").toLowerCase();
|
|
59
|
+
if (type === "text/css") {
|
|
50
60
|
$(el).remove();
|
|
51
61
|
return;
|
|
52
62
|
}
|
|
53
63
|
if (rel) {
|
|
54
64
|
const relValues = rel.split(/\s+/);
|
|
55
|
-
const hasNonEssential = relValues.some(value => NON_ESSENTIAL_RELS.includes(value) ||
|
|
65
|
+
const hasNonEssential = relValues.some((value) => NON_ESSENTIAL_RELS.includes(value) ||
|
|
66
|
+
(value === "shortcut" && relValues.includes("icon")));
|
|
56
67
|
if (hasNonEssential)
|
|
57
68
|
$(el).remove();
|
|
58
69
|
}
|
|
@@ -60,39 +71,60 @@ export const removeNonEssentialLinks = ($) => {
|
|
|
60
71
|
return $;
|
|
61
72
|
};
|
|
62
73
|
export const removeMetaTags = ($) => {
|
|
63
|
-
$(
|
|
74
|
+
$("meta").remove();
|
|
64
75
|
return $;
|
|
65
76
|
};
|
|
66
77
|
export const removeHtmlComments = ($) => {
|
|
67
|
-
$(
|
|
78
|
+
$("*")
|
|
79
|
+
.contents()
|
|
80
|
+
.each((_, node) => {
|
|
68
81
|
if (node.type === ElementType.Comment)
|
|
69
82
|
$(node).remove();
|
|
70
83
|
});
|
|
71
84
|
return $;
|
|
72
85
|
};
|
|
73
86
|
export const SVG_ATTRIBUTES_TO_REMOVE = [
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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",
|
|
84
116
|
];
|
|
85
117
|
export const stripSvgAttributes = ($) => {
|
|
86
|
-
$(
|
|
87
|
-
SVG_ATTRIBUTES_TO_REMOVE.forEach(attr => {
|
|
118
|
+
$("svg, svg *").each((_, el) => {
|
|
119
|
+
SVG_ATTRIBUTES_TO_REMOVE.forEach((attr) => {
|
|
88
120
|
$(el).removeAttr(attr);
|
|
89
121
|
});
|
|
90
122
|
});
|
|
91
123
|
return $;
|
|
92
124
|
};
|
|
93
|
-
export const minifyHtml = (html) => html.replace(/>\s+</g,
|
|
125
|
+
export const minifyHtml = (html) => html.replace(/>\s+</g, "><").trim();
|
|
94
126
|
export const sanitizeHtml = (html, options) => {
|
|
95
127
|
if (!options.shouldRemoveScripts && !options.shouldRemoveStyles)
|
|
96
128
|
return html;
|
|
97
|
-
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() ||
|
|
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() || ""));
|
|
98
130
|
};
|