single-file-cli 2.6.2 → 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/build.sh +1 -1
- package/deno.json +1 -1
- package/lib/deno-polyfill.js +6 -4
- package/lib/single-file-archive.js +5 -5
- package/lib/single-file-bundle.js +1 -1
- package/lib/version.js +1 -1
- package/package.json +1 -1
- package/test/e2e/canvas-round-trip.test.js +93 -0
- package/test/e2e/fidelity.test.js +214 -0
- package/test/e2e/saved-page-opens.test.js +79 -0
- package/test/fidelity/README.md +111 -0
- package/test/fidelity/browser.js +162 -0
- package/test/fidelity/make-font.js +294 -0
- package/test/fidelity/pages/duplicate-stylesheet/index.html +126 -0
- package/test/fidelity/pages/fonts/band.ttf +0 -0
- package/test/fidelity/pages/fonts/bar.ttf +0 -0
- package/test/fidelity/pages/fonts/block.ttf +0 -0
- package/test/fidelity/pages/frame-fonts/index.html +53 -0
- package/test/fidelity/pages/linked-stylesheet/index.html +30 -0
- package/test/fidelity/pages/linked-stylesheet/theme.css +38 -0
- package/test/fidelity/pages/synthetic-italic/index.html +59 -0
- package/test/fidelity/pages/unresolved-font-property/index.html +75 -0
- package/test/fidelity/pages/used-fonts/index.html +96 -0
- package/test/fidelity/server.js +70 -0
- package/test/target.js +49 -4
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--ink: #1b1b1f;
|
|
3
|
+
--paper: #fbfaf7;
|
|
4
|
+
--accent: #7a3b2e;
|
|
5
|
+
--edge: #d6d2c8;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
body {
|
|
9
|
+
background: var(--paper);
|
|
10
|
+
color: var(--ink);
|
|
11
|
+
font: 16px/1.6 Georgia, "Times New Roman", serif;
|
|
12
|
+
margin: 0;
|
|
13
|
+
padding: 40px;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
h1 {
|
|
17
|
+
font-size: 32px;
|
|
18
|
+
margin: 0 0 20px;
|
|
19
|
+
border-bottom: 4px solid var(--accent);
|
|
20
|
+
padding-bottom: 10px;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.panel {
|
|
24
|
+
border: 1px solid var(--edge);
|
|
25
|
+
background: #fff;
|
|
26
|
+
padding: 20px;
|
|
27
|
+
margin: 0 0 16px;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.panel b {
|
|
31
|
+
color: var(--accent);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.stripe {
|
|
35
|
+
height: 10px;
|
|
36
|
+
background: var(--accent);
|
|
37
|
+
margin: 24px 0;
|
|
38
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="utf-8">
|
|
6
|
+
<title>Synthetic italic</title>
|
|
7
|
+
<!--
|
|
8
|
+
Two faces, both declared with no style of their own, both drawn. The second is asked for in
|
|
9
|
+
italic, and since no italic face is declared the browser slants the normal one — a synthetic
|
|
10
|
+
oblique. The face is used; it is simply used at an angle.
|
|
11
|
+
|
|
12
|
+
What the page reports as its used fonts is read off the computed styles, and the family is
|
|
13
|
+
kept only when a loaded face answers for the computed font-style. A synthesized style has no
|
|
14
|
+
face behind it, so the match fails and the family is not recorded as used at all — while the
|
|
15
|
+
first sample keeps the list non-empty, so the guard for "nothing could be read" does not
|
|
16
|
+
apply either. The italic family is then declared, drawn, and pruned.
|
|
17
|
+
-->
|
|
18
|
+
<style>
|
|
19
|
+
@font-face {
|
|
20
|
+
font-family: "Fidelity Block";
|
|
21
|
+
src: url(../fonts/block.ttf) format("truetype");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@font-face {
|
|
25
|
+
font-family: "Fidelity Bar";
|
|
26
|
+
src: url(../fonts/bar.ttf) format("truetype");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
body {
|
|
30
|
+
background: #fff;
|
|
31
|
+
margin: 0;
|
|
32
|
+
padding: 40px;
|
|
33
|
+
font: 20px/1.5 monospace;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.sample {
|
|
37
|
+
font-size: 48px;
|
|
38
|
+
margin: 0 0 24px;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.upright {
|
|
42
|
+
font-family: "Fidelity Block";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.slanted {
|
|
46
|
+
font-family: "Fidelity Bar";
|
|
47
|
+
font-style: italic;
|
|
48
|
+
}
|
|
49
|
+
</style>
|
|
50
|
+
</head>
|
|
51
|
+
|
|
52
|
+
<body>
|
|
53
|
+
<p>Upright, drawn with a face declared for that style:</p>
|
|
54
|
+
<p class="sample upright">abcdefghij</p>
|
|
55
|
+
<p>Italic, drawn by slanting a face declared without one:</p>
|
|
56
|
+
<p class="sample slanted">abcdefghij</p>
|
|
57
|
+
</body>
|
|
58
|
+
|
|
59
|
+
</html>
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="utf-8">
|
|
6
|
+
<title>Unresolved font property</title>
|
|
7
|
+
<!--
|
|
8
|
+
The property holds a whole font shorthand, not a family list. The browser substitutes it and
|
|
9
|
+
draws the text in the face it names; the minifier must not read it as a list of families,
|
|
10
|
+
because the words in it are a style and a size and reading them as names would invent
|
|
11
|
+
families the document never had. So the family is genuinely unnameable from the stylesheets,
|
|
12
|
+
and the only thing left that knows it is the list of fonts the rendering reported.
|
|
13
|
+
|
|
14
|
+
The shape matters. A var() in family position — even one nested in a fallback — is walked
|
|
15
|
+
branch by branch and usually resolves, so a fixture written that way keeps its face through
|
|
16
|
+
the ordinary path and proves nothing about the case it was built for. This one was.
|
|
17
|
+
|
|
18
|
+
This is the page the pruning policy for undetermined values is decided on. Keeping every
|
|
19
|
+
declared face renders exactly like keeping the right one, so the two halves are checked
|
|
20
|
+
apart: the pixels say the face that was drawn survived, and the markup says the faces that
|
|
21
|
+
were not drawn are gone. A build that gives up on the document passes the first and fails
|
|
22
|
+
the second, which is what used to happen for every page holding one unreadable value.
|
|
23
|
+
-->
|
|
24
|
+
<style>
|
|
25
|
+
@font-face {
|
|
26
|
+
font-family: "Fidelity Block";
|
|
27
|
+
src: url(../fonts/block.ttf) format("truetype");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@font-face {
|
|
31
|
+
font-family: "Fidelity Unused One";
|
|
32
|
+
src: url(../fonts/band.ttf) format("truetype");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@font-face {
|
|
36
|
+
font-family: "Fidelity Unused Two";
|
|
37
|
+
src: url(../fonts/bar.ttf) format("truetype");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
body {
|
|
41
|
+
background: #fff;
|
|
42
|
+
margin: 0;
|
|
43
|
+
padding: 40px;
|
|
44
|
+
font: 20px/1.5 monospace;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.card {
|
|
48
|
+
--sample-font: 48px "Fidelity Block";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/* a second declaration, so that the property has no single value to substitute into the
|
|
52
|
+
shorthand. It names a face nothing draws with: a resolver that read these values as
|
|
53
|
+
family lists would keep that one, which is the other way this can go wrong */
|
|
54
|
+
.note {
|
|
55
|
+
--sample-font: 48px "Fidelity Unused One";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.card .sample {
|
|
59
|
+
font: var(--sample-font);
|
|
60
|
+
margin: 0;
|
|
61
|
+
}
|
|
62
|
+
</style>
|
|
63
|
+
</head>
|
|
64
|
+
|
|
65
|
+
<body>
|
|
66
|
+
<div class="card">
|
|
67
|
+
<p>The family below is named inside a property holding a whole font shorthand.</p>
|
|
68
|
+
<p class="sample">abcdefghij</p>
|
|
69
|
+
</div>
|
|
70
|
+
<div class="note">
|
|
71
|
+
<p>This section declares the same property with another value and draws nothing with it.</p>
|
|
72
|
+
</div>
|
|
73
|
+
</body>
|
|
74
|
+
|
|
75
|
+
</html>
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="utf-8">
|
|
6
|
+
<title>Used fonts</title>
|
|
7
|
+
<!--
|
|
8
|
+
Five declared faces, three of them drawn. The three are named in the three ways the font
|
|
9
|
+
minifier has to read: a plain family, a custom property declared on a DESCENDANT rather than
|
|
10
|
+
on :root, and the font shorthand. Anything that stops resolving one of them prunes a face
|
|
11
|
+
the page actually uses, and the block turns back into ordinary text.
|
|
12
|
+
|
|
13
|
+
Every glyph in these fonts is a filled rectangle, so a face that goes missing is not a
|
|
14
|
+
subtle reflow: the bar becomes words. The three shapes differ, so keeping the wrong face is
|
|
15
|
+
as visible as keeping none.
|
|
16
|
+
|
|
17
|
+
The two unused faces are here so that pruning has something to remove. A build that gives up
|
|
18
|
+
and keeps everything renders exactly like a correct one — that half of the contract is not
|
|
19
|
+
visible, and is asserted on the saved markup instead.
|
|
20
|
+
-->
|
|
21
|
+
<style>
|
|
22
|
+
@font-face {
|
|
23
|
+
font-family: "Fidelity Block";
|
|
24
|
+
src: url(../fonts/block.ttf) format("truetype");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@font-face {
|
|
28
|
+
font-family: "Fidelity Band";
|
|
29
|
+
src: url(../fonts/band.ttf) format("truetype");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@font-face {
|
|
33
|
+
font-family: "Fidelity Bar";
|
|
34
|
+
src: url(../fonts/bar.ttf) format("truetype");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@font-face {
|
|
38
|
+
font-family: "Fidelity Unused One";
|
|
39
|
+
src: url(../fonts/block.ttf) format("truetype");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@font-face {
|
|
43
|
+
font-family: "Fidelity Unused Two";
|
|
44
|
+
src: url(../fonts/band.ttf) format("truetype");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
body {
|
|
48
|
+
background: #fff;
|
|
49
|
+
margin: 0;
|
|
50
|
+
padding: 40px;
|
|
51
|
+
font: 20px/1.5 monospace;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.sample {
|
|
55
|
+
font-size: 48px;
|
|
56
|
+
margin: 0 0 24px;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.plain .sample {
|
|
60
|
+
font-family: "Fidelity Block";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.card {
|
|
64
|
+
--card-font: "Fidelity Band";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.card .sample {
|
|
68
|
+
font-family: var(--card-font), serif;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.shorthand {
|
|
72
|
+
--shorthand-font: "Fidelity Bar";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.shorthand .sample {
|
|
76
|
+
font: 48px var(--shorthand-font);
|
|
77
|
+
}
|
|
78
|
+
</style>
|
|
79
|
+
</head>
|
|
80
|
+
|
|
81
|
+
<body>
|
|
82
|
+
<section class="plain">
|
|
83
|
+
<p>Plain family name</p>
|
|
84
|
+
<p class="sample">abcdefghij</p>
|
|
85
|
+
</section>
|
|
86
|
+
<section class="card">
|
|
87
|
+
<p>Custom property declared on this section, not on the root</p>
|
|
88
|
+
<p class="sample">abcdefghij</p>
|
|
89
|
+
</section>
|
|
90
|
+
<section class="shorthand">
|
|
91
|
+
<p>Custom property read through the font shorthand</p>
|
|
92
|
+
<p class="sample">abcdefghij</p>
|
|
93
|
+
</section>
|
|
94
|
+
</body>
|
|
95
|
+
|
|
96
|
+
</html>
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/* global URL */
|
|
2
|
+
|
|
3
|
+
// A fixture is a directory, not a string of HTML: the interesting cases need a stylesheet the page
|
|
4
|
+
// links to, a font it loads, a frame it embeds. Serving them over http rather than opening them as
|
|
5
|
+
// file:// keeps the capture on the path a real save takes — same-origin frames, ordinary requests,
|
|
6
|
+
// no local-file exceptions.
|
|
7
|
+
import { createServer } from "node:http";
|
|
8
|
+
import { readFile } from "node:fs/promises";
|
|
9
|
+
import { join, normalize, extname } from "node:path";
|
|
10
|
+
|
|
11
|
+
const CONTENT_TYPES = {
|
|
12
|
+
".html": "text/html; charset=utf-8",
|
|
13
|
+
".css": "text/css; charset=utf-8",
|
|
14
|
+
".js": "text/javascript; charset=utf-8",
|
|
15
|
+
".json": "application/json",
|
|
16
|
+
".svg": "image/svg+xml",
|
|
17
|
+
".png": "image/png",
|
|
18
|
+
".ttf": "font/ttf",
|
|
19
|
+
".woff2": "font/woff2"
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// Chrome asks for a favicon on every navigation and its absence says nothing about the fixture.
|
|
23
|
+
// Nothing else is excused: the archive writer used to ask the captured site for a zip worker that
|
|
24
|
+
// site had never heard of, three times per save, and this list is where that would have been
|
|
25
|
+
// quietly tolerated. It is a real request to a real server, so it is a failure here.
|
|
26
|
+
const IGNORED_MISS_PATTERNS = [/^\/favicon\.ico$/];
|
|
27
|
+
|
|
28
|
+
export { startServer };
|
|
29
|
+
|
|
30
|
+
async function startServer(rootDirectory) {
|
|
31
|
+
// A page that failed to load renders identically to itself, so the noise floor is zero and the
|
|
32
|
+
// save of that same failure matches it: a fixture with a broken path passes every check while
|
|
33
|
+
// testing nothing. It has already happened here — a directory served as a file gave two
|
|
34
|
+
// beautifully identical 404 pages. Every miss is recorded so a check can refuse to conclude.
|
|
35
|
+
const misses = [];
|
|
36
|
+
const server = createServer((request, response) => {
|
|
37
|
+
// a fixture directory holds only what the fixture needs, and the path still gets normalised
|
|
38
|
+
// before it is joined: a test server that walks out of its root is a test server that can
|
|
39
|
+
// silently serve the file it was meant to prove absent
|
|
40
|
+
const pathname = decodeURIComponent(new URL(request.url, "http://localhost").pathname);
|
|
41
|
+
const relativePath = normalize(pathname).replace(/^(\.\.[/\\])+/, "").replace(/^[/\\]+/, "");
|
|
42
|
+
// a directory is named by the trailing slash and answered with its index, anything else is
|
|
43
|
+
// the file it names. Writing this as one join with a conditional last segment reads the same
|
|
44
|
+
// and is not: the root "/" leaves nothing to join, so it resolved to index.html/index.html
|
|
45
|
+
// and the server could serve every fixture but not its own root
|
|
46
|
+
const path = pathname.endsWith("/") ? join(rootDirectory, relativePath, "index.html") : join(rootDirectory, relativePath);
|
|
47
|
+
readFile(path)
|
|
48
|
+
.then(content => response
|
|
49
|
+
.writeHead(200, {
|
|
50
|
+
"content-type": CONTENT_TYPES[extname(path)] || "application/octet-stream",
|
|
51
|
+
// a sandboxed frame has an opaque origin, so the font it declares is fetched
|
|
52
|
+
// cross-origin and fonts are CORS-restricted. Without this the frame fixture
|
|
53
|
+
// would be measuring the headers of this server rather than the save
|
|
54
|
+
"access-control-allow-origin": "*"
|
|
55
|
+
})
|
|
56
|
+
.end(content))
|
|
57
|
+
.catch(() => {
|
|
58
|
+
if (!IGNORED_MISS_PATTERNS.some(pattern => pattern.test(pathname))) {
|
|
59
|
+
misses.push(pathname);
|
|
60
|
+
}
|
|
61
|
+
response.writeHead(404).end();
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
await new Promise(resolve => server.listen(0, "localhost", resolve));
|
|
65
|
+
return {
|
|
66
|
+
url: "http://localhost:" + server.address().port + "/",
|
|
67
|
+
misses,
|
|
68
|
+
close: () => new Promise(resolve => server.close(resolve))
|
|
69
|
+
};
|
|
70
|
+
}
|
package/test/target.js
CHANGED
|
@@ -10,17 +10,62 @@
|
|
|
10
10
|
//
|
|
11
11
|
// The missing-directory check is deliberately loud. A silent fallback to the repository root would
|
|
12
12
|
// reintroduce exactly the failure this exists to prevent — tests passing against the wrong code.
|
|
13
|
-
import { join, dirname } from "node:path";
|
|
13
|
+
import { join, dirname, relative } from "node:path";
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
15
|
-
import { existsSync } from "node:fs";
|
|
15
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
16
16
|
import process from "node:process";
|
|
17
17
|
|
|
18
|
+
const CORE_DIRECTORY_NAME = "single-file-core";
|
|
19
|
+
// what a rebuild does not read cannot make the build stale
|
|
20
|
+
const IGNORED_DIRECTORY_NAMES = ["node_modules", "test", "tmp", "dist", "doc"];
|
|
21
|
+
|
|
18
22
|
const repositoryDirectory = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
19
23
|
const useDevBuild = process.env.SINGLE_FILE_TARGET === "dev";
|
|
20
24
|
const cliDirectory = useDevBuild ? join(repositoryDirectory, ".dev") : repositoryDirectory;
|
|
21
25
|
|
|
22
|
-
if (useDevBuild
|
|
23
|
-
|
|
26
|
+
if (useDevBuild) {
|
|
27
|
+
const bundlePath = join(cliDirectory, "lib", "single-file-bundle.js");
|
|
28
|
+
if (!existsSync(bundlePath)) {
|
|
29
|
+
throw new Error("SINGLE_FILE_TARGET=dev is set but .dev/ holds no build — run ./build-dev.sh first");
|
|
30
|
+
}
|
|
31
|
+
checkDevBuildIsCurrent(bundlePath);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// A dev build is a snapshot and nothing invalidates it: edit single-file-core, forget to rebuild,
|
|
35
|
+
// and the suite runs the previous code while reporting on the current one. That failure is silent,
|
|
36
|
+
// and worse, it reads as good news — every variant of an experiment comes out identical, which
|
|
37
|
+
// looks exactly like "the change has no effect". It has already cost a full round of measurements.
|
|
38
|
+
// Comparing the newest core source against the bundle turns it into a refusal to start.
|
|
39
|
+
function checkDevBuildIsCurrent(bundlePath) {
|
|
40
|
+
const coreDirectory = join(repositoryDirectory, "..", CORE_DIRECTORY_NAME);
|
|
41
|
+
if (!existsSync(coreDirectory)) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const newest = getNewestSource(coreDirectory);
|
|
45
|
+
if (newest && newest.time > statSync(bundlePath).mtimeMs) {
|
|
46
|
+
throw new Error("the .dev/ build is older than " + join(CORE_DIRECTORY_NAME, relative(coreDirectory, newest.path)) +
|
|
47
|
+
" — run ./build-dev.sh, or the suite measures the previous core and reports it as the current one");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getNewestSource(directory, newest) {
|
|
52
|
+
readdirSync(directory, { withFileTypes: true }).forEach(entry => {
|
|
53
|
+
if (entry.name.startsWith(".")) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const path = join(directory, entry.name);
|
|
57
|
+
if (entry.isDirectory()) {
|
|
58
|
+
if (!IGNORED_DIRECTORY_NAMES.includes(entry.name)) {
|
|
59
|
+
newest = getNewestSource(path, newest);
|
|
60
|
+
}
|
|
61
|
+
} else if (entry.name.endsWith(".js") || entry.name.endsWith(".json")) {
|
|
62
|
+
const time = statSync(path).mtimeMs;
|
|
63
|
+
if (!newest || time > newest.time) {
|
|
64
|
+
newest = { path, time };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
return newest;
|
|
24
69
|
}
|
|
25
70
|
|
|
26
71
|
// Unit tests import generated modules directly. They have to go through here too, or they keep
|