single-file-cli 2.0.68 → 2.0.69

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/deno.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@single-file/single-file-cli",
3
- "version": "2.0.68",
3
+ "version": "2.0.69",
4
4
  "description": "SingleFile CLI",
5
5
  "nodeModulesDir": true,
6
6
  "exports": {
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
@@ -159,7 +159,7 @@ async function getPageData(options) {
159
159
  }
160
160
  try {
161
161
  await Fetch.continueRequest({ requestId });
162
- } catch (error) {
162
+ } catch {
163
163
  // ignored
164
164
  }
165
165
  });
@@ -177,6 +177,7 @@ async function getPageData(options) {
177
177
  if (options.browserCookies && options.browserCookies.length) {
178
178
  await Network.setCookies({ cookies: options.browserCookies });
179
179
  }
180
+ await Browser.setDownloadBehavior({ behavior: "deny" });
180
181
  await Page.addScriptToEvaluateOnNewDocument({
181
182
  source: getHookScriptSource(),
182
183
  runImmediately: true
@@ -200,14 +201,14 @@ async function getPageData(options) {
200
201
  if (options.embedScreenshotOptions) {
201
202
  try {
202
203
  screenshotOptions = JSON.parse(options.embedScreenshotOptions);
203
- } catch (error) {
204
+ } catch {
204
205
  // ignored
205
206
  }
206
207
  }
207
208
  screenshotOptions.format = "png";
208
209
  const { data } = await Page.captureScreenshot(screenshotOptions);
209
210
  await Runtime.evaluate({ expression: `globalThis.${SET_SCREENSHOT_FUNCTION_NAME}(${JSON.stringify(data)})`, contextId });
210
- } catch (error) {
211
+ } catch {
211
212
  await Runtime.evaluate({ expression: `globalThis.${SET_SCREENSHOT_FUNCTION_NAME}(${JSON.stringify("")})`, contextId });
212
213
  }
213
214
  }
@@ -221,14 +222,14 @@ async function getPageData(options) {
221
222
  if (options.embedPdfOptions) {
222
223
  try {
223
224
  pdfOptions = JSON.parse(options.embedPdfOptions);
224
- } catch (error) {
225
+ } catch {
225
226
  // ignored
226
227
  }
227
228
  }
228
229
  try {
229
230
  const { data } = await Page.printToPDF(pdfOptions);
230
231
  await Runtime.evaluate({ expression: `globalThis.${SET_PDF_FUNCTION_NAME}(${JSON.stringify(data)})`, contextId });
231
- } catch (error) {
232
+ } catch {
232
233
  await Runtime.evaluate({ expression: `globalThis.${SET_PDF_FUNCTION_NAME}(${JSON.stringify("")})`, contextId });
233
234
  }
234
235
  }
@@ -236,7 +237,7 @@ async function getPageData(options) {
236
237
  }
237
238
  const pageDataPromise = new Promise(resolve => {
238
239
  let pageDataResponse = "";
239
- Runtime.addEventListener("bindingCalled", async ({ params }) => {
240
+ Runtime.addEventListener("bindingCalled", ({ params }) => {
240
241
  if (params.name === SET_PAGE_DATA_FUNCTION_NAME) {
241
242
  const { payload } = params;
242
243
  if (payload.length) {
@@ -276,13 +277,19 @@ async function getPageData(options) {
276
277
  const browserWaitUntil = NETWORK_STATES[(NETWORK_STATES.indexOf(options.browserWaitUntil) + 1)];
277
278
  if (browserWaitUntil) {
278
279
  options.browserWaitUntil = browserWaitUntil;
280
+ await closeTarget();
279
281
  return await getPageData(options);
280
282
  }
281
283
  }
282
284
  throw error;
283
285
  } finally {
286
+ await closeTarget();
287
+ }
288
+
289
+ async function closeTarget() {
284
290
  if (targetInfo && !options.browserDebug) {
285
291
  await CDP.closeTarget(targetInfo.id);
292
+ targetInfo = null;
286
293
  }
287
294
  }
288
295
  }
@@ -374,6 +381,7 @@ async function getTopFrameContextId({ Page, Runtime }, options) {
374
381
  }
375
382
 
376
383
  async function waitForPageReady({ Page }, options) {
384
+ let resolveCallbackTimeout;
377
385
  await Page.setLifecycleEventsEnabled({ enabled: true });
378
386
  try {
379
387
  await new Promise((resolve, reject) => {
@@ -384,9 +392,12 @@ async function getTopFrameContextId({ Page, Runtime }, options) {
384
392
 
385
393
  function onLifecycleEvent({ params }) {
386
394
  const { frameId, name } = params;
387
- if (frameId === topFrameId && name === options.browserWaitUntil) {
388
- removeListeners();
389
- resolve();
395
+ if (frameId === topFrameId && name === options.browserWaitUntil || (resolveCallbackTimeout && NETWORK_STATES.indexOf(name) < NETWORK_STATES.indexOf(options.browserWaitUntil))) {
396
+ clearTimeout(resolveCallbackTimeout);
397
+ setTimeout(() => {
398
+ removeListeners();
399
+ resolve();
400
+ }, options.browserWaitUntilDelay);
390
401
  }
391
402
  }
392
403
 
@@ -394,6 +405,7 @@ async function getTopFrameContextId({ Page, Runtime }, options) {
394
405
  const { frame } = params;
395
406
  if (!frame.parentId) {
396
407
  if (frame.unreachableUrl) {
408
+ clearTimeout(resolveCallbackTimeout);
397
409
  removeListeners();
398
410
  reject(new Error("Unreachable URL: " + frame.unreachableUrl));
399
411
  } else {
@@ -433,7 +445,7 @@ async function getTopFrameContextId({ Page, Runtime }, options) {
433
445
  contextId
434
446
  });
435
447
  return result.value === true;
436
- } catch (error) {
448
+ } catch {
437
449
  // ignored
438
450
  }
439
451
  return false;
@@ -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
 
@@ -41,7 +41,7 @@ function initSingleFile() {
41
41
  xhrRequest.onreadystatechange = () => {
42
42
  if (xhrRequest.readyState == XMLHttpRequest.DONE) {
43
43
  resolve({
44
- arrayBuffer: async () => xhrRequest.response || new ArrayBuffer(),
44
+ arrayBuffer: () => xhrRequest.response || new ArrayBuffer(),
45
45
  headers: { get: headerName => xhrRequest.getResponseHeader(headerName) },
46
46
  status: xhrRequest.status
47
47
  });
package/lib/version.js CHANGED
@@ -1 +1 @@
1
- export const version = "2.0.68";
1
+ export const version = "2.0.69";
package/options.js CHANGED
@@ -50,6 +50,7 @@ const OPTIONS_INFO = {
50
50
  "browser-load-max-time": { description: "Maximum delay of time to wait for page loading in ms", type: "number", defaultValue: 60000 },
51
51
  "browser-wait-delay": { description: "Time to wait before capturing the page in ms", type: "number" },
52
52
  "browser-wait-until": { description: "When to consider the page is loaded (InteractiveTime, networkIdle, networkAlmostIdle, load, domContentLoaded)", type: "string", defaultValue: "networkIdle" },
53
+ "browser-wait-until-delay": { description: "Delay of time in ms to wait before considering the page is loaded when the value of --browser-wait-until is reached", type: "number", defaultValue: 1000 },
53
54
  "browser-wait-until-fallback": { description: "Retry with the next value of --browser-wait-until when a timeout error is thrown", type: "boolean", defaultValue: true },
54
55
  "browser-debug": { description: "Enable debug mode", type: "boolean" },
55
56
  "browser-script": { description: "Path of a script executed in the page (and all the frames) before it is loaded", type: "string[]" },
@@ -121,7 +122,7 @@ const OPTIONS_INFO = {
121
122
  "remove-alternative-images": { description: "Remove images for alternative sizes of screen", type: "boolean", defaultValue: true },
122
123
  "save-original-URLs": { description: "Save the original URLS in the embedded contents", type: "boolean" },
123
124
  "save-raw-page": { description: "Save the original page without interpreting it into the browser", type: "boolean" },
124
- "urls-file": { description: "Path to a text file containing a list of URLs (separated by a newline) to save", type: "string" },
125
+ "urls-file": { description: "Path to a text file containing a list of URLs (separated by a newline) to save. You can also pass the options after each URL (e.g. 'https://www.example.com --filename-template={page-title}.html')", type: "string" },
125
126
  "user-agent": { description: "User-agent of the browser", type: "string" },
126
127
  "accept-language": { description: "Accept language of the browser", type: "string" },
127
128
  "platform": { description: "Platform of the browser (default value is \"Android\" when using --browser-mobile-emulation)", type: "string" },
@@ -149,10 +150,10 @@ const OPTIONS_INFO = {
149
150
  };
150
151
 
151
152
  const { args, exit } = Deno;
152
- const options = await getOptions();
153
- export default options;
153
+ const options = getOptions();
154
+ export { options, parseArgs };
154
155
 
155
- async function getOptions() {
156
+ function getOptions() {
156
157
  const { positionals, options } = parseArgs(Array.from(args));
157
158
  const unknownOptions = [];
158
159
  positionals.forEach(positional => {
@@ -185,43 +186,6 @@ async function getOptions() {
185
186
  console.log(version); // eslint-disable-line no-console
186
187
  exit(0);
187
188
  }
188
- Object.keys(OPTIONS_INFO).forEach(optionName => {
189
- const optionInfo = getOptionInfo(optionName);
190
- const optionKey = getOptionKey(optionName, optionInfo);
191
- if (options[optionKey] === undefined && optionInfo.defaultValue !== undefined) {
192
- options[optionKey] = OPTIONS_INFO[optionName].defaultValue;
193
- }
194
- });
195
- options.acceptHeaders = {
196
- font: options.acceptHeaderFont,
197
- image: options.acceptHeaderImage,
198
- stylesheet: options.acceptHeaderStylesheet,
199
- script: options.acceptHeaderScript,
200
- document: options.acceptHeaderDocument
201
- };
202
- if (options.browserArgs) {
203
- const browserArguments = options.browserArguments || [];
204
- browserArguments.push(...JSON.parse(options.browserArgs));
205
- options.browserArgs = browserArguments;
206
- delete options.browserArguments;
207
- }
208
- if (options.browserArguments) {
209
- options.browserArgs = options.browserArguments;
210
- delete options.browserArguments;
211
- }
212
- if (options.errorFile) {
213
- options.errorsFile = options.errorFile;
214
- delete options.errorFile;
215
- }
216
- if (options.errorTracesDisabled) {
217
- options.errorsTracesDisabled = options.errorTracesDisabled;
218
- delete options.errorTracesDisabled;
219
- }
220
- delete options.acceptHeaderFont;
221
- delete options.acceptHeaderImage;
222
- delete options.acceptHeaderStylesheet;
223
- delete options.acceptHeaderScript;
224
- delete options.acceptHeaderDocument;
225
189
  return { ...options, url: positionals[0], output: positionals[1] };
226
190
  }
227
191
 
@@ -280,6 +244,71 @@ function parseArgs(args) {
280
244
  }
281
245
  result.options[optionKey] = optionValue;
282
246
  });
247
+ Object.keys(OPTIONS_INFO).forEach(optionName => {
248
+ const optionInfo = getOptionInfo(optionName);
249
+ const optionKey = getOptionKey(optionName, optionInfo);
250
+ if (result.options[optionKey] === undefined && optionInfo.defaultValue !== undefined) {
251
+ result.options[optionKey] = OPTIONS_INFO[optionName].defaultValue;
252
+ }
253
+ });
254
+ result.options.acceptHeaders = {
255
+ font: result.options.acceptHeaderFont,
256
+ image: result.options.acceptHeaderImage,
257
+ stylesheet: result.options.acceptHeaderStylesheet,
258
+ script: result.options.acceptHeaderScript,
259
+ document: result.options.acceptHeaderDocument
260
+ };
261
+ if (result.options.browserArgs) {
262
+ const browserArguments = result.options.browserArguments || [];
263
+ browserArguments.push(...JSON.parse(result.options.browserArgs));
264
+ result.options.browserArgs = browserArguments;
265
+ delete result.options.browserArguments;
266
+ }
267
+ if (result.options.browserArguments) {
268
+ result.options.browserArgs = result.options.browserArguments;
269
+ delete result.options.browserArguments;
270
+ }
271
+ if (result.options.errorFile) {
272
+ result.options.errorsFile = result.options.errorFile;
273
+ delete result.options.errorFile;
274
+ }
275
+ if (result.options.errorTracesDisabled) {
276
+ result.options.errorsTracesDisabled = result.options.errorTracesDisabled;
277
+ delete result.options.errorTracesDisabled;
278
+ }
279
+ if (result.options.crawlReplaceUrls) {
280
+ result.options.crawlReplaceURLs = result.options.crawlReplaceUrls;
281
+ delete result.options.crawlReplaceUrls;
282
+ }
283
+ if (result.options.saveOriginalUrls) {
284
+ result.options.saveOriginalURLs = result.options.saveOriginalUrls;
285
+ delete result.options.saveOriginalUrls;
286
+ }
287
+ if (result.options.browserRemoteDebuggingUrl) {
288
+ result.options.browserRemoteDebuggingURL = result.options.browserRemoteDebuggingUrl;
289
+ delete result.options.browserRemoteDebuggingUrl;
290
+ }
291
+ if (result.options.crawlRemoveUrlFragment) {
292
+ result.options.crawlRemoveURLFragment = result.options.crawlRemoveUrlFragment;
293
+ delete result.options.crawlRemoveUrlFragment;
294
+ }
295
+ if (result.options.compressCss) {
296
+ result.options.compressCSS = result.options.compressCss;
297
+ delete result.options.compressCss;
298
+ }
299
+ if (result.options.compressHtml) {
300
+ result.options.compressHTML = result.options.compressHtml;
301
+ delete result.options.compressHtml;
302
+ }
303
+ if (result.options.insertMetaCsp) {
304
+ result.options.insertMetaCSP = result.options.insertMetaCsp;
305
+ delete result.options.insertMetaCsp;
306
+ }
307
+ delete result.options.acceptHeaderFont;
308
+ delete result.options.acceptHeaderImage;
309
+ delete result.options.acceptHeaderStylesheet;
310
+ delete result.options.acceptHeaderScript;
311
+ delete result.options.acceptHeaderDocument;
283
312
  return result;
284
313
  }
285
314
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "single-file-cli",
3
- "version": "2.0.68",
3
+ "version": "2.0.69",
4
4
  "description": "SingleFile CLI",
5
5
  "author": "Gildas Lormeau",
6
6
  "engines": {
@@ -101,7 +101,17 @@ async function initialize(options) {
101
101
  async function capture(urls, options) {
102
102
  let newTasks;
103
103
  const taskUrls = tasks.map(task => task.url);
104
- newTasks = await Promise.all(urls.map(url => createTask(url, options)));
104
+ newTasks = await Promise.all(urls.map(value => {
105
+ let url, taskOptions;
106
+ if (Array.isArray(value)) {
107
+ url = value[0];
108
+ taskOptions = Object.assign({}, options, value[1]);
109
+ } else {
110
+ url = value;
111
+ taskOptions = options;
112
+ }
113
+ return createTask(url, taskOptions);
114
+ }));
105
115
  newTasks = newTasks.filter(task => task && !taskUrls.includes(task.url));
106
116
  if (newTasks.length) {
107
117
  tasks = tasks.concat(newTasks);
@@ -127,7 +137,7 @@ async function finish(options) {
127
137
  }
128
138
  });
129
139
  await writeTextFile(task.filename, pageContent);
130
- } catch (error) {
140
+ } catch {
131
141
  // ignored
132
142
  }
133
143
  }
@@ -137,7 +147,7 @@ async function finish(options) {
137
147
  }
138
148
  }
139
149
 
140
- async function runTasks() {
150
+ function runTasks() {
141
151
  const availableTasks = tasks.filter(task => !task.status).length;
142
152
  const processingTasks = tasks.filter(task => task.status == STATE_PROCESSING).length;
143
153
  const promisesTasks = [];
@@ -151,7 +161,7 @@ async function runNextTask() {
151
161
  const task = tasks.find(task => !task.status);
152
162
  if (task) {
153
163
  const options = task.options;
154
- let taskOptions = JSON.parse(JSON.stringify(options));
164
+ const taskOptions = JSON.parse(JSON.stringify(options));
155
165
  taskOptions.url = task.url;
156
166
  task.status = STATE_PROCESSING;
157
167
  await saveTasks();
@@ -166,6 +176,7 @@ async function runNextTask() {
166
176
  newTasks = newTasks.filter(task => task &&
167
177
  testMaxDepth(task) &&
168
178
  !tasks.find(otherTask => otherTask.url == task.url) &&
179
+ !newTasks.find(otherTask => otherTask != task && otherTask.url == task.url) &&
169
180
  (!options.crawlInnerLinksOnly || task.isInnerLink) &&
170
181
  (!options.crawlNoParent || (task.isChild || !task.isInnerLink)));
171
182
  tasks.splice(tasks.length, 0, ...newTasks);
@@ -282,7 +293,7 @@ async function capturePage(options) {
282
293
  if (options.outputJson) {
283
294
  filename += filename.endsWith(".json") ? "" : ".json";
284
295
  }
285
- const directoryName = await path.dirname(filename);
296
+ const directoryName = path.dirname(filename);
286
297
  if (directoryName !== ".") {
287
298
  await mkdir(directoryName, { recursive: true });
288
299
  }
@@ -338,7 +349,7 @@ async function getFilename(filename, options, index = 1) {
338
349
  if (options.filenameConflictAction != "skip") {
339
350
  return getFilename(filename, options, index + 1);
340
351
  }
341
- } catch (_) {
352
+ } catch {
342
353
  return newFilename;
343
354
  }
344
355
  }
@@ -24,7 +24,7 @@
24
24
  import { initialize } from "./single-file-cli-api.js";
25
25
  import { closeBrowser } from "./lib/browser.js";
26
26
  import { Deno } from "./lib/deno-polyfill.js";
27
- import options from "./options.js";
27
+ import { options, parseArgs } from "./options.js";
28
28
 
29
29
  const { readTextFile, readFile, exit, addSignalListener } = Deno;
30
30
 
@@ -54,7 +54,7 @@ async function run() {
54
54
  delete options.settingsFile;
55
55
  }
56
56
  if (options.urlsFile) {
57
- urls = (await readTextFile(options.urlsFile)).split("\n");
57
+ urls = await getUrlsFile(options.urlsFile);
58
58
  } else {
59
59
  urls = [options.url];
60
60
  }
@@ -80,7 +80,7 @@ async function run() {
80
80
  const cookiesContent = await readTextFile(options.browserCookiesFile);
81
81
  try {
82
82
  options.browserCookies = JSON.parse(cookiesContent);
83
- } catch (error) {
83
+ } catch {
84
84
  options.browserCookies = parseCookies(cookiesContent);
85
85
  }
86
86
  }
@@ -137,3 +137,56 @@ async function closeBrowserAndExit(code) {
137
137
  await closeBrowser();
138
138
  exit(code);
139
139
  }
140
+
141
+ async function getUrlsFile(urlsFile) {
142
+ let urls = (await readTextFile(urlsFile)).split("\n");
143
+ urls = urls.map(value => {
144
+ value = value.trim();
145
+ let optionPosition = value.indexOf(" --");
146
+ if (optionPosition < 0) {
147
+ optionPosition = value.indexOf("\t--");
148
+ }
149
+ if (optionPosition > 0) {
150
+ const url = value.substring(0, optionPosition).trim();
151
+ const argsString = value.substring(optionPosition + 1).trim();
152
+ const args = [];
153
+ let previousCharacter, previousPreviousCharacter, lastQuoteCharacter;
154
+ let lastCharIndex = 0;
155
+ for (let currentCharIndex = 0; currentCharIndex < argsString.length; currentCharIndex++) {
156
+ const character = argsString[currentCharIndex];
157
+ if (character == lastQuoteCharacter && (previousCharacter != "\\" || previousPreviousCharacter == "\\")) {
158
+ args.push(argsString.substring(lastCharIndex, currentCharIndex));
159
+ lastQuoteCharacter = null;
160
+ lastCharIndex = currentCharIndex + 1;
161
+ } else if (!lastQuoteCharacter) {
162
+ if (character == "'" || character == "\"") {
163
+ lastQuoteCharacter = argsString[currentCharIndex];
164
+ lastCharIndex = currentCharIndex + 1;
165
+ } else {
166
+ const isSpaceCharacter = character == " " || character == "\t";
167
+ if (isSpaceCharacter || character == "=") {
168
+ if (isSpaceCharacter && (currentCharIndex == lastCharIndex + 1)) {
169
+ lastCharIndex++;
170
+ } else if (lastCharIndex < currentCharIndex) {
171
+ args.push(argsString.substring(lastCharIndex, currentCharIndex));
172
+ lastCharIndex = currentCharIndex + 1;
173
+ } else {
174
+ lastCharIndex = currentCharIndex + 1;
175
+ }
176
+ }
177
+ }
178
+ }
179
+ previousPreviousCharacter = previousCharacter;
180
+ previousCharacter = character;
181
+ }
182
+ if (lastCharIndex < argsString.length) {
183
+ args.push(argsString.substring(lastCharIndex).trim());
184
+ }
185
+ const { options } = parseArgs(args);
186
+ return [url, options];
187
+ } else {
188
+ return value;
189
+ }
190
+ });
191
+ return urls;
192
+ }