single-file-cli 2.0.68 → 2.0.70

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.MD CHANGED
@@ -65,7 +65,7 @@ Make sure Chrome or a Chromium-based browser is installed in the default folder.
65
65
 
66
66
  - There are 3 ways to download the code of SingleFile, choose the one you prefer:
67
67
 
68
- - Install with `npm` (`npm` is installed with Node.js) and run with `npx`
68
+ - Install with `npm` and run `single-file` via `npx` (`npm` and `npx` are installed with Node.js)
69
69
 
70
70
  ```sh
71
71
  npm install "single-file-cli"
package/build.sh CHANGED
@@ -1,6 +1,12 @@
1
1
  #!/usr/bin/env bash
2
2
 
3
- deno vendor "npm:single-file-core@1.5.36"
3
+ mv package.json package.json.tmp
4
+ mv deno.json deno.json.tmp
5
+ mv deno.lock deno.lock.tmp
6
+ deno install --vendor --quiet "npm:single-file-core@1.5.40"
7
+ mv package.json.tmp package.json
8
+ mv deno.json.tmp deno.json
9
+ mv deno.lock.tmp deno.lock
4
10
 
5
11
  echo "
6
12
  import { build } from 'npm:esbuild';
@@ -82,5 +88,4 @@ const version = JSON.parse(await Deno.readTextFile('./deno.json')).version;
82
88
  await Deno.writeTextFile('lib/version.js', 'export const version = ' + JSON.stringify(version) + ';');
83
89
  " | deno run --allow-read --allow-write --allow-net --allow-run --allow-env --lock=node_modules/deno.lock.tmp -
84
90
 
85
- rm -rf node_modules
86
- rm -rf vendor
91
+ rm -rf node_modules
package/deno.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@single-file/single-file-cli",
3
- "version": "2.0.68",
3
+ "version": "2.0.70",
4
4
  "description": "SingleFile CLI",
5
- "nodeModulesDir": true,
5
+ "nodeModulesDir": "auto",
6
6
  "exports": {
7
7
  ".": "./single-file-cli-api.js"
8
8
  },
package/lib/browser.js CHANGED
@@ -92,7 +92,7 @@ async function launchBrowser(options = {}, indexPath = 0) {
92
92
  async function getDebugPort(port = getRandomDebugPort(), usedPorts = []) {
93
93
  try {
94
94
  await fetch("http://localhost:" + port + "/json/version");
95
- } catch (error) {
95
+ } catch {
96
96
  return port;
97
97
  }
98
98
  if (usedPorts.length < DEBUG_PORT_RANGE) {
@@ -121,7 +121,7 @@ async function closeBrowser() {
121
121
  try {
122
122
  await remove(profilePath, { recursive: true });
123
123
  profilePath = undefined;
124
- } catch (error) {
124
+ } catch {
125
125
  console.log("Warning: failed to remove profile directory: " + profilePath); // eslint-disable-line no-console
126
126
  }
127
127
  }
package/lib/cdp-client.js CHANGED
@@ -50,12 +50,19 @@ async function initialize(singleFileOptions) {
50
50
  }
51
51
 
52
52
  async function getPageData(options) {
53
+ const debugMessages = [];
53
54
  let targetInfo;
54
55
  try {
56
+ if (options.debugMessagesFile) {
57
+ debugMessages.push([Date.now(), ["Loading page", EMPTY_PAGE_URL]]);
58
+ }
55
59
  targetInfo = await CDP.createTarget(EMPTY_PAGE_URL);
56
60
  const consoleMessages = [];
57
61
  const { Browser, Security, Page, Emulation, Fetch, Network, Runtime, Debugger, Console } = new CDP(targetInfo);
58
62
  if (options.consoleMessagesFile) {
63
+ if (options.debugMessagesFile) {
64
+ debugMessages.push([Date.now(), ["Enabling console messages"]]);
65
+ }
59
66
  await Console.enable();
60
67
  Console.addEventListener("messageAdded", ({ params }) => {
61
68
  const { message } = params;
@@ -65,13 +72,22 @@ async function getPageData(options) {
65
72
  if (options.browserStartMinimized) {
66
73
  const { windowId, bounds } = await Browser.getWindowForTarget({ targetId: targetInfo.id });
67
74
  if (bounds.windowState !== MINIMIZED_WINDOW_STATE) {
75
+ if (options.debugMessagesFile) {
76
+ debugMessages.push([Date.now(), ["Minimizing window"]]);
77
+ }
68
78
  await Browser.setWindowBounds({ windowId, bounds: { windowState: MINIMIZED_WINDOW_STATE } });
69
79
  }
70
80
  }
71
81
  if (options.browserIgnoreHTTPSErrors !== undefined && options.browserIgnoreHTTPSErrors) {
82
+ if (options.debugMessagesFile) {
83
+ debugMessages.push([Date.now(), ["Ignoring HTTPS errors"]]);
84
+ }
72
85
  await Security.setIgnoreCertificateErrors({ ignore: true });
73
86
  }
74
87
  if (options.browserByPassCSP === undefined || options.browserByPassCSP) {
88
+ if (options.debugMessagesFile) {
89
+ debugMessages.push([Date.now(), ["Bypassing CSP"]]);
90
+ }
75
91
  await Page.setBypassCSP({ enabled: true });
76
92
  }
77
93
  if (options.browserMobileEmulation || options.browserDeviceWidth || options.browserDeviceHeight || options.browserDeviceScaleFactor || options.platform || options.acceptLanguage) {
@@ -91,12 +107,16 @@ async function getPageData(options) {
91
107
  const { result } = await Runtime.evaluate({ expression: "window.devicePixelRatio" });
92
108
  browserDeviceScaleFactor = result.value;
93
109
  }
94
- await Emulation.setDeviceMetricsOverride({
110
+ const deviceMetricsOptions = {
95
111
  mobile: Boolean(options.browserMobileEmulation),
96
112
  width: options.browserDeviceWidth || (options.browserMobileEmulation ? 360 : options.width || browserDeviceWidth),
97
113
  height: options.browserDeviceHeight || (options.browserMobileEmulation ? 800 : options.height || browserDeviceHeight),
98
114
  deviceScaleFactor: options.browserDeviceScaleFactor || (options.browserMobileEmulation ? 2 : browserDeviceScaleFactor)
99
- });
115
+ };
116
+ if (options.debugMessagesFile) {
117
+ debugMessages.push([Date.now(), ["Emulating device metrics", JSON.stringify(deviceMetricsOptions)]]);
118
+ }
119
+ await Emulation.setDeviceMetricsOverride(deviceMetricsOptions);
100
120
  }
101
121
  if (options.browserMobileEmulation || options.platform || options.acceptLanguage) {
102
122
  const { userAgent, product } = await Browser.getVersion();
@@ -109,21 +129,29 @@ async function getPageData(options) {
109
129
  if (options.platform || options.browserMobileEmulation) {
110
130
  agentOptions.platform = options.platform || "Android";
111
131
  }
132
+ if (options.debugMessagesFile) {
133
+ debugMessages.push([Date.now(), ["Emulating user agent", JSON.stringify(agentOptions)]]);
134
+ }
112
135
  await Emulation.setUserAgentOverride(agentOptions);
113
136
  }
114
137
  }
115
138
  const handleAuthRequests = Boolean(options.httpProxyUsername);
116
- const patterns = handleAuthRequests ? [{ requestStage: "Request" }, { requestStage: "Response", resourceType: "Document" }] : [{ requestStage: "Response", resourceType: "Document" }];
139
+ const patterns = handleAuthRequests ? [{ requestStage: "Request" }, { requestStage: "Response" }] : [{ requestStage: "Response" }];
117
140
  await Fetch.enable({ handleAuthRequests, patterns });
118
141
  if (handleAuthRequests) {
119
- Fetch.addEventListener("authRequired", ({ params }) => Fetch.continueWithAuth({
120
- requestId: params.requestId,
121
- authChallengeResponse: {
122
- response: "ProvideCredentials",
123
- username: options.httpProxyUsername,
124
- password: options.httpProxyPassword
142
+ Fetch.addEventListener("authRequired", async ({ params }) => {
143
+ if (options.debugMessagesFile) {
144
+ debugMessages.push([Date.now(), ["Authenticating"]]);
125
145
  }
126
- }));
146
+ await Fetch.continueWithAuth({
147
+ requestId: params.requestId,
148
+ authChallengeResponse: {
149
+ response: "ProvideCredentials",
150
+ username: options.httpProxyUsername,
151
+ password: options.httpProxyPassword
152
+ }
153
+ });
154
+ });
127
155
  }
128
156
  let url = options.url;
129
157
  let alternativeUrl = new URL(url);
@@ -134,12 +162,15 @@ async function getPageData(options) {
134
162
  let httpInfo;
135
163
  Fetch.addEventListener("requestPaused", async ({ params }) => {
136
164
  const { requestId, request, resourceType, responseHeaders, responseStatusCode, responseStatusText } = params;
137
- if (options.outputJson && !httpInfo && (request.url == url || request.url == alternativeUrl) && responseStatusCode !== undefined) {
165
+ if (resourceType == "Document" && (options.outputJson && !httpInfo && (request.url == url || request.url == alternativeUrl) && responseStatusCode !== undefined)) {
138
166
  if (REDIRECT_STATUS_CODES.includes(responseStatusCode)) {
139
167
  const redirect = responseHeaders.find(header => header.name.toLowerCase() == "location").value;
140
168
  if (redirect) {
141
169
  url = new URL(redirect, url).href;
142
170
  }
171
+ if (options.debugMessagesFile) {
172
+ debugMessages.push([Date.now(), ["Redirecting", url]]);
173
+ }
143
174
  } else {
144
175
  httpInfo = {
145
176
  request: {
@@ -156,18 +187,36 @@ async function getPageData(options) {
156
187
  }
157
188
  };
158
189
  }
190
+ } else if (options.blockedURLPatterns && options.blockedURLPatterns.length) {
191
+ const blockedURL = options.blockedURLPatterns.find(blockedURL => new RegExp(blockedURL).test(request.url));
192
+ if (blockedURL) {
193
+ try {
194
+ if (options.debugMessagesFile) {
195
+ debugMessages.push([Date.now(), ["Blocking request", request.url]]);
196
+ }
197
+ await Fetch.failRequest({ requestId, errorReason: "Aborted" });
198
+ } catch {
199
+ // ignored
200
+ }
201
+ }
159
202
  }
160
203
  try {
161
204
  await Fetch.continueRequest({ requestId });
162
- } catch (error) {
205
+ } catch {
163
206
  // ignored
164
207
  }
165
208
  });
166
209
  if (options.httpHeaders && options.httpHeaders.length) {
210
+ if (options.debugMessagesFile) {
211
+ debugMessages.push([Date.now(), ["Setting HTTP headers", JSON.stringify(options.httpHeaders)]]);
212
+ }
167
213
  await Network.setExtraHTTPHeaders({ headers: options.httpHeaders });
168
214
  }
169
215
  if (options.emulateMediaFeatures) {
170
216
  for (const mediaFeature of options.emulateMediaFeatures) {
217
+ if (options.debugMessagesFile) {
218
+ debugMessages.push([Date.now(), ["Emulating media feature", mediaFeature.name, mediaFeature.value]]);
219
+ }
171
220
  await Emulation.setEmulatedMedia({
172
221
  media: mediaFeature.name,
173
222
  features: mediaFeature.value.split(",").map(feature => feature.trim())
@@ -175,8 +224,12 @@ async function getPageData(options) {
175
224
  }
176
225
  }
177
226
  if (options.browserCookies && options.browserCookies.length) {
227
+ if (options.debugMessagesFile) {
228
+ debugMessages.push([Date.now(), ["Setting cookies", JSON.stringify(options.browserCookies)]]);
229
+ }
178
230
  await Network.setCookies({ cookies: options.browserCookies });
179
231
  }
232
+ await Browser.setDownloadBehavior({ behavior: "deny" });
180
233
  await Page.addScriptToEvaluateOnNewDocument({
181
234
  source: getHookScriptSource(),
182
235
  runImmediately: true
@@ -187,7 +240,7 @@ async function getPageData(options) {
187
240
  worldName: SINGLE_FILE_WORLD_NAME
188
241
  });
189
242
  const [contextId] = await Promise.all([
190
- loadPage({ Page, Runtime }, options),
243
+ loadPage({ Page, Runtime }, options, debugMessages),
191
244
  options.browserDebug ? waitForDebuggerReady({ Debugger }) : Promise.resolve()
192
245
  ]);
193
246
  await Runtime.addBinding({ name: SET_PAGE_DATA_FUNCTION_NAME, executionContextId: contextId });
@@ -195,19 +248,22 @@ async function getPageData(options) {
195
248
  await Runtime.addBinding({ name: CAPTURE_SCREENSHOT_FUNCTION_NAME, executionContextId: contextId });
196
249
  Runtime.addEventListener("bindingCalled", async ({ params }) => {
197
250
  if (params.name === CAPTURE_SCREENSHOT_FUNCTION_NAME) {
251
+ if (options.debugMessagesFile) {
252
+ debugMessages.push([Date.now(), ["Capturing screenshot"]]);
253
+ }
198
254
  try {
199
255
  let screenshotOptions = { captureBeyondViewport: true };
200
256
  if (options.embedScreenshotOptions) {
201
257
  try {
202
258
  screenshotOptions = JSON.parse(options.embedScreenshotOptions);
203
- } catch (error) {
259
+ } catch {
204
260
  // ignored
205
261
  }
206
262
  }
207
263
  screenshotOptions.format = "png";
208
264
  const { data } = await Page.captureScreenshot(screenshotOptions);
209
265
  await Runtime.evaluate({ expression: `globalThis.${SET_SCREENSHOT_FUNCTION_NAME}(${JSON.stringify(data)})`, contextId });
210
- } catch (error) {
266
+ } catch {
211
267
  await Runtime.evaluate({ expression: `globalThis.${SET_SCREENSHOT_FUNCTION_NAME}(${JSON.stringify("")})`, contextId });
212
268
  }
213
269
  }
@@ -217,18 +273,21 @@ async function getPageData(options) {
217
273
  await Runtime.addBinding({ name: PRINT_TO_PDF_FUNCTION_NAME, executionContextId: contextId });
218
274
  Runtime.addEventListener("bindingCalled", async ({ params }) => {
219
275
  if (params.name === PRINT_TO_PDF_FUNCTION_NAME) {
276
+ if (options.debugMessagesFile) {
277
+ debugMessages.push([Date.now(), ["Printing to PDF", options.embedPdfOptions || ""]]);
278
+ }
220
279
  let pdfOptions = {};
221
280
  if (options.embedPdfOptions) {
222
281
  try {
223
282
  pdfOptions = JSON.parse(options.embedPdfOptions);
224
- } catch (error) {
283
+ } catch {
225
284
  // ignored
226
285
  }
227
286
  }
228
287
  try {
229
288
  const { data } = await Page.printToPDF(pdfOptions);
230
289
  await Runtime.evaluate({ expression: `globalThis.${SET_PDF_FUNCTION_NAME}(${JSON.stringify(data)})`, contextId });
231
- } catch (error) {
290
+ } catch {
232
291
  await Runtime.evaluate({ expression: `globalThis.${SET_PDF_FUNCTION_NAME}(${JSON.stringify("")})`, contextId });
233
292
  }
234
293
  }
@@ -236,12 +295,15 @@ async function getPageData(options) {
236
295
  }
237
296
  const pageDataPromise = new Promise(resolve => {
238
297
  let pageDataResponse = "";
239
- Runtime.addEventListener("bindingCalled", async ({ params }) => {
298
+ Runtime.addEventListener("bindingCalled", ({ params }) => {
240
299
  if (params.name === SET_PAGE_DATA_FUNCTION_NAME) {
241
300
  const { payload } = params;
242
301
  if (payload.length) {
243
302
  pageDataResponse += payload;
244
303
  } else {
304
+ if (options.debugMessagesFile) {
305
+ debugMessages.push([Date.now(), ["Setting page data"]]);
306
+ }
245
307
  const result = JSON.parse(pageDataResponse);
246
308
  if (result.content instanceof Array) {
247
309
  result.content = new Uint8Array(result.content);
@@ -252,8 +314,14 @@ async function getPageData(options) {
252
314
  });
253
315
  });
254
316
  if (options.browserWaitDelay) {
317
+ if (options.debugMessagesFile) {
318
+ debugMessages.push([Date.now(), [`Waiting ${options.browserWaitDelay} ms`]]);
319
+ }
255
320
  await new Promise(resolve => setTimeout(resolve, options.browserWaitDelay));
256
321
  }
322
+ if (options.debugMessagesFile) {
323
+ debugMessages.push([Date.now(), ["Capturing page"]]);
324
+ }
257
325
  const { result } = await Runtime.evaluate({
258
326
  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])})`,
259
327
  awaitPromise: true,
@@ -269,20 +337,42 @@ async function getPageData(options) {
269
337
  await Console.disable();
270
338
  pageData.consoleMessages = consoleMessages;
271
339
  }
340
+ if (options.debugMessagesFile) {
341
+ pageData.debugMessages = debugMessages;
342
+ debugMessages.push([Date.now(), ["Returning page data"]]);
343
+ }
272
344
  Object.assign(pageData, httpInfo);
273
345
  return pageData;
274
346
  } catch (error) {
275
347
  if (error.code === LOAD_TIMEOUT_ERROR && options.browserWaitUntilFallback && options.browserWaitUntil) {
276
348
  const browserWaitUntil = NETWORK_STATES[(NETWORK_STATES.indexOf(options.browserWaitUntil) + 1)];
277
349
  if (browserWaitUntil) {
350
+ if (options.debugMessagesFile) {
351
+ debugMessages.push([Date.now(), ["Retrying with waitUntil", browserWaitUntil]]);
352
+ }
278
353
  options.browserWaitUntil = browserWaitUntil;
354
+ await closeTarget();
279
355
  return await getPageData(options);
280
356
  }
281
357
  }
358
+ if (options.consoleMessagesFile) {
359
+ error.consoleMessages = consoleMessages;
360
+ }
361
+ if (options.debugMessagesFile) {
362
+ error.debugMessages = debugMessages;
363
+ }
282
364
  throw error;
283
365
  } finally {
366
+ if (options.debugMessagesFile) {
367
+ debugMessages.push([Date.now(), ["Closing page"]]);
368
+ }
369
+ await closeTarget();
370
+ }
371
+
372
+ async function closeTarget() {
284
373
  if (targetInfo && !options.browserDebug) {
285
374
  await CDP.closeTarget(targetInfo.id);
375
+ targetInfo = null;
286
376
  }
287
377
  }
288
378
  }
@@ -328,14 +418,17 @@ function getPageDataScriptSource(options, [SET_SCREENSHOT_FUNCTION_NAME, SET_PDF
328
418
  });
329
419
  }
330
420
 
331
- async function loadPage({ Page, Runtime }, options) {
421
+ async function loadPage({ Page, Runtime }, options, debugMessages) {
332
422
  await Runtime.enable();
333
423
  await Page.enable();
334
424
  const loadTimeoutAbortController = new AbortController();
335
425
  const loadTimeoutAbortSignal = loadTimeoutAbortController.signal;
336
426
  try {
427
+ if (options.debugMessagesFile) {
428
+ debugMessages.push([Date.now(), ["Loading page", options.url]]);
429
+ }
337
430
  const [contextId] = await Promise.race([
338
- Promise.all([getTopFrameContextId({ Page, Runtime }, options), Page.navigate({ url: options.url })]),
431
+ Promise.all([getTopFrameContextId({ Page, Runtime }, options, debugMessages), Page.navigate({ url: options.url })]),
339
432
  waitForLoadTimeout(loadTimeoutAbortSignal, options.browserLoadMaxTime)
340
433
  ]);
341
434
  return contextId;
@@ -348,13 +441,16 @@ async function loadPage({ Page, Runtime }, options) {
348
441
  }
349
442
  }
350
443
 
351
- async function getTopFrameContextId({ Page, Runtime }, options) {
444
+ async function getTopFrameContextId({ Page, Runtime }, options, debugMessages) {
352
445
  const CONTEXT_CREATED_EVENT = "executionContextCreated";
353
446
  const contextIds = [];
354
447
  let topFrameId;
355
448
  try {
356
449
  Runtime.addEventListener(CONTEXT_CREATED_EVENT, onExecutionContextCreated);
357
450
  await waitForPageReady({ Page }, options);
451
+ if (options.debugMessagesFile) {
452
+ debugMessages.push([Date.now(), ["Getting execution context"]]);
453
+ }
358
454
  const contextId = await getContextId();
359
455
  if (contextId === undefined) {
360
456
  throw new Error("Execution context not found");
@@ -374,6 +470,7 @@ async function getTopFrameContextId({ Page, Runtime }, options) {
374
470
  }
375
471
 
376
472
  async function waitForPageReady({ Page }, options) {
473
+ let resolveCallbackTimeout;
377
474
  await Page.setLifecycleEventsEnabled({ enabled: true });
378
475
  try {
379
476
  await new Promise((resolve, reject) => {
@@ -384,9 +481,23 @@ async function getTopFrameContextId({ Page, Runtime }, options) {
384
481
 
385
482
  function onLifecycleEvent({ params }) {
386
483
  const { frameId, name } = params;
387
- if (frameId === topFrameId && name === options.browserWaitUntil) {
388
- removeListeners();
389
- resolve();
484
+ if (frameId === topFrameId) {
485
+ if (options.debugMessagesFile) {
486
+ debugMessages.push([Date.now(), ["Detecting lifecycle event", name]]);
487
+ }
488
+ if (name === options.browserWaitUntil || (resolveCallbackTimeout && NETWORK_STATES.indexOf(name) < NETWORK_STATES.indexOf(options.browserWaitUntil))) {
489
+ clearTimeout(resolveCallbackTimeout);
490
+ if (options.debugMessagesFile) {
491
+ debugMessages.push([Date.now(), [`Waiting ${options.browserWaitUntilDelay} ms`]]);
492
+ }
493
+ setTimeout(() => {
494
+ if (options.debugMessagesFile) {
495
+ debugMessages.push([Date.now(), ["Detecting page ready"]]);
496
+ }
497
+ removeListeners();
498
+ resolve();
499
+ }, options.browserWaitUntilDelay);
500
+ }
390
501
  }
391
502
  }
392
503
 
@@ -394,9 +505,13 @@ async function getTopFrameContextId({ Page, Runtime }, options) {
394
505
  const { frame } = params;
395
506
  if (!frame.parentId) {
396
507
  if (frame.unreachableUrl) {
508
+ clearTimeout(resolveCallbackTimeout);
397
509
  removeListeners();
398
510
  reject(new Error("Unreachable URL: " + frame.unreachableUrl));
399
511
  } else {
512
+ if (options.debugMessagesFile) {
513
+ debugMessages.push([Date.now(), ["Detecting top frame ID"]]);
514
+ }
400
515
  topFrameId = frame.id;
401
516
  }
402
517
  }
@@ -433,7 +548,7 @@ async function getTopFrameContextId({ Page, Runtime }, options) {
433
548
  contextId
434
549
  });
435
550
  return result.value === true;
436
- } catch (error) {
551
+ } catch {
437
552
  // ignored
438
553
  }
439
554
  return false;
package/lib/constants.js CHANGED
@@ -51,7 +51,9 @@ const BROWSER_ARGS = [
51
51
  "--enable-use-zoom-for-dsf=false",
52
52
  "--no-sandbox",
53
53
  "--no-startup-window",
54
- "--bwsi"
54
+ "--bwsi",
55
+ "--mute-audio",
56
+ "--deny-permission-prompts"
55
57
  ];
56
58
  const BROWSER_PATHS = {
57
59
  linux: [
@@ -21,6 +21,8 @@
21
21
  * Source.
22
22
  */
23
23
 
24
+ // deno-lint-ignore-file no-node-globals
25
+
24
26
  /* global globalThis, URL, Deno, process */
25
27
 
26
28
  import * as path from "path";
@@ -244,7 +246,7 @@ async function cwd() {
244
246
  }
245
247
  }
246
248
 
247
- async function dirname(filePath) {
249
+ function dirname(filePath) {
248
250
  return path.dirname(filePath);
249
251
  }
250
252