single-file-cli 2.8.0 → 2.9.1

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.
@@ -0,0 +1,1221 @@
1
+ /*
2
+ * Copyright 2010-2026 Gildas Lormeau
3
+ * contact : gildas.lormeau <at> gmail.com
4
+ *
5
+ * This file is part of SingleFile.
6
+ *
7
+ * The code in this file is free software: you can redistribute it and/or
8
+ * modify it under the terms of the GNU Affero General Public License
9
+ * (GNU AGPL) as published by the Free Software Foundation, either version 3
10
+ * of the License, or (at your option) any later version.
11
+ *
12
+ * The code in this file is distributed in the hope that it will be useful,
13
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
15
+ * General Public License for more details.
16
+ *
17
+ * As additional permission under GNU AGPL version 3 section 7, you may
18
+ * distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU
19
+ * AGPL normally required by section 4, provided you include this license
20
+ * notice and a URL through which recipients can access the Corresponding
21
+ * Source.
22
+ */
23
+
24
+ /* global setTimeout, clearTimeout, URL, AbortController, TextDecoder, window, btoa, ReadableStream, WritableStream */
25
+
26
+ import {
27
+ launchFirefox,
28
+ closeFirefox,
29
+ hasFirefoxExited,
30
+ getFirefoxOptions
31
+ } from "./firefox.js";
32
+ import { connect } from "./bidi.js";
33
+ import {
34
+ FETCH_FUNCTION_NAME,
35
+ RESOLVE_FETCH_FUNCTION_NAME,
36
+ REJECT_FETCH_FUNCTION_NAME,
37
+ getScriptSource,
38
+ getHookScriptSource,
39
+ getPageDataScriptSource
40
+ } from "./single-file-script.js";
41
+ import {
42
+ fetch,
43
+ waitForTimeout,
44
+ arrayBufferToBase64,
45
+ getAlternativeUrl
46
+ } from "./cdp-client-util.js";
47
+ import { webStreamsPonyfill } from "./single-file-bundle.js";
48
+
49
+ const LOAD_TIMEOUT_ERROR = "ERR_LOAD_TIMEOUT";
50
+ const CAPTURE_TIMEOUT_ERROR = "ERR_CAPTURE_TIMEOUT";
51
+ const NETWORK_STATES = ["InteractiveTime", "networkIdle", "networkAlmostIdle", "load", "DOMContentLoaded"];
52
+ const NETWORK_IDLE_STATE = "networkIdle";
53
+ const NETWORK_ALMOST_IDLE_STATE = "networkAlmostIdle";
54
+ const LOAD_STATE = "load";
55
+ const DOM_CONTENT_LOADED_STATE = "DOMContentLoaded";
56
+ const INTERACTIVE_TIME_STATE = "InteractiveTime";
57
+ const MOBILE_VIEWPORT_WIDTH = 360;
58
+ const MOBILE_VIEWPORT_HEIGHT = 800;
59
+ const MOBILE_DEVICE_SCALE_FACTOR = 2;
60
+ const NETWORK_IDLE_DELAY = 500;
61
+ const NETWORK_ALMOST_IDLE_MAX_REQUESTS = 2;
62
+ const SINGLE_FILE_SANDBOX_NAME = "singlefile";
63
+ const CAPTURE_SCREENSHOT_FUNCTION_NAME = "captureScreenshot";
64
+ const PRINT_TO_PDF_FUNCTION_NAME = "printToPDF";
65
+ const SET_SCREENSHOT_FUNCTION_NAME = "setScreenshot";
66
+ const SET_PDF_FUNCTION_NAME = "setPDF";
67
+ const SET_PAGE_DATA_FUNCTION_NAME = "setPageData";
68
+ const SINGLE_FILE_GLOBAL_DECLARATION = /var singlefile\s*=\s*/;
69
+ const SINGLE_FILE_GLOBAL_ASSIGNMENT = "globalThis.singlefile=window.singlefile=";
70
+ const SERVER_CONNECT_TIMEOUT = 30000;
71
+ const SESSION_TIMEOUT = 120000;
72
+ const SERVER_SESSION_PATH = "/session";
73
+ const WEB_SOCKET_PROTOCOLS = { "http:": "ws:", "https:": "wss:" };
74
+ const BROWSER_EXITED_MAX_DELAY = 2000;
75
+ const USER_AGENT_HEADER_NAME = "user-agent";
76
+ const INCHES_TO_CENTIMETERS = 2.54;
77
+ const CONSOLE_LOG_TYPE = "console";
78
+ const CONSOLE_API_SOURCE = "console-api";
79
+ const JAVASCRIPT_SOURCE = "javascript";
80
+ const WARNING_LEVEL = "warning";
81
+ const WARN_LEVEL = "warn";
82
+ const SESSION_EVENTS = [
83
+ "browsingContext.contextCreated",
84
+ "browsingContext.contextDestroyed",
85
+ "browsingContext.navigationStarted",
86
+ "browsingContext.navigationFailed",
87
+ "browsingContext.domContentLoaded",
88
+ "browsingContext.load",
89
+ "network.beforeRequestSent",
90
+ "network.responseStarted",
91
+ "network.responseCompleted",
92
+ "network.fetchError",
93
+ "network.authRequired",
94
+ "script.message"
95
+ ];
96
+ const CONSOLE_EVENT = "log.entryAdded";
97
+ const PROXY_AUTHENTICATION_REQUIRED_STATUS = 407;
98
+ const NO_TIMEOUT = { timeout: 0 };
99
+
100
+ let session, browserInfo = {};
101
+
102
+ export {
103
+ initialize,
104
+ getPageData,
105
+ closeBrowser
106
+ };
107
+
108
+ async function initialize(singleFileOptions) {
109
+ let server;
110
+ if (singleFileOptions.browserServer) {
111
+ server = { url: getServerUrl(singleFileOptions.browserServer), timeout: SERVER_CONNECT_TIMEOUT };
112
+ } else {
113
+ server = await launchFirefox(getFirefoxOptions(singleFileOptions));
114
+ }
115
+ try {
116
+ session = await connect(server.url, { timeout: server.timeout, isClosed: hasFirefoxExited });
117
+ const { capabilities } = await session.send("session.new", {
118
+ capabilities: {
119
+ alwaysMatch: {
120
+ acceptInsecureCerts: Boolean(singleFileOptions.browserIgnoreHTTPSErrors)
121
+ }
122
+ }
123
+ }, { timeout: SESSION_TIMEOUT });
124
+ browserInfo = { userAgent: capabilities.userAgent, browserName: capabilities.browserName, browserVersion: capabilities.browserVersion };
125
+ } catch (error) {
126
+ await closeBrowser();
127
+ throw error;
128
+ }
129
+ }
130
+
131
+ function getServerUrl(browserServer) {
132
+ const url = new URL(browserServer);
133
+ if (WEB_SOCKET_PROTOCOLS[url.protocol]) {
134
+ url.protocol = WEB_SOCKET_PROTOCOLS[url.protocol];
135
+ if (url.pathname == "/") {
136
+ url.pathname = SERVER_SESSION_PATH;
137
+ }
138
+ }
139
+ return url.href;
140
+ }
141
+
142
+ async function closeBrowser() {
143
+ if (session) {
144
+ try {
145
+ await Promise.race([
146
+ session.send("session.end"),
147
+ new Promise(resolve => setTimeout(resolve, BROWSER_EXITED_MAX_DELAY))
148
+ ]);
149
+ } catch {
150
+ // ignored
151
+ }
152
+ session.close();
153
+ session = undefined;
154
+ }
155
+ await closeFirefox();
156
+ }
157
+
158
+ async function getPageData(options) {
159
+ const blockedURLPatterns = (options.blockedURLPatterns || []).map(pattern => new RegExp(pattern));
160
+ const pageContext = {
161
+ options,
162
+ consoleMessages: [],
163
+ debugMessages: [],
164
+ httpInfo: {},
165
+ browserInfo,
166
+ blockedURLPatterns,
167
+ fetchAbortController: new AbortController(),
168
+ listeners: [],
169
+ preloadScripts: [],
170
+ subscriptions: [],
171
+ intercepts: []
172
+ };
173
+ let context;
174
+ try {
175
+ logData(["Creating tab"], pageContext);
176
+ ({ context } = await session.send("browsingContext.create", { type: "tab" }));
177
+ pageContext.context = context;
178
+ pageContext.contexts = new Set([context]);
179
+ await setupSubscriptions(pageContext);
180
+ setupContextTracking(pageContext);
181
+ setupConsoleLogging(pageContext);
182
+ await setupDeviceEmulation(pageContext);
183
+ await setupNetwork(pageContext);
184
+ await setupScriptInjection(pageContext);
185
+ const pageDataPromise = setupPageDataCapture(pageContext);
186
+ setupFetchRequests(pageContext);
187
+ await loadPage(pageContext);
188
+ await checkSingleFileContext(pageContext);
189
+ await capturePageData(pageContext);
190
+ return await finalizePageData(pageDataPromise, pageContext);
191
+ } catch (error) {
192
+ attachDebugInfo(error, pageContext);
193
+ throw error;
194
+ } finally {
195
+ logData(["Closing tab"], pageContext);
196
+ pageContext.fetchAbortController.abort();
197
+ await cleanup(pageContext);
198
+ logData(["Finishing"], pageContext);
199
+ }
200
+ }
201
+
202
+ async function setupSubscriptions({ options, context, subscriptions }) {
203
+ const events = options.consoleMessagesFile ? SESSION_EVENTS.concat(CONSOLE_EVENT) : SESSION_EVENTS;
204
+ const result = await session.send("session.subscribe", { events, contexts: [context] });
205
+ if (result && result.subscription) {
206
+ subscriptions.push(result.subscription);
207
+ }
208
+ }
209
+
210
+ function setupContextTracking(pageContext) {
211
+ const { contexts } = pageContext;
212
+ listen(pageContext, "browsingContext.contextCreated", params => {
213
+ if (params.parent && contexts.has(params.parent)) {
214
+ contexts.add(params.context);
215
+ }
216
+ });
217
+ listen(pageContext, "browsingContext.contextDestroyed", params => {
218
+ if (params.context !== pageContext.context) {
219
+ contexts.delete(params.context);
220
+ }
221
+ });
222
+ }
223
+
224
+ function isOwnContext(pageContext, params) {
225
+ return params.context !== null && params.context !== undefined && pageContext.contexts.has(params.context);
226
+ }
227
+
228
+ function setupConsoleLogging(pageContext) {
229
+ const { options, context, consoleMessages } = pageContext;
230
+ if (options.consoleMessagesFile) {
231
+ logData(["Enabling console messages"], pageContext);
232
+ listen(pageContext, CONSOLE_EVENT, params => {
233
+ if (params.source && params.source.context === context) {
234
+ const callFrame = params.stackTrace && params.stackTrace.callFrames && params.stackTrace.callFrames[0];
235
+ consoleMessages.push({
236
+ source: params.type === CONSOLE_LOG_TYPE ? CONSOLE_API_SOURCE : JAVASCRIPT_SOURCE,
237
+ level: params.level === WARN_LEVEL ? WARNING_LEVEL : params.level,
238
+ text: params.text,
239
+ url: callFrame && callFrame.url,
240
+ line: callFrame && callFrame.lineNumber,
241
+ column: callFrame && callFrame.columnNumber,
242
+ timestamp: params.timestamp
243
+ });
244
+ }
245
+ });
246
+ }
247
+ }
248
+
249
+ async function setupDeviceEmulation(pageContext) {
250
+ const { options, context } = pageContext;
251
+ const viewportOptions = { context };
252
+ const width = options.browserDeviceWidth || (options.browserMobileEmulation ? MOBILE_VIEWPORT_WIDTH : options.browserWidth);
253
+ const height = options.browserDeviceHeight || (options.browserMobileEmulation ? MOBILE_VIEWPORT_HEIGHT : options.browserHeight);
254
+ if (width && height) {
255
+ viewportOptions.viewport = { width, height };
256
+ }
257
+ const devicePixelRatio = options.browserDeviceScaleFactor || (options.browserMobileEmulation ? MOBILE_DEVICE_SCALE_FACTOR : undefined);
258
+ if (devicePixelRatio) {
259
+ viewportOptions.devicePixelRatio = devicePixelRatio;
260
+ }
261
+ if (viewportOptions.viewport || viewportOptions.devicePixelRatio) {
262
+ logData(["Emulating viewport", JSON.stringify(viewportOptions)], pageContext);
263
+ await session.send("browsingContext.setViewport", viewportOptions);
264
+ }
265
+ }
266
+
267
+ async function setupNetwork(pageContext) {
268
+ const { options, context, blockedURLPatterns, intercepts } = pageContext;
269
+ const hasBlockedURLPatterns = blockedURLPatterns.length > 0;
270
+ const hasHttpHeaders = Boolean(options.httpHeaders) && Object.keys(options.httpHeaders).length > 0;
271
+ const handleAuthRequests = Boolean(options.httpProxyUsername);
272
+ const phases = [];
273
+ if (hasBlockedURLPatterns || hasHttpHeaders) {
274
+ phases.push("beforeRequestSent");
275
+ }
276
+ if (handleAuthRequests) {
277
+ phases.push("authRequired");
278
+ }
279
+ if (phases.length) {
280
+ const { intercept } = await session.send("network.addIntercept", { phases, contexts: [context] });
281
+ intercepts.push(intercept);
282
+ }
283
+ if (handleAuthRequests) {
284
+ listen(pageContext, "network.authRequired", ignoringErrors(async params => {
285
+ if (!params.isBlocked || !isOwnContext(pageContext, params)) {
286
+ return;
287
+ }
288
+ const requestId = params.request.request;
289
+ if (params.response.status === PROXY_AUTHENTICATION_REQUIRED_STATUS) {
290
+ logData(["Authenticating"], pageContext);
291
+ await session.send("network.continueWithAuth", {
292
+ request: requestId,
293
+ action: "provideCredentials",
294
+ credentials: { type: "password", username: options.httpProxyUsername, password: options.httpProxyPassword }
295
+ });
296
+ } else {
297
+ await session.send("network.continueWithAuth", { request: requestId, action: "default" });
298
+ }
299
+ }, pageContext));
300
+ }
301
+ if (hasBlockedURLPatterns || hasHttpHeaders) {
302
+ listen(pageContext, "network.beforeRequestSent", ignoringErrors(async params => {
303
+ if (!params.isBlocked || !isOwnContext(pageContext, params)) {
304
+ return;
305
+ }
306
+ const requestId = params.request.request;
307
+ if (blockedURLPatterns.some(pattern => pattern.test(params.request.url))) {
308
+ logData(["Blocking request", params.request.url], pageContext);
309
+ await session.send("network.failRequest", { request: requestId });
310
+ } else if (hasHttpHeaders) {
311
+ await session.send("network.continueRequest", { request: requestId, headers: getMergedHeaders(params.request.headers, options.httpHeaders) });
312
+ } else {
313
+ await session.send("network.continueRequest", { request: requestId });
314
+ }
315
+ }, pageContext));
316
+ }
317
+ if (options.outputJson) {
318
+ setupHttpInfoCapture(pageContext);
319
+ }
320
+ if (options.browserCookies && options.browserCookies.length) {
321
+ await setupCookies(pageContext);
322
+ }
323
+ }
324
+
325
+ function getMergedHeaders(requestHeaders, extraHeaders) {
326
+ const headers = new Map();
327
+ requestHeaders.forEach(header => headers.set(header.name.toLowerCase(), header));
328
+ Object.entries(extraHeaders).forEach(([name, value]) => headers.set(name.toLowerCase(), { name, value: { type: "string", value } }));
329
+ return Array.from(headers.values());
330
+ }
331
+
332
+ function setupHttpInfoCapture(pageContext) {
333
+ const REDIRECT_STATUS_CODES = [301, 302, 303, 307, 308];
334
+ const DOCUMENT_RESOURCE_TYPE = "Document";
335
+ const { options, context, httpInfo } = pageContext;
336
+ const urlState = { url: options.url, alternativeUrl: getAlternativeUrl(options.url) };
337
+ listen(pageContext, "network.responseStarted", params => {
338
+ const { request, response, navigation } = params;
339
+ const shouldCapture = params.context === context && navigation && !httpInfo.request &&
340
+ (request.url === urlState.url || request.url === urlState.alternativeUrl);
341
+ if (shouldCapture) {
342
+ if (REDIRECT_STATUS_CODES.includes(response.status)) {
343
+ const redirect = getHeaderValue(response.headers, "location");
344
+ if (redirect) {
345
+ urlState.url = new URL(redirect, urlState.url).href;
346
+ }
347
+ logData(["Redirecting", urlState.url], pageContext);
348
+ } else {
349
+ Object.assign(httpInfo, {
350
+ request: {
351
+ url: request.url,
352
+ method: request.method,
353
+ headers: getHeadersObject(request.headers)
354
+ },
355
+ resourceType: DOCUMENT_RESOURCE_TYPE,
356
+ response: {
357
+ status: response.status,
358
+ statusText: response.statusText,
359
+ headers: response.headers.map(header => ({ name: header.name, value: header.value.value }))
360
+ }
361
+ });
362
+ }
363
+ }
364
+ });
365
+ }
366
+
367
+ function getHeaderValue(headers, name) {
368
+ const header = headers.find(header => header.name.toLowerCase() === name);
369
+ return header && header.value.value;
370
+ }
371
+
372
+ function getHeadersObject(headers) {
373
+ return Object.fromEntries(headers.map(header => [header.name, header.value.value]));
374
+ }
375
+
376
+ async function setupCookies(pageContext) {
377
+ const { options } = pageContext;
378
+ logData(["Setting cookies", JSON.stringify(options.browserCookies)], pageContext);
379
+ for (const cookie of options.browserCookies) {
380
+ const domain = cookie.domain || (cookie.url && new URL(cookie.url).hostname);
381
+ const cookieParams = {
382
+ name: cookie.name,
383
+ value: { type: "string", value: cookie.value },
384
+ domain
385
+ };
386
+ if (cookie.path) {
387
+ cookieParams.path = cookie.path;
388
+ }
389
+ if (cookie.secure !== undefined) {
390
+ cookieParams.secure = cookie.secure;
391
+ }
392
+ if (cookie.httpOnly !== undefined) {
393
+ cookieParams.httpOnly = cookie.httpOnly;
394
+ }
395
+ if (cookie.sameSite) {
396
+ cookieParams.sameSite = cookie.sameSite.toLowerCase();
397
+ }
398
+ if (cookie.expires) {
399
+ cookieParams.expiry = Math.round(cookie.expires);
400
+ }
401
+ await session.send("storage.setCookie", { cookie: cookieParams });
402
+ }
403
+ }
404
+
405
+ async function setupScriptInjection(pageContext) {
406
+ const { options, context, preloadScripts } = pageContext;
407
+ const scriptSource = (await getScriptSource(options)).replace(SINGLE_FILE_GLOBAL_DECLARATION, SINGLE_FILE_GLOBAL_ASSIGNMENT);
408
+ const hookScript = await session.send("script.addPreloadScript", {
409
+ functionDeclaration: "() => {" + getHookScriptSource() + "}",
410
+ contexts: [context]
411
+ });
412
+ preloadScripts.push(hookScript.script);
413
+ const channelNames = [SET_PAGE_DATA_FUNCTION_NAME, FETCH_FUNCTION_NAME];
414
+ if (options.embedScreenshot && options.compressContent) {
415
+ channelNames.push(CAPTURE_SCREENSHOT_FUNCTION_NAME);
416
+ }
417
+ if (options.embedPdf && options.compressContent) {
418
+ channelNames.push(PRINT_TO_PDF_FUNCTION_NAME);
419
+ }
420
+ const parameters = channelNames.map((name, index) => "channel" + index);
421
+ const sandboxPrelude = webStreamsPonyfill + ";(" + installSandboxGlobals.toString() + ")();" +
422
+ channelNames.map((name, index) => "globalThis[" + JSON.stringify(name) + "]=channel" + index + ";").join("");
423
+ const singleFileScript = await session.send("script.addPreloadScript", {
424
+ functionDeclaration: "(" + parameters.join(",") + ") => {" + sandboxPrelude + scriptSource + "}",
425
+ arguments: channelNames.map(name => ({ type: "channel", value: { channel: name } })),
426
+ sandbox: SINGLE_FILE_SANDBOX_NAME,
427
+ contexts: [context]
428
+ });
429
+ preloadScripts.push(singleFileScript.script);
430
+ }
431
+
432
+ function getSingleFileTarget(context) {
433
+ return { context, sandbox: SINGLE_FILE_SANDBOX_NAME };
434
+ }
435
+
436
+ function installSandboxGlobals() {
437
+ const BYTE_PRESERVING_ENCODING = "x-user-defined";
438
+ const BYTE_MASK = 0xff;
439
+ const CHUNK_SIZE = 8192;
440
+ const DEFAULT_BLOB_TYPE = "application/octet-stream";
441
+ const READY_STATE_EMPTY = 0;
442
+ const READY_STATE_LOADING = 1;
443
+ const READY_STATE_DONE = 2;
444
+ const RealBlob = globalThis.Blob;
445
+ const RealFileReader = globalThis.FileReader;
446
+ const RealURL = globalThis.URL;
447
+ const RealCompressionStream = globalThis.CompressionStream;
448
+ const RealDecompressionStream = globalThis.DecompressionStream;
449
+ const realCrypto = globalThis.crypto;
450
+ const nativeFetch = window.fetch.bind(window);
451
+ const byteDecoder = new TextDecoder(BYTE_PRESERVING_ENCODING);
452
+
453
+ function encodeUTF8(text) {
454
+ const bytes = [];
455
+ for (let index = 0; index < text.length; index++) {
456
+ let codePoint = text.codePointAt(index);
457
+ if (codePoint > 0xffff) {
458
+ index++;
459
+ }
460
+ if (codePoint >= 0xd800 && codePoint <= 0xdfff) {
461
+ codePoint = 0xfffd;
462
+ }
463
+ if (codePoint < 0x80) {
464
+ bytes.push(codePoint);
465
+ } else if (codePoint < 0x800) {
466
+ bytes.push(0xc0 | (codePoint >> 6), 0x80 | (codePoint & 0x3f));
467
+ } else if (codePoint < 0x10000) {
468
+ bytes.push(0xe0 | (codePoint >> 12), 0x80 | ((codePoint >> 6) & 0x3f), 0x80 | (codePoint & 0x3f));
469
+ } else {
470
+ bytes.push(0xf0 | (codePoint >> 18), 0x80 | ((codePoint >> 12) & 0x3f), 0x80 | ((codePoint >> 6) & 0x3f), 0x80 | (codePoint & 0x3f));
471
+ }
472
+ }
473
+ return new Uint8Array(bytes);
474
+ }
475
+
476
+ function binaryStringToBytes(text) {
477
+ const bytes = new Uint8Array(text.length);
478
+ for (let index = 0; index < text.length; index++) {
479
+ bytes[index] = text.charCodeAt(index) & BYTE_MASK;
480
+ }
481
+ return bytes;
482
+ }
483
+
484
+ function bytesToBinaryString(bytes) {
485
+ let text = "";
486
+ for (let offset = 0; offset < bytes.length; offset += CHUNK_SIZE) {
487
+ text += String.fromCharCode.apply(null, bytes.subarray(offset, offset + CHUNK_SIZE));
488
+ }
489
+ return text;
490
+ }
491
+
492
+ function isRealBlob(value) {
493
+ return value instanceof RealBlob;
494
+ }
495
+
496
+ function isSandboxBlob(value) {
497
+ return value !== null && typeof value == "object" && Object.getPrototypeOf(value) === SandboxBlob.prototype;
498
+ }
499
+
500
+ function readRealBlob(blob, method) {
501
+ return new Promise((resolve, reject) => {
502
+ const reader = new RealFileReader();
503
+ reader.onload = () => resolve(reader.result);
504
+ reader.onerror = () => reject(reader.error);
505
+ reader[method](blob);
506
+ });
507
+ }
508
+
509
+ function toBytes(part) {
510
+ if (typeof part == "string") {
511
+ return encodeUTF8(part);
512
+ }
513
+ if (ArrayBuffer.isView(part)) {
514
+ return new Uint8Array(part.buffer, part.byteOffset, part.byteLength).slice();
515
+ }
516
+ if (part instanceof ArrayBuffer) {
517
+ return new Uint8Array(part).slice();
518
+ }
519
+ return encodeUTF8(String(part));
520
+ }
521
+
522
+ class SandboxBlob {
523
+ #parts;
524
+ #size;
525
+ #type;
526
+ #bytes;
527
+ #range;
528
+
529
+ constructor(parts = [], options = {}) {
530
+ this.#type = String(options.type || "").toLowerCase();
531
+ this.#parts = Array.from(parts).map(part => isSandboxBlob(part) || isRealBlob(part) ? part : toBytes(part));
532
+ this.#size = this.#parts.reduce((size, part) => size + (part instanceof Uint8Array ? part.length : part.size), 0);
533
+ }
534
+
535
+ static [Symbol.hasInstance](value) {
536
+ return isSandboxBlob(value) || isRealBlob(value);
537
+ }
538
+
539
+ get size() {
540
+ return this.#size;
541
+ }
542
+
543
+ get type() {
544
+ return this.#type;
545
+ }
546
+
547
+ get [Symbol.toStringTag]() {
548
+ return "Blob";
549
+ }
550
+
551
+ slice(start = 0, end = this.#size, type = "") {
552
+ const size = this.#size;
553
+ start = start < 0 ? Math.max(size + start, 0) : Math.min(start, size);
554
+ end = end < 0 ? Math.max(size + end, 0) : Math.min(end, size);
555
+ const blob = new SandboxBlob([], { type });
556
+ blob.#range = { source: this, start, end: Math.max(end, start) };
557
+ blob.#size = Math.max(end - start, 0);
558
+ return blob;
559
+ }
560
+
561
+ bytesSync() {
562
+ if (!this.#bytes) {
563
+ if (this.#range) {
564
+ this.#bytes = this.#range.source.bytesSync().subarray(this.#range.start, this.#range.end).slice();
565
+ } else {
566
+ if (this.#parts.some(part => !(part instanceof Uint8Array) && !isSandboxBlob(part))) {
567
+ throw new Error("The blob content is not available synchronously");
568
+ }
569
+ this.#bytes = concatenate(this.#parts.map(part => part instanceof Uint8Array ? part : part.bytesSync()), this.#size);
570
+ }
571
+ }
572
+ return this.#bytes;
573
+ }
574
+
575
+ async bytes() {
576
+ if (!this.#bytes) {
577
+ if (this.#range) {
578
+ this.#bytes = (await this.#range.source.bytes()).subarray(this.#range.start, this.#range.end).slice();
579
+ } else {
580
+ const chunks = [];
581
+ for (const part of this.#parts) {
582
+ if (part instanceof Uint8Array) {
583
+ chunks.push(part);
584
+ } else if (isSandboxBlob(part)) {
585
+ chunks.push(await part.bytes());
586
+ } else {
587
+ chunks.push(binaryStringToBytes(await readRealBlob(part, "readAsBinaryString")));
588
+ }
589
+ }
590
+ this.#bytes = concatenate(chunks, this.#size);
591
+ }
592
+ }
593
+ return this.#bytes;
594
+ }
595
+
596
+ async arrayBuffer() {
597
+ return (await this.bytes()).buffer;
598
+ }
599
+
600
+ async text() {
601
+ return new TextDecoder().decode(await this.bytes());
602
+ }
603
+
604
+ stream() {
605
+ const blob = this;
606
+ return new ReadableStream({
607
+ async start(controller) {
608
+ controller.enqueue(await blob.bytes());
609
+ controller.close();
610
+ }
611
+ });
612
+ }
613
+
614
+ toRealBlob() {
615
+ return new RealBlob([this.bytesSync()], { type: this.#type });
616
+ }
617
+ }
618
+
619
+ function concatenate(chunks, size) {
620
+ const bytes = new Uint8Array(size);
621
+ let offset = 0;
622
+ chunks.forEach(chunk => {
623
+ bytes.set(chunk, offset);
624
+ offset += chunk.length;
625
+ });
626
+ return bytes;
627
+ }
628
+
629
+ class SandboxFileReader {
630
+ #listeners = new Map();
631
+
632
+ constructor() {
633
+ this.result = null;
634
+ this.error = null;
635
+ this.readyState = READY_STATE_EMPTY;
636
+ }
637
+
638
+ addEventListener(type, listener) {
639
+ if (!this.#listeners.has(type)) {
640
+ this.#listeners.set(type, new Set());
641
+ }
642
+ this.#listeners.get(type).add(listener);
643
+ }
644
+
645
+ removeEventListener(type, listener) {
646
+ const listeners = this.#listeners.get(type);
647
+ if (listeners) {
648
+ listeners.delete(listener);
649
+ }
650
+ }
651
+
652
+ dispatch(type) {
653
+ const event = { type, target: this };
654
+ const handler = this["on" + type];
655
+ if (typeof handler == "function") {
656
+ handler.call(this, event);
657
+ }
658
+ const listeners = this.#listeners.get(type);
659
+ if (listeners) {
660
+ Array.from(listeners).forEach(listener => listener.call(this, event));
661
+ }
662
+ }
663
+
664
+ abort() {
665
+ }
666
+
667
+ readAsDataURL(blob) {
668
+ this.#read(blob, "readAsDataURL", bytes => "data:" + (blob.type || DEFAULT_BLOB_TYPE) + ";base64," + btoa(bytesToBinaryString(bytes)));
669
+ }
670
+
671
+ readAsBinaryString(blob) {
672
+ this.#read(blob, "readAsBinaryString", bytes => bytesToBinaryString(bytes));
673
+ }
674
+
675
+ readAsText(blob, encoding) {
676
+ this.#read(blob, "readAsText", bytes => new TextDecoder(encoding || "utf-8").decode(bytes));
677
+ }
678
+
679
+ readAsArrayBuffer(blob) {
680
+ this.#read(blob, "readAsArrayBuffer", bytes => bytes.slice().buffer);
681
+ }
682
+
683
+ #read(blob, method, convert) {
684
+ this.readyState = READY_STATE_LOADING;
685
+ const resultPromise = isRealBlob(blob) ?
686
+ (method == "readAsArrayBuffer" ?
687
+ readRealBlob(blob, "readAsBinaryString").then(text => binaryStringToBytes(text).buffer) :
688
+ readRealBlob(blob, method)) :
689
+ blob.bytes().then(convert);
690
+ resultPromise.then(result => {
691
+ this.result = result;
692
+ this.readyState = READY_STATE_DONE;
693
+ this.dispatch("load");
694
+ this.dispatch("loadend");
695
+ }, error => {
696
+ this.error = error;
697
+ this.readyState = READY_STATE_DONE;
698
+ this.dispatch("error");
699
+ this.dispatch("loadend");
700
+ });
701
+ }
702
+ }
703
+ SandboxFileReader.EMPTY = READY_STATE_EMPTY;
704
+ SandboxFileReader.LOADING = READY_STATE_LOADING;
705
+ SandboxFileReader.DONE = READY_STATE_DONE;
706
+
707
+ class SandboxURL extends RealURL {
708
+ static createObjectURL(object) {
709
+ return RealURL.createObjectURL(isSandboxBlob(object) ? object.toRealBlob() : object);
710
+ }
711
+
712
+ static revokeObjectURL(url) {
713
+ return RealURL.revokeObjectURL(url);
714
+ }
715
+ }
716
+
717
+ class SandboxTextEncoder {
718
+ get encoding() {
719
+ return "utf-8";
720
+ }
721
+
722
+ encode(text = "") {
723
+ return encodeUTF8(String(text));
724
+ }
725
+
726
+ encodeInto(text, destination) {
727
+ text = String(text);
728
+ let read = 0, written = 0;
729
+ for (const character of text) {
730
+ const bytes = encodeUTF8(character);
731
+ if (written + bytes.length > destination.length) {
732
+ break;
733
+ }
734
+ destination.set(bytes, written);
735
+ written += bytes.length;
736
+ read += character.length;
737
+ }
738
+ return { read, written };
739
+ }
740
+ }
741
+
742
+ function createCodecStreamClass(RealCodecStream) {
743
+ return class SandboxCodecStream {
744
+ constructor(format) {
745
+ const codecStream = new RealCodecStream(format);
746
+ const nativeWriter = codecStream.writable.getWriter();
747
+ this.writable = new WritableStream({
748
+ write: chunk => nativeWriter.write(chunk),
749
+ close: () => nativeWriter.close(),
750
+ abort: reason => nativeWriter.abort(reason)
751
+ });
752
+ this.readable = new ReadableStream({
753
+ async start(controller) {
754
+ try {
755
+ const nativeReader = codecStream.readable.getReader();
756
+ for (;;) {
757
+ const { value, done } = await nativeReader.read();
758
+ if (done) {
759
+ break;
760
+ }
761
+ controller.enqueue(binaryStringToBytes(await readRealBlob(new RealBlob([value]), "readAsBinaryString")));
762
+ }
763
+ controller.close();
764
+ } catch (error) {
765
+ controller.error(error);
766
+ }
767
+ }
768
+ });
769
+ }
770
+ };
771
+ }
772
+
773
+ async function sandboxFetch(url, options) {
774
+ const response = await nativeFetch(url, options);
775
+ const text = byteDecoder.decode(await response.arrayBuffer());
776
+ return {
777
+ status: response.status,
778
+ statusText: response.statusText,
779
+ ok: response.ok,
780
+ url: response.url,
781
+ headers: response.headers,
782
+ arrayBuffer: () => Promise.resolve(binaryStringToBytes(text).buffer),
783
+ text: () => Promise.resolve(new TextDecoder().decode(binaryStringToBytes(text)))
784
+ };
785
+ }
786
+
787
+ globalThis.fetch = sandboxFetch;
788
+ globalThis.Blob = SandboxBlob;
789
+ globalThis.FileReader = SandboxFileReader;
790
+ globalThis.URL = SandboxURL;
791
+ globalThis.TextEncoder = SandboxTextEncoder;
792
+ Object.defineProperty(globalThis, "crypto", {
793
+ value: {
794
+ getRandomValues: array => realCrypto.getRandomValues(array),
795
+ randomUUID: () => realCrypto.randomUUID(),
796
+ subtle: undefined
797
+ },
798
+ configurable: true,
799
+ writable: true
800
+ });
801
+ const streams = globalThis.WebStreamsPolyfill;
802
+ globalThis.ReadableStream = streams.ReadableStream;
803
+ globalThis.WritableStream = streams.WritableStream;
804
+ globalThis.TransformStream = streams.TransformStream;
805
+ globalThis.ByteLengthQueuingStrategy = streams.ByteLengthQueuingStrategy;
806
+ globalThis.CountQueuingStrategy = streams.CountQueuingStrategy;
807
+ globalThis.CompressionStream = createCodecStreamClass(RealCompressionStream);
808
+ globalThis.DecompressionStream = createCodecStreamClass(RealDecompressionStream);
809
+ }
810
+
811
+ function setupPageDataCapture(pageContext) {
812
+ const { context } = pageContext;
813
+ return new Promise((resolve, reject) => {
814
+ let pageDataResponse = "";
815
+ listen(pageContext, "script.message", params => {
816
+ if (params.channel === SET_PAGE_DATA_FUNCTION_NAME && params.source.context === context) {
817
+ const payload = params.data.value;
818
+ if (payload.length) {
819
+ pageDataResponse += payload;
820
+ } else {
821
+ logData(["Setting page data"], pageContext);
822
+ try {
823
+ const result = JSON.parse(pageDataResponse);
824
+ if (result.content instanceof Array) {
825
+ result.content = new Uint8Array(result.content);
826
+ }
827
+ resolve(result);
828
+ } catch (error) {
829
+ reject(error);
830
+ }
831
+ }
832
+ }
833
+ });
834
+ });
835
+ }
836
+
837
+ function setupFetchRequests(pageContext) {
838
+ const { options } = pageContext;
839
+ listen(pageContext, "script.message", ignoringErrors(async params => {
840
+ if (params.channel === FETCH_FUNCTION_NAME) {
841
+ await handleFetchRequest(params, pageContext);
842
+ } else if (params.channel === CAPTURE_SCREENSHOT_FUNCTION_NAME) {
843
+ await handleScreenshotRequest(params, pageContext);
844
+ } else if (params.channel === PRINT_TO_PDF_FUNCTION_NAME) {
845
+ await handlePdfRequest(params, pageContext);
846
+ }
847
+ }, { options, debugMessages: pageContext.debugMessages }));
848
+ }
849
+
850
+ async function handleFetchRequest(params, pageContext) {
851
+ const BLOCKED_URL_ERROR_MESSAGE = "Blocked URL";
852
+ const { options, browserInfo, blockedURLPatterns, fetchAbortController } = pageContext;
853
+ const { realm } = params.source;
854
+ const { requestId, url, options: fetchOptions } = JSON.parse(params.data.value);
855
+ logData(["Fetching URL", url], pageContext);
856
+ try {
857
+ if (blockedURLPatterns.some(pattern => pattern.test(url))) {
858
+ logData(["Blocking request", url], pageContext);
859
+ throw new Error(BLOCKED_URL_ERROR_MESSAGE);
860
+ }
861
+ const headers = Object.assign({}, fetchOptions.headers, options.httpHeaders);
862
+ if (browserInfo.userAgent && !Object.keys(headers).some(name => name.toLowerCase() == USER_AGENT_HEADER_NAME)) {
863
+ headers[USER_AGENT_HEADER_NAME] = browserInfo.userAgent;
864
+ }
865
+ const response = await fetch(url, Object.assign({}, fetchOptions, { headers, signal: fetchAbortController.signal }));
866
+ const arrayBuffer = await response.arrayBuffer();
867
+ const result = {
868
+ status: response.status,
869
+ headers: Object.fromEntries(response.headers.entries()),
870
+ data: arrayBufferToBase64(arrayBuffer)
871
+ };
872
+ await callBrowserFunction(realm, RESOLVE_FETCH_FUNCTION_NAME, [requestId, result]);
873
+ } catch (error) {
874
+ await callBrowserFunction(realm, REJECT_FETCH_FUNCTION_NAME, [requestId, { error: error.message, code: error.code }]);
875
+ }
876
+ }
877
+
878
+ async function handleScreenshotRequest(params, pageContext) {
879
+ const { options, context } = pageContext;
880
+ const { realm } = params.source;
881
+ logData(["Capturing screenshot"], pageContext);
882
+ try {
883
+ const screenshotOptions = { context, origin: "document", format: { type: "image/png" } };
884
+ try {
885
+ const cdpOptions = options.embedScreenshotOptions ? JSON.parse(options.embedScreenshotOptions) : {};
886
+ if (cdpOptions.clip) {
887
+ screenshotOptions.clip = Object.assign({ type: "box" }, cdpOptions.clip);
888
+ }
889
+ if (cdpOptions.captureBeyondViewport === false) {
890
+ screenshotOptions.origin = "viewport";
891
+ }
892
+ } catch {
893
+ // ignored
894
+ }
895
+ const { data } = await session.send("browsingContext.captureScreenshot", screenshotOptions, NO_TIMEOUT);
896
+ await callBrowserFunction(realm, SET_SCREENSHOT_FUNCTION_NAME, [data]);
897
+ } catch {
898
+ await callBrowserFunction(realm, SET_SCREENSHOT_FUNCTION_NAME, [""]);
899
+ }
900
+ }
901
+
902
+ async function handlePdfRequest(params, pageContext) {
903
+ const { options, context } = pageContext;
904
+ const { realm } = params.source;
905
+ logData(["Printing to PDF", options.embedPdfOptions || ""], pageContext);
906
+ try {
907
+ const { data } = await session.send("browsingContext.print", getPrintOptions(context, options.embedPdfOptions), NO_TIMEOUT);
908
+ await callBrowserFunction(realm, SET_PDF_FUNCTION_NAME, [data]);
909
+ } catch {
910
+ await callBrowserFunction(realm, SET_PDF_FUNCTION_NAME, [""]);
911
+ }
912
+ }
913
+
914
+ function getPrintOptions(context, optionsString) {
915
+ const printOptions = { context, background: true };
916
+ let cdpOptions = {};
917
+ if (optionsString) {
918
+ try {
919
+ cdpOptions = JSON.parse(optionsString);
920
+ } catch {
921
+ // ignored
922
+ }
923
+ }
924
+ if (cdpOptions.printBackground !== undefined) {
925
+ printOptions.background = cdpOptions.printBackground;
926
+ }
927
+ if (cdpOptions.landscape) {
928
+ printOptions.orientation = "landscape";
929
+ }
930
+ if (cdpOptions.scale) {
931
+ printOptions.scale = cdpOptions.scale;
932
+ }
933
+ if (cdpOptions.pageRanges) {
934
+ printOptions.pageRanges = String(cdpOptions.pageRanges).split(",").map(range => range.trim());
935
+ }
936
+ if (cdpOptions.paperWidth || cdpOptions.paperHeight) {
937
+ printOptions.page = {};
938
+ if (cdpOptions.paperWidth) {
939
+ printOptions.page.width = cdpOptions.paperWidth * INCHES_TO_CENTIMETERS;
940
+ }
941
+ if (cdpOptions.paperHeight) {
942
+ printOptions.page.height = cdpOptions.paperHeight * INCHES_TO_CENTIMETERS;
943
+ }
944
+ }
945
+ const margins = [["marginTop", "top"], ["marginBottom", "bottom"], ["marginLeft", "left"], ["marginRight", "right"]]
946
+ .filter(([cdpName]) => cdpOptions[cdpName] !== undefined);
947
+ if (margins.length) {
948
+ printOptions.margin = Object.fromEntries(margins.map(([cdpName, name]) => [name, cdpOptions[cdpName] * INCHES_TO_CENTIMETERS]));
949
+ }
950
+ return printOptions;
951
+ }
952
+
953
+ async function callBrowserFunction(realm, functionName, args) {
954
+ await session.send("script.callFunction", {
955
+ functionDeclaration: "(json) => globalThis." + functionName + "(...JSON.parse(json))",
956
+ arguments: [{ type: "string", value: JSON.stringify(args) }],
957
+ target: { realm },
958
+ awaitPromise: false
959
+ });
960
+ }
961
+
962
+ async function loadPage(pageContext) {
963
+ const LOAD_TIMEOUT_ERROR_MESSAGE = "Load timeout";
964
+ const UNREACHABLE_URL_ERROR_MESSAGE = "Unreachable URL";
965
+ const { options, context } = pageContext;
966
+ const waitUntil = options.browserWaitUntil === INTERACTIVE_TIME_STATE ? NETWORK_IDLE_STATE : options.browserWaitUntil;
967
+ const state = { reachedStateIndex: -1, navigation: undefined, pendingRequests: new Set(), loaded: false };
968
+ const loadTimeoutAbortController = new AbortController();
969
+ const loadTimeoutAbortSignal = loadTimeoutAbortController.signal;
970
+ try {
971
+ logData(["Loading page", options.url], pageContext);
972
+ const readyPromise = waitForPageReadyState(state, waitUntil, pageContext);
973
+ const navigatePromise = session.send("browsingContext.navigate", { context, url: options.url, wait: "none" }, NO_TIMEOUT).catch(error => {
974
+ state.settle();
975
+ throw new Error(UNREACHABLE_URL_ERROR_MESSAGE + ": " + options.url + " (" + error.message + ")");
976
+ });
977
+ await Promise.race([
978
+ Promise.all([
979
+ readyPromise,
980
+ navigatePromise
981
+ ]),
982
+ waitForTimeout(loadTimeoutAbortSignal, options.browserLoadMaxTime, LOAD_TIMEOUT_ERROR_MESSAGE, LOAD_TIMEOUT_ERROR).catch(async error => {
983
+ if (options.browserWaitUntilFallback && state.reachedStateIndex >= 0) {
984
+ const reachedState = NETWORK_STATES[state.reachedStateIndex];
985
+ logData(["Stopping the page loading, reached state", reachedState], pageContext);
986
+ console.warn(`Warning: ${options.url} did not reach ${options.browserWaitUntil} within ${options.browserLoadMaxTime} ms, captured as it was at ${reachedState}`); // eslint-disable-line no-console
987
+ await session.send("script.evaluate", { expression: "window.stop()", target: { context }, awaitPromise: false });
988
+ state.settle();
989
+ return readyPromise;
990
+ }
991
+ throw error;
992
+ })
993
+ ]);
994
+ } finally {
995
+ if (!loadTimeoutAbortSignal.aborted) {
996
+ loadTimeoutAbortController.abort();
997
+ }
998
+ }
999
+ }
1000
+
1001
+ function waitForPageReadyState(state, waitUntil, pageContext) {
1002
+ const UNREACHABLE_URL_ERROR_MESSAGE = "Unreachable URL";
1003
+ const { options, context } = pageContext;
1004
+ return new Promise((resolve, reject) => {
1005
+ const timeoutState = { timeoutId: undefined, idleTimeoutId: undefined };
1006
+ const removers = [];
1007
+ const cleanup = () => {
1008
+ clearTimeout(timeoutState.idleTimeoutId);
1009
+ removers.forEach(remover => remover());
1010
+ };
1011
+ state.settle = () => {
1012
+ clearTimeout(timeoutState.timeoutId);
1013
+ cleanup();
1014
+ resolve();
1015
+ };
1016
+ removers.push(listen(pageContext, "browsingContext.navigationStarted", params => {
1017
+ if (params.context === context) {
1018
+ logData(["Detecting navigation", params.url], pageContext);
1019
+ clearTimeout(timeoutState.timeoutId);
1020
+ clearTimeout(timeoutState.idleTimeoutId);
1021
+ timeoutState.timeoutId = undefined;
1022
+ state.reachedStateIndex = -1;
1023
+ state.loaded = false;
1024
+ state.navigation = params.navigation;
1025
+ state.pendingRequests.clear();
1026
+ }
1027
+ }));
1028
+ removers.push(listen(pageContext, "browsingContext.navigationFailed", params => {
1029
+ if (params.context === context && params.navigation === state.navigation) {
1030
+ logData(["Detecting unreachable URL", params.url], pageContext);
1031
+ clearTimeout(timeoutState.timeoutId);
1032
+ cleanup();
1033
+ reject(new Error(UNREACHABLE_URL_ERROR_MESSAGE + ": " + params.url));
1034
+ }
1035
+ }));
1036
+ removers.push(listen(pageContext, "browsingContext.domContentLoaded", params => {
1037
+ if (params.context === context && params.navigation === state.navigation) {
1038
+ onStateReached(DOM_CONTENT_LOADED_STATE);
1039
+ }
1040
+ }));
1041
+ removers.push(listen(pageContext, "browsingContext.load", params => {
1042
+ if (params.context === context && params.navigation === state.navigation) {
1043
+ state.loaded = true;
1044
+ onStateReached(LOAD_STATE);
1045
+ scheduleNetworkIdleCheck();
1046
+ }
1047
+ }));
1048
+ removers.push(listen(pageContext, "network.beforeRequestSent", params => {
1049
+ if (isOwnContext(pageContext, params)) {
1050
+ state.pendingRequests.add(params.request.request);
1051
+ scheduleNetworkIdleCheck();
1052
+ }
1053
+ }));
1054
+ removers.push(listen(pageContext, "network.responseCompleted", params => {
1055
+ if (isOwnContext(pageContext, params)) {
1056
+ state.pendingRequests.delete(params.request.request);
1057
+ scheduleNetworkIdleCheck();
1058
+ }
1059
+ }));
1060
+ removers.push(listen(pageContext, "network.fetchError", params => {
1061
+ if (isOwnContext(pageContext, params)) {
1062
+ state.pendingRequests.delete(params.request.request);
1063
+ scheduleNetworkIdleCheck();
1064
+ }
1065
+ }));
1066
+
1067
+ function scheduleNetworkIdleCheck() {
1068
+ clearTimeout(timeoutState.idleTimeoutId);
1069
+ if (state.loaded) {
1070
+ timeoutState.idleTimeoutId = setTimeout(() => {
1071
+ if (state.pendingRequests.size <= NETWORK_ALMOST_IDLE_MAX_REQUESTS) {
1072
+ onStateReached(NETWORK_ALMOST_IDLE_STATE);
1073
+ }
1074
+ if (state.pendingRequests.size == 0) {
1075
+ onStateReached(NETWORK_IDLE_STATE);
1076
+ }
1077
+ }, NETWORK_IDLE_DELAY);
1078
+ }
1079
+ }
1080
+
1081
+ function onStateReached(name) {
1082
+ logData(["Detecting lifecycle event", name], pageContext);
1083
+ const stateIndex = NETWORK_STATES.indexOf(name);
1084
+ if (state.reachedStateIndex == -1 || stateIndex < state.reachedStateIndex) {
1085
+ state.reachedStateIndex = stateIndex;
1086
+ }
1087
+ const shouldResolve = name === waitUntil ||
1088
+ (timeoutState.timeoutId && stateIndex < NETWORK_STATES.indexOf(waitUntil));
1089
+ if (shouldResolve) {
1090
+ clearTimeout(timeoutState.timeoutId);
1091
+ logData([`Waiting ${options.browserWaitUntilDelay} ms`], pageContext);
1092
+ timeoutState.timeoutId = setTimeout(() => {
1093
+ logData(["Detecting page ready"], pageContext);
1094
+ cleanup();
1095
+ resolve();
1096
+ }, options.browserWaitUntilDelay);
1097
+ }
1098
+ }
1099
+ });
1100
+ }
1101
+
1102
+ async function checkSingleFileContext(pageContext) {
1103
+ const SINGLE_FILE_DETECTION_TEST = "typeof singlefile !== 'undefined'";
1104
+ const NO_VALID_CONTEXT_ERROR_MESSAGE = "No valid SingleFile execution context found";
1105
+ const { context } = pageContext;
1106
+ logData(["Getting execution context"], pageContext);
1107
+ const { result } = await session.send("script.evaluate", {
1108
+ expression: SINGLE_FILE_DETECTION_TEST,
1109
+ target: getSingleFileTarget(context),
1110
+ awaitPromise: false
1111
+ });
1112
+ if (!result || result.value !== true) {
1113
+ throw new Error(NO_VALID_CONTEXT_ERROR_MESSAGE);
1114
+ }
1115
+ }
1116
+
1117
+ async function capturePageData(pageContext) {
1118
+ const CAPTURE_TIMEOUT_ERROR_MESSAGE = "Capture timeout";
1119
+ const EXCEPTION_TYPE = "exception";
1120
+ const { options, context } = pageContext;
1121
+ const captureTimeoutAbortController = new AbortController();
1122
+ const captureTimeoutAbortSignal = captureTimeoutAbortController.signal;
1123
+ if (options.browserWaitDelay) {
1124
+ logData([`Waiting ${options.browserWaitDelay} ms`], pageContext);
1125
+ await new Promise(resolve => setTimeout(resolve, options.browserWaitDelay));
1126
+ }
1127
+ try {
1128
+ logData(["Capturing page"], pageContext);
1129
+ const captureScript = `(${getPageDataScriptSource.toString()})(${JSON.stringify(options)},${JSON.stringify([
1130
+ SET_SCREENSHOT_FUNCTION_NAME,
1131
+ SET_PDF_FUNCTION_NAME,
1132
+ SET_PAGE_DATA_FUNCTION_NAME,
1133
+ CAPTURE_SCREENSHOT_FUNCTION_NAME,
1134
+ PRINT_TO_PDF_FUNCTION_NAME
1135
+ ])})`;
1136
+ const result = await Promise.race([
1137
+ session.send("script.evaluate", {
1138
+ expression: captureScript,
1139
+ target: getSingleFileTarget(context),
1140
+ awaitPromise: true,
1141
+ resultOwnership: "none"
1142
+ }, NO_TIMEOUT),
1143
+ waitForTimeout(captureTimeoutAbortSignal, options.browserCaptureMaxTime, CAPTURE_TIMEOUT_ERROR_MESSAGE, CAPTURE_TIMEOUT_ERROR)
1144
+ ]);
1145
+ if (result.type === EXCEPTION_TYPE) {
1146
+ const { exceptionDetails } = result;
1147
+ logData(["Capture exception", JSON.stringify(exceptionDetails)], pageContext);
1148
+ throw new Error(exceptionDetails.text || (exceptionDetails.exception && exceptionDetails.exception.value) || "Capture failed");
1149
+ }
1150
+ } finally {
1151
+ if (!captureTimeoutAbortSignal.aborted) {
1152
+ captureTimeoutAbortController.abort();
1153
+ }
1154
+ }
1155
+ }
1156
+
1157
+ async function finalizePageData(pageDataPromise, pageContext) {
1158
+ const { options, consoleMessages, debugMessages, httpInfo } = pageContext;
1159
+ const pageData = await pageDataPromise;
1160
+ logData(["Returning page data"], pageContext);
1161
+ if (options.consoleMessagesFile) {
1162
+ pageData.consoleMessages = consoleMessages;
1163
+ }
1164
+ if (options.debugMessagesFile) {
1165
+ pageData.debugMessages = debugMessages;
1166
+ }
1167
+ Object.assign(pageData, httpInfo);
1168
+ if (options.browserWaitEndDelay) {
1169
+ logData([`Waiting ${options.browserWaitEndDelay} ms after processing`], pageContext);
1170
+ await new Promise(resolve => setTimeout(resolve, options.browserWaitEndDelay));
1171
+ }
1172
+ return pageData;
1173
+ }
1174
+
1175
+ async function cleanup(pageContext) {
1176
+ const { options, context, listeners, preloadScripts, subscriptions, intercepts } = pageContext;
1177
+ listeners.forEach(([eventName, listener]) => session.removeEventListener(eventName, listener));
1178
+ for (const intercept of intercepts) {
1179
+ await session.send("network.removeIntercept", { intercept }).catch(() => { });
1180
+ }
1181
+ for (const script of preloadScripts) {
1182
+ await session.send("script.removePreloadScript", { script }).catch(() => { });
1183
+ }
1184
+ if (subscriptions.length) {
1185
+ await session.send("session.unsubscribe", { subscriptions }).catch(() => { });
1186
+ }
1187
+ if (context && !options.browserDebug) {
1188
+ await session.send("browsingContext.close", { context }).catch(() => { });
1189
+ }
1190
+ }
1191
+
1192
+ function listen(pageContext, eventName, listener) {
1193
+ session.addEventListener(eventName, listener);
1194
+ pageContext.listeners.push([eventName, listener]);
1195
+ return () => session.removeEventListener(eventName, listener);
1196
+ }
1197
+
1198
+ function attachDebugInfo(error, { options, consoleMessages, debugMessages }) {
1199
+ if (options.consoleMessagesFile) {
1200
+ error.consoleMessages = consoleMessages;
1201
+ }
1202
+ if (options.debugMessagesFile) {
1203
+ error.debugMessages = debugMessages;
1204
+ }
1205
+ }
1206
+
1207
+ function ignoringErrors(listener, pageContext) {
1208
+ return async event => {
1209
+ try {
1210
+ await listener(event);
1211
+ } catch (error) {
1212
+ logData(["Ignoring event listener error", error.message], pageContext);
1213
+ }
1214
+ };
1215
+ }
1216
+
1217
+ function logData(data, { options, debugMessages }) {
1218
+ if (options.debugMessagesFile) {
1219
+ debugMessages.push([Date.now(), data]);
1220
+ }
1221
+ }