single-file-cli 2.2.2 → 2.3.0

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/version.js CHANGED
@@ -1 +1 @@
1
- export const version = "2.2.2";
1
+ export const version = "2.3.0";
package/options.js CHANGED
@@ -53,7 +53,7 @@ const CATEGORIES = [
53
53
  ];
54
54
 
55
55
  const OPTIONS_INFO = [{
56
- "browser-server": { description: "Server to connect to", type: "string" },
56
+ "browser-server": { description: "Server to connect to", type: "string", alias: "browser-remote-debugging-url" },
57
57
  "browser-headless": { description: "Run the browser in headless mode", type: "boolean", defaultValue: true },
58
58
  "browser-executable-path": { description: "Path to chrome/chromium executable", type: "string" },
59
59
  "browser-width": { description: "Width of the browser viewport in pixels", type: "number", defaultValue: 1280 },
@@ -64,7 +64,7 @@ const OPTIONS_INFO = [{
64
64
  "browser-single-process": { description: "Run the browser as a single process (enabled by default on Windows only, current browsers on other platforms do not support this mode)", type: "boolean", defaultValue: build.os == "windows" },
65
65
  "browser-start-minimized": { description: "Minimize the browser", type: "boolean" },
66
66
  "browser-ignore-insecure-certs": { description: "Ignore HTTPs errors", type: "boolean" },
67
- "browser-remote-debugging-URL": { description: "Remote debugging URL", type: "string" }
67
+ "browser-bypass-CSP": { description: "Bypass the Content Security Policy of the page, needed to save pages enforcing Trusted Types with browsers based on Chromium 150 or older", type: "boolean", defaultValue: false }
68
68
  }, {
69
69
  "browser-load-max-time": { description: "Maximum delay of time to wait for loading the page in ms", type: "number", defaultValue: 60000 },
70
70
  "browser-capture-max-time": { description: "Maximum delay of time to wait for capturing the page in ms", type: "number", defaultValue: 60000 },
@@ -164,6 +164,7 @@ const OPTIONS_INFO = [{
164
164
  "crawl-save-archive": { description: "Save all the crawled pages into a single (self-extracting) ZIP file, requires --compress-content", type: "boolean" },
165
165
  "crawl-save-archive-dedup": { description: "Deduplicate identical resources shared between pages when using --crawl-save-archive", type: "boolean" },
166
166
  "crawl-save-archive-mark-unarchived-links": { description: "Mark links to pages not saved in the archive when using --crawl-save-archive", type: "boolean" },
167
+ "crawl-save-archive-page-transitions": { description: "Page transitions when navigating in the archive saved with --crawl-save-archive. The possible values are \"auto\" (default, i.e. transitions run when pages opt in via CSS), \"fade\" and \"none\"", type: "string", defaultValue: "auto" },
167
168
  "crawl-save-archive-toc": { description: "Save a table of contents page into the archive when using --crawl-save-archive", type: "boolean" },
168
169
  }, {
169
170
  "browser-script": { description: "Path of a script executed in the page (and all the frames) before it is loaded", type: "string[]" },
@@ -454,6 +455,10 @@ function parseArgs(args, setDefaultValues = true) {
454
455
  result.options.errorsTracesDisabled = result.options.errorTracesDisabled;
455
456
  delete result.options.errorTracesDisabled;
456
457
  }
458
+ if (result.options.browserRemoteDebuggingUrl !== undefined) {
459
+ result.options.browserServer = result.options.browserRemoteDebuggingUrl;
460
+ delete result.options.browserRemoteDebuggingUrl;
461
+ }
457
462
  if (result.options.filenameReplacedCharacters) {
458
463
  const filenameReplacedCharacters = result.options.filenameReplacedCharacters;
459
464
  result.options.filenameReplacedCharacters = [];
@@ -556,7 +561,7 @@ function getOptionInfo(optionName) {
556
561
  let result;
557
562
  OPTIONS_INFO.forEach(categoryOptions => {
558
563
  Object.keys(categoryOptions).forEach(keyName => {
559
- if (keyName.toLowerCase() == optionName.toLowerCase() || categoryOptions[keyName].alias == optionName.toLowerCase()) {
564
+ if (keyName.toLowerCase() == optionName.toLowerCase() || (categoryOptions[keyName].alias && categoryOptions[keyName].alias.toLowerCase() == optionName.toLowerCase())) {
560
565
  result = { name: keyName, info: categoryOptions[keyName] };
561
566
  }
562
567
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "single-file-cli",
3
- "version": "2.2.2",
3
+ "version": "2.3.0",
4
4
  "description": "SingleFile CLI",
5
5
  "author": "Gildas Lormeau",
6
6
  "license": "AGPL-3.0-or-later",
@@ -61,7 +61,7 @@ const STATE_PROCESSING = "processing";
61
61
  const STATE_PROCESSED = "processed";
62
62
 
63
63
  const { readTextFile, writeTextFile, readFile, writeFile, stdout, mkdir, makeTempDir, remove, stat, errors } = Deno;
64
- let tasks = [], maxParallelWorkers, sessionFilename, archiveTempDirectory;
64
+ let tasks = [], maxParallelWorkers, sessionFilename, archiveTempDirectory, errorCount = 0;
65
65
 
66
66
  export { initialize };
67
67
 
@@ -79,6 +79,12 @@ async function initialize(options) {
79
79
  if (options.crawlSaveArchiveToc && !options.crawlSaveArchive) {
80
80
  throw new Error("--crawl-save-archive-toc requires --crawl-save-archive");
81
81
  }
82
+ if (options.crawlSaveArchivePageTransitions !== undefined && !["auto", "fade", "none"].includes(options.crawlSaveArchivePageTransitions)) {
83
+ throw new Error("--crawl-save-archive-page-transitions must be \"auto\", \"fade\" or \"none\"");
84
+ }
85
+ if (options.crawlSaveArchivePageTransitions !== undefined && options.crawlSaveArchivePageTransitions != "auto" && !options.crawlSaveArchive) {
86
+ throw new Error("--crawl-save-archive-page-transitions requires --crawl-save-archive");
87
+ }
82
88
  if (options.crawlSaveArchive) {
83
89
  if (!options.compressContent) {
84
90
  throw new Error("--crawl-save-archive requires --compress-content");
@@ -174,8 +180,9 @@ async function finish(options) {
174
180
  }
175
181
  }
176
182
  if (!options.browserDebug && !options.browserServer) {
177
- return backend.closeBrowser();
183
+ await backend.closeBrowser();
178
184
  }
185
+ return errorCount;
179
186
  }
180
187
 
181
188
  async function savePagesArchive(options) {
@@ -192,6 +199,7 @@ async function savePagesArchive(options) {
192
199
  dedupPages: options.crawlSaveArchiveDedup,
193
200
  markUnarchivedLinks: options.crawlSaveArchiveMarkUnarchivedLinks,
194
201
  tocPage: options.crawlSaveArchiveToc,
202
+ pageTransitions: options.crawlSaveArchivePageTransitions,
195
203
  selfExtractingArchive: options.selfExtractingArchive,
196
204
  extractDataFromPage: options.extractDataFromPage,
197
205
  preventAppendedData: options.preventAppendedData,
@@ -391,6 +399,7 @@ async function capturePage(options) {
391
399
  }
392
400
  return pageData;
393
401
  } catch (error) {
402
+ errorCount++;
394
403
  const date = new Date();
395
404
  let message = `[${date.toISOString()}] URL: ${options.url} Error: ${error.message || error}`;
396
405
  if (!options.errorsTracesDisabled) {
@@ -71,7 +71,10 @@ async function run() {
71
71
  options.retrieveLinks = true;
72
72
  const singlefile = await initialize(options);
73
73
  await singlefile.capture(urls);
74
- await singlefile.finish();
74
+ const errorCount = await singlefile.finish();
75
+ if (errorCount) {
76
+ exit(1);
77
+ }
75
78
  } catch (error) {
76
79
  console.error(error.message || error); // eslint-disable-line no-console
77
80
  await closeBrowserAndExit(-1);
@@ -11,15 +11,21 @@ import process from "node:process";
11
11
  const execFileAsync = promisify(execFile);
12
12
  const cliDirectory = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
13
13
 
14
- test("errors file lines include the error message", { timeout: 120000 }, async () => {
14
+ test("errors file lines include the error message and the exit code is nonzero", { timeout: 120000 }, async () => {
15
15
  const directory = await mkdtemp(join(tmpdir(), "single-file-test-"));
16
16
  try {
17
17
  const errorsPath = join(directory, "errors.txt");
18
18
  const url = "http://localhost:1/";
19
- await execFileAsync(process.execPath, [
20
- "single-file-node.js", url, join(directory, "out.html"),
21
- "--errors-file", errorsPath
22
- ], { cwd: cliDirectory });
19
+ let exitCode = 0;
20
+ try {
21
+ await execFileAsync(process.execPath, [
22
+ "single-file-node.js", url, join(directory, "out.html"),
23
+ "--errors-file", errorsPath
24
+ ], { cwd: cliDirectory });
25
+ } catch (error) {
26
+ exitCode = error.code;
27
+ }
28
+ assert.equal(exitCode, 1);
23
29
  const content = await readFile(errorsPath, "utf8");
24
30
  assert.ok(content.includes("URL: " + url));
25
31
  const errorMessage = content.match(/Error: (.*)/);
@@ -35,3 +35,34 @@ test("duplicate urls in a urls file are captured once", { timeout: 120000 }, asy
35
35
  server.close();
36
36
  }
37
37
  });
38
+
39
+ test("a failed capture in a urls file batch sets a nonzero exit code without aborting the batch", { timeout: 120000 }, async () => {
40
+ const server = createServer((_, response) => response
41
+ .writeHead(200, { "content-type": "text/html" })
42
+ .end("<html><head><title>Good Page</title></head><body>content</body></html>"));
43
+ await new Promise(resolve => server.listen(0, "localhost", resolve));
44
+ const directory = await mkdtemp(join(tmpdir(), "single-file-test-"));
45
+ try {
46
+ const goodUrl = "http://localhost:" + server.address().port + "/";
47
+ const urlsFilePath = join(directory, "urls.txt");
48
+ await writeFile(urlsFilePath, "http://localhost:1/\n" + goodUrl + "\n");
49
+ let exitCode = 0, stderr = "";
50
+ try {
51
+ ({ stderr } = await execFileAsync(process.execPath, [
52
+ "single-file-node.js",
53
+ "--urls-file", urlsFilePath,
54
+ "--output-directory", directory,
55
+ "--filename-template", "{page-title}.html"
56
+ ], { cwd: cliDirectory }));
57
+ } catch (error) {
58
+ exitCode = error.code;
59
+ stderr = error.stderr;
60
+ }
61
+ assert.equal(exitCode, 1);
62
+ const files = (await readdir(directory)).filter(file => file.endsWith(".html"));
63
+ assert.deepEqual(files, ["Good Page.html"], "stderr: " + stderr);
64
+ } finally {
65
+ await rm(directory, { recursive: true });
66
+ server.close();
67
+ }
68
+ });
@@ -31,13 +31,26 @@ test("the table of contents groups by origin when the crawl crossed hosts", asyn
31
31
  assert.ok(toc.includes("<summary>https://other.example</summary>"));
32
32
  });
33
33
 
34
- async function createArchive(pages) {
34
+ test("the page transitions setting is stored in the manifest except the default", async () => {
35
+ for (const pageTransitions of [undefined, "auto"]) {
36
+ const archive = await createArchive([{ url: "https://example.com/", title: "Home" }], { pageTransitions });
37
+ const manifest = JSON.parse(await readEntry(archive, "sfz-pages.json"));
38
+ assert.equal(manifest.pageTransitions, undefined, "the default is not stored");
39
+ }
40
+ for (const pageTransitions of ["fade", "none"]) {
41
+ const archive = await createArchive([{ url: "https://example.com/", title: "Home" }], { pageTransitions });
42
+ const manifest = JSON.parse(await readEntry(archive, "sfz-pages.json"));
43
+ assert.equal(manifest.pageTransitions, pageTransitions);
44
+ }
45
+ });
46
+
47
+ async function createArchive(pages, options = {}) {
35
48
  return createPagesArchive(pages.map(page => ({
36
49
  url: page.url,
37
50
  originalUrls: [page.url],
38
51
  title: page.title,
39
52
  getData: () => createPageData(page)
40
- })), { tocPage: true, selfExtractingArchive: false });
53
+ })), Object.assign({ tocPage: true, selfExtractingArchive: false }, options));
41
54
  }
42
55
 
43
56
  async function createPageData(page) {
@@ -27,6 +27,9 @@ test("aliases map to the canonical option", () => {
27
27
  assert.equal(parse(["--error-traces-disabled", "false"]).errorsTracesDisabled, false);
28
28
  assert.equal(parse(["--errors-traces-disabled", "false"]).errorsTracesDisabled, false);
29
29
  assert.equal(parse([]).errorsTracesDisabled, true);
30
+ assert.equal(parse(["--browser-remote-debugging-URL", "http://localhost:9222"]).browserServer, "http://localhost:9222");
31
+ assert.equal(parse(["--browser-server", "http://localhost:9222"]).browserServer, "http://localhost:9222");
32
+ assert.equal("browserRemoteDebuggingUrl" in parse(["--browser-remote-debugging-url", "http://localhost:9222"]), false);
30
33
  });
31
34
 
32
35
  test("browser arguments are merged", () => {