single-file-cli 2.0.77 → 2.0.79

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