single-file-cli 2.0.76 → 2.0.78

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/cdp-client.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright 2010-2024 Gildas Lormeau
2
+ * Copyright 2010-2025 Gildas Lormeau
3
3
  * contact : gildas.lormeau <at> gmail.com
4
4
  *
5
5
  * This file is part of SingleFile.
@@ -21,436 +21,379 @@
21
21
  * Source.
22
22
  */
23
23
 
24
- /* global setTimeout, clearTimeout, URL, fetch, AbortController, window, top, singlefile */
24
+ /* global setTimeout, clearTimeout, URL, AbortController */
25
25
 
26
- import { launchBrowser, closeBrowser } from "./browser.js";
27
- import { getScriptSource, getHookScriptSource } from "./single-file-script.js";
28
- import { CDP, options } from "simple-cdp";
26
+ import {
27
+ launchBrowser,
28
+ closeBrowser
29
+ } from "./browser.js";
30
+ import {
31
+ CDP,
32
+ options
33
+ } from "simple-cdp";
34
+ import {
35
+ FETCH_FUNCTION_NAME,
36
+ RESOLVE_FETCH_FUNCTION_NAME,
37
+ REJECT_FETCH_FUNCTION_NAME,
38
+ getScriptSource,
39
+ getHookScriptSource,
40
+ getPageDataScriptSource
41
+ } from "./single-file-script.js";
42
+ import {
43
+ fetch,
44
+ waitForTimeout,
45
+ arrayBufferToBase64,
46
+ getAlternativeUrl
47
+ } from "./cdp-client-util.js";
29
48
 
30
49
  const LOAD_TIMEOUT_ERROR = "ERR_LOAD_TIMEOUT";
31
50
  const CAPTURE_TIMEOUT_ERROR = "ERR_CAPTURE_TIMEOUT";
32
51
  const NETWORK_STATES = ["InteractiveTime", "networkIdle", "networkAlmostIdle", "load", "DOMContentLoaded"];
33
52
  const MINIMIZED_WINDOW_STATE = "minimized";
34
53
  const SINGLE_FILE_WORLD_NAME = "singlefile";
35
- const EMPTY_PAGE_URL = "about:blank";
36
54
  const CAPTURE_SCREENSHOT_FUNCTION_NAME = "captureScreenshot";
37
55
  const PRINT_TO_PDF_FUNCTION_NAME = "printToPDF";
38
56
  const SET_SCREENSHOT_FUNCTION_NAME = "setScreenshot";
39
57
  const SET_PDF_FUNCTION_NAME = "setPDF";
40
58
  const SET_PAGE_DATA_FUNCTION_NAME = "setPageData";
41
- const REDIRECT_STATUS_CODES = [301, 302, 303, 307, 308];
59
+ const BINDING_CALLED_EVENT_TYPE = "bindingCalled";
42
60
 
43
- export { initialize, getPageData, closeBrowser };
61
+ export {
62
+ initialize,
63
+ getPageData,
64
+ closeBrowser
65
+ };
44
66
 
45
67
  async function initialize(singleFileOptions) {
46
68
  if (singleFileOptions.browserServer) {
47
69
  options.apiUrl = singleFileOptions.browserServer;
48
70
  } else {
49
- options.apiUrl = "http://localhost:" + (await launchBrowser(getBrowserOptions(singleFileOptions)));
71
+ const LOCALHOST = "http://localhost:";
72
+ const browserOptions = {};
73
+ browserOptions.args = singleFileOptions.browserArgs;
74
+ browserOptions.headless = singleFileOptions.browserHeadless;
75
+ browserOptions.executablePath = singleFileOptions.browserExecutablePath;
76
+ browserOptions.debug = singleFileOptions.browserDebug;
77
+ browserOptions.disableWebSecurity = singleFileOptions.browserDisableWebSecurity;
78
+ browserOptions.width = singleFileOptions.browserWidth;
79
+ browserOptions.height = singleFileOptions.browserHeight;
80
+ browserOptions.userAgent = singleFileOptions.userAgent;
81
+ browserOptions.httpProxyServer = singleFileOptions.httpProxyServer;
82
+ options.apiUrl = LOCALHOST + (await launchBrowser(browserOptions));
50
83
  }
51
84
  }
52
85
 
53
86
  async function getPageData(options) {
54
- const debugMessages = [];
55
- const consoleMessages = [];
87
+ const EMPTY_PAGE_URL = "about:blank";
88
+ const pageContext = { options, consoleMessages: [], debugMessages: [], httpInfo: {} };
56
89
  let targetInfo;
57
90
  try {
58
- if (options.debugMessagesFile) {
59
- debugMessages.push([Date.now(), ["Loading page", EMPTY_PAGE_URL]]);
60
- }
91
+ logData(["Loading page", EMPTY_PAGE_URL], pageContext);
61
92
  targetInfo = await CDP.createTarget(EMPTY_PAGE_URL);
62
- const { Browser, Security, Page, Emulation, Fetch, Network, Runtime, Debugger, Console } = new CDP(targetInfo);
63
- if (options.consoleMessagesFile) {
64
- if (options.debugMessagesFile) {
65
- debugMessages.push([Date.now(), ["Enabling console messages"]]);
66
- }
67
- await Console.enable();
68
- Console.addEventListener("messageAdded", ({ params }) => {
69
- const { message } = params;
70
- consoleMessages.push(message);
71
- });
72
- }
73
- if (options.browserStartMinimized) {
74
- const { windowId, bounds } = await Browser.getWindowForTarget({ targetId: targetInfo.id });
75
- if (bounds.windowState !== MINIMIZED_WINDOW_STATE) {
76
- if (options.debugMessagesFile) {
77
- debugMessages.push([Date.now(), ["Minimizing window"]]);
78
- }
79
- await Browser.setWindowBounds({ windowId, bounds: { windowState: MINIMIZED_WINDOW_STATE } });
80
- }
81
- }
82
- if (options.browserIgnoreHTTPSErrors !== undefined && options.browserIgnoreHTTPSErrors) {
83
- if (options.debugMessagesFile) {
84
- debugMessages.push([Date.now(), ["Ignoring HTTPS errors"]]);
85
- }
86
- await Security.setIgnoreCertificateErrors({ ignore: true });
87
- }
88
- if (options.browserByPassCSP === undefined || options.browserByPassCSP) {
89
- if (options.debugMessagesFile) {
90
- debugMessages.push([Date.now(), ["Bypassing CSP"]]);
91
- }
92
- await Page.setBypassCSP({ enabled: true });
93
- }
94
- if (options.browserMobileEmulation || options.browserDeviceWidth || options.browserDeviceHeight || options.browserDeviceScaleFactor || options.platform || options.acceptLanguage) {
95
- if (options.browserMobileEmulation || options.browserDeviceWidth || options.browserDeviceHeight || options.browserDeviceScaleFactor) {
96
- let browserDeviceWidth;
97
- if (!options.browserDeviceWidth) {
98
- const { result } = await Runtime.evaluate({ expression: "window.innerWidth" });
99
- browserDeviceWidth = result.value;
100
- }
101
- let browserDeviceHeight;
102
- if (!options.browserDeviceHeight) {
103
- const { result } = await Runtime.evaluate({ expression: "window.innerHeight" });
104
- browserDeviceHeight = result.value;
105
- }
106
- let browserDeviceScaleFactor;
107
- if (!options.browserDeviceScaleFactor) {
108
- const { result } = await Runtime.evaluate({ expression: "window.devicePixelRatio" });
109
- browserDeviceScaleFactor = result.value;
110
- }
111
- const deviceMetricsOptions = {
112
- mobile: Boolean(options.browserMobileEmulation),
113
- width: options.browserDeviceWidth || (options.browserMobileEmulation ? 360 : options.width || browserDeviceWidth),
114
- height: options.browserDeviceHeight || (options.browserMobileEmulation ? 800 : options.height || browserDeviceHeight),
115
- deviceScaleFactor: options.browserDeviceScaleFactor || (options.browserMobileEmulation ? 2 : browserDeviceScaleFactor)
116
- };
117
- if (options.debugMessagesFile) {
118
- debugMessages.push([Date.now(), ["Emulating device metrics", JSON.stringify(deviceMetricsOptions)]]);
119
- }
120
- await Emulation.setDeviceMetricsOverride(deviceMetricsOptions);
121
- }
122
- if (options.browserMobileEmulation || options.platform || options.acceptLanguage) {
123
- const { userAgent, product } = await Browser.getVersion();
124
- const agentOptions = {
125
- userAgent: options.userAgent || (options.browserMobileEmulation ? "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) " + product + " Mobile Safari/537.36" : userAgent)
126
- };
127
- if (options.acceptLanguage) {
128
- agentOptions.acceptLanguage = options.acceptLanguage;
129
- }
130
- if (options.platform || options.browserMobileEmulation) {
131
- agentOptions.platform = options.platform || "Android";
132
- }
133
- if (options.debugMessagesFile) {
134
- debugMessages.push([Date.now(), ["Emulating user agent", JSON.stringify(agentOptions)]]);
135
- }
136
- await Emulation.setUserAgentOverride(agentOptions);
137
- }
93
+ const cdp = new CDP(targetInfo);
94
+ await setupConsoleLogging(cdp, pageContext);
95
+ await setupBrowserWindow(cdp, targetInfo.id, pageContext);
96
+ await setupSecurity(cdp, pageContext);
97
+ await setupDeviceEmulation(cdp, pageContext);
98
+ await setupNetworkInterception(cdp, pageContext);
99
+ await setupScriptInjection(cdp, pageContext);
100
+ const contextId = await getContextId(cdp, pageContext);
101
+ const pageDataPromise = setupPageDataCapture(cdp, contextId, pageContext);
102
+ await setupBindings(cdp, contextId, pageContext);
103
+ await capturePageData(cdp, contextId, pageContext);
104
+ await disableCdpDomains(cdp, pageContext);
105
+ return await finalizePageData(pageDataPromise, pageContext);
106
+ } catch (error) {
107
+ if (shouldRetryWithFallback(error)) {
108
+ return await retryWithFallback();
138
109
  }
139
- const handleAuthRequests = Boolean(options.httpProxyUsername);
140
- const patterns = handleAuthRequests ? [{ requestStage: "Request" }, { requestStage: "Response" }] : [{ requestStage: "Response" }];
141
- await Fetch.enable({ handleAuthRequests, patterns });
142
- if (handleAuthRequests) {
143
- Fetch.addEventListener("authRequired", async ({ params }) => {
144
- if (options.debugMessagesFile) {
145
- debugMessages.push([Date.now(), ["Authenticating"]]);
146
- }
147
- await Fetch.continueWithAuth({
148
- requestId: params.requestId,
149
- authChallengeResponse: {
150
- response: "ProvideCredentials",
151
- username: options.httpProxyUsername,
152
- password: options.httpProxyPassword
153
- }
154
- });
155
- });
110
+ attachDebugInfo(error, pageContext);
111
+ throw error;
112
+ } finally {
113
+ logData(["Closing page"], pageContext);
114
+ await closeTarget();
115
+ logData(["Finishing"], pageContext);
116
+ }
117
+
118
+ function shouldRetryWithFallback(error) {
119
+ return error.code === LOAD_TIMEOUT_ERROR &&
120
+ options.browserWaitUntilFallback &&
121
+ options.browserWaitUntil &&
122
+ NETWORK_STATES.indexOf(options.browserWaitUntil) < NETWORK_STATES.length - 1;
123
+ }
124
+
125
+ async function retryWithFallback() {
126
+ const browserWaitUntil = NETWORK_STATES[(NETWORK_STATES.indexOf(options.browserWaitUntil) + 1)];
127
+ logData(["Retrying with waitUntil", browserWaitUntil], pageContext);
128
+ options.browserWaitUntil = browserWaitUntil;
129
+ await closeTarget();
130
+ return await getPageData(options);
131
+ }
132
+
133
+ async function closeTarget() {
134
+ if (targetInfo && !options.browserDebug) {
135
+ await CDP.closeTarget(targetInfo.id);
136
+ targetInfo = null;
156
137
  }
157
- let url = options.url;
158
- let alternativeUrl = new URL(url);
159
- if (!alternativeUrl.pathname.endsWith("/")) {
160
- alternativeUrl.pathname = alternativeUrl.pathname + "/";
138
+ }
139
+ }
140
+
141
+ async function setupConsoleLogging({ Console }, { options, consoleMessages, debugMessages }) {
142
+ const CONSOLE_MESSAGE_ADDED_EVENT_TYPE = "messageAdded";
143
+ if (options.consoleMessagesFile) {
144
+ logData(["Enabling console messages"], { options, debugMessages });
145
+ await Console.enable();
146
+ Console.addEventListener(CONSOLE_MESSAGE_ADDED_EVENT_TYPE, ({ params }) => {
147
+ consoleMessages.push(params.message);
148
+ });
149
+ }
150
+ }
151
+
152
+ async function setupBrowserWindow({ Browser }, targetId, { options, debugMessages }) {
153
+ if (options.browserStartMinimized) {
154
+ const { windowId, bounds } = await Browser.getWindowForTarget({ targetId });
155
+ if (bounds.windowState !== MINIMIZED_WINDOW_STATE) {
156
+ logData(["Minimizing window"], { options, debugMessages });
157
+ await Browser.setWindowBounds({ windowId, bounds: { windowState: MINIMIZED_WINDOW_STATE } });
161
158
  }
162
- alternativeUrl = alternativeUrl.href;
163
- let httpInfo;
164
- Fetch.addEventListener("requestPaused", async ({ params }) => {
165
- const { requestId, request, resourceType, responseHeaders, responseStatusCode, responseStatusText } = params;
166
- if (resourceType == "Document" && (options.outputJson && !httpInfo && (request.url == url || request.url == alternativeUrl) && responseStatusCode !== undefined)) {
167
- if (REDIRECT_STATUS_CODES.includes(responseStatusCode)) {
168
- const redirect = responseHeaders.find(header => header.name.toLowerCase() == "location").value;
169
- if (redirect) {
170
- url = new URL(redirect, url).href;
171
- }
172
- if (options.debugMessagesFile) {
173
- debugMessages.push([Date.now(), ["Redirecting", url]]);
174
- }
175
- } else {
176
- httpInfo = {
177
- request: {
178
- url: request.url,
179
- method: request.method,
180
- headers: request.headers,
181
- referrerPolicy: request.referrerPolicy
182
- },
183
- resourceType,
184
- response: {
185
- status: responseStatusCode,
186
- statusText: responseStatusText,
187
- headers: responseHeaders
188
- }
189
- };
190
- }
191
- } else if (options.blockedURLPatterns && options.blockedURLPatterns.length) {
192
- const blockedURL = options.blockedURLPatterns.find(blockedURL => new RegExp(blockedURL).test(request.url));
193
- if (blockedURL) {
194
- try {
195
- if (options.debugMessagesFile) {
196
- debugMessages.push([Date.now(), ["Blocking request", request.url]]);
197
- }
198
- await Fetch.failRequest({ requestId, errorReason: "Aborted" });
199
- } catch {
200
- // ignored
201
- }
202
- }
159
+ }
160
+ }
161
+
162
+ async function setupSecurity({ Security }, { options, debugMessages }) {
163
+ if (options.browserIgnoreHTTPSErrors !== undefined && options.browserIgnoreHTTPSErrors) {
164
+ logData(["Ignoring HTTPS errors"], { options, debugMessages });
165
+ await Security.setIgnoreCertificateErrors({ ignore: true });
166
+ }
167
+ }
168
+
169
+ async function setupDeviceEmulation({ Browser, Emulation, Runtime }, { options, debugMessages }) {
170
+ const needsDeviceMetrics = options.browserMobileEmulation || options.browserDeviceWidth ||
171
+ options.browserDeviceHeight || options.browserDeviceScaleFactor;
172
+ const needsUserAgent = options.browserMobileEmulation || options.platform || options.acceptLanguage;
173
+ if (needsDeviceMetrics) {
174
+ await setupDeviceMetrics({ Emulation, Runtime }, { options, debugMessages });
175
+ }
176
+ if (needsUserAgent) {
177
+ await setupUserAgent({ Browser, Emulation }, { options, debugMessages });
178
+ }
179
+ }
180
+
181
+ async function setupDeviceMetrics({ Emulation, Runtime }, { options, debugMessages }) {
182
+ const INNER_WIDTH_PROPERTY = "window.innerWidth";
183
+ const INNER_HEIGHT_PROPERTY = "window.innerHeight";
184
+ const DEVICE_PIXEL_RATIO_PROPERTY = "window.devicePixelRatio";
185
+ const browserDeviceWidth = options.browserDeviceWidth ||
186
+ (await Runtime.evaluate({ expression: INNER_WIDTH_PROPERTY })).result.value;
187
+ const browserDeviceHeight = options.browserDeviceHeight ||
188
+ (await Runtime.evaluate({ expression: INNER_HEIGHT_PROPERTY })).result.value;
189
+ const browserDeviceScaleFactor = options.browserDeviceScaleFactor ||
190
+ (await Runtime.evaluate({ expression: DEVICE_PIXEL_RATIO_PROPERTY })).result.value;
191
+ const deviceMetricsOptions = {
192
+ mobile: Boolean(options.browserMobileEmulation),
193
+ width: options.browserDeviceWidth || (options.browserMobileEmulation ? 360 : options.width || browserDeviceWidth),
194
+ height: options.browserDeviceHeight || (options.browserMobileEmulation ? 800 : options.height || browserDeviceHeight),
195
+ deviceScaleFactor: options.browserDeviceScaleFactor || (options.browserMobileEmulation ? 2 : browserDeviceScaleFactor)
196
+ };
197
+ logData(["Emulating device metrics", JSON.stringify(deviceMetricsOptions)], { options, debugMessages });
198
+ await Emulation.setDeviceMetricsOverride(deviceMetricsOptions);
199
+ }
200
+
201
+ async function setupUserAgent({ Browser, Emulation }, { options, debugMessages }) {
202
+ const ANDROID_PLATFORM = "Android";
203
+ const { userAgent, product } = await Browser.getVersion();
204
+ const defaultMobileUA = `Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) ${product} Mobile Safari/537.36`;
205
+ const agentOptions = {
206
+ userAgent: options.userAgent || (options.browserMobileEmulation ? defaultMobileUA : userAgent)
207
+ };
208
+ if (options.acceptLanguage) {
209
+ agentOptions.acceptLanguage = options.acceptLanguage;
210
+ }
211
+ if (options.platform || options.browserMobileEmulation) {
212
+ agentOptions.platform = options.platform || ANDROID_PLATFORM;
213
+ }
214
+ logData(["Emulating user agent", JSON.stringify(agentOptions)], { options, debugMessages });
215
+ await Emulation.setUserAgentOverride(agentOptions);
216
+ }
217
+
218
+ async function setupNetworkInterception({ Browser, Emulation, Fetch, Network }, { options, debugMessages, httpInfo }) {
219
+ const REQUEST_STAGE = "Request";
220
+ const RESPONSE_STAGE = "Response";
221
+ const DENY_BEHAVIOR = "deny";
222
+ const handleAuthRequests = Boolean(options.httpProxyUsername);
223
+ const patterns = handleAuthRequests ?
224
+ [{ requestStage: REQUEST_STAGE }, { requestStage: RESPONSE_STAGE }] :
225
+ [{ requestStage: RESPONSE_STAGE }];
226
+ await Fetch.enable({ handleAuthRequests, patterns });
227
+ if (handleAuthRequests) {
228
+ setupProxyAuth({ Fetch }, { options, debugMessages });
229
+ }
230
+ setupRequestInterception({ Fetch }, { options, debugMessages, httpInfo });
231
+ if (options.httpHeaders) {
232
+ await setupHttpHeaders({ Network }, { options, debugMessages });
233
+ }
234
+ if (options.emulateMediaFeatures) {
235
+ await setupMediaFeatures({ Emulation }, { options, debugMessages });
236
+ }
237
+ if (options.browserCookies && options.browserCookies.length) {
238
+ await setupCookies({ Network }, { options, debugMessages });
239
+ }
240
+ await Browser.setDownloadBehavior({ behavior: DENY_BEHAVIOR });
241
+ }
242
+
243
+ function setupProxyAuth({ Fetch }, { options, debugMessages }) {
244
+ const AUTH_REQUIRED_EVENT_TYPE = "authRequired";
245
+ const PROVIDE_CREDENTIALS_RESPONSE = "ProvideCredentials";
246
+ Fetch.addEventListener(AUTH_REQUIRED_EVENT_TYPE, async ({ params }) => {
247
+ logData(["Authenticating"], { options, debugMessages });
248
+ await Fetch.continueWithAuth({
249
+ requestId: params.requestId,
250
+ authChallengeResponse: {
251
+ response: PROVIDE_CREDENTIALS_RESPONSE,
252
+ username: options.httpProxyUsername,
253
+ password: options.httpProxyPassword
203
254
  }
255
+ });
256
+ });
257
+ }
258
+
259
+ function setupRequestInterception({ Fetch }, { options, debugMessages, httpInfo }) {
260
+ const REQUEST_PAUSED_EVENT_TYPE = "requestPaused";
261
+ const ABORTED_ERROR_REASON = "Aborted";
262
+ const urlState = { url: options.url, alternativeUrl: getAlternativeUrl(options.url) };
263
+ Fetch.addEventListener(REQUEST_PAUSED_EVENT_TYPE, async ({ params }) => {
264
+ const { requestId, request } = params;
265
+ captureHttpInfo(params, urlState, { options, debugMessages, httpInfo });
266
+ if (shouldBlockRequest(request.url)) {
204
267
  try {
205
- await Fetch.continueRequest({ requestId });
268
+ await Fetch.failRequest({ requestId, errorReason: ABORTED_ERROR_REASON });
269
+ return;
206
270
  } catch {
207
271
  // ignored
208
272
  }
209
- });
210
- if (options.httpHeaders) {
211
- if (options.debugMessagesFile) {
212
- debugMessages.push([Date.now(), ["Setting HTTP headers", JSON.stringify(options.httpHeaders)]]);
213
- }
214
- await Network.enable();
215
- await Network.setExtraHTTPHeaders({ headers: options.httpHeaders });
216
273
  }
217
- if (options.emulateMediaFeatures) {
218
- for (const mediaFeature of options.emulateMediaFeatures) {
219
- if (options.debugMessagesFile) {
220
- debugMessages.push([Date.now(), ["Emulating media feature", mediaFeature.name, mediaFeature.value]]);
221
- }
222
- await Emulation.setEmulatedMedia({
223
- media: mediaFeature.name,
224
- features: mediaFeature.value.split(",").map(feature => feature.trim())
225
- });
226
- }
227
- }
228
- if (options.browserCookies && options.browserCookies.length) {
229
- if (options.debugMessagesFile) {
230
- debugMessages.push([Date.now(), ["Setting cookies", JSON.stringify(options.browserCookies)]]);
231
- }
232
- await Network.setCookies({ cookies: options.browserCookies });
233
- }
234
- await Browser.setDownloadBehavior({ behavior: "deny" });
235
- await Page.addScriptToEvaluateOnNewDocument({
236
- source: getHookScriptSource(),
237
- runImmediately: true
238
- });
239
- await Page.addScriptToEvaluateOnNewDocument({
240
- source: await getScriptSource(options),
241
- runImmediately: true,
242
- worldName: SINGLE_FILE_WORLD_NAME
243
- });
244
- const [contextId] = await Promise.all([
245
- loadPage({ Page, Runtime }, options, debugMessages),
246
- options.browserDebug ? waitForDebuggerReady({ Debugger }) : Promise.resolve()
247
- ]);
248
- await Runtime.addBinding({ name: SET_PAGE_DATA_FUNCTION_NAME, executionContextId: contextId });
249
- if (options.embedScreenshot && options.compressContent) {
250
- await Runtime.addBinding({ name: CAPTURE_SCREENSHOT_FUNCTION_NAME, executionContextId: contextId });
251
- Runtime.addEventListener("bindingCalled", async ({ params }) => {
252
- if (params.name === CAPTURE_SCREENSHOT_FUNCTION_NAME) {
253
- if (options.debugMessagesFile) {
254
- debugMessages.push([Date.now(), ["Capturing screenshot"]]);
255
- }
256
- try {
257
- let screenshotOptions = { captureBeyondViewport: true };
258
- if (options.embedScreenshotOptions) {
259
- try {
260
- screenshotOptions = JSON.parse(options.embedScreenshotOptions);
261
- } catch {
262
- // ignored
263
- }
264
- }
265
- screenshotOptions.format = "png";
266
- const { data } = await Page.captureScreenshot(screenshotOptions);
267
- await Runtime.evaluate({ expression: `globalThis.${SET_SCREENSHOT_FUNCTION_NAME}(${JSON.stringify(data)})`, contextId });
268
- } catch {
269
- await Runtime.evaluate({ expression: `globalThis.${SET_SCREENSHOT_FUNCTION_NAME}(${JSON.stringify("")})`, contextId });
270
- }
271
- }
272
- });
273
- }
274
- if (options.embedPdf) {
275
- await Runtime.addBinding({ name: PRINT_TO_PDF_FUNCTION_NAME, executionContextId: contextId });
276
- Runtime.addEventListener("bindingCalled", async ({ params }) => {
277
- if (params.name === PRINT_TO_PDF_FUNCTION_NAME) {
278
- if (options.debugMessagesFile) {
279
- debugMessages.push([Date.now(), ["Printing to PDF", options.embedPdfOptions || ""]]);
280
- }
281
- let pdfOptions = {};
282
- if (options.embedPdfOptions) {
283
- try {
284
- pdfOptions = JSON.parse(options.embedPdfOptions);
285
- } catch {
286
- // ignored
287
- }
288
- }
289
- try {
290
- const { data } = await Page.printToPDF(pdfOptions);
291
- await Runtime.evaluate({ expression: `globalThis.${SET_PDF_FUNCTION_NAME}(${JSON.stringify(data)})`, contextId });
292
- } catch {
293
- await Runtime.evaluate({ expression: `globalThis.${SET_PDF_FUNCTION_NAME}(${JSON.stringify("")})`, contextId });
294
- }
295
- }
296
- });
297
- }
298
- const pageDataPromise = new Promise(resolve => {
299
- let pageDataResponse = "";
300
- Runtime.addEventListener("bindingCalled", ({ params }) => {
301
- if (params.name === SET_PAGE_DATA_FUNCTION_NAME) {
302
- const { payload } = params;
303
- if (payload.length) {
304
- pageDataResponse += payload;
305
- } else {
306
- if (options.debugMessagesFile) {
307
- debugMessages.push([Date.now(), ["Setting page data"]]);
308
- }
309
- const result = JSON.parse(pageDataResponse);
310
- if (result.content instanceof Array) {
311
- result.content = new Uint8Array(result.content);
312
- }
313
- resolve(result);
314
- }
315
- }
316
- });
317
- });
318
- if (options.browserWaitDelay) {
319
- if (options.debugMessagesFile) {
320
- debugMessages.push([Date.now(), [`Waiting ${options.browserWaitDelay} ms`]]);
321
- }
322
- await new Promise(resolve => setTimeout(resolve, options.browserWaitDelay));
323
- }
324
- const captureTimeoutAbortController = new AbortController();
325
- const captureTimeoutAbortSignal = captureTimeoutAbortController.signal;
326
274
  try {
327
- if (options.debugMessagesFile) {
328
- debugMessages.push([Date.now(), ["Capturing page"]]);
329
- }
330
- const { result } = await Promise.race([
331
- Runtime.evaluate({
332
- expression: `(${getPageDataScriptSource.toString()})(${JSON.stringify(options)},${JSON.stringify([SET_SCREENSHOT_FUNCTION_NAME, SET_PDF_FUNCTION_NAME, SET_PAGE_DATA_FUNCTION_NAME, CAPTURE_SCREENSHOT_FUNCTION_NAME, PRINT_TO_PDF_FUNCTION_NAME])})`,
333
- awaitPromise: true,
334
- returnByValue: true,
335
- contextId
336
- }),
337
- waitForTimeout(captureTimeoutAbortSignal, options.browserCaptureMaxTime, "Capture timeout", CAPTURE_TIMEOUT_ERROR)
338
- ]);
339
- const { subtype, description } = result;
340
- if (subtype === "error") {
341
- throw new Error(description);
342
- }
343
- } finally {
344
- if (!captureTimeoutAbortSignal.aborted) {
345
- captureTimeoutAbortController.abort();
346
- }
347
- await Runtime.disable();
348
- await Page.disable();
349
- if (options.httpHeaders) {
350
- await Network.disable();
351
- }
275
+ await Fetch.continueRequest({ requestId });
276
+ } catch {
277
+ // ignored
352
278
  }
353
- const pageData = await pageDataPromise;
354
- if (options.consoleMessagesFile) {
355
- await Console.disable();
356
- pageData.consoleMessages = consoleMessages;
279
+ });
280
+
281
+ function shouldBlockRequest(requestUrl) {
282
+ if (!options.blockedURLPatterns || !options.blockedURLPatterns.length) {
283
+ return false;
357
284
  }
358
- if (options.debugMessagesFile) {
359
- pageData.debugMessages = debugMessages;
360
- debugMessages.push([Date.now(), ["Returning page data"]]);
285
+ const blockedURL = options.blockedURLPatterns.find(pattern =>
286
+ new RegExp(pattern).test(requestUrl)
287
+ );
288
+ if (blockedURL) {
289
+ logData(["Blocking request", requestUrl], { options, debugMessages });
290
+ return true;
361
291
  }
362
- Object.assign(pageData, httpInfo);
363
- return pageData;
364
- } catch (error) {
365
- if (error.code === LOAD_TIMEOUT_ERROR && options.browserWaitUntilFallback && options.browserWaitUntil) {
366
- const browserWaitUntil = NETWORK_STATES[(NETWORK_STATES.indexOf(options.browserWaitUntil) + 1)];
367
- if (browserWaitUntil) {
368
- if (options.debugMessagesFile) {
369
- debugMessages.push([Date.now(), ["Retrying with waitUntil", browserWaitUntil]]);
370
- }
371
- options.browserWaitUntil = browserWaitUntil;
372
- await closeTarget();
373
- return await getPageData(options);
292
+ return false;
293
+ }
294
+ }
295
+
296
+ function captureHttpInfo(params, urlState, { options, debugMessages, httpInfo }) {
297
+ const REDIRECT_STATUS_CODES = [301, 302, 303, 307, 308];
298
+ const DOCUMENT_RESOURCE_TYPE = "Document";
299
+ const LOCATION_HEADER_NAME = "location";
300
+ const { request, resourceType, responseHeaders, responseStatusCode, responseStatusText } = params;
301
+ const shouldCapture = resourceType === DOCUMENT_RESOURCE_TYPE &&
302
+ options.outputJson &&
303
+ !httpInfo.request &&
304
+ responseStatusCode !== undefined &&
305
+ (request.url === urlState.url || request.url === urlState.alternativeUrl);
306
+ if (shouldCapture) {
307
+ if (REDIRECT_STATUS_CODES.includes(responseStatusCode)) {
308
+ const redirect = responseHeaders.find(header => header.name.toLowerCase() === LOCATION_HEADER_NAME)?.value;
309
+ if (redirect) {
310
+ urlState.url = new URL(redirect, urlState.url).href;
374
311
  }
375
- }
376
- if (options.consoleMessagesFile) {
377
- error.consoleMessages = consoleMessages;
378
- }
379
- if (options.debugMessagesFile) {
380
- error.debugMessages = debugMessages;
381
- }
382
- throw error;
383
- } finally {
384
- if (options.debugMessagesFile) {
385
- debugMessages.push([Date.now(), ["Closing page"]]);
386
- }
387
- await closeTarget();
388
- if (options.debugMessagesFile) {
389
- debugMessages.push([Date.now(), ["Finishing"]]);
312
+ logData(["Redirecting", urlState.url], { options, debugMessages });
313
+ } else {
314
+ Object.assign(httpInfo, {
315
+ request: {
316
+ url: request.url,
317
+ method: request.method,
318
+ headers: request.headers,
319
+ referrerPolicy: request.referrerPolicy
320
+ },
321
+ resourceType,
322
+ response: {
323
+ status: responseStatusCode,
324
+ statusText: responseStatusText,
325
+ headers: responseHeaders
326
+ }
327
+ });
390
328
  }
391
329
  }
330
+ }
392
331
 
393
- async function closeTarget() {
394
- if (targetInfo && !options.browserDebug) {
395
- await CDP.closeTarget(targetInfo.id);
396
- targetInfo = null;
397
- }
332
+ async function setupHttpHeaders({ Network }, { options, debugMessages }) {
333
+ logData(["Setting HTTP headers", JSON.stringify(options.httpHeaders)], { options, debugMessages });
334
+ await Network.enable();
335
+ await Network.setExtraHTTPHeaders({ headers: options.httpHeaders });
336
+ }
337
+
338
+ async function setupMediaFeatures({ Emulation }, { options, debugMessages }) {
339
+ for (const mediaFeature of options.emulateMediaFeatures) {
340
+ logData(["Emulating media feature", mediaFeature.name, mediaFeature.value], { options, debugMessages });
341
+ await Emulation.setEmulatedMedia({
342
+ media: mediaFeature.name,
343
+ features: mediaFeature.value.split(",").map(feature => feature.trim())
344
+ });
398
345
  }
399
346
  }
400
347
 
401
- function getPageDataScriptSource(options, [SET_SCREENSHOT_FUNCTION_NAME, SET_PDF_FUNCTION_NAME, SET_PAGE_DATA_FUNCTION_NAME, CAPTURE_SCREENSHOT_FUNCTION_NAME, PRINT_TO_PDF_FUNCTION_NAME]) {
402
- if ((options.embedScreenshot || options.embedPdf) && options.compressContent) {
403
- let screenshot, pdf;
404
- if (options.embedScreenshot) {
405
- screenshot = new Promise(resolve => globalThis[SET_SCREENSHOT_FUNCTION_NAME] = async data =>
406
- resolve(Array.from(new Uint8Array(await (await fetch("data:image/png;base64," + data)).arrayBuffer()))));
407
- }
408
- if (options.embedPdf) {
409
- pdf = new Promise(resolve => globalThis[SET_PDF_FUNCTION_NAME] = async data =>
410
- resolve(Array.from(new Uint8Array(await (await fetch("data:application/pdf;base64," + data)).arrayBuffer()))));
411
- }
412
- let pendingCapture;
413
- options.onprogress = async event => {
414
- if (event.type == event.RESOURCES_INITIALIZING && window == top && !pendingCapture) {
415
- pendingCapture = true;
416
- if (globalThis[CAPTURE_SCREENSHOT_FUNCTION_NAME]) {
417
- globalThis[CAPTURE_SCREENSHOT_FUNCTION_NAME]("");
418
- options.embeddedImage = await screenshot;
419
- }
420
- if (globalThis[PRINT_TO_PDF_FUNCTION_NAME]) {
421
- globalThis[PRINT_TO_PDF_FUNCTION_NAME]("");
422
- options.embeddedPdf = await pdf;
423
- }
348
+ async function setupCookies({ Network }, { options, debugMessages }) {
349
+ logData(["Setting cookies", JSON.stringify(options.browserCookies)], { options, debugMessages });
350
+ await Network.setCookies({ cookies: options.browserCookies });
351
+ }
352
+
353
+ async function setupScriptInjection({ Page }, { options }) {
354
+ await Page.addScriptToEvaluateOnNewDocument({
355
+ source: getHookScriptSource(),
356
+ runImmediately: true
357
+ });
358
+ await Page.addScriptToEvaluateOnNewDocument({
359
+ source: await getScriptSource(options),
360
+ runImmediately: true,
361
+ worldName: SINGLE_FILE_WORLD_NAME
362
+ });
363
+ }
364
+
365
+ async function getContextId({ Debugger, Page, Runtime }, { options, debugMessages }) {
366
+ const [contextId] = await Promise.all([
367
+ loadPage({ Page, Runtime }, { options, debugMessages }),
368
+ options.browserDebug ? waitForDebuggerReady() : Promise.resolve()
369
+ ]);
370
+ return contextId;
371
+
372
+ async function waitForDebuggerReady() {
373
+ await Debugger.enable();
374
+ await Debugger.pause();
375
+ await new Promise(resolve => {
376
+ const RESUMED_EVENT = "resumed";
377
+ Debugger.addEventListener(RESUMED_EVENT, onResumed);
378
+ function onResumed() {
379
+ Debugger.removeEventListener(RESUMED_EVENT, onResumed);
380
+ resolve();
424
381
  }
425
- };
382
+ });
426
383
  }
427
- const MAX_CONTENT_SIZE = 32 * 1024 * 1024;
428
- return singlefile.getPageData(options).then(data => {
429
- if (data.content instanceof Uint8Array) {
430
- data.content = Array.from(data.content);
431
- }
432
- data = JSON.stringify(data);
433
- let indexData = 0;
434
- do {
435
- globalThis[SET_PAGE_DATA_FUNCTION_NAME](data.slice(indexData, indexData + MAX_CONTENT_SIZE));
436
- indexData += MAX_CONTENT_SIZE;
437
- } while (indexData < data.length);
438
- globalThis[SET_PAGE_DATA_FUNCTION_NAME]("");
439
- });
440
384
  }
441
385
 
442
- async function loadPage({ Page, Runtime }, options, debugMessages) {
386
+ async function loadPage({ Page, Runtime }, { options, debugMessages }) {
387
+ const LOAD_TIMEOUT_ERROR_MESSAGE = "Load timeout";
443
388
  await Runtime.enable();
444
389
  await Page.enable();
445
390
  const loadTimeoutAbortController = new AbortController();
446
391
  const loadTimeoutAbortSignal = loadTimeoutAbortController.signal;
447
392
  try {
448
- if (options.debugMessagesFile) {
449
- debugMessages.push([Date.now(), ["Loading page", options.url]]);
450
- }
393
+ logData(["Loading page", options.url], { options, debugMessages });
451
394
  const [contextId] = await Promise.race([
452
- Promise.all([getTopFrameContextId({ Page, Runtime }, options, debugMessages), Page.navigate({ url: options.url })]),
453
- waitForTimeout(loadTimeoutAbortSignal, options.browserLoadMaxTime, "Load timeout", LOAD_TIMEOUT_ERROR)
395
+ Promise.all([getTopFrameContextId({ Page, Runtime }, { options, debugMessages }), Page.navigate({ url: options.url })]),
396
+ waitForTimeout(loadTimeoutAbortSignal, options.browserLoadMaxTime, LOAD_TIMEOUT_ERROR_MESSAGE, LOAD_TIMEOUT_ERROR)
454
397
  ]);
455
398
  return contextId;
456
399
  } finally {
@@ -462,167 +405,311 @@ async function loadPage({ Page, Runtime }, options, debugMessages) {
462
405
  }
463
406
  }
464
407
 
465
- async function getTopFrameContextId({ Page, Runtime }, options, debugMessages) {
466
- const CONTEXT_CREATED_EVENT = "executionContextCreated";
467
- const contextIds = [];
468
- let topFrameId;
408
+ async function getTopFrameContextId({ Page, Runtime }, { options, debugMessages }) {
409
+ await Page.setLifecycleEventsEnabled({ enabled: true });
410
+ const state = { topFrameId: undefined, contextIds: [] };
411
+ const removeContextListener = setupContextCreatedListener({ Runtime }, state);
469
412
  try {
470
- Runtime.addEventListener(CONTEXT_CREATED_EVENT, onExecutionContextCreated);
471
- await waitForPageReady({ Page }, options);
472
- if (options.debugMessagesFile) {
473
- debugMessages.push([Date.now(), ["Getting execution context"]]);
474
- }
475
- const contextId = await getContextId();
476
- if (contextId === undefined) {
477
- throw new Error("Execution context not found");
478
- } else {
479
- return contextId;
480
- }
413
+ await waitForPageReadyState({ Page }, state, { options, debugMessages });
414
+ const contextId = await findValidSingleFileContext({ Runtime }, state.contextIds, { options, debugMessages });
415
+ return contextId;
481
416
  } finally {
482
- Runtime.removeEventListener(CONTEXT_CREATED_EVENT, onExecutionContextCreated);
417
+ removeContextListener();
418
+ await Page.setLifecycleEventsEnabled({ enabled: false });
483
419
  }
420
+ }
484
421
 
485
- function onExecutionContextCreated({ params }) {
422
+ function setupContextCreatedListener({ Runtime }, state) {
423
+ const EXECUTION_CONTEXT_CREATED_EVENT_TYPE = "executionContextCreated";
424
+ const onContextCreated = ({ params }) => {
486
425
  const { context } = params;
487
- const { name, auxData = {} } = context;
488
- if (name === SINGLE_FILE_WORLD_NAME && topFrameId !== undefined && auxData.frameId === topFrameId) {
489
- contextIds.unshift(context.id);
426
+ if (context.name === SINGLE_FILE_WORLD_NAME && context.auxData?.frameId === state.topFrameId) {
427
+ state.contextIds.push(context.id);
490
428
  }
491
- }
492
-
493
- async function waitForPageReady({ Page }, options) {
494
- let resolveCallbackTimeout;
495
- await Page.setLifecycleEventsEnabled({ enabled: true });
496
- try {
497
- await new Promise((resolve, reject) => {
498
- const LIFE_CYCLE_EVENT = "lifecycleEvent";
499
- const FRAME_NAVIGATED_EVENT = "frameNavigated";
500
- Page.addEventListener(LIFE_CYCLE_EVENT, onLifecycleEvent);
501
- Page.addEventListener(FRAME_NAVIGATED_EVENT, onFrameNavigated);
502
-
503
- function onLifecycleEvent({ params }) {
504
- const { frameId, name } = params;
505
- if (frameId === topFrameId) {
506
- if (options.debugMessagesFile) {
507
- debugMessages.push([Date.now(), ["Detecting lifecycle event", name]]);
508
- }
509
- if (name === options.browserWaitUntil || (resolveCallbackTimeout && NETWORK_STATES.indexOf(name) < NETWORK_STATES.indexOf(options.browserWaitUntil))) {
510
- clearTimeout(resolveCallbackTimeout);
511
- if (options.debugMessagesFile) {
512
- debugMessages.push([Date.now(), [`Waiting ${options.browserWaitUntilDelay} ms`]]);
513
- }
514
- setTimeout(() => {
515
- if (options.debugMessagesFile) {
516
- debugMessages.push([Date.now(), ["Detecting page ready"]]);
517
- }
518
- removeListeners();
519
- resolve();
520
- }, options.browserWaitUntilDelay);
521
- }
522
- }
523
- }
429
+ };
430
+ Runtime.addEventListener(EXECUTION_CONTEXT_CREATED_EVENT_TYPE, onContextCreated);
431
+ return () => Runtime.removeEventListener(EXECUTION_CONTEXT_CREATED_EVENT_TYPE, onContextCreated);
432
+ }
524
433
 
525
- function onFrameNavigated({ params }) {
526
- const { frame } = params;
527
- if (!frame.parentId) {
528
- if (frame.unreachableUrl) {
529
- clearTimeout(resolveCallbackTimeout);
530
- removeListeners();
531
- reject(new Error("Unreachable URL: " + frame.unreachableUrl));
532
- } else {
533
- if (options.debugMessagesFile) {
534
- debugMessages.push([Date.now(), ["Detecting top frame ID"]]);
535
- }
536
- topFrameId = frame.id;
537
- }
538
- }
539
- }
434
+ async function waitForPageReadyState({ Page }, state, { options, debugMessages }) {
435
+ const LIFE_CYCLE_EVENT_TYPE = "lifecycleEvent";
436
+ const FRAME_NAVIGATED_EVENT_TYPE = "frameNavigated";
437
+ await new Promise((resolve, reject) => {
438
+ const timeoutState = { timeoutId: undefined };
439
+ const cleanup = () => {
440
+ Page.removeEventListener(LIFE_CYCLE_EVENT_TYPE, onLifecycleEvent);
441
+ Page.removeEventListener(FRAME_NAVIGATED_EVENT_TYPE, onFrameNavigated);
442
+ };
443
+ const onLifecycleEvent = createLifecycleEventHandler(state, timeoutState, resolve, cleanup, { options, debugMessages });
444
+ const onFrameNavigated = createFrameNavigatedHandler(state, timeoutState, reject, cleanup, { options, debugMessages });
445
+ Page.addEventListener(LIFE_CYCLE_EVENT_TYPE, onLifecycleEvent);
446
+ Page.addEventListener(FRAME_NAVIGATED_EVENT_TYPE, onFrameNavigated);
447
+ });
448
+ }
540
449
 
541
- function removeListeners() {
542
- Page.removeEventListener(LIFE_CYCLE_EVENT, onLifecycleEvent);
543
- Page.removeEventListener(FRAME_NAVIGATED_EVENT, onFrameNavigated);
544
- }
545
- });
546
- } finally {
547
- await Page.setLifecycleEventsEnabled({ enabled: false });
450
+ function createLifecycleEventHandler(state, timeoutState, resolve, cleanup, { options, debugMessages }) {
451
+ return ({ params }) => {
452
+ const { frameId, name } = params;
453
+ if (frameId === state.topFrameId) {
454
+ logData(["Detecting lifecycle event", name], { options, debugMessages });
455
+ }
456
+ const shouldResolve = name === options.browserWaitUntil ||
457
+ (timeoutState.timeoutId && NETWORK_STATES.indexOf(name) < NETWORK_STATES.indexOf(options.browserWaitUntil));
458
+ if (shouldResolve) {
459
+ clearTimeout(timeoutState.timeoutId);
460
+ logData([`Waiting ${options.browserWaitUntilDelay} ms`], { options, debugMessages });
461
+ setTimeout(() => {
462
+ logData(["Detecting page ready"], { options, debugMessages });
463
+ cleanup();
464
+ resolve();
465
+ }, options.browserWaitUntilDelay);
548
466
  }
549
- }
467
+ };
468
+ };
550
469
 
551
- async function getContextId() {
552
- let contextId;
553
- if (contextIds.length) {
554
- let contextIdIndex = 0;
555
- do {
556
- if (await testExecutionContext(contextIds[contextIdIndex])) {
557
- contextId = contextIds[contextIdIndex];
558
- }
559
- contextIdIndex++;
560
- } while (contextId === undefined && contextIdIndex < contextIds.length);
470
+ function createFrameNavigatedHandler(state, timeoutState, reject, cleanup, { options, debugMessages }) {
471
+ const UNREACHABLE_URL_ERROR_MESSAGE = "Unreachable URL";
472
+ return ({ params }) => {
473
+ const { frame } = params;
474
+ if (!frame.parentId) {
475
+ if (frame.unreachableUrl) {
476
+ clearTimeout(timeoutState.timeoutId);
477
+ cleanup();
478
+ reject(new Error(UNREACHABLE_URL_ERROR_MESSAGE + ": " + frame.unreachableUrl));
479
+ } else {
480
+ logData(["Detecting top frame ID"], { options, debugMessages });
481
+ state.topFrameId = frame.id;
482
+ }
561
483
  }
562
- return contextId;
563
- }
484
+ };
485
+ }
564
486
 
565
- async function testExecutionContext(contextId) {
487
+ async function findValidSingleFileContext({ Runtime }, contextIds, { options, debugMessages }) {
488
+ const CONTEXT_NOT_FOUND_ERROR_MESSAGE = "Execution context not found for SingleFile world";
489
+ const SINGLE_FILE_DETECTION_TEST = "typeof singlefile !== 'undefined'";
490
+ const NO_VALID_CONTEXT_ERROR_MESSAGE = "No valid SingleFile execution context found";
491
+ logData(["Getting execution context"], { options, debugMessages });
492
+ if (!contextIds.length) {
493
+ throw new Error(CONTEXT_NOT_FOUND_ERROR_MESSAGE);
494
+ }
495
+ for (const contextId of contextIds) {
566
496
  try {
567
497
  const { result } = await Runtime.evaluate({
568
- expression: "singlefile !== undefined",
498
+ expression: SINGLE_FILE_DETECTION_TEST,
569
499
  contextId
570
500
  });
571
- return result.value === true;
501
+ if (result.value === true) {
502
+ return contextId;
503
+ }
572
504
  } catch {
573
505
  // ignored
574
506
  }
575
- return false;
576
507
  }
508
+ throw new Error(NO_VALID_CONTEXT_ERROR_MESSAGE);
509
+ }
510
+
511
+ function setupPageDataCapture({ Runtime }, contextId, { options, debugMessages }) {
512
+ return new Promise(resolve => {
513
+ let pageDataResponse = "";
514
+ Runtime.addEventListener(BINDING_CALLED_EVENT_TYPE, ({ params }) => {
515
+ if (params.name === SET_PAGE_DATA_FUNCTION_NAME) {
516
+ const { payload } = params;
517
+ if (payload.length) {
518
+ pageDataResponse += payload;
519
+ } else {
520
+ logData(["Setting page data"], { options, debugMessages });
521
+ const result = JSON.parse(pageDataResponse);
522
+ if (result.content instanceof Array) {
523
+ result.content = new Uint8Array(result.content);
524
+ }
525
+ resolve(result);
526
+ }
527
+ }
528
+ });
529
+ });
530
+ }
531
+
532
+ async function setupBindings({ Page, Runtime }, contextId, { options, debugMessages }) {
533
+ await Runtime.addBinding({ name: SET_PAGE_DATA_FUNCTION_NAME, executionContextId: contextId });
534
+ if (options.embedScreenshot && options.compressContent) {
535
+ await setupScreenshotCapture({ Page, Runtime }, contextId, { options, debugMessages });
536
+ }
537
+ if (options.embedPdf) {
538
+ await setupPdfCapture({ Page, Runtime }, contextId, { options, debugMessages });
539
+ }
540
+ await Runtime.addBinding({ name: FETCH_FUNCTION_NAME, executionContextId: contextId });
541
+ Runtime.addEventListener(BINDING_CALLED_EVENT_TYPE, async ({ params }) => {
542
+ if (params.name === FETCH_FUNCTION_NAME) {
543
+ await handleFetchRequest({ Runtime }, params, contextId, { options, debugMessages });
544
+ }
545
+ });
577
546
  }
578
547
 
579
- function waitForTimeout(abortSignal, maxDelay, errorMessage, errorCode) {
580
- return new Promise((resolve, reject) => {
581
- const ABORT_EVENT = "abort";
582
- abortSignal.addEventListener(ABORT_EVENT, onAbort);
583
- const timeoutId = setTimeout(() => {
584
- abortSignal.removeEventListener(ABORT_EVENT, onAbort);
585
- const error = new Error(errorMessage);
586
- error.code = errorCode;
587
- reject(error);
588
- }, maxDelay);
548
+ async function setupScreenshotCapture({ Page, Runtime }, contextId, { options, debugMessages }) {
549
+ await Runtime.addBinding({ name: CAPTURE_SCREENSHOT_FUNCTION_NAME, executionContextId: contextId });
550
+ Runtime.addEventListener(BINDING_CALLED_EVENT_TYPE, async ({ params }) => {
551
+ if (params.name === CAPTURE_SCREENSHOT_FUNCTION_NAME) {
552
+ logData(["Capturing screenshot"], { options, debugMessages });
553
+ try {
554
+ const screenshotOptions = parseScreenshotOptions(options.embedScreenshotOptions);
555
+ const { data } = await Page.captureScreenshot(screenshotOptions);
556
+ await callBrowserFunction({ Runtime }, contextId, SET_SCREENSHOT_FUNCTION_NAME, [data]);
557
+ } catch {
558
+ await callBrowserFunction({ Runtime }, contextId, SET_SCREENSHOT_FUNCTION_NAME, [""]);
559
+ }
560
+ }
561
+ });
589
562
 
590
- function onAbort() {
591
- abortSignal.removeEventListener(ABORT_EVENT, onAbort);
592
- clearTimeout(timeoutId);
593
- resolve();
563
+ function parseScreenshotOptions(optionsString) {
564
+ const PNG_FORMAT = "png";
565
+ let screenshotOptions = { captureBeyondViewport: true };
566
+ if (optionsString) {
567
+ try {
568
+ screenshotOptions = JSON.parse(optionsString);
569
+ } catch {
570
+ // ignored
571
+ }
572
+ }
573
+ screenshotOptions.format = PNG_FORMAT;
574
+ return screenshotOptions;
575
+ }
576
+ }
577
+
578
+ async function setupPdfCapture({ Page, Runtime }, contextId, { options, debugMessages }) {
579
+ await Runtime.addBinding({ name: PRINT_TO_PDF_FUNCTION_NAME, executionContextId: contextId });
580
+ Runtime.addEventListener(BINDING_CALLED_EVENT_TYPE, async ({ params }) => {
581
+ if (params.name !== PRINT_TO_PDF_FUNCTION_NAME) {
582
+ logData(["Printing to PDF", options.embedPdfOptions || ""], { options, debugMessages });
583
+ const pdfOptions = parsePdfOptions(options.embedPdfOptions);
584
+ try {
585
+ const { data } = await Page.printToPDF(pdfOptions);
586
+ await callBrowserFunction({ Runtime }, contextId, SET_PDF_FUNCTION_NAME, [data]);
587
+ } catch {
588
+ await callBrowserFunction({ Runtime }, contextId, SET_PDF_FUNCTION_NAME, [""]);
589
+ }
594
590
  }
595
591
  });
592
+
593
+ function parsePdfOptions(optionsString) {
594
+ let pdfOptions = {};
595
+ if (optionsString) {
596
+ try {
597
+ pdfOptions = JSON.parse(optionsString);
598
+ } catch {
599
+ // ignored
600
+ }
601
+ }
602
+ return pdfOptions;
603
+ }
596
604
  }
597
605
 
598
- async function waitForDebuggerReady({ Debugger }) {
599
- await Debugger.enable();
606
+ async function handleFetchRequest({ Runtime }, params, contextId, { options, debugMessages }) {
607
+ const { payload } = params;
608
+ const { requestId, url, options: fetchOptions } = JSON.parse(payload);
609
+ logData(["Fetching URL", url], { options, debugMessages });
600
610
  try {
601
- await Debugger.pause();
602
- await new Promise(resolve => {
603
- const RESUMED_EVENT = "resumed";
604
- Debugger.addEventListener(RESUMED_EVENT, onResumed);
611
+ const response = await fetch(url, fetchOptions);
612
+ const arrayBuffer = await response.arrayBuffer();
613
+ const base64Data = arrayBufferToBase64(arrayBuffer);
614
+ const result = {
615
+ status: response.status,
616
+ headers: Object.fromEntries(response.headers.entries()),
617
+ data: base64Data
618
+ };
619
+ await callBrowserFunction({ Runtime }, contextId, RESOLVE_FETCH_FUNCTION_NAME, [requestId, result]);
620
+ } catch (error) {
621
+ const errorResult = {
622
+ error: error.message,
623
+ code: error.code
624
+ };
625
+ await callBrowserFunction({ Runtime }, contextId, REJECT_FETCH_FUNCTION_NAME, [requestId, errorResult]);
626
+ }
627
+ }
605
628
 
606
- function onResumed() {
607
- Debugger.removeEventListener(RESUMED_EVENT, onResumed);
608
- resolve();
609
- }
610
- });
629
+ async function callBrowserFunction({ Runtime }, contextId, functionName, args) {
630
+ const serializedArgs = args.map(arg => JSON.stringify(arg)).join(", ");
631
+ await Runtime.evaluate({
632
+ expression: `globalThis.${functionName}(${serializedArgs})`,
633
+ contextId
634
+ });
635
+ }
636
+
637
+ async function capturePageData({ Runtime }, contextId, { options, debugMessages }) {
638
+ const CAPTURE_TIMEOUT_ERROR_MESSAGE = "Capture timeout";
639
+ const ERROR_SUBTYPE = "error";
640
+ const captureTimeoutAbortController = new AbortController();
641
+ const captureTimeoutAbortSignal = captureTimeoutAbortController.signal;
642
+ if (options.browserWaitDelay) {
643
+ logData([`Waiting ${options.browserWaitDelay} ms`], { options, debugMessages });
644
+ await new Promise(resolve => setTimeout(resolve, options.browserWaitDelay));
645
+ }
646
+ try {
647
+ logData(["Capturing page"], { options, debugMessages });
648
+ const captureScript = `(${getPageDataScriptSource.toString()})(${JSON.stringify(options)},${JSON.stringify([
649
+ SET_SCREENSHOT_FUNCTION_NAME,
650
+ SET_PDF_FUNCTION_NAME,
651
+ SET_PAGE_DATA_FUNCTION_NAME,
652
+ CAPTURE_SCREENSHOT_FUNCTION_NAME,
653
+ PRINT_TO_PDF_FUNCTION_NAME
654
+ ])})`;
655
+ const { result } = await Promise.race([
656
+ Runtime.evaluate({
657
+ expression: captureScript,
658
+ awaitPromise: true,
659
+ returnByValue: true,
660
+ contextId
661
+ }),
662
+ waitForTimeout(captureTimeoutAbortSignal, options.browserCaptureMaxTime, CAPTURE_TIMEOUT_ERROR_MESSAGE, CAPTURE_TIMEOUT_ERROR)
663
+ ]);
664
+ if (result.subtype === ERROR_SUBTYPE) {
665
+ throw new Error(result.description);
666
+ }
611
667
  } finally {
612
- await Debugger.disable();
668
+ if (!captureTimeoutAbortSignal.aborted) {
669
+ captureTimeoutAbortController.abort();
670
+ }
671
+ }
672
+ }
673
+
674
+ async function disableCdpDomains({ Console, Network, Page, Runtime }, { options }) {
675
+ await Runtime.disable();
676
+ await Page.disable();
677
+ if (options.httpHeaders) {
678
+ await Network.disable();
679
+ }
680
+ if (options.consoleMessagesFile) {
681
+ await Console.disable();
682
+ }
683
+ }
684
+
685
+ async function finalizePageData(pageDataPromise, { options, consoleMessages, debugMessages, httpInfo }) {
686
+ const pageData = await pageDataPromise;
687
+ logData(["Returning page data"], { options, debugMessages });
688
+ if (options.consoleMessagesFile) {
689
+ pageData.consoleMessages = consoleMessages;
690
+ }
691
+ if (options.debugMessagesFile) {
692
+ pageData.debugMessages = debugMessages;
693
+ }
694
+ Object.assign(pageData, httpInfo);
695
+ if (options.browserWaitEndDelay) {
696
+ logData([`Waiting ${options.browserWaitEndDelay} ms after processing`], { options, debugMessages });
697
+ await new Promise(resolve => setTimeout(resolve, options.browserWaitEndDelay));
613
698
  }
699
+ return pageData;
614
700
  }
615
701
 
616
- function getBrowserOptions(options) {
617
- const browserOptions = {};
618
- browserOptions.args = options.browserArgs;
619
- browserOptions.headless = options.browserHeadless;
620
- browserOptions.executablePath = options.browserExecutablePath;
621
- browserOptions.debug = options.browserDebug;
622
- browserOptions.disableWebSecurity = options.browserDisableWebSecurity === undefined || options.browserDisableWebSecurity;
623
- browserOptions.width = options.browserWidth;
624
- browserOptions.height = options.browserHeight;
625
- browserOptions.userAgent = options.userAgent;
626
- browserOptions.httpProxyServer = options.httpProxyServer;
627
- return browserOptions;
702
+ function attachDebugInfo(error, { options, consoleMessages, debugMessages }) {
703
+ if (options.consoleMessagesFile) {
704
+ error.consoleMessages = consoleMessages;
705
+ }
706
+ if (options.debugMessagesFile) {
707
+ error.debugMessages = debugMessages;
708
+ }
628
709
  }
710
+
711
+ function logData(data, { options, debugMessages }) {
712
+ if (options.debugMessagesFile) {
713
+ debugMessages.push([Date.now(), data]);
714
+ }
715
+ }