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.
Files changed (43) hide show
  1. package/.github/workflows/ci.yml +24 -0
  2. package/build-dev.sh +35 -14
  3. package/build.sh +1 -1
  4. package/deno.json +1 -1
  5. package/eslint.config.mjs +1 -1
  6. package/lib/deno-polyfill.js +6 -4
  7. package/lib/single-file-archive.js +4 -4
  8. package/lib/single-file-bundle.js +1 -1
  9. package/lib/version.js +1 -1
  10. package/package.json +2 -1
  11. package/test/e2e/automation-detection.test.js +2 -3
  12. package/test/e2e/backend-fetch.test.js +2 -3
  13. package/test/e2e/blocked-url-pattern.test.js +2 -3
  14. package/test/e2e/browser-profile.test.js +2 -3
  15. package/test/e2e/compress-content.test.js +3 -4
  16. package/test/e2e/crawl-save-archive.test.js +3 -4
  17. package/test/e2e/crawl.test.js +2 -3
  18. package/test/e2e/errors-file.test.js +2 -3
  19. package/test/e2e/fidelity.test.js +214 -0
  20. package/test/e2e/frame-gate.test.js +2 -3
  21. package/test/e2e/http-header.test.js +2 -3
  22. package/test/e2e/oopif.test.js +2 -3
  23. package/test/e2e/output-json.test.js +2 -3
  24. package/test/e2e/saved-page-opens.test.js +79 -0
  25. package/test/e2e/service-worker.test.js +2 -3
  26. package/test/e2e/timeouts.test.js +2 -3
  27. package/test/e2e/urls-file.test.js +2 -3
  28. package/test/fidelity/README.md +111 -0
  29. package/test/fidelity/browser.js +162 -0
  30. package/test/fidelity/make-font.js +294 -0
  31. package/test/fidelity/pages/duplicate-stylesheet/index.html +126 -0
  32. package/test/fidelity/pages/fonts/band.ttf +0 -0
  33. package/test/fidelity/pages/fonts/bar.ttf +0 -0
  34. package/test/fidelity/pages/fonts/block.ttf +0 -0
  35. package/test/fidelity/pages/frame-fonts/index.html +53 -0
  36. package/test/fidelity/pages/linked-stylesheet/index.html +30 -0
  37. package/test/fidelity/pages/linked-stylesheet/theme.css +38 -0
  38. package/test/fidelity/pages/synthetic-italic/index.html +59 -0
  39. package/test/fidelity/pages/unresolved-font-property/index.html +75 -0
  40. package/test/fidelity/pages/used-fonts/index.html +96 -0
  41. package/test/fidelity/server.js +70 -0
  42. package/test/target.js +77 -0
  43. 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>
@@ -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=&quot;utf-8&quot;>
43
+ <style>
44
+ @font-face { font-family: &quot;Fidelity Frame&quot;; src: url(../fonts/bar.ttf) format(&quot;truetype&quot;) }
45
+ body { margin: 0; padding: 20px; background: #fff }
46
+ p { font: 48px &quot;Fidelity Frame&quot;, 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 { createPagesArchive, configure, ZipWriter, ZipReader, Uint8ArrayReader, Uint8ArrayWriter, TextReader, TextWriter } from "../../lib/single-file-archive.js";
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