single-file-cli 2.6.2 → 2.6.3
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/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,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
|