single-file-cli 2.6.1 → 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/.github/workflows/ci.yml +24 -0
- package/build-dev.sh +35 -14
- package/build.sh +1 -1
- package/deno.json +1 -1
- package/eslint.config.mjs +1 -1
- package/lib/deno-polyfill.js +6 -4
- package/lib/single-file-archive.js +4 -4
- package/lib/single-file-bundle.js +1 -1
- package/lib/version.js +1 -1
- package/package.json +2 -1
- package/test/e2e/automation-detection.test.js +2 -3
- package/test/e2e/backend-fetch.test.js +2 -3
- package/test/e2e/blocked-url-pattern.test.js +2 -3
- package/test/e2e/browser-profile.test.js +2 -3
- package/test/e2e/compress-content.test.js +3 -4
- package/test/e2e/crawl-save-archive.test.js +3 -4
- package/test/e2e/crawl.test.js +2 -3
- package/test/e2e/errors-file.test.js +2 -3
- package/test/e2e/fidelity.test.js +214 -0
- package/test/e2e/frame-gate.test.js +2 -3
- package/test/e2e/http-header.test.js +2 -3
- package/test/e2e/oopif.test.js +2 -3
- package/test/e2e/output-json.test.js +2 -3
- package/test/e2e/saved-page-opens.test.js +79 -0
- package/test/e2e/service-worker.test.js +2 -3
- package/test/e2e/timeouts.test.js +2 -3
- package/test/e2e/urls-file.test.js +2 -3
- 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 +77 -0
- package/test/unit/archive-packager.test.js +2 -1
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="utf-8">
|
|
6
|
+
<title>Duplicate stylesheet</title>
|
|
7
|
+
<!--
|
|
8
|
+
Two <style> elements with byte-identical content. The archive writer groups them, stores the
|
|
9
|
+
content once as an entry, and points a <link> at it. Both elements are rewritten: the second
|
|
10
|
+
because it is a duplicate, the first because it is the one the duplicates were folded into —
|
|
11
|
+
leaving that one inline stored the same stylesheet twice, once as the entry and once in the
|
|
12
|
+
page.
|
|
13
|
+
|
|
14
|
+
The attributes are here because the rewrite replaces the element rather than editing it. An
|
|
15
|
+
id a script looks up and a class a selector matches have to survive that, and they were
|
|
16
|
+
being dropped: the archive came back with a bare <link>.
|
|
17
|
+
-->
|
|
18
|
+
<style id="palette" class="theme" data-role="tokens">
|
|
19
|
+
:root {
|
|
20
|
+
--ink: #17202a;
|
|
21
|
+
--paper: #f4f4f2;
|
|
22
|
+
--accent: #1f5673;
|
|
23
|
+
--edge: #c9c9c4;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
body {
|
|
27
|
+
background: var(--paper);
|
|
28
|
+
color: var(--ink);
|
|
29
|
+
font: 16px/1.6 Georgia, "Times New Roman", serif;
|
|
30
|
+
margin: 0;
|
|
31
|
+
padding: 40px;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
h1 {
|
|
35
|
+
font-size: 34px;
|
|
36
|
+
margin: 0 0 24px;
|
|
37
|
+
border-bottom: 3px solid var(--accent);
|
|
38
|
+
padding-bottom: 12px;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.grid {
|
|
42
|
+
display: grid;
|
|
43
|
+
grid-template-columns: repeat(3, 1fr);
|
|
44
|
+
gap: 16px;
|
|
45
|
+
margin: 24px 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.cell {
|
|
49
|
+
background: #fff;
|
|
50
|
+
border: 1px solid var(--edge);
|
|
51
|
+
padding: 20px;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.cell b {
|
|
55
|
+
color: var(--accent);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.rule {
|
|
59
|
+
height: 8px;
|
|
60
|
+
background: var(--accent);
|
|
61
|
+
margin: 32px 0;
|
|
62
|
+
}
|
|
63
|
+
</style>
|
|
64
|
+
<style>
|
|
65
|
+
:root {
|
|
66
|
+
--ink: #17202a;
|
|
67
|
+
--paper: #f4f4f2;
|
|
68
|
+
--accent: #1f5673;
|
|
69
|
+
--edge: #c9c9c4;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
body {
|
|
73
|
+
background: var(--paper);
|
|
74
|
+
color: var(--ink);
|
|
75
|
+
font: 16px/1.6 Georgia, "Times New Roman", serif;
|
|
76
|
+
margin: 0;
|
|
77
|
+
padding: 40px;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
h1 {
|
|
81
|
+
font-size: 34px;
|
|
82
|
+
margin: 0 0 24px;
|
|
83
|
+
border-bottom: 3px solid var(--accent);
|
|
84
|
+
padding-bottom: 12px;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.grid {
|
|
88
|
+
display: grid;
|
|
89
|
+
grid-template-columns: repeat(3, 1fr);
|
|
90
|
+
gap: 16px;
|
|
91
|
+
margin: 24px 0;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.cell {
|
|
95
|
+
background: #fff;
|
|
96
|
+
border: 1px solid var(--edge);
|
|
97
|
+
padding: 20px;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.cell b {
|
|
101
|
+
color: var(--accent);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.rule {
|
|
105
|
+
height: 8px;
|
|
106
|
+
background: var(--accent);
|
|
107
|
+
margin: 32px 0;
|
|
108
|
+
}
|
|
109
|
+
</style>
|
|
110
|
+
</head>
|
|
111
|
+
|
|
112
|
+
<body>
|
|
113
|
+
<h1>Duplicate stylesheet</h1>
|
|
114
|
+
<p>The two style elements above hold the same declarations. Everything on this page is drawn by
|
|
115
|
+
them, so a stylesheet that goes missing in the save takes the whole layout with it.</p>
|
|
116
|
+
<div class="grid">
|
|
117
|
+
<div class="cell"><b>One</b><br>Bordered cell drawn by the shared stylesheet.</div>
|
|
118
|
+
<div class="cell"><b>Two</b><br>Bordered cell drawn by the shared stylesheet.</div>
|
|
119
|
+
<div class="cell"><b>Three</b><br>Bordered cell drawn by the shared stylesheet.</div>
|
|
120
|
+
</div>
|
|
121
|
+
<div class="rule"></div>
|
|
122
|
+
<p>The grid, the rule above and the serif family all come from the custom properties declared in
|
|
123
|
+
the same block, so the comparison fails loudly rather than subtly.</p>
|
|
124
|
+
</body>
|
|
125
|
+
|
|
126
|
+
</html>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="utf-8">
|
|
6
|
+
<title>Frame fonts</title>
|
|
7
|
+
<!--
|
|
8
|
+
The frame is sandboxed, which gives it an opaque origin and puts its contentDocument out of
|
|
9
|
+
reach. SingleFile then re-parses it from its srcdoc with DOMParser, and that document is
|
|
10
|
+
never rendered: it reports no font as used at all.
|
|
11
|
+
|
|
12
|
+
An empty list of used fonts is not a short one. Every rendered element has a computed
|
|
13
|
+
font-family, so nothing rendered can report none — an empty list means the styles could not
|
|
14
|
+
be read. Read as an answer instead of as an absence, it said "this frame uses no font" and
|
|
15
|
+
every face the frame declared was pruned. Measured on derstandard.at, where a newsletter box
|
|
16
|
+
inside such a frame fell back to a system font, and on MDN, where the text in the CSS demo
|
|
17
|
+
reflowed.
|
|
18
|
+
|
|
19
|
+
The frame declares its own face and uses it, and the face is not declared anywhere in the
|
|
20
|
+
parent: nothing outside the frame can keep it alive.
|
|
21
|
+
-->
|
|
22
|
+
<style>
|
|
23
|
+
body {
|
|
24
|
+
background: #fff;
|
|
25
|
+
margin: 0;
|
|
26
|
+
padding: 40px;
|
|
27
|
+
font: 20px/1.5 monospace;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
iframe {
|
|
31
|
+
width: 700px;
|
|
32
|
+
height: 220px;
|
|
33
|
+
border: 2px solid #333;
|
|
34
|
+
}
|
|
35
|
+
</style>
|
|
36
|
+
</head>
|
|
37
|
+
|
|
38
|
+
<body>
|
|
39
|
+
<p>The frame below declares and uses a face that the page around it never names.</p>
|
|
40
|
+
<iframe sandbox title="framed" srcdoc="
|
|
41
|
+
<!DOCTYPE html>
|
|
42
|
+
<meta charset="utf-8">
|
|
43
|
+
<style>
|
|
44
|
+
@font-face { font-family: "Fidelity Frame"; src: url(../fonts/bar.ttf) format("truetype") }
|
|
45
|
+
body { margin: 0; padding: 20px; background: #fff }
|
|
46
|
+
p { font: 48px "Fidelity Frame", serif; margin: 0 0 16px }
|
|
47
|
+
</style>
|
|
48
|
+
<p>abcdefghij</p>
|
|
49
|
+
<p>klmnopqrst</p>
|
|
50
|
+
"></iframe>
|
|
51
|
+
</body>
|
|
52
|
+
|
|
53
|
+
</html>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="utf-8">
|
|
6
|
+
<title>Linked stylesheet</title>
|
|
7
|
+
<!--
|
|
8
|
+
An external stylesheet, linked with the attributes a page uses to find it again: an id a
|
|
9
|
+
theme switcher looks up, a class a selector matches, a data attribute a script reads.
|
|
10
|
+
|
|
11
|
+
A plain save has nowhere to put an external stylesheet, so the link becomes a style element
|
|
12
|
+
holding its content — a brand-new element, built with the media and the text and nothing
|
|
13
|
+
else. Everything the page used to identify that stylesheet was dropped, on every plain save
|
|
14
|
+
of every page with an external stylesheet. The archive path had the same defect in the other
|
|
15
|
+
direction and was fixed first; this is its mirror.
|
|
16
|
+
-->
|
|
17
|
+
<link rel="stylesheet" type="text/css" href="theme.css" id="theme" class="site-theme" data-role="tokens" title="Site theme">
|
|
18
|
+
</head>
|
|
19
|
+
|
|
20
|
+
<body>
|
|
21
|
+
<h1>Linked stylesheet</h1>
|
|
22
|
+
<p>Everything below is drawn by the linked stylesheet, so losing it in the save is not subtle.</p>
|
|
23
|
+
<div class="panel"><b>Panel</b> — bordered by the linked stylesheet.</div>
|
|
24
|
+
<div class="panel"><b>Panel</b> — bordered by the linked stylesheet.</div>
|
|
25
|
+
<div class="stripe"></div>
|
|
26
|
+
<p>The element carrying that stylesheet is looked up by id, by class and by data attribute, none
|
|
27
|
+
of which survives a rewrite that keeps only the text.</p>
|
|
28
|
+
</body>
|
|
29
|
+
|
|
30
|
+
</html>
|
|
@@ -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
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// Which directory the e2e tests spawn the CLI from decides what they actually prove. By default it
|
|
2
|
+
// is the repository root, which runs the committed lib/ — a build of the *pinned npm release* of
|
|
3
|
+
// single-file-core. So after fixing something in a local single-file-core checkout, a green suite
|
|
4
|
+
// here says nothing about the fix: the tests never loaded that code.
|
|
5
|
+
//
|
|
6
|
+
// SINGLE_FILE_TARGET=dev points them at .dev/ instead, the tree build-dev.sh stages from
|
|
7
|
+
// ../single-file-core. Same suite, same assertions, against the unreleased core:
|
|
8
|
+
//
|
|
9
|
+
// ./build-dev.sh && npm run test:dev
|
|
10
|
+
//
|
|
11
|
+
// The missing-directory check is deliberately loud. A silent fallback to the repository root would
|
|
12
|
+
// reintroduce exactly the failure this exists to prevent — tests passing against the wrong code.
|
|
13
|
+
import { join, dirname, relative } from "node:path";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
16
|
+
import process from "node:process";
|
|
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
|
+
|
|
22
|
+
const repositoryDirectory = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
23
|
+
const useDevBuild = process.env.SINGLE_FILE_TARGET === "dev";
|
|
24
|
+
const cliDirectory = useDevBuild ? join(repositoryDirectory, ".dev") : repositoryDirectory;
|
|
25
|
+
|
|
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;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Unit tests import generated modules directly. They have to go through here too, or they keep
|
|
72
|
+
// testing the released build while the e2e tests exercise the dev one.
|
|
73
|
+
function importLibModule(name) {
|
|
74
|
+
return import(join(cliDirectory, "lib", name));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export { cliDirectory, repositoryDirectory, useDevBuild, importLibModule };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { test } from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import {
|
|
3
|
+
import { importLibModule } from "../target.js";
|
|
4
|
+
const { createPagesArchive, configure, ZipWriter, ZipReader, Uint8ArrayReader, Uint8ArrayWriter, TextReader, TextWriter } = await importLibModule("single-file-archive.js");
|
|
4
5
|
|
|
5
6
|
configure({ useWebWorkers: false });
|
|
6
7
|
|