single-file-cli 2.3.0 → 2.3.1

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.3.0";
1
+ export const version = "2.3.1";
package/options.js CHANGED
@@ -336,7 +336,7 @@ function printUsage() {
336
336
  optionType = optionType.replace("[]", "*");
337
337
  }
338
338
  const optionDescription = optionInfo.description;
339
- const optionDefaultValue = optionInfo.defaultValue === undefined ? "" : `(default: ${JSON.stringify(optionInfo.defaultValue)})`;
339
+ const optionDefaultValue = optionInfo.defaultValue === undefined ? "" : `(default: ${JSON.stringify(optionInfo.defaultValue).replace(/[\u007f-\u009f]/g, character => "\\u" + character.charCodeAt(0).toString(16).padStart(4, "0"))})`;
340
340
  console.log(` --${optionName}: ${optionDescription} <${optionType}> ${optionDefaultValue}`); // eslint-disable-line no-console
341
341
  });
342
342
  console.log(""); // eslint-disable-line no-console
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "single-file-cli",
3
- "version": "2.3.0",
3
+ "version": "2.3.1",
4
4
  "description": "SingleFile CLI",
5
5
  "author": "Gildas Lormeau",
6
6
  "license": "AGPL-3.0-or-later",
@@ -250,6 +250,12 @@ async function runNextTask() {
250
250
  task.promise = capturePage(taskOptions);
251
251
  const pageData = await task.promise;
252
252
  task.status = STATE_PROCESSED;
253
+ if (options.crawlLinks || tasks.length > 1) {
254
+ const processedCount = tasks.filter(task => task.status == STATE_PROCESSED).length;
255
+ const filenameInfo = pageData && pageData.filename && !options.crawlSaveArchive ? " (" + pageData.filename + ")" : "";
256
+ // written to stderr so that stdout stays parseable when using --dump-content
257
+ console.error(`[${processedCount}/${tasks.length}] ${pageData ? "saved" : "failed"} ${task.url}${filenameInfo}`); // eslint-disable-line no-console
258
+ }
253
259
  if (pageData) {
254
260
  task.filename = pageData.filename;
255
261
  task.title = pageData.title;
@@ -0,0 +1,86 @@
1
+ /* global URL */
2
+
3
+ import { test } from "node:test";
4
+ import assert from "node:assert/strict";
5
+ import { createServer } from "node:http";
6
+ import { execFile } from "node:child_process";
7
+ import { promisify } from "node:util";
8
+ import { mkdtemp, readFile, rm } from "node:fs/promises";
9
+ import { tmpdir } from "node:os";
10
+ import { join, dirname } from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+ import process from "node:process";
13
+
14
+ const execFileAsync = promisify(execFile);
15
+ const cliDirectory = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
16
+ const FRAME_MARKER = "OUT_OF_PROCESS_FRAME_CONTENT";
17
+
18
+ test("blocked URL patterns and extra HTTP headers apply in cross-origin frames", { timeout: 120000 }, async () => {
19
+ const BLOCKED_STYLE = "rgb(123,45,67)";
20
+ const GATED_STYLE = "rgb(89,89,89)";
21
+ const frameServer = createServer((request, response) => {
22
+ const { pathname } = new URL(request.url, "http://127.0.0.1");
23
+ if (pathname === "/tracker.css") {
24
+ response.writeHead(200, { "content-type": "text/css" }).end("h2 { color: " + BLOCKED_STYLE + "; }");
25
+ } else if (pathname === "/gated.css") {
26
+ if (request.headers["x-test-header"] === "yes") {
27
+ response.writeHead(200, { "content-type": "text/css" }).end("h3 { color: " + GATED_STYLE + "; }");
28
+ } else {
29
+ response.writeHead(404).end();
30
+ }
31
+ } else {
32
+ response.writeHead(200, { "content-type": "text/html" }).end(
33
+ "<html><head><link rel=\"stylesheet\" href=\"/tracker.css\"><link rel=\"stylesheet\" href=\"/gated.css\"></head>" +
34
+ "<body><h2>blocked</h2><h3>gated</h3></body></html>");
35
+ }
36
+ });
37
+ await new Promise(resolve => frameServer.listen(0, "127.0.0.1", resolve));
38
+ const frameUrl = "http://127.0.0.1:" + frameServer.address().port + "/frame.html";
39
+ const server = createServer((request, response) => {
40
+ response.writeHead(200, { "content-type": "text/html" }).end("<html><body><iframe src=\"" + frameUrl + "\"></iframe></body></html>");
41
+ });
42
+ await new Promise(resolve => server.listen(0, "localhost", resolve));
43
+ const directory = await mkdtemp(join(tmpdir(), "single-file-test-"));
44
+ try {
45
+ const outputPath = join(directory, "out.html");
46
+ const url = "http://localhost:" + server.address().port + "/top.html";
47
+ await execFileAsync(process.execPath, [
48
+ "single-file-node.js", url, outputPath,
49
+ "--blocked-URL-pattern", "tracker",
50
+ "--http-header", "x-test-header=yes"
51
+ ], { cwd: cliDirectory });
52
+ const content = await readFile(outputPath, "utf8");
53
+ assert.ok(!content.includes(BLOCKED_STYLE), "a blocked stylesheet leaked into the frame");
54
+ assert.ok(content.includes(GATED_STYLE), "the extra HTTP header was not sent for a frame resource");
55
+ } finally {
56
+ await rm(directory, { recursive: true });
57
+ server.close();
58
+ frameServer.close();
59
+ }
60
+ });
61
+
62
+ test("cross-origin frames are captured in the saved page", { timeout: 120000 }, async () => {
63
+ // localhost and 127.0.0.1 are different sites, so site isolation renders
64
+ // the frame out of process, in a separate CDP target
65
+ const frameServer = createServer((request, response) => {
66
+ response.writeHead(200, { "content-type": "text/html" }).end("<html><body><p>" + FRAME_MARKER + "</p></body></html>");
67
+ });
68
+ await new Promise(resolve => frameServer.listen(0, "127.0.0.1", resolve));
69
+ const frameUrl = "http://127.0.0.1:" + frameServer.address().port + "/frame.html";
70
+ const server = createServer((request, response) => {
71
+ response.writeHead(200, { "content-type": "text/html" }).end("<html><body><iframe src=\"" + frameUrl + "\"></iframe></body></html>");
72
+ });
73
+ await new Promise(resolve => server.listen(0, "localhost", resolve));
74
+ const directory = await mkdtemp(join(tmpdir(), "single-file-test-"));
75
+ try {
76
+ const outputPath = join(directory, "out.html");
77
+ const url = "http://localhost:" + server.address().port + "/top.html";
78
+ await execFileAsync(process.execPath, ["single-file-node.js", url, outputPath], { cwd: cliDirectory });
79
+ const content = await readFile(outputPath, "utf8");
80
+ assert.ok(content.includes(FRAME_MARKER), "the out-of-process frame was saved empty");
81
+ } finally {
82
+ await rm(directory, { recursive: true });
83
+ server.close();
84
+ frameServer.close();
85
+ }
86
+ });