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.
@@ -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 && !existsSync(join(cliDirectory, "lib", "single-file-bundle.js"))) {
23
- throw new Error("SINGLE_FILE_TARGET=dev is set but .dev/ holds no build — run ./build-dev.sh first");
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