single-file-cli 2.4.1 → 2.5.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/build.sh +1 -1
- package/deno.json +1 -1
- 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 +2 -0
- package/package.json +1 -1
- package/single-file-cli-api.js +5 -0
- package/test/e2e/compress-content.test.js +93 -0
- package/test/e2e/crawl-save-archive.test.js +6 -0
package/lib/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = "2.
|
|
1
|
+
export const version = "2.5.0";
|
package/options.js
CHANGED
|
@@ -135,6 +135,7 @@ const OPTIONS_INFO = [{
|
|
|
135
135
|
"create-root-directory": { description: "Create a root directory based on the timestamp", type: "boolean" },
|
|
136
136
|
"extract-data-from-page": { description: "Extract compressed data from the page instead of fetching the page in order to create universal self-extracting HTML files", type: "boolean", defaultValue: true },
|
|
137
137
|
"prevent-appended-data": { description: "Prevent appending data after the compressed data when creating self-extracting HTML files", type: "boolean" },
|
|
138
|
+
"declare-appended-data": { description: "Declare the data appended after the compressed data as the comment of the ZIP archive, for readers rejecting undeclared trailing bytes (e.g. java.util.zip); ZIP tools then print that data when listing the archive", type: "boolean" },
|
|
138
139
|
"embed-screenshot": { description: "Embed a screenshot of the page as a PNG file in the compressed file (self-extracting HTML or ZIP file). When enabled, the resulting file can be read as a ZIP file or a PNG image.", type: "boolean" },
|
|
139
140
|
"embed-screenshot-options": { description: "Options passed to the CDP method `Page.captureScreenshot()` given as a JSON string (e.g. { \"captureBeyondViewport\": false })", type: "string" },
|
|
140
141
|
"embedded-image": { description: "Path to a PNG image to embed in the compressed file.", type: "string" },
|
|
@@ -166,6 +167,7 @@ const OPTIONS_INFO = [{
|
|
|
166
167
|
"crawl-save-archive-dedup": { description: "Deduplicate identical resources shared between pages when using --crawl-save-archive", type: "boolean" },
|
|
167
168
|
"crawl-save-archive-mark-unarchived-links": { description: "Mark links to pages not saved in the archive when using --crawl-save-archive", type: "boolean" },
|
|
168
169
|
"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" },
|
|
170
|
+
"crawl-save-archive-page-list": { description: "Insert the list of the crawled pages in the prelude of the archive when using --crawl-save-archive, so that indexing tools can read it without extracting the archive", type: "boolean" },
|
|
169
171
|
"crawl-save-archive-toc": { description: "Save a table of contents page into the archive when using --crawl-save-archive", type: "boolean" },
|
|
170
172
|
}, {
|
|
171
173
|
"browser-script": { description: "Path of a script executed in the page (and all the frames) before it is loaded", type: "string[]" },
|
package/package.json
CHANGED
package/single-file-cli-api.js
CHANGED
|
@@ -79,6 +79,9 @@ 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.crawlSaveArchivePageList && !options.crawlSaveArchive) {
|
|
83
|
+
throw new Error("--crawl-save-archive-page-list requires --crawl-save-archive");
|
|
84
|
+
}
|
|
82
85
|
if (options.crawlSaveArchivePageTransitions !== undefined && !["auto", "fade", "none"].includes(options.crawlSaveArchivePageTransitions)) {
|
|
83
86
|
throw new Error("--crawl-save-archive-page-transitions must be \"auto\", \"fade\" or \"none\"");
|
|
84
87
|
}
|
|
@@ -199,10 +202,12 @@ async function savePagesArchive(options) {
|
|
|
199
202
|
dedupPages: options.crawlSaveArchiveDedup,
|
|
200
203
|
markUnarchivedLinks: options.crawlSaveArchiveMarkUnarchivedLinks,
|
|
201
204
|
tocPage: options.crawlSaveArchiveToc,
|
|
205
|
+
pageList: options.crawlSaveArchivePageList,
|
|
202
206
|
pageTransitions: options.crawlSaveArchivePageTransitions,
|
|
203
207
|
selfExtractingArchive: options.selfExtractingArchive,
|
|
204
208
|
extractDataFromPage: options.extractDataFromPage,
|
|
205
209
|
preventAppendedData: options.preventAppendedData,
|
|
210
|
+
declareAppendedData: options.declareAppendedData,
|
|
206
211
|
includeBOM: options.includeBOM,
|
|
207
212
|
insertMetaCSP: options.insertMetaCSP,
|
|
208
213
|
insertCanonicalLink: options.insertCanonicalLink,
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/* global URL, TextDecoder */
|
|
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
|
+
import { configure, ZipReader, Uint8ArrayReader } from "../../lib/single-file-archive.js";
|
|
14
|
+
|
|
15
|
+
const execFileAsync = promisify(execFile);
|
|
16
|
+
const cliDirectory = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
17
|
+
const TEST_TIMEOUT = 120000;
|
|
18
|
+
const END_OF_CENTRAL_DIRECTORY_SIGNATURE = 0x06054b50;
|
|
19
|
+
const END_OF_CENTRAL_DIRECTORY_LENGTH = 22;
|
|
20
|
+
const COMMENT_LENGTH_OFFSET = 20;
|
|
21
|
+
|
|
22
|
+
const capturePromises = new Map();
|
|
23
|
+
|
|
24
|
+
test("data is appended after the end of central directory record by default", { timeout: TEST_TIMEOUT }, async () => {
|
|
25
|
+
const { data, stderr } = await getCaptureResult();
|
|
26
|
+
const { offset, commentLength } = findEndOfCentralDirectory(data);
|
|
27
|
+
assert.equal(commentLength, 0, "stderr: " + stderr);
|
|
28
|
+
assert.ok(data.length > offset + END_OF_CENTRAL_DIRECTORY_LENGTH, "the capture appends no data to declare");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("--declare-appended-data declares the appended data as the archive comment", { timeout: TEST_TIMEOUT }, async () => {
|
|
32
|
+
const { data, stderr } = await getCaptureResult(true);
|
|
33
|
+
const { offset, commentLength } = findEndOfCentralDirectory(data);
|
|
34
|
+
assert.ok(commentLength > 0, "stderr: " + stderr);
|
|
35
|
+
assert.equal(offset + END_OF_CENTRAL_DIRECTORY_LENGTH + commentLength, data.length,
|
|
36
|
+
"the comment must cover every byte after the end of central directory record");
|
|
37
|
+
const comment = new TextDecoder("windows-1252").decode(data.subarray(data.length - commentLength));
|
|
38
|
+
assert.ok(comment.startsWith("-->"), "the comment must hold the appended data itself, got " + JSON.stringify(comment.slice(0, 32)));
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("the archive stays readable whether or not the appended data is declared", { timeout: TEST_TIMEOUT }, async () => {
|
|
42
|
+
for (const declareAppendedData of [false, true]) {
|
|
43
|
+
const { data } = await getCaptureResult(declareAppendedData);
|
|
44
|
+
configure({ useWebWorkers: false });
|
|
45
|
+
const zipReader = new ZipReader(new Uint8ArrayReader(data));
|
|
46
|
+
const entryNames = (await zipReader.getEntries()).map(entry => entry.filename);
|
|
47
|
+
assert.ok(entryNames.includes("index.html"), "declareAppendedData: " + declareAppendedData);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
function getCaptureResult(declareAppendedData = false) {
|
|
52
|
+
if (!capturePromises.has(declareAppendedData)) {
|
|
53
|
+
capturePromises.set(declareAppendedData, runCapture(declareAppendedData));
|
|
54
|
+
}
|
|
55
|
+
return capturePromises.get(declareAppendedData);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function runCapture(declareAppendedData) {
|
|
59
|
+
const server = createServer((request, response) => {
|
|
60
|
+
const { pathname } = new URL(request.url, "http://localhost");
|
|
61
|
+
if (pathname === "/") {
|
|
62
|
+
response.writeHead(200, { "content-type": "text/html" })
|
|
63
|
+
.end("<html><head><title>Compressed Page</title></head><body>content</body></html>");
|
|
64
|
+
} else {
|
|
65
|
+
response.writeHead(404).end();
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
await new Promise(resolve => server.listen(0, "localhost", resolve));
|
|
69
|
+
const directory = await mkdtemp(join(tmpdir(), "single-file-test-"));
|
|
70
|
+
try {
|
|
71
|
+
const origin = "http://localhost:" + server.address().port;
|
|
72
|
+
const { stderr } = await execFileAsync(process.execPath, [
|
|
73
|
+
"single-file-node.js", origin + "/", join(directory, "page.html"),
|
|
74
|
+
"--compress-content",
|
|
75
|
+
...(declareAppendedData ? ["--declare-appended-data"] : [])
|
|
76
|
+
], { cwd: cliDirectory });
|
|
77
|
+
const data = new Uint8Array(await readFile(join(directory, "page.html")));
|
|
78
|
+
return { data, stderr };
|
|
79
|
+
} finally {
|
|
80
|
+
await rm(directory, { recursive: true });
|
|
81
|
+
server.close();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function findEndOfCentralDirectory(data) {
|
|
86
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
87
|
+
for (let offset = data.length - END_OF_CENTRAL_DIRECTORY_LENGTH; offset >= 0; offset--) {
|
|
88
|
+
if (view.getUint32(offset, true) === END_OF_CENTRAL_DIRECTORY_SIGNATURE) {
|
|
89
|
+
return { offset, commentLength: view.getUint16(offset + COMMENT_LENGTH_OFFSET, true) };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
throw new Error("end of central directory record not found");
|
|
93
|
+
}
|
|
@@ -96,6 +96,12 @@ test("--crawl-save-archive-toc requires --crawl-save-archive", { timeout: TEST_T
|
|
|
96
96
|
error => error.stderr.includes("--crawl-save-archive-toc requires --crawl-save-archive"));
|
|
97
97
|
});
|
|
98
98
|
|
|
99
|
+
test("--crawl-save-archive-page-list requires --crawl-save-archive", { timeout: TEST_TIMEOUT }, async () => {
|
|
100
|
+
await assert.rejects(
|
|
101
|
+
execFileAsync(process.execPath, ["single-file-node.js", "http://localhost/", "--compress-content", "--crawl-save-archive-page-list"], { cwd: cliDirectory }),
|
|
102
|
+
error => error.stderr.includes("--crawl-save-archive-page-list requires --crawl-save-archive"));
|
|
103
|
+
});
|
|
104
|
+
|
|
99
105
|
test("--crawl-save-archive-mark-unarchived-links requires --crawl-save-archive", { timeout: TEST_TIMEOUT }, async () => {
|
|
100
106
|
await assert.rejects(
|
|
101
107
|
execFileAsync(process.execPath, ["single-file-node.js", "http://localhost/", "--compress-content", "--crawl-save-archive-mark-unarchived-links"], { cwd: cliDirectory }),
|