@anubhav-choubey/web-for-agents 0.1.0 → 0.3.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/README.md +77 -512
- package/dist/bin.js +6 -6
- package/dist/{chunk-AZG5MLNG.js → chunk-2Y3LLYEC.js} +5 -1
- package/dist/{chunk-IF32TPGR.js → chunk-357ATBMS.js} +369 -130
- package/dist/chunk-4STTPCBR.js +25 -0
- package/dist/chunk-CGIIB6AF.js +127 -0
- package/dist/{chunk-HONWT6GK.js → chunk-K3B53RIO.js} +66 -34
- package/dist/chunk-L3AYWCSX.js +33 -0
- package/dist/chunk-MIVGE5TM.js +34 -0
- package/dist/chunk-OKCYZE6C.js +245 -0
- package/dist/{chunk-5TNG4QMP.js → chunk-PGZH33B5.js} +20 -2
- package/dist/{chunk-ZYMNO5AO.js → chunk-T42S3WRV.js} +161 -62
- package/dist/cli.js +6 -6
- package/dist/{happyDom-XVDHYJDM.js → happyDom-ZEFNMD7B.js} +3 -3
- package/dist/index.d.ts +89 -11
- package/dist/index.js +113 -246
- package/dist/overlay.js +22 -42
- package/dist/{renderChild-Y2RFSYY4.js → renderChild-PSQLAIUW.js} +27 -4
- package/dist/vite.d.ts +116 -3
- package/dist/vite.js +867 -192
- package/package.json +1 -1
- package/dist/chunk-CKXHTGGT.js +0 -25
- package/dist/chunk-FEAKVNMG.js +0 -12
- package/dist/chunk-LCLDQ3A3.js +0 -26
- package/dist/chunk-LCVI2PRP.js +0 -13
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// src/shared/agent.ts
|
|
2
|
+
var ACTION_NAME_RE = /^[A-Za-z0-9_.-]{1,128}$/;
|
|
3
|
+
function isValidActionName(name) {
|
|
4
|
+
return typeof name === "string" && ACTION_NAME_RE.test(name);
|
|
5
|
+
}
|
|
6
|
+
var ACTION_NAME_RULE = "use 1-128 chars of A-Z a-z 0-9 _ - .";
|
|
7
|
+
var AGENT_GLOBAL_VERSION = 1;
|
|
8
|
+
var AGENT_MAP_VERSION = 1;
|
|
9
|
+
|
|
10
|
+
// src/shared/text.ts
|
|
11
|
+
function collapseWhitespace(s) {
|
|
12
|
+
return (s ?? "").replace(/\s+/g, " ").trim();
|
|
13
|
+
}
|
|
14
|
+
function stripBom(s) {
|
|
15
|
+
return s.charCodeAt(0) === 65279 ? s.slice(1) : s;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export {
|
|
19
|
+
isValidActionName,
|
|
20
|
+
ACTION_NAME_RULE,
|
|
21
|
+
AGENT_GLOBAL_VERSION,
|
|
22
|
+
AGENT_MAP_VERSION,
|
|
23
|
+
collapseWhitespace,
|
|
24
|
+
stripBom
|
|
25
|
+
};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// src/shared/bots.ts
|
|
2
|
+
var AI_BOTS = {
|
|
3
|
+
training: [
|
|
4
|
+
"GPTBot",
|
|
5
|
+
"ClaudeBot",
|
|
6
|
+
"CCBot",
|
|
7
|
+
"Google-Extended",
|
|
8
|
+
"Applebot-Extended",
|
|
9
|
+
"Meta-ExternalAgent",
|
|
10
|
+
"Bytespider",
|
|
11
|
+
"cohere-training-data-crawler"
|
|
12
|
+
],
|
|
13
|
+
search: [
|
|
14
|
+
"OAI-SearchBot",
|
|
15
|
+
"ChatGPT-User",
|
|
16
|
+
"Claude-SearchBot",
|
|
17
|
+
"Claude-User",
|
|
18
|
+
"PerplexityBot",
|
|
19
|
+
"Perplexity-User"
|
|
20
|
+
]
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// src/shared/htmlScan.ts
|
|
24
|
+
var isSpace = (c) => c === 32 || c === 9 || c === 10 || c === 12 || c === 13;
|
|
25
|
+
var isAlpha = (c) => c >= 65 && c <= 90 || c >= 97 && c <= 122;
|
|
26
|
+
function findRawEnd(html, name, from) {
|
|
27
|
+
let i = html.indexOf("</", from);
|
|
28
|
+
while (i !== -1) {
|
|
29
|
+
if (html.slice(i + 2, i + 2 + name.length).toLowerCase() === name) return i;
|
|
30
|
+
i = html.indexOf("</", i + 2);
|
|
31
|
+
}
|
|
32
|
+
return -1;
|
|
33
|
+
}
|
|
34
|
+
function* scanHtml(html, rawText) {
|
|
35
|
+
const n = html.length;
|
|
36
|
+
let i = 0;
|
|
37
|
+
while (i < n) {
|
|
38
|
+
const lt = html.indexOf("<", i);
|
|
39
|
+
if (lt === -1) return;
|
|
40
|
+
const c1 = lt + 1 < n ? html.charCodeAt(lt + 1) : 0;
|
|
41
|
+
if (c1 === 33 || c1 === 63) {
|
|
42
|
+
let end;
|
|
43
|
+
if (html.startsWith("<!--", lt)) {
|
|
44
|
+
const close = html.indexOf("-->", lt + 4);
|
|
45
|
+
end = close === -1 ? n : close + 3;
|
|
46
|
+
} else {
|
|
47
|
+
const close = html.indexOf(">", lt + 2);
|
|
48
|
+
end = close === -1 ? n : close + 1;
|
|
49
|
+
}
|
|
50
|
+
yield { type: "comment", start: lt, end };
|
|
51
|
+
i = end;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const closing = c1 === 47;
|
|
55
|
+
const nameStart = lt + (closing ? 2 : 1);
|
|
56
|
+
if (!isAlpha(nameStart < n ? html.charCodeAt(nameStart) : 0)) {
|
|
57
|
+
i = lt + 1;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
let p = nameStart;
|
|
61
|
+
while (p < n) {
|
|
62
|
+
const c = html.charCodeAt(p);
|
|
63
|
+
if (isSpace(c) || c === 62 || c === 47) break;
|
|
64
|
+
p++;
|
|
65
|
+
}
|
|
66
|
+
const name = html.slice(nameStart, p).toLowerCase();
|
|
67
|
+
const attrs = [];
|
|
68
|
+
let selfClosing = false;
|
|
69
|
+
while (p < n) {
|
|
70
|
+
const c = html.charCodeAt(p);
|
|
71
|
+
if (isSpace(c)) {
|
|
72
|
+
p++;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (c === 62) {
|
|
76
|
+
p++;
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
if (c === 47) {
|
|
80
|
+
selfClosing = p + 1 < n && html.charCodeAt(p + 1) === 62;
|
|
81
|
+
p++;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const as = p;
|
|
85
|
+
while (p < n) {
|
|
86
|
+
const d = html.charCodeAt(p);
|
|
87
|
+
if (isSpace(d) || d === 62 || d === 47 || d === 61) break;
|
|
88
|
+
p++;
|
|
89
|
+
}
|
|
90
|
+
if (p === as) {
|
|
91
|
+
p++;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const attr = { name: html.slice(as, p).toLowerCase(), value: "", valueStart: -1, valueEnd: -1, quote: "" };
|
|
95
|
+
while (p < n && isSpace(html.charCodeAt(p))) p++;
|
|
96
|
+
if (p < n && html.charCodeAt(p) === 61) {
|
|
97
|
+
p++;
|
|
98
|
+
while (p < n && isSpace(html.charCodeAt(p))) p++;
|
|
99
|
+
const q = p < n ? html[p] : "";
|
|
100
|
+
if (q === '"' || q === "'") {
|
|
101
|
+
const close = html.indexOf(q, p + 1);
|
|
102
|
+
const stop = close === -1 ? n : close;
|
|
103
|
+
Object.assign(attr, { value: html.slice(p + 1, stop), valueStart: p + 1, valueEnd: stop, quote: q });
|
|
104
|
+
p = close === -1 ? n : close + 1;
|
|
105
|
+
} else {
|
|
106
|
+
const vs = p;
|
|
107
|
+
while (p < n && !isSpace(html.charCodeAt(p)) && html.charCodeAt(p) !== 62) p++;
|
|
108
|
+
Object.assign(attr, { value: html.slice(vs, p), valueStart: vs, valueEnd: p });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
attrs.push(attr);
|
|
112
|
+
}
|
|
113
|
+
yield { type: "tag", closing, name, attrs, selfClosing, start: lt, end: p };
|
|
114
|
+
i = p;
|
|
115
|
+
if (!closing && !selfClosing && rawText.has(name)) {
|
|
116
|
+
const end = findRawEnd(html, name, i);
|
|
117
|
+
const stop = end === -1 ? n : end;
|
|
118
|
+
yield { type: "raw", name, start: i, end: stop };
|
|
119
|
+
i = stop;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export {
|
|
125
|
+
AI_BOTS,
|
|
126
|
+
scanHtml
|
|
127
|
+
};
|
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
import {
|
|
2
2
|
READY_GLOBAL
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-2Y3LLYEC.js";
|
|
4
4
|
import {
|
|
5
5
|
POLL_MS,
|
|
6
6
|
RenderError,
|
|
7
7
|
SERIALIZE_EXPR,
|
|
8
8
|
SETTLED_POLL_MS,
|
|
9
|
+
SNAPSHOT_GRACE_MS,
|
|
10
|
+
TimeoutError,
|
|
11
|
+
assertNoPageErrors,
|
|
9
12
|
blockedHint,
|
|
10
13
|
createDeadline,
|
|
11
14
|
createOriginPolicy,
|
|
12
15
|
readyTimeoutMessage,
|
|
13
16
|
runInPage,
|
|
17
|
+
snapshotWarning,
|
|
14
18
|
waitForReady,
|
|
15
19
|
withoutBlockedLoads
|
|
16
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-PGZH33B5.js";
|
|
17
21
|
import {
|
|
18
22
|
LOG_PREFIX,
|
|
19
23
|
errorMessage
|
|
@@ -56,8 +60,11 @@ var PROBE_SCRIPT = `(function () {
|
|
|
56
60
|
};
|
|
57
61
|
}
|
|
58
62
|
if (w.MutationObserver) {
|
|
63
|
+
// Content changes only: attribute churn (class, style, aria-*, data-* animations) never settles
|
|
64
|
+
// on an animated page. Attributes are still saved, so a late attribute-only update (a meta tag or
|
|
65
|
+
// an img src changed alone, after the content is quiet) may be captured before or after it lands.
|
|
59
66
|
new w.MutationObserver(touch).observe(document, {
|
|
60
|
-
childList: true, subtree: true,
|
|
67
|
+
childList: true, subtree: true, characterData: true
|
|
61
68
|
});
|
|
62
69
|
}
|
|
63
70
|
w.addEventListener("error", function (e) {
|
|
@@ -74,12 +81,25 @@ var PROBE_SCRIPT = `(function () {
|
|
|
74
81
|
p.errors.push(String((r && r.message) || r || "unhandledrejection")); touch();
|
|
75
82
|
});
|
|
76
83
|
})();`;
|
|
77
|
-
function readyCheck(idleMs = IDLE_MS) {
|
|
78
|
-
|
|
84
|
+
function readyCheck(idleMs = IDLE_MS, readySelector) {
|
|
85
|
+
if (readySelector === void 0) {
|
|
86
|
+
return `(function () {
|
|
79
87
|
var p = window.${PROBE_GLOBAL};
|
|
80
88
|
if (!p) return { ready: false, errors: [], path: location.pathname };
|
|
81
89
|
var ready = document.readyState === "complete" && p.inflight === 0 && Date.now() - p.last >= ${idleMs} && window.${READY_GLOBAL} !== false;
|
|
82
90
|
return { ready: ready, errors: p.errors.slice(), path: location.pathname };
|
|
91
|
+
})()`;
|
|
92
|
+
}
|
|
93
|
+
const sel = JSON.stringify(readySelector);
|
|
94
|
+
const invalid = JSON.stringify(`prerender.readySelector ${sel} is not a valid CSS selector: `);
|
|
95
|
+
return `(function () {
|
|
96
|
+
var p = window.${PROBE_GLOBAL};
|
|
97
|
+
if (!p) return { ready: false, errors: [], path: location.pathname };
|
|
98
|
+
var found;
|
|
99
|
+
try { found = document.querySelector(${sel}) !== null; }
|
|
100
|
+
catch (e) { return { ready: false, errors: [${invalid} + String((e && e.message) || e)], path: location.pathname }; }
|
|
101
|
+
var ready = document.readyState === "complete" && p.inflight === 0 && found && window.${READY_GLOBAL} !== false;
|
|
102
|
+
return { ready: ready, errors: p.errors.slice(), path: location.pathname };
|
|
83
103
|
})()`;
|
|
84
104
|
}
|
|
85
105
|
var READY_CHECK = readyCheck(IDLE_MS);
|
|
@@ -250,6 +270,24 @@ function createHappyDomRenderer(opts = {}) {
|
|
|
250
270
|
}
|
|
251
271
|
}
|
|
252
272
|
});
|
|
273
|
+
const waitUntilReady = async (page, blocked, deadline, timeoutMs) => {
|
|
274
|
+
const poll = async (check2) => {
|
|
275
|
+
const r = page.evaluate(check2);
|
|
276
|
+
return { ready: r.ready, errors: withoutBlockedLoads([...r.errors], blocked) };
|
|
277
|
+
};
|
|
278
|
+
if (opts.readySelector !== void 0) {
|
|
279
|
+
const check2 = readyCheck(IDLE_MS, opts.readySelector);
|
|
280
|
+
return waitForReady(() => poll(check2), timeoutMs, POLL_MS);
|
|
281
|
+
}
|
|
282
|
+
let timer;
|
|
283
|
+
const settled = await Promise.race([
|
|
284
|
+
page.waitUntilComplete().then(() => true),
|
|
285
|
+
new Promise((r) => timer = setTimeout(() => r(false), Math.min(deadline.remaining(), 2e3)))
|
|
286
|
+
]).finally(() => clearTimeout(timer));
|
|
287
|
+
const idleMs = opts.idleMs ?? (settled ? SETTLED_IDLE_MS : IDLE_MS);
|
|
288
|
+
const check = readyCheck(idleMs);
|
|
289
|
+
return waitForReady(() => poll(check), timeoutMs, Math.min(settled ? SETTLED_POLL_MS : POLL_MS, idleMs));
|
|
290
|
+
};
|
|
253
291
|
return {
|
|
254
292
|
name: "happy-dom",
|
|
255
293
|
async render(url, timeoutMs, inPage) {
|
|
@@ -265,48 +303,42 @@ function createHappyDomRenderer(opts = {}) {
|
|
|
265
303
|
};
|
|
266
304
|
const sink = pageSink(page);
|
|
267
305
|
attach(sink);
|
|
268
|
-
const
|
|
306
|
+
const within = (d, text, work) => d.race(Promise.race([work, sink.failed]), text);
|
|
307
|
+
const step = (work) => within(deadline, timeoutText, work);
|
|
308
|
+
let timedOut = false;
|
|
269
309
|
try {
|
|
270
310
|
try {
|
|
271
311
|
await step(page.goto(url));
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
waitForReady(
|
|
283
|
-
async () => {
|
|
284
|
-
const r = page.evaluate(check);
|
|
285
|
-
return { ready: r.ready, errors: withoutBlockedLoads([...r.errors], state.blocked) };
|
|
286
|
-
},
|
|
287
|
-
timeoutMs,
|
|
288
|
-
Math.min(settled ? SETTLED_POLL_MS : POLL_MS, idleMs)
|
|
289
|
-
)
|
|
290
|
-
);
|
|
312
|
+
try {
|
|
313
|
+
await step(waitUntilReady(page, state.blocked, deadline, timeoutMs));
|
|
314
|
+
} catch (e) {
|
|
315
|
+
if (!(opts.onTimeout === "snapshot" && e instanceof TimeoutError)) throw e;
|
|
316
|
+
timedOut = true;
|
|
317
|
+
await assertNoPageErrors(() => {
|
|
318
|
+
const r = page.evaluate(readyCheck(IDLE_MS, opts.readySelector));
|
|
319
|
+
return withoutBlockedLoads([...r.errors], state.blocked);
|
|
320
|
+
});
|
|
321
|
+
}
|
|
291
322
|
} catch (e) {
|
|
292
323
|
throw new RenderError("happy-dom", errorMessage(e) + blockedHint(state.blocked) + syncHint(state.syncRefused));
|
|
293
324
|
}
|
|
294
325
|
const html = page.evaluate(SERIALIZE_EXPR);
|
|
295
326
|
const finalPath = new URL(page.url).pathname;
|
|
327
|
+
const after = timedOut ? createDeadline(SNAPSHOT_GRACE_MS) : deadline;
|
|
328
|
+
const afterMs = timedOut ? SNAPSHOT_GRACE_MS : timeoutMs;
|
|
296
329
|
const results = await runInPage(
|
|
297
330
|
"happy-dom",
|
|
298
331
|
inPage,
|
|
299
|
-
(expr) =>
|
|
300
|
-
|
|
301
|
-
|
|
332
|
+
(expr) => within(after, readyTimeoutMessage(afterMs), Promise.resolve().then(() => page.evaluate(expr))),
|
|
333
|
+
after,
|
|
334
|
+
afterMs
|
|
302
335
|
);
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
finalPath,
|
|
306
|
-
inPage: results,
|
|
336
|
+
const warnings = [
|
|
337
|
+
...timedOut ? [snapshotWarning(timeoutMs)] : [],
|
|
307
338
|
// The page rendered anyway, but without what the refused requests would have loaded.
|
|
308
|
-
...
|
|
309
|
-
|
|
339
|
+
...[...new Map(state.syncRefused.map((r) => [r.kind + r.url, r])).values()].map(syncRefusalText)
|
|
340
|
+
];
|
|
341
|
+
return { html, finalPath, inPage: results, ...warnings.length > 0 ? { warnings } : {} };
|
|
310
342
|
} finally {
|
|
311
343
|
active.delete(state);
|
|
312
344
|
await page.close().catch(() => {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// src/shared/metaLength.ts
|
|
2
|
+
var TITLE_MAX_CHARS = 60;
|
|
3
|
+
var DESCRIPTION_MIN_CHARS = 50;
|
|
4
|
+
var DESCRIPTION_MAX_CHARS = 160;
|
|
5
|
+
function metaLengthWarnings(pages) {
|
|
6
|
+
const longTitle = [];
|
|
7
|
+
const shortDesc = [];
|
|
8
|
+
const longDesc = [];
|
|
9
|
+
for (const { route, title, description } of pages) {
|
|
10
|
+
if (title && title.length > TITLE_MAX_CHARS) longTitle.push(`${route} (${title.length})`);
|
|
11
|
+
if (!description) continue;
|
|
12
|
+
if (description.length < DESCRIPTION_MIN_CHARS) shortDesc.push(`${route} (${description.length})`);
|
|
13
|
+
else if (description.length > DESCRIPTION_MAX_CHARS) longDesc.push(`${route} (${description.length})`);
|
|
14
|
+
}
|
|
15
|
+
const out = [];
|
|
16
|
+
if (longTitle.length > 0) {
|
|
17
|
+
out.push(`[web-for-agents] Title longer than ${TITLE_MAX_CHARS} characters on ${longTitle.join(", ")}`);
|
|
18
|
+
}
|
|
19
|
+
if (shortDesc.length > 0) {
|
|
20
|
+
out.push(`[web-for-agents] Meta description shorter than ${DESCRIPTION_MIN_CHARS} characters on ${shortDesc.join(", ")}`);
|
|
21
|
+
}
|
|
22
|
+
if (longDesc.length > 0) {
|
|
23
|
+
out.push(`[web-for-agents] Meta description longer than ${DESCRIPTION_MAX_CHARS} characters on ${longDesc.join(", ")}`);
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export {
|
|
29
|
+
TITLE_MAX_CHARS,
|
|
30
|
+
DESCRIPTION_MIN_CHARS,
|
|
31
|
+
DESCRIPTION_MAX_CHARS,
|
|
32
|
+
metaLengthWarnings
|
|
33
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// src/shared/jsonLd.ts
|
|
2
|
+
var MAX_JSON_LD_NODES = 1e5;
|
|
3
|
+
function jsonLdNodes(value) {
|
|
4
|
+
const out = [];
|
|
5
|
+
const stack = [value];
|
|
6
|
+
for (let visited = 0; stack.length > 0 && visited < MAX_JSON_LD_NODES; visited++) {
|
|
7
|
+
const v = stack.pop();
|
|
8
|
+
if (Array.isArray(v)) {
|
|
9
|
+
for (let i = v.length - 1; i >= 0; i--) stack.push(v[i]);
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
12
|
+
if (v === null || typeof v !== "object") continue;
|
|
13
|
+
const o = v;
|
|
14
|
+
out.push(o);
|
|
15
|
+
if ("@graph" in o) stack.push(o["@graph"]);
|
|
16
|
+
}
|
|
17
|
+
return out;
|
|
18
|
+
}
|
|
19
|
+
function nodeTypes(node) {
|
|
20
|
+
const t = node["@type"];
|
|
21
|
+
if (typeof t === "string") return [t];
|
|
22
|
+
if (Array.isArray(t)) return t.filter((x) => typeof x === "string");
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
function jsonLdTypes(value) {
|
|
26
|
+
return jsonLdNodes(value).flatMap(nodeTypes);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export {
|
|
30
|
+
MAX_JSON_LD_NODES,
|
|
31
|
+
jsonLdNodes,
|
|
32
|
+
nodeTypes,
|
|
33
|
+
jsonLdTypes
|
|
34
|
+
};
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import {
|
|
2
|
+
collapseWhitespace
|
|
3
|
+
} from "./chunk-4STTPCBR.js";
|
|
4
|
+
import {
|
|
5
|
+
ACTION_PARAM_ATTR,
|
|
6
|
+
HIDDEN_SELECTOR,
|
|
7
|
+
WEBMCP_TOOLPARAMDESCRIPTION_ATTR
|
|
8
|
+
} from "./chunk-2Y3LLYEC.js";
|
|
9
|
+
|
|
10
|
+
// src/shared/object.ts
|
|
11
|
+
var hasOwn = (o, k) => Object.prototype.hasOwnProperty.call(o, k);
|
|
12
|
+
|
|
13
|
+
// src/runtime/agent/dom.ts
|
|
14
|
+
var INERT_OR_CLOSED = "[inert], dialog:not([open]), details:not([open]) > :not(summary)";
|
|
15
|
+
var CONCEALED = `${HIDDEN_SELECTOR}, ${INERT_OR_CLOSED}`;
|
|
16
|
+
function inDisabledFieldset(el) {
|
|
17
|
+
for (let child = el, p = el.parentElement; p; child = p, p = p.parentElement) {
|
|
18
|
+
if (p.matches("fieldset[disabled]") && child !== p.querySelector(":scope > legend")) return true;
|
|
19
|
+
}
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
function isDisabled(el) {
|
|
23
|
+
return el.hasAttribute("disabled") || el.getAttribute("aria-disabled") === "true" || inDisabledFieldset(el);
|
|
24
|
+
}
|
|
25
|
+
function isConcealed(el) {
|
|
26
|
+
if (!el.closest(CONCEALED)) return false;
|
|
27
|
+
const labels = el.tagName === "INPUT" && /^(checkbox|radio)$/.test(el.type) ? el.labels : null;
|
|
28
|
+
return !(labels && el.getAttribute("aria-hidden") !== "true" && !el.matches(INERT_OR_CLOSED) && !el.parentElement?.closest(CONCEALED) && Array.from(labels).some((l) => !isConcealed(l) && !isDisabled(l)));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// src/runtime/agent/formSchema.ts
|
|
32
|
+
var EMPTY_OBJECT_SCHEMA = Object.freeze({
|
|
33
|
+
type: "object",
|
|
34
|
+
properties: Object.freeze({}),
|
|
35
|
+
additionalProperties: false
|
|
36
|
+
});
|
|
37
|
+
var EXCLUDED_TYPES = /* @__PURE__ */ new Set(["hidden", "password", "file", "submit", "button", "reset", "image"]);
|
|
38
|
+
function isAgentField(e) {
|
|
39
|
+
const tag = e.tagName;
|
|
40
|
+
if (tag !== "INPUT" && tag !== "SELECT" && tag !== "TEXTAREA") return false;
|
|
41
|
+
if (!e.getAttribute("name")) return false;
|
|
42
|
+
if (e.hasAttribute("disabled") || e.hasAttribute("readonly") || inDisabledFieldset(e)) return false;
|
|
43
|
+
if (tag === "INPUT" && EXCLUDED_TYPES.has((e.getAttribute("type") ?? "text").toLowerCase())) return false;
|
|
44
|
+
return !isConcealed(e);
|
|
45
|
+
}
|
|
46
|
+
function agentFields(form) {
|
|
47
|
+
const groups = /* @__PURE__ */ new Map();
|
|
48
|
+
for (const el of Array.from(form.elements)) {
|
|
49
|
+
if (!isAgentField(el)) continue;
|
|
50
|
+
const name = el.getAttribute("name");
|
|
51
|
+
const list = groups.get(name);
|
|
52
|
+
if (list) list.push(el);
|
|
53
|
+
else groups.set(name, [el]);
|
|
54
|
+
}
|
|
55
|
+
return groups;
|
|
56
|
+
}
|
|
57
|
+
function labelText(el) {
|
|
58
|
+
const labels = el.labels ? Array.from(el.labels) : [];
|
|
59
|
+
if (labels.length === 0) {
|
|
60
|
+
const wrap = el.closest("label");
|
|
61
|
+
if (wrap) labels.push(wrap);
|
|
62
|
+
}
|
|
63
|
+
return collapseWhitespace(
|
|
64
|
+
labels.map((l) => {
|
|
65
|
+
const c = l.cloneNode(true);
|
|
66
|
+
c.querySelectorAll("input,select,textarea,button").forEach((n) => n.remove());
|
|
67
|
+
return c.textContent;
|
|
68
|
+
}).join(" ")
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
function describe(el) {
|
|
72
|
+
return collapseWhitespace(el.getAttribute(ACTION_PARAM_ATTR)) || collapseWhitespace(el.getAttribute(WEBMCP_TOOLPARAMDESCRIPTION_ATTR)) || labelText(el) || collapseWhitespace(el.getAttribute("aria-label")) || collapseWhitespace(el.getAttribute("placeholder"));
|
|
73
|
+
}
|
|
74
|
+
function num(el, attr) {
|
|
75
|
+
const v = el.getAttribute(attr);
|
|
76
|
+
if (v === null || v.trim() === "") return void 0;
|
|
77
|
+
const n = Number(v);
|
|
78
|
+
return Number.isFinite(n) ? n : void 0;
|
|
79
|
+
}
|
|
80
|
+
function inputSchema(el) {
|
|
81
|
+
const type = el.tagName === "TEXTAREA" ? "textarea" : (el.getAttribute("type") ?? "text").toLowerCase();
|
|
82
|
+
const s = {};
|
|
83
|
+
if (type === "checkbox") {
|
|
84
|
+
s.type = "boolean";
|
|
85
|
+
return s;
|
|
86
|
+
}
|
|
87
|
+
if (type === "number" || type === "range") {
|
|
88
|
+
const step = el.getAttribute("step");
|
|
89
|
+
s.type = step === null || /^\d+$/.test(step.trim()) ? "integer" : "number";
|
|
90
|
+
const min = num(el, "min");
|
|
91
|
+
const max = num(el, "max");
|
|
92
|
+
if (min !== void 0) s.minimum = min;
|
|
93
|
+
if (max !== void 0) s.maximum = max;
|
|
94
|
+
return s;
|
|
95
|
+
}
|
|
96
|
+
s.type = "string";
|
|
97
|
+
if (type === "email") s.format = "email";
|
|
98
|
+
else if (type === "url") s.format = "uri";
|
|
99
|
+
else if (type === "date") s.format = "date";
|
|
100
|
+
else if (type === "time") s.format = "time";
|
|
101
|
+
const minLength = num(el, "minlength");
|
|
102
|
+
const maxLength = num(el, "maxlength");
|
|
103
|
+
if (minLength !== void 0) s.minLength = minLength;
|
|
104
|
+
if (maxLength !== void 0) s.maxLength = maxLength;
|
|
105
|
+
const pattern = el.getAttribute("pattern");
|
|
106
|
+
if (pattern) s.pattern = `^(?:${pattern})$`;
|
|
107
|
+
return s;
|
|
108
|
+
}
|
|
109
|
+
function withDescription(s, desc) {
|
|
110
|
+
if (desc) s.description = desc;
|
|
111
|
+
return s;
|
|
112
|
+
}
|
|
113
|
+
function selectSchema(sel, desc) {
|
|
114
|
+
const opts = Array.from(sel.querySelectorAll("option")).filter((o) => !o.hasAttribute("disabled") && !o.hasAttribute("hidden") && !o.closest("optgroup[disabled]")).map((o) => ({ value: o.getAttribute("value") ?? collapseWhitespace(o.textContent), label: collapseWhitespace(o.textContent) })).filter((o) => o.value !== "");
|
|
115
|
+
const values = opts.map((o) => o.value);
|
|
116
|
+
const list = opts.map((o) => o.label && o.label !== o.value ? `${o.value} (${o.label})` : o.value).join(", ");
|
|
117
|
+
const schema = sel.hasAttribute("multiple") ? { type: "array", items: { type: "string", enum: values } } : { type: "string", enum: values };
|
|
118
|
+
return withDescription(schema, [desc, `Options: ${list}`].filter(Boolean).join(". "));
|
|
119
|
+
}
|
|
120
|
+
var isType = (e, t) => e.tagName === "INPUT" && (e.getAttribute("type") ?? "").toLowerCase() === t;
|
|
121
|
+
function groupSchema(els) {
|
|
122
|
+
const first = els[0];
|
|
123
|
+
const desc = els.map(describe).find(Boolean) ?? "";
|
|
124
|
+
if (first.tagName === "SELECT") return selectSchema(first, desc);
|
|
125
|
+
const values = () => els.map((e) => e.getAttribute("value") ?? "on");
|
|
126
|
+
if (els.every((e) => isType(e, "radio"))) return withDescription({ type: "string", enum: values() }, desc);
|
|
127
|
+
if (els.length > 1 && els.every((e) => isType(e, "checkbox"))) {
|
|
128
|
+
return withDescription({ type: "array", items: { type: "string", enum: values() } }, desc);
|
|
129
|
+
}
|
|
130
|
+
return withDescription(inputSchema(first), desc);
|
|
131
|
+
}
|
|
132
|
+
function schemaFromForm(form) {
|
|
133
|
+
const properties = /* @__PURE__ */ Object.create(null);
|
|
134
|
+
const required = [];
|
|
135
|
+
for (const [name, els] of agentFields(form)) {
|
|
136
|
+
properties[name] = groupSchema(els);
|
|
137
|
+
if (els.some((e) => e.hasAttribute("required"))) required.push(name);
|
|
138
|
+
}
|
|
139
|
+
const out = { type: "object", properties, additionalProperties: false };
|
|
140
|
+
if (required.length) out.required = required;
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
var MAX_REGEX_INPUT = 1e4;
|
|
144
|
+
function checkValue(s, v, field, errors) {
|
|
145
|
+
const bad = (message) => {
|
|
146
|
+
errors.push({ field, message });
|
|
147
|
+
};
|
|
148
|
+
switch (s.type) {
|
|
149
|
+
case "string":
|
|
150
|
+
if (typeof v !== "string") return bad("must be string");
|
|
151
|
+
break;
|
|
152
|
+
case "number":
|
|
153
|
+
if (typeof v !== "number" || !Number.isFinite(v)) return bad("must be number");
|
|
154
|
+
break;
|
|
155
|
+
case "integer":
|
|
156
|
+
if (typeof v !== "number" || !Number.isInteger(v)) return bad("must be integer");
|
|
157
|
+
break;
|
|
158
|
+
case "boolean":
|
|
159
|
+
if (typeof v !== "boolean") return bad("must be boolean");
|
|
160
|
+
break;
|
|
161
|
+
case "array":
|
|
162
|
+
if (!Array.isArray(v)) return bad("must be array");
|
|
163
|
+
break;
|
|
164
|
+
case "object":
|
|
165
|
+
if (!isPlainObject(v)) return bad("must be object");
|
|
166
|
+
checkObject(s, v, `${field}.`, errors);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (s.enum && !s.enum.includes(v)) {
|
|
170
|
+
return bad(`must be one of: ${s.enum.join(", ")}`);
|
|
171
|
+
}
|
|
172
|
+
if (typeof v === "string") {
|
|
173
|
+
if (s.minLength !== void 0 && v.length < s.minLength) bad(`must be at least ${s.minLength} characters`);
|
|
174
|
+
if (s.maxLength !== void 0 && v.length > s.maxLength) bad(`must be at most ${s.maxLength} characters`);
|
|
175
|
+
if ((s.pattern !== void 0 || s.format === "email") && v.length > MAX_REGEX_INPUT) {
|
|
176
|
+
return bad(`must be at most ${MAX_REGEX_INPUT} characters to be checked`);
|
|
177
|
+
}
|
|
178
|
+
if (s.pattern !== void 0) {
|
|
179
|
+
let ok = false;
|
|
180
|
+
try {
|
|
181
|
+
ok = new RegExp(s.pattern).test(v);
|
|
182
|
+
} catch {
|
|
183
|
+
ok = false;
|
|
184
|
+
}
|
|
185
|
+
if (!ok) bad(`must match pattern ${s.pattern}`);
|
|
186
|
+
}
|
|
187
|
+
if (s.format === "email" && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v)) bad("must be a valid email");
|
|
188
|
+
if (s.format === "uri") {
|
|
189
|
+
try {
|
|
190
|
+
new URL(v);
|
|
191
|
+
} catch {
|
|
192
|
+
bad("must be a valid URI");
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (typeof v === "number") {
|
|
197
|
+
if (s.minimum !== void 0 && v < s.minimum) bad(`must be >= ${s.minimum}`);
|
|
198
|
+
if (s.maximum !== void 0 && v > s.maximum) bad(`must be <= ${s.maximum}`);
|
|
199
|
+
}
|
|
200
|
+
if (Array.isArray(v) && s.items) {
|
|
201
|
+
v.forEach((item, i) => checkValue(s.items, item, `${field}[${i}]`, errors));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
function isPlainObject(v) {
|
|
205
|
+
if (typeof v !== "object" || v === null) return false;
|
|
206
|
+
const proto = Object.getPrototypeOf(v);
|
|
207
|
+
return proto === Object.prototype || proto === null;
|
|
208
|
+
}
|
|
209
|
+
function checkObject(schema, obj, prefix, errors) {
|
|
210
|
+
const props = schema.properties ?? {};
|
|
211
|
+
for (const name of schema.required ?? []) {
|
|
212
|
+
if (!hasOwn(obj, name) || obj[name] === void 0) errors.push({ field: prefix + name, message: "is required" });
|
|
213
|
+
}
|
|
214
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
215
|
+
const prop = props[key];
|
|
216
|
+
if (!hasOwn(props, key) || !prop) {
|
|
217
|
+
if (schema.additionalProperties !== true) errors.push({ field: prefix + key, message: "is not an allowed field" });
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (value === void 0) continue;
|
|
221
|
+
checkValue(prop, value, prefix + key, errors);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
function validateInput(schema, input) {
|
|
225
|
+
if (!isPlainObject(input)) return { ok: false, errors: [{ field: "", message: "input must be an object" }] };
|
|
226
|
+
const errors = [];
|
|
227
|
+
checkObject(schema, input, "", errors);
|
|
228
|
+
return errors.length ? { ok: false, errors } : { ok: true, value: input };
|
|
229
|
+
}
|
|
230
|
+
function validateFormInput(schema, input) {
|
|
231
|
+
const r = validateInput({ ...schema, additionalProperties: true }, input);
|
|
232
|
+
return r.ok ? r : validateInput(schema, input);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export {
|
|
236
|
+
hasOwn,
|
|
237
|
+
inDisabledFieldset,
|
|
238
|
+
isDisabled,
|
|
239
|
+
isConcealed,
|
|
240
|
+
EMPTY_OBJECT_SCHEMA,
|
|
241
|
+
agentFields,
|
|
242
|
+
schemaFromForm,
|
|
243
|
+
validateInput,
|
|
244
|
+
validateFormInput
|
|
245
|
+
};
|
|
@@ -52,15 +52,29 @@ function withoutBlockedLoads(errors, blocked) {
|
|
|
52
52
|
function readyTimeoutMessage(timeoutMs) {
|
|
53
53
|
return `did not become ready within ${timeoutMs}ms`;
|
|
54
54
|
}
|
|
55
|
+
var TimeoutError = class extends Error {
|
|
56
|
+
constructor(message) {
|
|
57
|
+
super(message);
|
|
58
|
+
this.name = "TimeoutError";
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
var SNAPSHOT_GRACE_MS = 5e3;
|
|
62
|
+
function snapshotWarning(timeoutMs) {
|
|
63
|
+
return `${readyTimeoutMessage(timeoutMs)}; saved the page as it was at the deadline (prerender.onTimeout: "snapshot"). If it is still loading or animating, set prerender.readySelector to an element that marks it ready`;
|
|
64
|
+
}
|
|
55
65
|
var POLL_MS = 50;
|
|
56
66
|
var SETTLED_POLL_MS = 20;
|
|
67
|
+
async function assertNoPageErrors(readErrors) {
|
|
68
|
+
const errors = await readErrors();
|
|
69
|
+
if (errors.length) throw new Error(errors[0]);
|
|
70
|
+
}
|
|
57
71
|
async function waitForReady(check, timeoutMs, pollMs = POLL_MS) {
|
|
58
72
|
const deadline = Date.now() + timeoutMs;
|
|
59
73
|
for (; ; ) {
|
|
60
74
|
const { ready, errors } = await check();
|
|
61
75
|
if (errors.length) throw new Error(errors[0]);
|
|
62
76
|
if (ready) return;
|
|
63
|
-
if (Date.now() >= deadline) throw new
|
|
77
|
+
if (Date.now() >= deadline) throw new TimeoutError(readyTimeoutMessage(timeoutMs));
|
|
64
78
|
await new Promise((r) => setTimeout(r, pollMs));
|
|
65
79
|
}
|
|
66
80
|
}
|
|
@@ -72,7 +86,7 @@ function createDeadline(timeoutMs) {
|
|
|
72
86
|
race(work, message) {
|
|
73
87
|
let timer;
|
|
74
88
|
const expired = new Promise((_, reject) => {
|
|
75
|
-
timer = setTimeout(() => reject(new
|
|
89
|
+
timer = setTimeout(() => reject(new TimeoutError(message)), remaining());
|
|
76
90
|
});
|
|
77
91
|
work.catch(() => {
|
|
78
92
|
});
|
|
@@ -103,8 +117,12 @@ export {
|
|
|
103
117
|
blockedHint,
|
|
104
118
|
withoutBlockedLoads,
|
|
105
119
|
readyTimeoutMessage,
|
|
120
|
+
TimeoutError,
|
|
121
|
+
SNAPSHOT_GRACE_MS,
|
|
122
|
+
snapshotWarning,
|
|
106
123
|
POLL_MS,
|
|
107
124
|
SETTLED_POLL_MS,
|
|
125
|
+
assertNoPageErrors,
|
|
108
126
|
waitForReady,
|
|
109
127
|
createDeadline,
|
|
110
128
|
runInPage
|