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/Dockerfile +1 -0
- package/README.MD +6 -2
- package/build.sh +1 -1
- package/deno.json +1 -1
- package/lib/archive-packager.js +3 -0
- package/lib/cdp-client-util.js +4 -1
- package/lib/cdp-client.js +24 -20
- package/lib/single-file-archive.js +6 -6
- package/lib/single-file-bundle.js +1 -1
- package/lib/version.js +1 -1
- package/options.js +8 -3
- package/package.json +1 -1
- package/single-file-cli-api.js +11 -2
- package/single-file-launcher.js +4 -1
- package/test/e2e/errors-file.test.js +11 -5
- package/test/e2e/urls-file.test.js +31 -0
- package/test/unit/archive-packager.test.js +15 -2
- package/test/unit/options.test.js +3 -0
package/lib/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = "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-
|
|
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
package/single-file-cli-api.js
CHANGED
|
@@ -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
|
-
|
|
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) {
|
package/single-file-launcher.js
CHANGED
|
@@ -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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
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", () => {
|