single-file-cli 2.6.3 → 2.6.4

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.6.3";
1
+ export const version = "2.6.4";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "single-file-cli",
3
- "version": "2.6.3",
3
+ "version": "2.6.4",
4
4
  "description": "SingleFile CLI",
5
5
  "author": "Gildas Lormeau",
6
6
  "license": "AGPL-3.0-or-later",
@@ -0,0 +1,93 @@
1
+ /* global URL */
2
+
3
+ // A canvas drawing is saved as a background-image data URI on the canvas element itself. Save the
4
+ // saved page again and that element is a real, EMPTY canvas — no script ran to draw into it — so
5
+ // toDataURL() returned a blank bitmap and it OVERWROTE the picture the first save had stored.
6
+ // Measured on the MDN Canvas API page and reproduced here: generation 1 held the drawing, and
7
+ // generation 2 held a blank PNG of the same dimensions, 890 against 874 bytes. Nothing but
8
+ // decoding the image would have caught it — a blank PNG is not conspicuously small.
9
+ //
10
+ // The two controls matter as much as the round trip: a canvas that is drawn on top of a CSS
11
+ // background must still store its drawing, and a canvas nothing ever drew into must keep behaving
12
+ // as it always has. The fix skips the capture only when the fresh bitmap is blank AND the element
13
+ // already carries a background image, so both of those paths have to stay untouched.
14
+ //
15
+ // The fix shipped in single-file-core 1.5.119, so these run against the committed lib/ as well.
16
+
17
+ import { test } from "node:test";
18
+ import assert from "node:assert/strict";
19
+ import { createServer } from "node:http";
20
+ import { execFile } from "node:child_process";
21
+ import { promisify } from "node:util";
22
+ import { mkdtemp, readFile, rm } from "node:fs/promises";
23
+ import { tmpdir } from "node:os";
24
+ import { join } from "node:path";
25
+ import { pathToFileURL } from "node:url";
26
+ import process from "node:process";
27
+ import { cliDirectory } from "../target.js";
28
+
29
+ const execFileAsync = promisify(execFile);
30
+ const TEST_TIMEOUT = 180000;
31
+ const RED_DOT = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==";
32
+ const DRAWING = "<script>const context=document.getElementById(\"probe\").getContext(\"2d\");context.fillStyle=\"#00aa00\";context.fillRect(10,10,150,80);</script>";
33
+ const PAGES = {
34
+ "/drawn": "<html><head><title>drawn</title></head><body><canvas id=\"probe\" width=\"200\" height=\"100\"></canvas>" + DRAWING + "</body></html>",
35
+ "/drawn-with-background": "<html><head><title>drawn with background</title></head><body><canvas id=\"probe\" width=\"200\" height=\"100\" style=\"background-image:url(" + RED_DOT + ")\"></canvas>" + DRAWING + "</body></html>",
36
+ "/blank": "<html><head><title>blank</title></head><body><canvas id=\"probe\" width=\"200\" height=\"100\"></canvas></body></html>"
37
+ };
38
+
39
+ test("re-saving a saved page keeps the canvas drawing", { timeout: TEST_TIMEOUT }, async () => {
40
+ const generations = await capture("/drawn", 3);
41
+ assert.ok(generations[0], "the first save stored no canvas image");
42
+ assert.equal(generations[1], generations[0], "the second save replaced the canvas image");
43
+ assert.equal(generations[2], generations[0], "the third save replaced the canvas image");
44
+ });
45
+
46
+ test("a canvas drawn over a background image still stores its drawing", { timeout: TEST_TIMEOUT }, async () => {
47
+ const [saved] = await capture("/drawn-with-background", 1);
48
+ assert.ok(saved, "the save stored no canvas image");
49
+ assert.notEqual(saved, RED_DOT.split(",")[1], "the save kept the background image instead of the drawing");
50
+ });
51
+
52
+ test("a canvas nothing drew into is still captured when there is no background", { timeout: TEST_TIMEOUT }, async () => {
53
+ const [saved] = await capture("/blank", 1);
54
+ assert.ok(saved, "the save stored no canvas image");
55
+ });
56
+
57
+ // returns the base64 payload of the canvas background-image of each generation, each one captured
58
+ // from the file the previous one produced
59
+ async function capture(pathname, generations) {
60
+ const server = createServer((request, response) => {
61
+ const page = PAGES[new URL(request.url, "http://localhost").pathname];
62
+ if (page) {
63
+ response.writeHead(200, { "content-type": "text/html", "cache-control": "no-store" }).end(page);
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 images = [];
72
+ let source = "http://localhost:" + server.address().port + pathname;
73
+ for (let generation = 0; generation < generations; generation++) {
74
+ const savedPath = join(directory, "generation-" + generation + ".html");
75
+ await runCli([source, savedPath]);
76
+ images.push(getCanvasImage(await readFile(savedPath, "utf-8")));
77
+ source = pathToFileURL(savedPath).href;
78
+ }
79
+ return images;
80
+ } finally {
81
+ await rm(directory, { recursive: true });
82
+ server.close();
83
+ }
84
+ }
85
+
86
+ function getCanvasImage(html) {
87
+ const match = html.match(/background-image:\s*url\(["']?data:image\/png;base64,([^"')]+)["']?\)/);
88
+ return match && match[1];
89
+ }
90
+
91
+ function runCli(args) {
92
+ return execFileAsync(process.execPath, ["single-file-node.js", ...args], { cwd: cliDirectory });
93
+ }