polite-crawler 0.0.0-stage → 0.1.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.
Files changed (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +293 -2
  3. package/build/browser-gJ_h4AhB.mjs +511 -0
  4. package/build/browser-gJ_h4AhB.mjs.map +1 -0
  5. package/build/classify-CrfyXcwo.d.mts +51 -0
  6. package/build/classify.d.mts +2 -0
  7. package/build/classify.mjs +116 -0
  8. package/build/classify.mjs.map +1 -0
  9. package/build/cli.mjs +348 -0
  10. package/build/cli.mjs.map +1 -0
  11. package/build/hours.d.mts +113 -0
  12. package/build/hours.mjs +905 -0
  13. package/build/hours.mjs.map +1 -0
  14. package/build/html-nWYFVsRU.d.mts +98 -0
  15. package/build/html-oNpGAHwi.mjs +1038 -0
  16. package/build/html-oNpGAHwi.mjs.map +1 -0
  17. package/build/html.d.mts +2 -0
  18. package/build/html.mjs +1 -0
  19. package/build/index.d.mts +131 -0
  20. package/build/index.mjs +8 -0
  21. package/build/mcp.d.mts +40 -0
  22. package/build/mcp.mjs +480 -0
  23. package/build/mcp.mjs.map +1 -0
  24. package/build/open-CJR4b44v.mjs +4888 -0
  25. package/build/open-CJR4b44v.mjs.map +1 -0
  26. package/build/open-CZqwY7GH.d.mts +1461 -0
  27. package/build/politeness-C5bZ1OKM.d.mts +274 -0
  28. package/build/politeness.d.mts +2 -0
  29. package/build/politeness.mjs +372 -0
  30. package/build/politeness.mjs.map +1 -0
  31. package/build/robots-R-qaEBOb.d.mts +144 -0
  32. package/build/robots.d.mts +2 -0
  33. package/build/robots.mjs +369 -0
  34. package/build/robots.mjs.map +1 -0
  35. package/build/signals-BHpy2xb2.d.mts +46 -0
  36. package/build/signals.d.mts +2 -0
  37. package/build/signals.mjs +113 -0
  38. package/build/signals.mjs.map +1 -0
  39. package/build/version-rVx8_y1_.mjs +11 -0
  40. package/build/version-rVx8_y1_.mjs.map +1 -0
  41. package/package.json +131 -3
  42. package/record.schema.json +198 -0
@@ -0,0 +1,511 @@
1
+ import { basename, dirname, join } from "node:path";
2
+ import { existsSync } from "node:fs";
3
+ //#region \0rolldown/runtime.js
4
+ var __defProp = Object.defineProperty;
5
+ var __exportAll = (all, no_symbols) => {
6
+ let target = {};
7
+ for (var name in all) __defProp(target, name, {
8
+ get: all[name],
9
+ enumerable: true
10
+ });
11
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
12
+ return target;
13
+ };
14
+ //#endregion
15
+ //#region src/render.ts
16
+ /**
17
+ * This error tells that the renderer did not render the page, and that the page or the site is
18
+ * not at fault. The reason is one of these:
19
+ *
20
+ * - `refused`: `allowRequest` refused the first navigation, a step of its redirects, or a later
21
+ * navigation of the page that left it on an error page. `url` names the refused URL.
22
+ * - `download`: the URL gives a file, not a page.
23
+ * - `scheme`: the URL is not an http or https URL.
24
+ * - `browser`: the renderer has no browser (`playwright-core` or its browser is not installed). The
25
+ * crawler then renders no more pages, and gives each one the note at once.
26
+ *
27
+ * A crawler does not count this error as a failure of the host.
28
+ */
29
+ var RenderRefused = class extends Error {
30
+ reason;
31
+ /** This URL is the request that `allowRequest` or the limit of redirects refused, when the reason is `refused`. */
32
+ url;
33
+ constructor(reason, message, url) {
34
+ super(message);
35
+ this.name = "RenderRefused";
36
+ this.reason = reason;
37
+ this.url = url;
38
+ }
39
+ };
40
+ /** The browser does not ask for these. The text of a page needs none of them. */
41
+ const BLOCKED_TYPES = /* @__PURE__ */ new Set([
42
+ "image",
43
+ "font",
44
+ "media"
45
+ ]);
46
+ /** These statuses send the request on to the URL in `Location` (the Fetch standard). */
47
+ const REDIRECTS = /* @__PURE__ */ new Set([
48
+ 301,
49
+ 302,
50
+ 303,
51
+ 307,
52
+ 308
53
+ ]);
54
+ const MAX_REDIRECTS = 5;
55
+ /** The text is stable when it does not grow for this time. */
56
+ const SETTLED_MS = 1e3;
57
+ const POLL_MS = 250;
58
+ const SETTLE_CAP_MS = 12e3;
59
+ /** A page with no text after this time has nothing more to show. */
60
+ const EMPTY_SETTLE_MS = 3e3;
61
+ /** The part of the deadline that is kept to read the page after the wait. */
62
+ const READ_RESERVE_MS = 2e3;
63
+ /** A context that does not close in this time takes its browser with it. */
64
+ const CLOSE_MS = 5e3;
65
+ /** A browser that does not open a context in this time is replaced. */
66
+ const OPEN_MS = 5e3;
67
+ /** The text of the error when the optional package `playwright-core` is not installed. */
68
+ const NO_PLAYWRIGHT = "rendering needs the package playwright-core and a browser: npm install playwright-core, then npx playwright-core install chromium-headless-shell. For the MCP server with npx, start it with npx -y -p polite-crawler -p playwright-core polite-crawler mcp";
69
+ const launch = async () => {
70
+ let chromium;
71
+ try {
72
+ ({chromium} = await import("playwright-core"));
73
+ } catch (cause) {
74
+ throw new Error(NO_PLAYWRIGHT, { cause });
75
+ }
76
+ return chromium.launch({ headless: true });
77
+ };
78
+ /** This script gives the length of the text that the page shows: its body, its open shadow roots and its frames of the same origin. */
79
+ const MEASURE = `(() => {
80
+ let total = document.body ? document.body.innerText.length : 0;
81
+ const visit = (root) => {
82
+ for (const element of root.querySelectorAll("*")) {
83
+ if (element.shadowRoot) {
84
+ total += (element.shadowRoot.textContent || "").length;
85
+ visit(element.shadowRoot);
86
+ }
87
+ }
88
+ };
89
+ visit(document);
90
+ for (const frame of document.querySelectorAll("iframe, frame")) {
91
+ try {
92
+ const inner = frame.contentDocument;
93
+ if (inner && inner.body) total += inner.body.innerText.length;
94
+ } catch (error) {}
95
+ }
96
+ return total;
97
+ })()`;
98
+ /** This script gives the HTML of the page with its open shadow roots, and the body of each frame of the same origin. */
99
+ const SERIALIZE = `(() => {
100
+ const roots = [];
101
+ const visit = (root) => {
102
+ for (const element of root.querySelectorAll("*")) {
103
+ if (element.shadowRoot) {
104
+ roots.push(element.shadowRoot);
105
+ visit(element.shadowRoot);
106
+ }
107
+ }
108
+ };
109
+ visit(document);
110
+ const root = document.documentElement;
111
+ if (!root) return { html: "", frames: [] };
112
+ const quote = (value) => value.replace(/&/g, "&").replace(/"/g, """);
113
+ const open = "<html" + Array.from(root.attributes, (item) => " " + item.name + '="' + quote(item.value) + '"').join("") + ">";
114
+ const inner =
115
+ roots.length > 0 && typeof root.getHTML === "function"
116
+ ? root.getHTML({ serializableShadowRoots: true, shadowRoots: roots })
117
+ : root.innerHTML;
118
+ const frames = [];
119
+ for (const frame of document.querySelectorAll("iframe, frame")) {
120
+ try {
121
+ const doc = frame.contentDocument;
122
+ if (doc && doc.body) frames.push({ src: frame.src || "", html: doc.body.innerHTML });
123
+ } catch (error) {}
124
+ }
125
+ return { html: "<!DOCTYPE html>" + open + inner + "</html>", frames };
126
+ })()`;
127
+ /** This function puts the body of each frame at the end of the page, in a section that names the frame. */
128
+ const withFrames = ({ html, frames }) => {
129
+ if (frames.length === 0) return html;
130
+ const sections = frames.map(({ src, html: body }) => `<section data-polite-frame="${src.replace(/&/g, "&amp;").replace(/"/g, "&quot;")}">${body}</section>`).join("");
131
+ const end = html.toLowerCase().lastIndexOf("</body>");
132
+ return end === -1 ? `${html}${sections}` : `${html.slice(0, end)}${sections}${html.slice(end)}`;
133
+ };
134
+ const messageOf = (error) => error instanceof Error ? error.message : String(error);
135
+ const sleep = (ms) => new Promise((resolve) => {
136
+ setTimeout(resolve, Math.max(0, ms)).unref();
137
+ });
138
+ /** This function gives the value of a promise, or undefined when the time ends first. */
139
+ const within = async (promise, ms) => {
140
+ let timer;
141
+ const late = new Promise((resolve) => {
142
+ timer = setTimeout(() => resolve(void 0), Math.max(0, ms));
143
+ });
144
+ try {
145
+ return await Promise.race([promise, late]);
146
+ } finally {
147
+ clearTimeout(timer);
148
+ }
149
+ };
150
+ const playwrightRenderer = (options = {}) => {
151
+ const idleMs = options.idleMs ?? 12e4;
152
+ const start = options.launch ?? launch;
153
+ let browser;
154
+ let inFlight = 0;
155
+ let idle;
156
+ const shutSoon = () => {
157
+ if (idle !== void 0) clearTimeout(idle);
158
+ idle = setTimeout(() => {
159
+ idle = void 0;
160
+ if (inFlight > 0 || browser === void 0) return;
161
+ const closing = browser;
162
+ browser = void 0;
163
+ closing.then((running) => running.close()).catch(() => void 0);
164
+ }, idleMs);
165
+ idle.unref();
166
+ };
167
+ const acquire = async () => {
168
+ for (let attempt = 0;; attempt += 1) {
169
+ if (browser === void 0) {
170
+ const launching = start().then((running) => {
171
+ running.on("disconnected", () => {
172
+ if (browser === launching) browser = void 0;
173
+ });
174
+ return running;
175
+ });
176
+ launching.catch(() => {
177
+ if (browser === launching) browser = void 0;
178
+ });
179
+ browser = launching;
180
+ }
181
+ const held = browser;
182
+ const running = await held;
183
+ if (running.isConnected() || attempt > 0) return running;
184
+ if (browser === held) browser = void 0;
185
+ }
186
+ };
187
+ /** This function gives up on a browser that does not answer. The next page launches a new one. */
188
+ const forget = (running) => {
189
+ const held = browser;
190
+ if (held !== void 0) held.then((current) => {
191
+ if (current === running && browser === held) browser = void 0;
192
+ }, () => void 0);
193
+ running.close().catch(() => void 0);
194
+ };
195
+ const renderPage = async (url, { userAgent, timeoutMs, headers, allowRequest, maxRedirects = MAX_REDIRECTS, signal }) => {
196
+ if (signal?.aborted === true) throw signal.reason instanceof Error ? signal.reason : /* @__PURE__ */ new Error("the render was cancelled");
197
+ const deadline = Date.now() + timeoutMs;
198
+ const left = () => Math.max(1, deadline - Date.now());
199
+ let running;
200
+ let context;
201
+ let shownPage;
202
+ /** The last navigation of the page that the route refused: the reason, and the refused URL. */
203
+ let refused;
204
+ /** The last navigation of the page that the route answered: the URL that the browser asked for, and the URL of the answer. */
205
+ let navigated;
206
+ const extra = Object.fromEntries(Object.entries(headers ?? {}).map(([name, value]) => [name.toLowerCase(), value]));
207
+ const contextOptions = {
208
+ userAgent,
209
+ serviceWorkers: "block",
210
+ acceptDownloads: false,
211
+ viewport: {
212
+ width: 1280,
213
+ height: 900
214
+ }
215
+ };
216
+ const ask = async (url) => allowRequest === void 0 ? true : Promise.resolve().then(() => allowRequest(url)).catch(() => false);
217
+ /**
218
+ * This function sends one request of the page and follows its redirects. Each step is a new
219
+ * decision of `allowRequest`, and a step past `maxRedirects` is refused. It gives the answer,
220
+ * or the reason of a refusal.
221
+ *
222
+ * These rules are the rules of a browser. Each step gets the cookies of its own URL from the
223
+ * context. A credential does not go to a different origin. A 303, or a 301 or 302 after a POST,
224
+ * changes the request to a GET.
225
+ */
226
+ const follow = async (route, sent) => {
227
+ const request = route.request();
228
+ let url = request.url();
229
+ let method = request.method();
230
+ let headers = {
231
+ ...request.headers(),
232
+ ...extra
233
+ };
234
+ let response = await route.fetch({
235
+ maxRedirects: 0,
236
+ timeout: left(),
237
+ ...sent === void 0 ? {} : { headers: sent }
238
+ });
239
+ for (let hop = 0;; hop += 1) {
240
+ const location = REDIRECTS.has(response.status()) ? response.headers()["location"] : void 0;
241
+ if (location === void 0) return {
242
+ response,
243
+ url
244
+ };
245
+ let next;
246
+ try {
247
+ next = new URL(location, url);
248
+ } catch {
249
+ return {
250
+ refusal: `${url} redirects to a URL that does not parse`,
251
+ url
252
+ };
253
+ }
254
+ if (hop >= maxRedirects) return {
255
+ refusal: `${request.url()} redirects more than ${String(maxRedirects)} times`,
256
+ url: next.href
257
+ };
258
+ if (!await ask(next.href)) return {
259
+ refusal: `the request for ${next.href}, a redirect of ${url}, was refused`,
260
+ url: next.href
261
+ };
262
+ const status = response.status();
263
+ const get = status === 303 || (status === 301 || status === 302) && method === "POST";
264
+ const crossOrigin = next.origin !== new URL(url).origin;
265
+ headers = Object.fromEntries(Object.entries(headers).filter(([name]) => name !== "cookie" && !(crossOrigin && name === "authorization") && !(get && (name === "content-type" || name === "content-length"))));
266
+ if (get) method = "GET";
267
+ const body = get ? void 0 : request.postDataBuffer() ?? void 0;
268
+ response = await context.request.fetch(next.href, {
269
+ method,
270
+ headers,
271
+ maxRedirects: 0,
272
+ timeout: left(),
273
+ ...body === void 0 ? {} : { data: body }
274
+ });
275
+ url = next.href;
276
+ }
277
+ };
278
+ /** This function tells if a request is a navigation of the page itself, not of a frame or a popup. */
279
+ const ofPage = (request) => {
280
+ if (!request.isNavigationRequest()) return false;
281
+ try {
282
+ return request.frame().parentFrame() === null && request.frame().page() === shownPage;
283
+ } catch {
284
+ return false;
285
+ }
286
+ };
287
+ const decide = async (route) => {
288
+ const request = route.request();
289
+ const main = ofPage(request);
290
+ try {
291
+ if (BLOCKED_TYPES.has(request.resourceType())) {
292
+ await route.abort("blockedbyclient");
293
+ return;
294
+ }
295
+ if (!await ask(request.url())) {
296
+ if (main) refused = {
297
+ message: `the request for ${request.url()} was refused`,
298
+ url: request.url()
299
+ };
300
+ await route.abort("blockedbyclient");
301
+ return;
302
+ }
303
+ const sent = Object.keys(extra).length === 0 ? void 0 : {
304
+ ...Object.fromEntries(Object.entries(await request.allHeaders()).filter(([name]) => !name.startsWith(":") && name !== "host")),
305
+ ...extra
306
+ };
307
+ if (allowRequest === void 0) {
308
+ await route.continue(sent === void 0 ? void 0 : { headers: sent });
309
+ return;
310
+ }
311
+ const answer = await follow(route, sent);
312
+ if ("refusal" in answer) {
313
+ if (main) refused = {
314
+ message: answer.refusal,
315
+ url: answer.url
316
+ };
317
+ await route.abort("blockedbyclient");
318
+ return;
319
+ }
320
+ if (main) navigated = {
321
+ asked: request.url(),
322
+ final: answer.url
323
+ };
324
+ await route.fulfill({ response: answer.response });
325
+ } catch {}
326
+ };
327
+ const work = (async () => {
328
+ running = await acquire();
329
+ const opened = await within(running.newContext(contextOptions).catch((error) => {
330
+ if (!/closed|disconnected/i.test(messageOf(error))) throw error;
331
+ }), Math.min(OPEN_MS, left()));
332
+ if (opened === void 0) {
333
+ forget(running);
334
+ running = await acquire();
335
+ context = await running.newContext(contextOptions);
336
+ } else context = opened;
337
+ await context.route("**/*", decide);
338
+ if (allowRequest !== void 0 && typeof context.routeWebSocket === "function") await context.routeWebSocket(/.*/, async (socket) => {
339
+ if (await Promise.resolve().then(() => allowRequest(socket.url().replace(/^ws/i, "http"))).catch(() => false)) socket.connectToServer();
340
+ else await socket.close({
341
+ code: 1008,
342
+ reason: "refused"
343
+ }).catch(() => void 0);
344
+ });
345
+ const page = await context.newPage();
346
+ shownPage = page;
347
+ context.on("page", (other) => {
348
+ if (other !== page) other.close().catch(() => void 0);
349
+ });
350
+ page.on("dialog", (dialog) => {
351
+ dialog.dismiss().catch(() => void 0);
352
+ });
353
+ let response;
354
+ try {
355
+ response = await page.goto(url, {
356
+ waitUntil: "domcontentloaded",
357
+ timeout: Math.max(1, left() - READ_RESERVE_MS)
358
+ });
359
+ } catch (error) {
360
+ if (refused !== void 0) throw new RenderRefused("refused", refused.message, refused.url);
361
+ if (/Download is starting/i.test(messageOf(error))) throw new RenderRefused("download", `${url} gives a file, not a page`);
362
+ throw error;
363
+ }
364
+ const settleEnd = Math.min(deadline - READ_RESERVE_MS, Date.now() + SETTLE_CAP_MS);
365
+ const settleStart = Date.now();
366
+ let last = -1;
367
+ let since = Date.now();
368
+ while (Date.now() < settleEnd) {
369
+ const measured = page.evaluate(MEASURE).catch(async (error) => {
370
+ if (!/destroyed|navigat/i.test(messageOf(error))) throw error;
371
+ await page.waitForLoadState("domcontentloaded", { timeout: left() }).catch(() => void 0);
372
+ return NaN;
373
+ });
374
+ const length = await within(measured, settleEnd - Date.now());
375
+ if (length === void 0) break;
376
+ if (length !== last) {
377
+ last = length;
378
+ since = Date.now();
379
+ } else if (Date.now() - since >= SETTLED_MS && (length > 0 || Date.now() - settleStart >= EMPTY_SETTLE_MS)) break;
380
+ await sleep(Math.min(POLL_MS, settleEnd - Date.now()));
381
+ }
382
+ let serialized;
383
+ while (serialized === void 0) try {
384
+ serialized = await page.evaluate(SERIALIZE);
385
+ } catch (error) {
386
+ if (!/destroyed|navigat/i.test(messageOf(error)) || Date.now() >= deadline) throw error;
387
+ await page.waitForLoadState("domcontentloaded", { timeout: left() }).catch(() => void 0);
388
+ }
389
+ if (refused !== void 0 && page.url().startsWith("chrome-error:")) throw new RenderRefused("refused", `the page moved: ${refused.message}`, refused.url);
390
+ const shown = page.url().replace(/#.*$/s, "");
391
+ return {
392
+ html: withFrames(serialized),
393
+ finalUrl: navigated !== void 0 && shown === navigated.asked ? navigated.final : page.url(),
394
+ status: response?.status() ?? null,
395
+ headers: response?.headers() ?? {}
396
+ };
397
+ })();
398
+ work.catch(() => void 0);
399
+ let timer;
400
+ let cancel;
401
+ const expired = new Promise((_, reject) => {
402
+ timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`render timed out after ${String(timeoutMs)} ms`)), timeoutMs);
403
+ if (signal !== void 0) {
404
+ cancel = () => reject(signal.reason instanceof Error ? signal.reason : /* @__PURE__ */ new Error("the render was cancelled"));
405
+ signal.addEventListener("abort", cancel, { once: true });
406
+ }
407
+ });
408
+ const closeContext = async () => {
409
+ const open = context;
410
+ if (open === void 0) return;
411
+ if (await within(open.close().then(() => true, () => true), CLOSE_MS) === void 0 && running !== void 0) forget(running);
412
+ };
413
+ try {
414
+ return await Promise.race([work, expired]);
415
+ } finally {
416
+ clearTimeout(timer);
417
+ if (cancel !== void 0) signal?.removeEventListener("abort", cancel);
418
+ if (context !== void 0) await closeContext();
419
+ else work.then(closeContext, closeContext).catch(() => void 0);
420
+ }
421
+ };
422
+ return {
423
+ open: () => browser !== void 0,
424
+ async render(url, renderOptions) {
425
+ let target;
426
+ try {
427
+ target = new URL(url);
428
+ } catch {
429
+ throw new RenderRefused("scheme", `${url} is not a URL`);
430
+ }
431
+ if (target.protocol !== "http:" && target.protocol !== "https:") throw new RenderRefused("scheme", `the renderer reads http and https pages, not ${target.protocol}`);
432
+ if (idle !== void 0) {
433
+ clearTimeout(idle);
434
+ idle = void 0;
435
+ }
436
+ inFlight += 1;
437
+ try {
438
+ return await renderPage(target.href, renderOptions);
439
+ } finally {
440
+ inFlight -= 1;
441
+ if (inFlight === 0) shutSoon();
442
+ }
443
+ },
444
+ async close() {
445
+ if (idle !== void 0) clearTimeout(idle);
446
+ idle = void 0;
447
+ const launched = browser;
448
+ browser = void 0;
449
+ if (launched !== void 0) await launched.then((open) => open.close(), () => void 0);
450
+ }
451
+ };
452
+ };
453
+ //#endregion
454
+ //#region src/browser.ts
455
+ var browser_exports = /* @__PURE__ */ __exportAll({
456
+ NO_BROWSER: () => NO_BROWSER,
457
+ browserMissing: () => browserMissing,
458
+ checkedRenderer: () => checkedRenderer
459
+ });
460
+ /** The text of the note when `playwright-core` is installed, but its browser is not. */
461
+ const NO_BROWSER = "rendering needs a browser: npx playwright-core install chromium-headless-shell";
462
+ /**
463
+ * This function tells if the default renderer can start a browser. It gives no value when it can,
464
+ * and the reason when it cannot. The command line and the MCP server use it one time at start.
465
+ * Then a missing browser costs no wait and no error capture for each page that needs JavaScript.
466
+ *
467
+ * The check does not start the browser. It finds the full Chromium build or the headless shell of
468
+ * the same revision on the disk. When the check cannot find the answer, it gives no value, and the
469
+ * renderer then tries.
470
+ */
471
+ const browserMissing = async (load = () => import("playwright-core")) => {
472
+ let chromium;
473
+ try {
474
+ ({chromium} = await load());
475
+ } catch {
476
+ return NO_PLAYWRIGHT;
477
+ }
478
+ try {
479
+ const executable = chromium.executablePath();
480
+ if (existsSync(executable)) return void 0;
481
+ let folder = dirname(executable);
482
+ while (dirname(folder) !== folder && !/^chromium-\d+$/.test(basename(folder))) folder = dirname(folder);
483
+ const revision = /^chromium-(\d+)$/.exec(basename(folder))?.[1];
484
+ if (revision === void 0) return void 0;
485
+ return existsSync(join(dirname(folder), `chromium_headless_shell-${revision}`)) ? void 0 : NO_BROWSER;
486
+ } catch {
487
+ return;
488
+ }
489
+ };
490
+ /**
491
+ * This function gives a renderer that checks one time, before its first page, that the browser is
492
+ * on the disk (`browserMissing`). With no browser, each page gets a `RenderRefused` with the reason
493
+ * `browser` at once, and the renderer opens nothing. `createRuntime` puts its default renderer in
494
+ * it, so a library with no browser costs no wait and no error capture.
495
+ */
496
+ const checkedRenderer = (renderer, check = () => browserMissing()) => {
497
+ let missing;
498
+ return {
499
+ async render(url, options) {
500
+ missing ??= check().catch(() => void 0);
501
+ const reason = await missing;
502
+ if (reason !== void 0) throw new RenderRefused("browser", reason);
503
+ return renderer.render(url, options);
504
+ },
505
+ close: () => renderer.close()
506
+ };
507
+ };
508
+ //#endregion
509
+ export { RenderRefused as a, NO_PLAYWRIGHT as i, browser_exports as n, playwrightRenderer as o, checkedRenderer as r, __exportAll as s, browserMissing as t };
510
+
511
+ //# sourceMappingURL=browser-gJ_h4AhB.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser-gJ_h4AhB.mjs","names":[],"sources":["../src/render.ts","../src/browser.ts"],"sourcesContent":["import type { APIResponse, Browser, BrowserContext, Page, Request, Route } from \"playwright-core\";\n\n/**\n * The renderer opens the headless browser only for a page that shows almost nothing without\n * JavaScript. The settings:\n *\n * - The default `chromium-headless-shell` of Playwright, the lighter build.\n * - One browser for many pages, and a new context for each page. Thus no cookie or storage goes\n * from one site to the next. A context has its own HTTP cache, so routing each request costs\n * no cache across sites.\n * - Each request of the page goes through a route, the first navigation too. The route refuses\n * images, fonts and media by their resource type. It asks `allowRequest` about each other\n * request, and it sends `headers` with it.\n * - Playwright routes only the first request of a redirect chain. Thus with `allowRequest`, the\n * route sends each request itself, follows each redirect itself and asks about each step. The\n * browser then gets each body whole, not as a stream.\n * - No download, no popup, no service worker and no dialog.\n * - A wait until the visible text stops growing, with a limit. Playwright discourages\n * `networkidle`. The text includes open shadow roots and the frames of the same origin.\n * - One deadline for the whole render: the navigation, the wait and the reading of the page.\n * - After a crash or a disconnection, the next page launches a new browser.\n * - The browser closes after two idle minutes. An idle Chromium holds hundreds of megabytes, so\n * it stays open only while pages need it. The open browser keeps the process alive, so a\n * script uses `close()` when it is done.\n */\n\nexport interface Rendered {\n readonly html: string;\n readonly finalUrl: string;\n readonly status: number | null;\n readonly headers: Readonly<Record<string, string>>;\n}\n\n/** These are the options of one render. */\nexport interface RenderOptions {\n readonly userAgent: string;\n /** This value is the deadline of the whole render, in milliseconds. */\n readonly timeoutMs: number;\n /** These headers go with each request of the page, for example the `From` header. */\n readonly headers?: Readonly<Record<string, string>> | undefined;\n /**\n * This function decides each request that the page makes, the first navigation included. When\n * it gives false, the browser does not send the request. The crawler uses it to apply the\n * egress policy and robots.txt to the requests of the page.\n */\n readonly allowRequest?: ((url: string) => Promise<boolean> | boolean) | undefined;\n /** This value is the maximum number of redirects of one request of the page. The default is 5, as for a plain fetch. */\n readonly maxRedirects?: number | undefined;\n /** When this signal aborts, the renderer stops the page: it closes the context, and the render rejects. */\n readonly signal?: AbortSignal | undefined;\n}\n\nexport interface Renderer {\n render(url: string, options: RenderOptions): Promise<Rendered>;\n close(): Promise<void>;\n}\n\n/** The reason of a `RenderRefused`. */\nexport type RefusedReason = \"refused\" | \"download\" | \"scheme\" | \"browser\";\n\n/**\n * This error tells that the renderer did not render the page, and that the page or the site is\n * not at fault. The reason is one of these:\n *\n * - `refused`: `allowRequest` refused the first navigation, a step of its redirects, or a later\n * navigation of the page that left it on an error page. `url` names the refused URL.\n * - `download`: the URL gives a file, not a page.\n * - `scheme`: the URL is not an http or https URL.\n * - `browser`: the renderer has no browser (`playwright-core` or its browser is not installed). The\n * crawler then renders no more pages, and gives each one the note at once.\n *\n * A crawler does not count this error as a failure of the host.\n */\nexport class RenderRefused extends Error {\n readonly reason: RefusedReason;\n /** This URL is the request that `allowRequest` or the limit of redirects refused, when the reason is `refused`. */\n readonly url: string | undefined;\n constructor(reason: RefusedReason, message: string, url?: string) {\n super(message);\n this.name = \"RenderRefused\";\n this.reason = reason;\n this.url = url;\n }\n}\n\n/** The browser does not ask for these. The text of a page needs none of them. */\nconst BLOCKED_TYPES = new Set([\"image\", \"font\", \"media\"]);\n/** These statuses send the request on to the URL in `Location` (the Fetch standard). */\nconst REDIRECTS = new Set([301, 302, 303, 307, 308]);\nconst MAX_REDIRECTS = 5;\n\n/** The text is stable when it does not grow for this time. */\nconst SETTLED_MS = 1000;\nconst POLL_MS = 250;\nconst SETTLE_CAP_MS = 12_000;\n/** A page with no text after this time has nothing more to show. */\nconst EMPTY_SETTLE_MS = 3000;\n/** The part of the deadline that is kept to read the page after the wait. */\nconst READ_RESERVE_MS = 2000;\n/** A context that does not close in this time takes its browser with it. */\nconst CLOSE_MS = 5000;\n/** A browser that does not open a context in this time is replaced. */\nconst OPEN_MS = 5000;\n\n/** The text of the error when the optional package `playwright-core` is not installed. */\nexport const NO_PLAYWRIGHT =\n \"rendering needs the package playwright-core and a browser: npm install playwright-core, then npx playwright-core install chromium-headless-shell. For the MCP server with npx, start it with npx -y -p polite-crawler -p playwright-core polite-crawler mcp\";\n\n// playwright-core is an optional peer: it loads only when a page first needs a browser\nconst launch = async (): Promise<Browser> => {\n let chromium: typeof import(\"playwright-core\").chromium;\n try {\n ({ chromium } = await import(\"playwright-core\"));\n } catch (cause) {\n throw new Error(NO_PLAYWRIGHT, { cause });\n }\n return chromium.launch({ headless: true });\n};\n\n/** This script gives the length of the text that the page shows: its body, its open shadow roots and its frames of the same origin. */\nconst MEASURE = `(() => {\n let total = document.body ? document.body.innerText.length : 0;\n const visit = (root) => {\n for (const element of root.querySelectorAll(\"*\")) {\n if (element.shadowRoot) {\n total += (element.shadowRoot.textContent || \"\").length;\n visit(element.shadowRoot);\n }\n }\n };\n visit(document);\n for (const frame of document.querySelectorAll(\"iframe, frame\")) {\n try {\n const inner = frame.contentDocument;\n if (inner && inner.body) total += inner.body.innerText.length;\n } catch (error) {}\n }\n return total;\n})()`;\n\n/** This script gives the HTML of the page with its open shadow roots, and the body of each frame of the same origin. */\nconst SERIALIZE = `(() => {\n const roots = [];\n const visit = (root) => {\n for (const element of root.querySelectorAll(\"*\")) {\n if (element.shadowRoot) {\n roots.push(element.shadowRoot);\n visit(element.shadowRoot);\n }\n }\n };\n visit(document);\n const root = document.documentElement;\n if (!root) return { html: \"\", frames: [] };\n const quote = (value) => value.replace(/&/g, \"&amp;\").replace(/\"/g, \"&quot;\");\n const open = \"<html\" + Array.from(root.attributes, (item) => \" \" + item.name + '=\"' + quote(item.value) + '\"').join(\"\") + \">\";\n const inner =\n roots.length > 0 && typeof root.getHTML === \"function\"\n ? root.getHTML({ serializableShadowRoots: true, shadowRoots: roots })\n : root.innerHTML;\n const frames = [];\n for (const frame of document.querySelectorAll(\"iframe, frame\")) {\n try {\n const doc = frame.contentDocument;\n if (doc && doc.body) frames.push({ src: frame.src || \"\", html: doc.body.innerHTML });\n } catch (error) {}\n }\n return { html: \"<!DOCTYPE html>\" + open + inner + \"</html>\", frames };\n})()`;\n\ninterface Serialized {\n readonly html: string;\n readonly frames: ReadonlyArray<{ readonly src: string; readonly html: string }>;\n}\n\n/** This function puts the body of each frame at the end of the page, in a section that names the frame. */\nconst withFrames = ({ html, frames }: Serialized): string => {\n if (frames.length === 0) return html;\n const sections = frames\n .map(\n ({ src, html: body }) =>\n `<section data-polite-frame=\"${src.replace(/&/g, \"&amp;\").replace(/\"/g, \"&quot;\")}\">${body}</section>`,\n )\n .join(\"\");\n const end = html.toLowerCase().lastIndexOf(\"</body>\");\n return end === -1 ? `${html}${sections}` : `${html.slice(0, end)}${sections}${html.slice(end)}`;\n};\n\nconst messageOf = (error: unknown): string =>\n error instanceof Error ? error.message : String(error);\n\nconst sleep = (ms: number): Promise<void> =>\n new Promise((resolve) => {\n setTimeout(resolve, Math.max(0, ms)).unref();\n });\n\n/** This function gives the value of a promise, or undefined when the time ends first. */\nconst within = async <T>(promise: Promise<T>, ms: number): Promise<T | undefined> => {\n let timer: ReturnType<typeof setTimeout> | undefined;\n const late = new Promise<undefined>((resolve) => {\n timer = setTimeout(() => resolve(undefined), Math.max(0, ms));\n });\n try {\n return await Promise.race([promise, late]);\n } finally {\n clearTimeout(timer);\n }\n};\n\nexport interface PlaywrightRenderer extends Renderer {\n /** This function tells if a browser is open. */\n readonly open: () => boolean;\n}\n\nexport const playwrightRenderer = (\n options: {\n readonly idleMs?: number;\n /**\n * This function starts the browser: it gives a `Browser` of `playwright-core`. The default is\n * the headless shell of Playwright. The type names no type of `playwright-core`, so a program\n * with no `playwright-core` can check its types.\n */\n readonly launch?: (() => Promise<object>) | undefined;\n } = {},\n): PlaywrightRenderer => {\n const idleMs = options.idleMs ?? 120_000;\n const start = (options.launch ?? launch) as () => Promise<Browser>;\n let browser: Promise<Browser> | undefined;\n let inFlight = 0;\n let idle: ReturnType<typeof setTimeout> | undefined;\n const shutSoon = (): void => {\n if (idle !== undefined) clearTimeout(idle);\n idle = setTimeout(() => {\n idle = undefined;\n if (inFlight > 0 || browser === undefined) return;\n const closing = browser;\n browser = undefined;\n void closing.then((running) => running.close()).catch(() => undefined);\n }, idleMs);\n // the timer alone does not keep the process alive; the open browser does\n idle.unref();\n };\n const acquire = async (): Promise<Browser> => {\n for (let attempt = 0; ; attempt += 1) {\n if (browser === undefined) {\n const launching: Promise<Browser> = start().then((running) => {\n // a crash, a kill or a closed pipe: the next page launches a new browser\n running.on(\"disconnected\", () => {\n if (browser === launching) browser = undefined;\n });\n return running;\n });\n // a failed launch is not kept, so the next page tries again; a newer\n // launch that replaced this one is not touched\n launching.catch(() => {\n if (browser === launching) browser = undefined;\n });\n browser = launching;\n }\n const held = browser;\n const running = await held;\n if (running.isConnected() || attempt > 0) return running;\n if (browser === held) browser = undefined;\n }\n };\n /** This function gives up on a browser that does not answer. The next page launches a new one. */\n const forget = (running: Browser): void => {\n const held = browser;\n if (held !== undefined) {\n void held.then(\n (current) => {\n if (current === running && browser === held) browser = undefined;\n },\n () => undefined,\n );\n }\n void running.close().catch(() => undefined);\n };\n\n const renderPage = async (\n url: string,\n {\n userAgent,\n timeoutMs,\n headers,\n allowRequest,\n maxRedirects = MAX_REDIRECTS,\n signal,\n }: RenderOptions,\n ): Promise<Rendered> => {\n if (signal?.aborted === true)\n throw signal.reason instanceof Error ? signal.reason : new Error(\"the render was cancelled\");\n const deadline = Date.now() + timeoutMs;\n const left = (): number => Math.max(1, deadline - Date.now());\n let running: Browser | undefined;\n let context: BrowserContext | undefined;\n let shownPage: Page | undefined;\n /** The last navigation of the page that the route refused: the reason, and the refused URL. */\n let refused: { readonly message: string; readonly url: string } | undefined;\n /** The last navigation of the page that the route answered: the URL that the browser asked for, and the URL of the answer. */\n let navigated: { readonly asked: string; readonly final: string } | undefined;\n const extra = Object.fromEntries(\n Object.entries(headers ?? {}).map(([name, value]) => [name.toLowerCase(), value]),\n );\n const contextOptions = {\n userAgent,\n serviceWorkers: \"block\" as const,\n acceptDownloads: false,\n viewport: { width: 1280, height: 900 },\n };\n const ask = async (url: string): Promise<boolean> =>\n allowRequest === undefined\n ? true\n : Promise.resolve()\n .then(() => allowRequest(url))\n .catch(() => false);\n /**\n * This function sends one request of the page and follows its redirects. Each step is a new\n * decision of `allowRequest`, and a step past `maxRedirects` is refused. It gives the answer,\n * or the reason of a refusal.\n *\n * These rules are the rules of a browser. Each step gets the cookies of its own URL from the\n * context. A credential does not go to a different origin. A 303, or a 301 or 302 after a POST,\n * changes the request to a GET.\n */\n const follow = async (\n route: Route,\n sent: Record<string, string> | undefined,\n ): Promise<\n | { readonly response: APIResponse; readonly url: string }\n | { readonly refusal: string; readonly url: string }\n > => {\n const request = route.request();\n let url = request.url();\n let method = request.method();\n let headers: Record<string, string> = { ...request.headers(), ...extra };\n let response = await route.fetch({\n maxRedirects: 0,\n timeout: left(),\n ...(sent === undefined ? {} : { headers: sent }),\n });\n for (let hop = 0; ; hop += 1) {\n const location = REDIRECTS.has(response.status())\n ? response.headers()[\"location\"]\n : undefined;\n if (location === undefined) return { response, url };\n let next: URL;\n try {\n next = new URL(location, url);\n } catch {\n return { refusal: `${url} redirects to a URL that does not parse`, url };\n }\n if (hop >= maxRedirects) {\n return {\n refusal: `${request.url()} redirects more than ${String(maxRedirects)} times`,\n url: next.href,\n };\n }\n if (!(await ask(next.href))) {\n return {\n refusal: `the request for ${next.href}, a redirect of ${url}, was refused`,\n url: next.href,\n };\n }\n const status = response.status();\n const get = status === 303 || ((status === 301 || status === 302) && method === \"POST\");\n const crossOrigin = next.origin !== new URL(url).origin;\n headers = Object.fromEntries(\n Object.entries(headers).filter(\n ([name]) =>\n name !== \"cookie\" &&\n !(crossOrigin && name === \"authorization\") &&\n !(get && (name === \"content-type\" || name === \"content-length\")),\n ),\n );\n if (get) method = \"GET\";\n const body = get ? undefined : (request.postDataBuffer() ?? undefined);\n response = await context!.request.fetch(next.href, {\n method,\n headers,\n maxRedirects: 0,\n timeout: left(),\n ...(body === undefined ? {} : { data: body }),\n });\n url = next.href;\n }\n };\n /** This function tells if a request is a navigation of the page itself, not of a frame or a popup. */\n const ofPage = (request: Request): boolean => {\n if (!request.isNavigationRequest()) return false;\n try {\n return request.frame().parentFrame() === null && request.frame().page() === shownPage;\n } catch {\n // a popup asks for its first page before it has a frame\n return false;\n }\n };\n const decide = async (route: Route): Promise<void> => {\n const request = route.request();\n const main = ofPage(request);\n try {\n if (BLOCKED_TYPES.has(request.resourceType())) {\n await route.abort(\"blockedbyclient\");\n return;\n }\n if (!(await ask(request.url()))) {\n if (main)\n refused = {\n message: `the request for ${request.url()} was refused`,\n url: request.url(),\n };\n await route.abort(\"blockedbyclient\");\n return;\n }\n const sent =\n Object.keys(extra).length === 0\n ? undefined\n : {\n ...Object.fromEntries(\n Object.entries(await request.allHeaders()).filter(\n ([name]) => !name.startsWith(\":\") && name !== \"host\",\n ),\n ),\n ...extra,\n };\n if (allowRequest === undefined) {\n await route.continue(sent === undefined ? undefined : { headers: sent });\n return;\n }\n const answer = await follow(route, sent);\n if (\"refusal\" in answer) {\n if (main) refused = { message: answer.refusal, url: answer.url };\n await route.abort(\"blockedbyclient\");\n return;\n }\n if (main) navigated = { asked: request.url(), final: answer.url };\n await route.fulfill({ response: answer.response });\n } catch {\n // the page closed while the request waited: nothing to answer\n }\n };\n const work = (async (): Promise<Rendered> => {\n running = await acquire();\n // a browser that was killed can keep its pipe open and answer nothing:\n // it gets a few seconds, and then a new browser takes its place\n const opened = await within(\n running.newContext(contextOptions).catch((error: unknown) => {\n if (!/closed|disconnected/i.test(messageOf(error))) throw error;\n return undefined;\n }),\n Math.min(OPEN_MS, left()),\n );\n if (opened === undefined) {\n forget(running);\n running = await acquire();\n context = await running.newContext(contextOptions);\n } else {\n context = opened;\n }\n await context.route(\"**/*\", decide);\n if (allowRequest !== undefined && typeof context.routeWebSocket === \"function\") {\n await context.routeWebSocket(/.*/, async (socket) => {\n const allowed = await Promise.resolve()\n .then(() => allowRequest(socket.url().replace(/^ws/i, \"http\")))\n .catch(() => false);\n if (allowed) socket.connectToServer();\n else await socket.close({ code: 1008, reason: \"refused\" }).catch(() => undefined);\n });\n }\n const page: Page = await context.newPage();\n shownPage = page;\n // a popup is not the page, and a dialog waits for no one\n context.on(\"page\", (other) => {\n if (other !== page) void other.close().catch(() => undefined);\n });\n page.on(\"dialog\", (dialog) => {\n void dialog.dismiss().catch(() => undefined);\n });\n let response;\n try {\n response = await page.goto(url, {\n waitUntil: \"domcontentloaded\",\n timeout: Math.max(1, left() - READ_RESERVE_MS),\n });\n } catch (error) {\n if (refused !== undefined) throw new RenderRefused(\"refused\", refused.message, refused.url);\n if (/Download is starting/i.test(messageOf(error))) {\n throw new RenderRefused(\"download\", `${url} gives a file, not a page`);\n }\n throw error;\n }\n const settleEnd = Math.min(deadline - READ_RESERVE_MS, Date.now() + SETTLE_CAP_MS);\n const settleStart = Date.now();\n let last = -1;\n let since = Date.now();\n while (Date.now() < settleEnd) {\n const measured = (page.evaluate(MEASURE) as Promise<number>).catch(\n async (error: unknown) => {\n // a script of the page can start a navigation while the text is measured: the next poll measures the new document\n if (!/destroyed|navigat/i.test(messageOf(error))) throw error;\n await page\n .waitForLoadState(\"domcontentloaded\", { timeout: left() })\n .catch(() => undefined);\n // no length equals NaN, so the wait goes on\n return Number.NaN;\n },\n );\n const length = await within(measured, settleEnd - Date.now());\n // a page that keeps its thread busy does not answer: the reading below decides\n if (length === undefined) break;\n if (length !== last) {\n last = length;\n since = Date.now();\n } else if (\n Date.now() - since >= SETTLED_MS &&\n (length > 0 || Date.now() - settleStart >= EMPTY_SETTLE_MS)\n ) {\n break;\n }\n await sleep(Math.min(POLL_MS, settleEnd - Date.now()));\n }\n // a navigation can replace the document while it is read: the read waits for the new document\n // and tries again, until the deadline (a page can navigate many times in a row)\n let serialized: Serialized | undefined;\n while (serialized === undefined) {\n try {\n serialized = (await page.evaluate(SERIALIZE)) as Serialized;\n } catch (error) {\n if (!/destroyed|navigat/i.test(messageOf(error)) || Date.now() >= deadline) throw error;\n await page\n .waitForLoadState(\"domcontentloaded\", { timeout: left() })\n .catch(() => undefined);\n }\n }\n // a later navigation that the route refused leaves the browser on its error page, which is not the page\n if (refused !== undefined && page.url().startsWith(\"chrome-error:\")) {\n throw new RenderRefused(\"refused\", `the page moved: ${refused.message}`, refused.url);\n }\n // the route followed the redirects of the navigation, so the browser shows the first URL\n const shown = page.url().replace(/#.*$/s, \"\");\n return {\n html: withFrames(serialized),\n finalUrl:\n navigated !== undefined && shown === navigated.asked ? navigated.final : page.url(),\n status: response?.status() ?? null,\n headers: response?.headers() ?? {},\n };\n })();\n // the deadline can win the race; the work then ends when its context closes\n work.catch(() => undefined);\n let timer: ReturnType<typeof setTimeout> | undefined;\n let cancel: (() => void) | undefined;\n const expired = new Promise<never>((_, reject) => {\n timer = setTimeout(\n () => reject(new Error(`render timed out after ${String(timeoutMs)} ms`)),\n timeoutMs,\n );\n // a cancelled render ends now; the context closes below, and that stops the page\n if (signal !== undefined) {\n cancel = () =>\n reject(\n signal.reason instanceof Error ? signal.reason : new Error(\"the render was cancelled\"),\n );\n signal.addEventListener(\"abort\", cancel, { once: true });\n }\n });\n const closeContext = async (): Promise<void> => {\n const open = context;\n if (open === undefined) return;\n const closed = await within(\n open.close().then(\n () => true,\n () => true,\n ),\n CLOSE_MS,\n );\n if (closed === undefined && running !== undefined) forget(running);\n };\n try {\n return await Promise.race([work, expired]);\n } finally {\n clearTimeout(timer);\n if (cancel !== undefined) signal?.removeEventListener(\"abort\", cancel);\n if (context !== undefined) await closeContext();\n // a context made after the deadline is closed when it arrives\n else void work.then(closeContext, closeContext).catch(() => undefined);\n }\n };\n\n return {\n open: () => browser !== undefined,\n async render(url, renderOptions) {\n let target: URL;\n try {\n target = new URL(url);\n } catch {\n throw new RenderRefused(\"scheme\", `${url} is not a URL`);\n }\n if (target.protocol !== \"http:\" && target.protocol !== \"https:\") {\n throw new RenderRefused(\n \"scheme\",\n `the renderer reads http and https pages, not ${target.protocol}`,\n );\n }\n if (idle !== undefined) {\n clearTimeout(idle);\n idle = undefined;\n }\n inFlight += 1;\n try {\n return await renderPage(target.href, renderOptions);\n } finally {\n inFlight -= 1;\n if (inFlight === 0) shutSoon();\n }\n },\n async close() {\n if (idle !== undefined) clearTimeout(idle);\n idle = undefined;\n const launched = browser;\n browser = undefined;\n // a launch that failed has nothing to close, and closing never throws:\n // the idle timer calls this with no one to catch it\n if (launched !== undefined)\n await launched.then(\n (open) => open.close(),\n () => undefined,\n );\n },\n };\n};\n","import { existsSync } from \"node:fs\";\nimport { basename, dirname, join } from \"node:path\";\nimport type { Renderer } from \"./render.ts\";\nimport { NO_PLAYWRIGHT, RenderRefused } from \"./render.ts\";\n\n/** The text of the note when `playwright-core` is installed, but its browser is not. */\nexport const NO_BROWSER =\n \"rendering needs a browser: npx playwright-core install chromium-headless-shell\";\n\n/** This is the part of `playwright-core` that the check uses. */\ninterface Chromium {\n executablePath(): string;\n}\n\n/**\n * This function tells if the default renderer can start a browser. It gives no value when it can,\n * and the reason when it cannot. The command line and the MCP server use it one time at start.\n * Then a missing browser costs no wait and no error capture for each page that needs JavaScript.\n *\n * The check does not start the browser. It finds the full Chromium build or the headless shell of\n * the same revision on the disk. When the check cannot find the answer, it gives no value, and the\n * renderer then tries.\n */\nexport const browserMissing = async (\n load: () => Promise<{ readonly chromium: Chromium }> = () => import(\"playwright-core\"),\n): Promise<string | undefined> => {\n let chromium: Chromium;\n try {\n ({ chromium } = await load());\n } catch {\n return NO_PLAYWRIGHT;\n }\n try {\n const executable = chromium.executablePath();\n if (existsSync(executable)) return undefined;\n // the headless shell is in a folder next to the folder of the full build, with the same revision\n let folder = dirname(executable);\n while (dirname(folder) !== folder && !/^chromium-\\d+$/.test(basename(folder)))\n folder = dirname(folder);\n const revision = /^chromium-(\\d+)$/.exec(basename(folder))?.[1];\n if (revision === undefined) return undefined;\n return existsSync(join(dirname(folder), `chromium_headless_shell-${revision}`))\n ? undefined\n : NO_BROWSER;\n } catch {\n return undefined;\n }\n};\n\n/**\n * This function gives a renderer that checks one time, before its first page, that the browser is\n * on the disk (`browserMissing`). With no browser, each page gets a `RenderRefused` with the reason\n * `browser` at once, and the renderer opens nothing. `createRuntime` puts its default renderer in\n * it, so a library with no browser costs no wait and no error capture.\n */\nexport const checkedRenderer = (\n renderer: Renderer,\n check: () => Promise<string | undefined> = () => browserMissing(),\n): Renderer => {\n let missing: Promise<string | undefined> | undefined;\n return {\n async render(url, options) {\n missing ??= check().catch(() => undefined);\n const reason = await missing;\n if (reason !== undefined) throw new RenderRefused(\"browser\", reason);\n return renderer.render(url, options);\n },\n close: () => renderer.close(),\n };\n};\n"],"mappings":"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"}
@@ -0,0 +1,51 @@
1
+ //#region src/classify.d.ts
2
+ /**
3
+ * The outcome tells what a response means for the crawl. The crawler decides it before it keeps
4
+ * the body as content. The crawler sees a refusal and stops. It does not try again with a
5
+ * different identity.
6
+ *
7
+ * - `challenge`: a bot challenge page. Cloudflare marks it with `cf-mitigated: challenge`. AWS WAF
8
+ * answers 202 (challenge) or 405 (CAPTCHA) with an `x-amzn-waf-action` header. The host stops
9
+ * for 24 hours.
10
+ * - `payment-required` (402): a site that wants payment or a license for a crawl. The crawler
11
+ * records the price and does not pay. The host stops for 24 hours.
12
+ * - `blocked` (401, 403): RFC 9110 tells a client not to send the request again automatically.
13
+ * The host stops for 1 hour.
14
+ * - `rate-limited` (429) and `unavailable` (502, 503, 504): the host waits until the time of
15
+ * Retry-After, or for a backoff with jitter. A busy gateway is a reason to wait, not a failure.
16
+ * - `error`: a different status, or no answer. A 5xx or no answer counts toward the stop of the
17
+ * host. A different 4xx (a 400 or a 422) is a problem of the request, and it does not count.
18
+ *
19
+ * The politeness policy (`PolitenessPolicy`) sets each time of a stop.
20
+ */
21
+ type Outcome = "ok" | "not-modified" | "redirect" | "challenge" | "payment-required" | "blocked" | "not-found" | "rate-limited" | "unavailable" | "error";
22
+ interface Classified {
23
+ readonly outcome: Outcome;
24
+ /** This value is the wait that the server asked for, when it asked for one. */
25
+ readonly retryAfterMs: number | undefined;
26
+ readonly note: string | undefined;
27
+ }
28
+ /**
29
+ * This function reads an HTTP date in the three forms of RFC 9110 §5.6.7: the IMF-fixdate, the
30
+ * RFC 850 form and the asctime form. It gives milliseconds since the epoch, or no value for a text
31
+ * in a different form. It does not use `Date.parse`, which reads many texts that are not dates.
32
+ */
33
+ declare const parseHttpDate: (value: string | undefined) => number | undefined;
34
+ /**
35
+ * Retry-After is either delta-seconds or an HTTP date (RFC 9110 §10.2.3). The function reads only
36
+ * these two forms. A different text, for example `1.5` or `-5`, gives no value, so the backoff
37
+ * applies.
38
+ */
39
+ declare const parseRetryAfter: (value: string | undefined, now: number) => number | undefined;
40
+ /** After these outcomes, the crawler does not ask the host again until the stop ends. */
41
+ declare const STOPS_HOST: ReadonlySet<Outcome>;
42
+ /**
43
+ * This function tells if a status is a client error with no meaning for the host. Such a status
44
+ * is a 4xx other than 401, 402, 403, 404, 408, 410 and 429. The request was wrong, and a
45
+ * different request can succeed. Thus the failure count of the host does not change.
46
+ */
47
+ declare const isClientError: (status: number) => boolean;
48
+ declare const classify: (status: number, header: (name: string) => string | undefined, now: number) => Classified;
49
+ //#endregion
50
+ export { isClientError as a, classify as i, Outcome as n, parseHttpDate as o, STOPS_HOST as r, parseRetryAfter as s, Classified as t };
51
+ //# sourceMappingURL=classify-CrfyXcwo.d.mts.map
@@ -0,0 +1,2 @@
1
+ import { a as isClientError, i as classify, n as Outcome, o as parseHttpDate, r as STOPS_HOST, s as parseRetryAfter, t as Classified } from "./classify-CrfyXcwo.mjs";
2
+ export { Classified, Outcome, STOPS_HOST, classify, isClientError, parseHttpDate, parseRetryAfter };