@mark1russell7/site-pages 0.1.9 → 0.2.0
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/README.md +117 -1
- package/build/chrome.d.ts +82 -0
- package/build/chrome.d.ts.map +1 -0
- package/build/chrome.js +129 -0
- package/build/chrome.js.map +1 -0
- package/build/cli.js +73 -11
- package/build/cli.js.map +1 -1
- package/build/vite.d.ts +72 -0
- package/build/vite.d.ts.map +1 -0
- package/build/vite.js +75 -0
- package/build/vite.js.map +1 -0
- package/package.json +12 -2
- package/styles/shell.css +166 -0
package/README.md
CHANGED
|
@@ -61,7 +61,9 @@ site-pages check --url https://owner.github.io/name/
|
|
|
61
61
|
|
|
62
62
|
A view fails for each of these problems:
|
|
63
63
|
|
|
64
|
-
- The page is wider than the screen, or an element goes past the right edge.
|
|
64
|
+
- The page is wider than the screen, or an element goes past the right edge. An element in a box that scrolls or clips does not count here.
|
|
65
|
+
- A box with `overflow-x: hidden` or `clip` cuts text at its right edge, at any width. A visitor cannot reach that text.
|
|
66
|
+
- A table goes out of view at a phone width. The table is in a box that scrolls, and a text of the table ends past the edge of the box.
|
|
65
67
|
- A text area hides a part of its text.
|
|
66
68
|
- The page shows no text.
|
|
67
69
|
- The page writes a console error, or a script throws an exception.
|
|
@@ -69,6 +71,41 @@ A view fails for each of these problems:
|
|
|
69
71
|
- A script or a style sheet does not load, from any origin. An example is a module script that imports `node:perf_hooks`.
|
|
70
72
|
- A check of the probe fails, or the probe makes no check.
|
|
71
73
|
|
|
74
|
+
The text of the problem has the element, the box, the two widths, the width of the view and the start of the text. For example:
|
|
75
|
+
|
|
76
|
+
```text
|
|
77
|
+
a table goes out of view at 360 px: table.api-table in div.table-scroll: content of 736 px in a box of 344 px, "Note". Make the table fit, give the box the class scroll-cue, or add data-overflow-ok to the box.
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The rule for a box that scrolls examines only tables, for these reasons:
|
|
81
|
+
|
|
82
|
+
- A code block, a hex view, a row of tabs and a carousel are meant to scroll, and a visitor sees that they scroll.
|
|
83
|
+
- A table in a box that scrolls looks complete on a phone, with no scroll bar. The visitor does not see the last columns.
|
|
84
|
+
- The class `scroll-cue` shows a shade at the edge of the box, so a table in such a box does not count. The shade shows only when the cells have no background color of their own.
|
|
85
|
+
- A false alarm costs more than a miss, because each false alarm needs an exception in the site.
|
|
86
|
+
|
|
87
|
+
A table is `table`, `role="table"` or `role="grid"`. A desktop view has a scroll bar, so a table in a box that scrolls is not a problem there. `report.json` still shows the table in `measure.cut`.
|
|
88
|
+
|
|
89
|
+
These items do not count as cut text:
|
|
90
|
+
|
|
91
|
+
- Text in a box with `text-overflow: ellipsis`.
|
|
92
|
+
- Text for a screen reader: `.sr`, `.sr-only`, `.visually-hidden`, `.skip`, or an element of 1 px or less, with or without a clip.
|
|
93
|
+
- Hidden text: `display: none`, `visibility: hidden`, `opacity: 0` or `aria-hidden="true"`.
|
|
94
|
+
- A decorative element with no text, and text in an `svg`.
|
|
95
|
+
- An overflow of 1 px or less. This tolerance is for rounding.
|
|
96
|
+
|
|
97
|
+
Vertical overflow does not count, because an accordion and a list with a line clamp use it on purpose.
|
|
98
|
+
|
|
99
|
+
The attribute `data-overflow-ok` marks a box that is meant to scroll or to clip. Put it on the box or on an element around the box:
|
|
100
|
+
|
|
101
|
+
```html
|
|
102
|
+
<div class="result-scroll" data-overflow-ok><table>...</table></div>
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Use the attribute for a data table with many columns, a marquee or a slider. For an API table, the better fix is a layout that fits the phone.
|
|
106
|
+
|
|
107
|
+
A passing check proves what it asserts, not the page. Read the screenshots at full size before a merge. When a read finds a fault that a script can find, add it to this check.
|
|
108
|
+
|
|
72
109
|
The probe is a JavaScript file or a TypeScript file (`.ts`, `.mts` or `.cts`). Its code is the body of an async function. These functions are available:
|
|
73
110
|
|
|
74
111
|
| Function | Operation |
|
|
@@ -297,6 +334,85 @@ The class sets `overflow-x: auto` and the background color of the box. Set `--sc
|
|
|
297
334
|
}
|
|
298
335
|
```
|
|
299
336
|
|
|
337
|
+
## The page helpers
|
|
338
|
+
|
|
339
|
+
The entry `@mark1russell7/site-pages/chrome` has small helpers for the script of a page. It imports no Node.js module, so a site can bundle it for the browser. The package root does not export these helpers. Thus a Node.js program that imports the root gets no DOM types.
|
|
340
|
+
|
|
341
|
+
| Function | Operation |
|
|
342
|
+
| --- | --- |
|
|
343
|
+
| `h(tag, attrs, ...children)` | Makes an element. The attribute `class` sets the class name. `false`, `null` and `undefined` give no attribute and no child. |
|
|
344
|
+
| `byId(id)` | Gives the element with the ID. It throws when the page has no such element. |
|
|
345
|
+
| `setupTheme(button, storageKey)` | Makes a theme button: auto, light, dark, in turn. It sets `data-theme` on the `html` element and keeps the choice in `localStorage`. |
|
|
346
|
+
| `currentTheme()`, `nextTheme(storageKey)`, `applyTheme(theme, storageKey)` | Read the theme, set the next theme, or set one theme. A site with its own button, for example a React component, uses them. |
|
|
347
|
+
| `copyText(button, text, options)` | Copies a text. The button shows "Copied" or "Copy failed" for 1600 ms. |
|
|
348
|
+
| `copyToClipboard(text)` | Copies a text with the Clipboard API, else with `document.execCommand`. It gives `true` after a copy. |
|
|
349
|
+
| `showBuild(element, build, repository)` | Adds "Version 1.2.3, build abc1234 of 2026-11-03" to an element. The commit is a link to GitHub. |
|
|
350
|
+
|
|
351
|
+
```ts
|
|
352
|
+
import { byId, copyText, h, setupTheme } from "@mark1russell7/site-pages/chrome";
|
|
353
|
+
|
|
354
|
+
setupTheme(byId<HTMLButtonElement>("theme"), "my-site-theme");
|
|
355
|
+
const copy = h("button", { type: "button", class: "copy" }, "Copy");
|
|
356
|
+
copy.addEventListener("click", () => void copyText(copy, "pnpm add my-package"));
|
|
357
|
+
byId("install").append(copy);
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
The theme `auto` removes `data-theme`, and the CSS of the site then obeys `prefers-color-scheme`. Put this script in the `head` of the page, with the same key. It sets the theme before the first paint, so a dark page does not show white for a moment. A site with the plugin `siteShell` does not need this script, because the plugin adds it:
|
|
361
|
+
|
|
362
|
+
```html
|
|
363
|
+
<script>
|
|
364
|
+
try {
|
|
365
|
+
const t = localStorage.getItem("my-site-theme");
|
|
366
|
+
if (t === "light" || t === "dark") document.documentElement.dataset.theme = t;
|
|
367
|
+
} catch {}
|
|
368
|
+
</script>
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
## The shell of a site
|
|
372
|
+
|
|
373
|
+
The shell is the part of a page that is not its content. The package has two parts of the shell, and each part is optional.
|
|
374
|
+
|
|
375
|
+
The entry `@mark1russell7/site-pages/vite` is for `vite.config.ts`. The plugin `siteShell` sets `base` from `SITE_BASE`, sets `build.target` to `es2022`, defines `__BUILD__`, and adds the theme script to the `head` of each HTML page.
|
|
376
|
+
|
|
377
|
+
```ts
|
|
378
|
+
import { defineConfig } from "vite";
|
|
379
|
+
import { siteShell } from "@mark1russell7/site-pages/vite";
|
|
380
|
+
|
|
381
|
+
export default defineConfig({
|
|
382
|
+
root: "site",
|
|
383
|
+
plugins: [siteShell({ themeKey: "my-site-theme" })],
|
|
384
|
+
});
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
```ts
|
|
388
|
+
import { byId, setupTheme, showBuild, type BuildInfo } from "@mark1russell7/site-pages/chrome";
|
|
389
|
+
|
|
390
|
+
declare const __BUILD__: BuildInfo;
|
|
391
|
+
|
|
392
|
+
setupTheme(byId<HTMLButtonElement>("theme"), "my-site-theme");
|
|
393
|
+
showBuild(byId("build"), __BUILD__, "owner/my-site");
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
| Export of `./vite` | Operation |
|
|
397
|
+
| --- | --- |
|
|
398
|
+
| `siteShell({ themeKey, packageFile })` | Makes the plugin. A `base` or a `build.target` in the config of the site has priority. |
|
|
399
|
+
| `themeScript(storageKey)` | Gives the text of the theme script, for a page without the plugin. |
|
|
400
|
+
| `siteBase(env)` | Gives `SITE_BASE` with one `/` at the end, or `/`. |
|
|
401
|
+
| `buildInfo(version, env, now)` | Gives the data of `__BUILD__`: the short commit, the date and the version. |
|
|
402
|
+
|
|
403
|
+
The version comes from `../package.json`, relative to the folder of the site. Set `packageFile` to another file, or to `false` for a site with no version.
|
|
404
|
+
|
|
405
|
+
The file `@mark1russell7/site-pages/shell.css` has the tokens and the rules of the shell. The rules are for the document, the skip link, the header, the links, the theme button, the main column and the footer. Import it before the style sheet of the site, so the rules of the site come last:
|
|
406
|
+
|
|
407
|
+
```ts
|
|
408
|
+
import "@mark1russell7/site-pages/shell.css";
|
|
409
|
+
import "./style.css";
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
The file sets no look for the content. Buttons, cards, code and tables stay in the site. The header, the navigation, the footer text and each widget also stay in the site, because each site has its own words there.
|
|
413
|
+
|
|
414
|
+
A site with its own look does not import `shell.css`. It uses only the plugin and `showBuild`, or nothing from this section.
|
|
415
|
+
|
|
300
416
|
## A GitHub Pages workflow
|
|
301
417
|
|
|
302
418
|
This workflow builds the site, adds the two pages, and deploys. It also starts every 3 hours, so the status page stays current without a push. A scheduled run builds the newest commit of `main` that passed CI.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The small helpers of a page in the browser: the element helper, the element lookup, the theme
|
|
3
|
+
* button, the copy button and the build stamp. This module imports no Node.js module, so a site can
|
|
4
|
+
* bundle it. Import it from `@mark1russell7/site-pages/chrome`, because the package root also has
|
|
5
|
+
* Node.js code.
|
|
6
|
+
*/
|
|
7
|
+
/** A child of `h`: a node or a text. The values `null`, `undefined` and `false` give no child. */
|
|
8
|
+
export type Child = Node | string | null | undefined | false;
|
|
9
|
+
/** The value of an attribute of `h`. The value `true` gives an empty attribute. */
|
|
10
|
+
export type AttributeValue = string | number | boolean | null | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* This function makes an element with attributes and children. An attribute with the value
|
|
13
|
+
* `false`, `null` or `undefined` is not set. The attribute `class` sets the class name.
|
|
14
|
+
*/
|
|
15
|
+
export declare function h<K extends keyof HTMLElementTagNameMap>(tag: K, attrs?: Readonly<Record<string, AttributeValue>>, ...children: readonly (Child | readonly Child[])[]): HTMLElementTagNameMap[K];
|
|
16
|
+
/**
|
|
17
|
+
* This function gives the element with the ID. The page has each ID that the code asks for. Thus
|
|
18
|
+
* a missing ID is an error of the page, and the function throws an `Error`.
|
|
19
|
+
*/
|
|
20
|
+
export declare function byId<T extends HTMLElement = HTMLElement>(id: string): T;
|
|
21
|
+
/** The themes of the theme button, in the sequence of the clicks. `auto` follows the setting of the system. */
|
|
22
|
+
export declare const THEMES: readonly ["auto", "light", "dark"];
|
|
23
|
+
/** A theme of the page. */
|
|
24
|
+
export type Theme = (typeof THEMES)[number];
|
|
25
|
+
/**
|
|
26
|
+
* This function gives the theme of the page from the attribute `data-theme` of the `html` element.
|
|
27
|
+
* Without the attribute, or with another value, the theme is `auto`.
|
|
28
|
+
*/
|
|
29
|
+
export declare function currentTheme(): Theme;
|
|
30
|
+
/**
|
|
31
|
+
* This function sets the theme of the page, and keeps it in `localStorage` under `storageKey`.
|
|
32
|
+
* The theme `auto` removes the attribute `data-theme` and the stored value.
|
|
33
|
+
*/
|
|
34
|
+
export declare function applyTheme(theme: Theme, storageKey: string): void;
|
|
35
|
+
/** This function sets the next theme after the current theme (auto, light, dark, auto), and gives it. */
|
|
36
|
+
export declare function nextTheme(storageKey: string): Theme;
|
|
37
|
+
/**
|
|
38
|
+
* This function makes a theme button. Each click sets the next theme. The text of the button is
|
|
39
|
+
* "Theme: auto", "Theme: light" or "Theme: dark", and its `aria-label` tells the same theme.
|
|
40
|
+
*
|
|
41
|
+
* @remarks Before the first paint, a script in the `head` of the page reads `storageKey` from
|
|
42
|
+
* `localStorage` and sets `data-theme`. Thus a dark page does not show white for a moment. The
|
|
43
|
+
* plugin `siteShell` of the entry `./vite` adds that script.
|
|
44
|
+
*/
|
|
45
|
+
export declare function setupTheme(button: HTMLButtonElement, storageKey: string): void;
|
|
46
|
+
/** The data of a build: `siteShell` writes it, and `showBuild` shows it. */
|
|
47
|
+
export interface BuildInfo {
|
|
48
|
+
/** The first 7 characters of the commit, or `local` for a build outside CI. */
|
|
49
|
+
readonly sha: string;
|
|
50
|
+
/** The date of the build, as `YYYY-MM-DD`. */
|
|
51
|
+
readonly date: string;
|
|
52
|
+
/** The version of the package. A site without a version does not set it. */
|
|
53
|
+
readonly version?: string | undefined;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* This function shows the version and the build of the site in an element. The text is "Version
|
|
57
|
+
* 1.2.3, build abc1234 of 2026-11-03", and the commit is a link to GitHub. A build with no version
|
|
58
|
+
* starts with "Build", and a local build has no link.
|
|
59
|
+
*
|
|
60
|
+
* @param repository - The repository as "owner/name".
|
|
61
|
+
*/
|
|
62
|
+
export declare function showBuild(element: HTMLElement, build: BuildInfo, repository: string): void;
|
|
63
|
+
/**
|
|
64
|
+
* This function copies a text to the clipboard, and gives `true` when the copy is done. It uses
|
|
65
|
+
* the Clipboard API. When that API is not available or refuses, it uses `document.execCommand`.
|
|
66
|
+
*/
|
|
67
|
+
export declare function copyToClipboard(text: string): Promise<boolean>;
|
|
68
|
+
/** The options of `copyText`. */
|
|
69
|
+
export interface CopyTextOptions {
|
|
70
|
+
/** The text of the button after a copy. The default is "Copied". */
|
|
71
|
+
readonly copied?: string;
|
|
72
|
+
/** The text of the button after a failure. The default is "Copy failed". */
|
|
73
|
+
readonly failed?: string;
|
|
74
|
+
/** The time in milliseconds until the button shows its label again. The default is 1600. */
|
|
75
|
+
readonly ms?: number;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* This function copies a text to the clipboard with `copyToClipboard`. The button tells the result
|
|
79
|
+
* for a short time, then it shows its label again. The function gives the result of the copy.
|
|
80
|
+
*/
|
|
81
|
+
export declare function copyText(button: HTMLButtonElement, text: string, options?: CopyTextOptions): Promise<boolean>;
|
|
82
|
+
//# sourceMappingURL=chrome.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chrome.d.ts","names":[],"sources":["../src/chrome.ts"],"mappings":";;;;;;;YASY,QAAQ;;YAGR;;;;;wBAMI,EAAE,gBAAgB,uBAChC,KAAK,GACL,QAAO,SAAS,eAAe,qBAC5B,oBAAoB,iBAAiB,aACvC,sBAAsB;;;;;wBAkBT,KAAK,UAAU,cAAc,aAAa,aAAa;;qBAO1D;;YAGD,gBAAgB;;;;;wBAMZ,gBAAgB;;;;;wBAShB,WAAW,OAAO,OAAO;;wBAazB,UAAU,qBAAqB;;;;;;;;;wBAc/B,WAAW,QAAQ,mBAAmB;;iBAUrC;;WAEN;;WAEA;;WAEA;;;;;;;;;wBAUK,UAAU,SAAS,aAAa,OAAO,WAAW;;;;;wBAS5C,gBAAgB,eAAe;;iBA0BpC;;WAEN;;WAEA;;WAEA;;;;;;wBAOW,SAAS,QAAQ,mBAAmB,cAAc,UAAS,kBAAuB"}
|
package/build/chrome.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// src/chrome.ts
|
|
2
|
+
/**
|
|
3
|
+
* This function makes an element with attributes and children. An attribute with the value
|
|
4
|
+
* `false`, `null` or `undefined` is not set. The attribute `class` sets the class name.
|
|
5
|
+
*/
|
|
6
|
+
function h(tag, attrs = {}, ...children) {
|
|
7
|
+
const el = document.createElement(tag);
|
|
8
|
+
for (const [name, value] of Object.entries(attrs)) {
|
|
9
|
+
if (value === false || value === null || value === void 0) continue;
|
|
10
|
+
if (name === "class") el.className = String(value);
|
|
11
|
+
else el.setAttribute(name, value === true ? "" : String(value));
|
|
12
|
+
}
|
|
13
|
+
for (const child of children.flat()) {
|
|
14
|
+
if (child === null || child === void 0 || child === false) continue;
|
|
15
|
+
el.append(child);
|
|
16
|
+
}
|
|
17
|
+
return el;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* This function gives the element with the ID. The page has each ID that the code asks for. Thus
|
|
21
|
+
* a missing ID is an error of the page, and the function throws an `Error`.
|
|
22
|
+
*/
|
|
23
|
+
function byId(id) {
|
|
24
|
+
const el = document.getElementById(id);
|
|
25
|
+
if (el === null) throw new Error(`The page has no element "${id}".`);
|
|
26
|
+
return el;
|
|
27
|
+
}
|
|
28
|
+
/** The themes of the theme button, in the sequence of the clicks. `auto` follows the setting of the system. */
|
|
29
|
+
const THEMES = [
|
|
30
|
+
"auto",
|
|
31
|
+
"light",
|
|
32
|
+
"dark"
|
|
33
|
+
];
|
|
34
|
+
/**
|
|
35
|
+
* This function gives the theme of the page from the attribute `data-theme` of the `html` element.
|
|
36
|
+
* Without the attribute, or with another value, the theme is `auto`.
|
|
37
|
+
*/
|
|
38
|
+
function currentTheme() {
|
|
39
|
+
const theme = document.documentElement.dataset["theme"];
|
|
40
|
+
return theme === "light" || theme === "dark" ? theme : "auto";
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* This function sets the theme of the page, and keeps it in `localStorage` under `storageKey`.
|
|
44
|
+
* The theme `auto` removes the attribute `data-theme` and the stored value.
|
|
45
|
+
*/
|
|
46
|
+
function applyTheme(theme, storageKey) {
|
|
47
|
+
const root = document.documentElement;
|
|
48
|
+
if (theme === "auto") delete root.dataset["theme"];
|
|
49
|
+
else root.dataset["theme"] = theme;
|
|
50
|
+
try {
|
|
51
|
+
if (theme === "auto") localStorage.removeItem(storageKey);
|
|
52
|
+
else localStorage.setItem(storageKey, theme);
|
|
53
|
+
} catch {}
|
|
54
|
+
}
|
|
55
|
+
/** This function sets the next theme after the current theme (auto, light, dark, auto), and gives it. */
|
|
56
|
+
function nextTheme(storageKey) {
|
|
57
|
+
const next = THEMES[(THEMES.indexOf(currentTheme()) + 1) % THEMES.length] ?? "auto";
|
|
58
|
+
applyTheme(next, storageKey);
|
|
59
|
+
return next;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* This function makes a theme button. Each click sets the next theme. The text of the button is
|
|
63
|
+
* "Theme: auto", "Theme: light" or "Theme: dark", and its `aria-label` tells the same theme.
|
|
64
|
+
*
|
|
65
|
+
* @remarks Before the first paint, a script in the `head` of the page reads `storageKey` from
|
|
66
|
+
* `localStorage` and sets `data-theme`. Thus a dark page does not show white for a moment. The
|
|
67
|
+
* plugin `siteShell` of the entry `./vite` adds that script.
|
|
68
|
+
*/
|
|
69
|
+
function setupTheme(button, storageKey) {
|
|
70
|
+
const show = (theme) => {
|
|
71
|
+
button.textContent = `Theme: ${theme}`;
|
|
72
|
+
button.setAttribute("aria-label", `Color theme: ${theme}. Select to change.`);
|
|
73
|
+
};
|
|
74
|
+
show(currentTheme());
|
|
75
|
+
button.addEventListener("click", () => show(nextTheme(storageKey)));
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* This function shows the version and the build of the site in an element. The text is "Version
|
|
79
|
+
* 1.2.3, build abc1234 of 2026-11-03", and the commit is a link to GitHub. A build with no version
|
|
80
|
+
* starts with "Build", and a local build has no link.
|
|
81
|
+
*
|
|
82
|
+
* @param repository - The repository as "owner/name".
|
|
83
|
+
*/
|
|
84
|
+
function showBuild(element, build, repository) {
|
|
85
|
+
const commit = build.sha === "local" ? "local" : h("a", { href: `https://github.com/${repository}/commit/${build.sha}` }, build.sha);
|
|
86
|
+
element.append(build.version === void 0 ? "Build " : `Version ${build.version}, build `, commit, ` of ${build.date}`);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* This function copies a text to the clipboard, and gives `true` when the copy is done. It uses
|
|
90
|
+
* the Clipboard API. When that API is not available or refuses, it uses `document.execCommand`.
|
|
91
|
+
*/
|
|
92
|
+
async function copyToClipboard(text) {
|
|
93
|
+
try {
|
|
94
|
+
await navigator.clipboard.writeText(text);
|
|
95
|
+
return true;
|
|
96
|
+
} catch {}
|
|
97
|
+
try {
|
|
98
|
+
const area = document.createElement("textarea");
|
|
99
|
+
area.value = text;
|
|
100
|
+
area.setAttribute("readonly", "");
|
|
101
|
+
area.style.position = "fixed";
|
|
102
|
+
area.style.opacity = "0";
|
|
103
|
+
document.body.append(area);
|
|
104
|
+
area.select();
|
|
105
|
+
try {
|
|
106
|
+
return document.execCommand("copy");
|
|
107
|
+
} finally {
|
|
108
|
+
area.remove();
|
|
109
|
+
}
|
|
110
|
+
} catch {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* This function copies a text to the clipboard with `copyToClipboard`. The button tells the result
|
|
116
|
+
* for a short time, then it shows its label again. The function gives the result of the copy.
|
|
117
|
+
*/
|
|
118
|
+
async function copyText(button, text, options = {}) {
|
|
119
|
+
const label = button.textContent;
|
|
120
|
+
const ok = await copyToClipboard(text);
|
|
121
|
+
button.textContent = ok ? options.copied ?? "Copied" : options.failed ?? "Copy failed";
|
|
122
|
+
window.setTimeout(() => {
|
|
123
|
+
button.textContent = label;
|
|
124
|
+
}, options.ms ?? 1600);
|
|
125
|
+
return ok;
|
|
126
|
+
}
|
|
127
|
+
export { THEMES, applyTheme, byId, copyText, copyToClipboard, currentTheme, h, nextTheme, setupTheme, showBuild };
|
|
128
|
+
|
|
129
|
+
//# sourceMappingURL=chrome.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chrome.js","names":[],"sources":["../src/chrome.ts"],"sourcesContent":["/// <reference lib=\"dom\" />\n/**\n * The small helpers of a page in the browser: the element helper, the element lookup, the theme\n * button, the copy button and the build stamp. This module imports no Node.js module, so a site can\n * bundle it. Import it from `@mark1russell7/site-pages/chrome`, because the package root also has\n * Node.js code.\n */\n\n/** A child of `h`: a node or a text. The values `null`, `undefined` and `false` give no child. */\nexport type Child = Node | string | null | undefined | false;\n\n/** The value of an attribute of `h`. The value `true` gives an empty attribute. */\nexport type AttributeValue = string | number | boolean | null | undefined;\n\n/**\n * This function makes an element with attributes and children. An attribute with the value\n * `false`, `null` or `undefined` is not set. The attribute `class` sets the class name.\n */\nexport function h<K extends keyof HTMLElementTagNameMap>(\n tag: K,\n attrs: Readonly<Record<string, AttributeValue>> = {},\n ...children: readonly (Child | readonly Child[])[]\n): HTMLElementTagNameMap[K] {\n const el = document.createElement(tag);\n for (const [name, value] of Object.entries(attrs)) {\n if (value === false || value === null || value === undefined) continue;\n if (name === \"class\") el.className = String(value);\n else el.setAttribute(name, value === true ? \"\" : String(value));\n }\n for (const child of children.flat()) {\n if (child === null || child === undefined || child === false) continue;\n el.append(child);\n }\n return el;\n}\n\n/**\n * This function gives the element with the ID. The page has each ID that the code asks for. Thus\n * a missing ID is an error of the page, and the function throws an `Error`.\n */\nexport function byId<T extends HTMLElement = HTMLElement>(id: string): T {\n const el = document.getElementById(id);\n if (el === null) throw new Error(`The page has no element \"${id}\".`);\n return el as T;\n}\n\n/** The themes of the theme button, in the sequence of the clicks. `auto` follows the setting of the system. */\nexport const THEMES: readonly [\"auto\", \"light\", \"dark\"] = [\"auto\", \"light\", \"dark\"];\n\n/** A theme of the page. */\nexport type Theme = (typeof THEMES)[number];\n\n/**\n * This function gives the theme of the page from the attribute `data-theme` of the `html` element.\n * Without the attribute, or with another value, the theme is `auto`.\n */\nexport function currentTheme(): Theme {\n const theme = document.documentElement.dataset[\"theme\"];\n return theme === \"light\" || theme === \"dark\" ? theme : \"auto\";\n}\n\n/**\n * This function sets the theme of the page, and keeps it in `localStorage` under `storageKey`.\n * The theme `auto` removes the attribute `data-theme` and the stored value.\n */\nexport function applyTheme(theme: Theme, storageKey: string): void {\n const root = document.documentElement;\n if (theme === \"auto\") delete root.dataset[\"theme\"];\n else root.dataset[\"theme\"] = theme;\n try {\n if (theme === \"auto\") localStorage.removeItem(storageKey);\n else localStorage.setItem(storageKey, theme);\n } catch {\n // A browser with no storage keeps the theme until the page closes.\n }\n}\n\n/** This function sets the next theme after the current theme (auto, light, dark, auto), and gives it. */\nexport function nextTheme(storageKey: string): Theme {\n const next = THEMES[(THEMES.indexOf(currentTheme()) + 1) % THEMES.length] ?? \"auto\";\n applyTheme(next, storageKey);\n return next;\n}\n\n/**\n * This function makes a theme button. Each click sets the next theme. The text of the button is\n * \"Theme: auto\", \"Theme: light\" or \"Theme: dark\", and its `aria-label` tells the same theme.\n *\n * @remarks Before the first paint, a script in the `head` of the page reads `storageKey` from\n * `localStorage` and sets `data-theme`. Thus a dark page does not show white for a moment. The\n * plugin `siteShell` of the entry `./vite` adds that script.\n */\nexport function setupTheme(button: HTMLButtonElement, storageKey: string): void {\n const show = (theme: Theme): void => {\n button.textContent = `Theme: ${theme}`;\n button.setAttribute(\"aria-label\", `Color theme: ${theme}. Select to change.`);\n };\n show(currentTheme());\n button.addEventListener(\"click\", () => show(nextTheme(storageKey)));\n}\n\n/** The data of a build: `siteShell` writes it, and `showBuild` shows it. */\nexport interface BuildInfo {\n /** The first 7 characters of the commit, or `local` for a build outside CI. */\n readonly sha: string;\n /** The date of the build, as `YYYY-MM-DD`. */\n readonly date: string;\n /** The version of the package. A site without a version does not set it. */\n readonly version?: string | undefined;\n}\n\n/**\n * This function shows the version and the build of the site in an element. The text is \"Version\n * 1.2.3, build abc1234 of 2026-11-03\", and the commit is a link to GitHub. A build with no version\n * starts with \"Build\", and a local build has no link.\n *\n * @param repository - The repository as \"owner/name\".\n */\nexport function showBuild(element: HTMLElement, build: BuildInfo, repository: string): void {\n const commit = build.sha === \"local\" ? \"local\" : h(\"a\", { href: `https://github.com/${repository}/commit/${build.sha}` }, build.sha);\n element.append(build.version === undefined ? \"Build \" : `Version ${build.version}, build `, commit, ` of ${build.date}`);\n}\n\n/**\n * This function copies a text to the clipboard, and gives `true` when the copy is done. It uses\n * the Clipboard API. When that API is not available or refuses, it uses `document.execCommand`.\n */\nexport async function copyToClipboard(text: string): Promise<boolean> {\n try {\n await navigator.clipboard.writeText(text);\n return true;\n } catch {\n // An insecure page has no Clipboard API, and a browser can refuse the copy. The old command is the fallback.\n }\n try {\n const area = document.createElement(\"textarea\");\n area.value = text;\n area.setAttribute(\"readonly\", \"\");\n area.style.position = \"fixed\";\n area.style.opacity = \"0\";\n document.body.append(area);\n area.select();\n try {\n return document.execCommand(\"copy\");\n } finally {\n area.remove();\n }\n } catch {\n return false;\n }\n}\n\n/** The options of `copyText`. */\nexport interface CopyTextOptions {\n /** The text of the button after a copy. The default is \"Copied\". */\n readonly copied?: string;\n /** The text of the button after a failure. The default is \"Copy failed\". */\n readonly failed?: string;\n /** The time in milliseconds until the button shows its label again. The default is 1600. */\n readonly ms?: number;\n}\n\n/**\n * This function copies a text to the clipboard with `copyToClipboard`. The button tells the result\n * for a short time, then it shows its label again. The function gives the result of the copy.\n */\nexport async function copyText(button: HTMLButtonElement, text: string, options: CopyTextOptions = {}): Promise<boolean> {\n const label = button.textContent;\n const ok = await copyToClipboard(text);\n button.textContent = ok ? (options.copied ?? \"Copied\") : (options.failed ?? \"Copy failed\");\n window.setTimeout(() => {\n button.textContent = label;\n }, options.ms ?? 1600);\n return ok;\n}\n"],"mappings":";;;;;AAkBA,SAAgB,EACd,KACA,QAAkD,CAAC,GACnD,GAAG,UACuB;CAC1B,MAAM,KAAK,SAAS,cAAc,GAAG;CACrC,KAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,KAAK,GAAG;EACjD,IAAI,UAAU,SAAS,UAAU,QAAQ,UAAU,KAAA,GAAW;EAC9D,IAAI,SAAS,SAAS,GAAG,YAAY,OAAO,KAAK;OAC5C,GAAG,aAAa,MAAM,UAAU,OAAO,KAAK,OAAO,KAAK,CAAC;CAChE;CACA,KAAK,MAAM,SAAS,SAAS,KAAK,GAAG;EACnC,IAAI,UAAU,QAAQ,UAAU,KAAA,KAAa,UAAU,OAAO;EAC9D,GAAG,OAAO,KAAK;CACjB;CACA,OAAO;AACT;;;;;AAMA,SAAgB,KAA0C,IAAe;CACvE,MAAM,KAAK,SAAS,eAAe,EAAE;CACrC,IAAI,OAAO,MAAM,MAAM,IAAI,MAAM,4BAA4B,GAAG,GAAG;CACnE,OAAO;AACT;;AAGA,MAAa,SAA6C;CAAC;CAAQ;CAAS;AAAM;;;;;AASlF,SAAgB,eAAsB;CACpC,MAAM,QAAQ,SAAS,gBAAgB,QAAQ;CAC/C,OAAO,UAAU,WAAW,UAAU,SAAS,QAAQ;AACzD;;;;;AAMA,SAAgB,WAAW,OAAc,YAA0B;CACjE,MAAM,OAAO,SAAS;CACtB,IAAI,UAAU,QAAQ,OAAO,KAAK,QAAQ;MACrC,KAAK,QAAQ,WAAW;CAC7B,IAAI;EACF,IAAI,UAAU,QAAQ,aAAa,WAAW,UAAU;OACnD,aAAa,QAAQ,YAAY,KAAK;CAC7C,QAAQ,CAER;AACF;;AAGA,SAAgB,UAAU,YAA2B;CACnD,MAAM,OAAO,QAAQ,OAAO,QAAQ,aAAa,CAAC,IAAI,KAAK,OAAO,WAAW;CAC7E,WAAW,MAAM,UAAU;CAC3B,OAAO;AACT;;;;;;;;;AAUA,SAAgB,WAAW,QAA2B,YAA0B;CAC9E,MAAM,QAAQ,UAAuB;EACnC,OAAO,cAAc,UAAU;EAC/B,OAAO,aAAa,cAAc,gBAAgB,MAAM,oBAAoB;CAC9E;CACA,KAAK,aAAa,CAAC;CACnB,OAAO,iBAAiB,eAAe,KAAK,UAAU,UAAU,CAAC,CAAC;AACpE;;;;;;;;AAmBA,SAAgB,UAAU,SAAsB,OAAkB,YAA0B;CAC1F,MAAM,SAAS,MAAM,QAAQ,UAAU,UAAU,EAAE,KAAK,EAAE,MAAM,sBAAsB,WAAW,UAAU,MAAM,MAAM,GAAG,MAAM,GAAG;CACnI,QAAQ,OAAO,MAAM,YAAY,KAAA,IAAY,WAAW,WAAW,MAAM,QAAQ,WAAW,QAAQ,OAAO,MAAM,MAAM;AACzH;;;;;AAMA,eAAsB,gBAAgB,MAAgC;CACpE,IAAI;EACF,MAAM,UAAU,UAAU,UAAU,IAAI;EACxC,OAAO;CACT,QAAQ,CAER;CACA,IAAI;EACF,MAAM,OAAO,SAAS,cAAc,UAAU;EAC9C,KAAK,QAAQ;EACb,KAAK,aAAa,YAAY,EAAE;EAChC,KAAK,MAAM,WAAW;EACtB,KAAK,MAAM,UAAU;EACrB,SAAS,KAAK,OAAO,IAAI;EACzB,KAAK,OAAO;EACZ,IAAI;GACF,OAAO,SAAS,YAAY,MAAM;EACpC,UAAU;GACR,KAAK,OAAO;EACd;CACF,QAAQ;EACN,OAAO;CACT;AACF;;;;;AAgBA,eAAsB,SAAS,QAA2B,MAAc,UAA2B,CAAC,GAAqB;CACvH,MAAM,QAAQ,OAAO;CACrB,MAAM,KAAK,MAAM,gBAAgB,IAAI;CACrC,OAAO,cAAc,KAAM,QAAQ,UAAU,WAAa,QAAQ,UAAU;CAC5E,OAAO,iBAAiB;EACtB,OAAO,cAAc;CACvB,GAAG,QAAQ,MAAM,IAAI;CACrB,OAAO;AACT"}
|
package/build/cli.js
CHANGED
|
@@ -199,6 +199,22 @@ async function connect(url) {
|
|
|
199
199
|
* - an element in `.sr`, `.sr-only`, `.visually-hidden` or `.skip` (text for screen readers and skip links),
|
|
200
200
|
* - an element in a `pre`, other than the `pre` itself.
|
|
201
201
|
* Of a chain of elements past the edge, only the outermost one counts.
|
|
202
|
+
*
|
|
203
|
+
* `past` skips each element in a container that scrolls or clips, so `cut` examines those containers.
|
|
204
|
+
* A box is the nearest element around a text, below `body`, with `overflow-x` other than `visible`. Its content is wider
|
|
205
|
+
* than the box by more than 1 px. An element with `overflow-x: clip` is always a box, because it can report no overflow.
|
|
206
|
+
* The text counts when a line of it ends more than 1 px past the right edge of the box.
|
|
207
|
+
*
|
|
208
|
+
* A box that scrolls counts only for text in a table (`table`, `role="table"` or `role="grid"`).
|
|
209
|
+
* A code block, a hex view, a row of tabs and a carousel in such a box are meant to scroll.
|
|
210
|
+
* A box that clips counts for each text, because a visitor cannot reach that text. These texts do not count:
|
|
211
|
+
* - a text in a box with `text-overflow` other than `clip` (a person chose the ellipsis),
|
|
212
|
+
* - a text in an element with `data-overflow-ok`, or in that element itself (a person chose the overflow),
|
|
213
|
+
* - a text in a box with the class `scroll-cue` that scrolls (a shade shows the visitor that it scrolls),
|
|
214
|
+
* - a text in `.sr`, `.sr-only`, `.visually-hidden`, `.skip`, `[aria-hidden="true"]`, `select`, `textarea` or `svg`,
|
|
215
|
+
* - a text in a fixed element, which the box does not clip,
|
|
216
|
+
* - a text that is not visible, or with an element around it of 1 px or less or with `opacity: 0`.
|
|
217
|
+
* Only the first text of a box counts. An element with no text does not count. Vertical overflow does not count.
|
|
202
218
|
*/
|
|
203
219
|
const MEASURE = `(() => {
|
|
204
220
|
const root = document.documentElement;
|
|
@@ -227,6 +243,46 @@ const MEASURE = `(() => {
|
|
|
227
243
|
const clipped = [...document.querySelectorAll("textarea")]
|
|
228
244
|
.filter((e) => { const s = getComputedStyle(e); return (s.overflowY === "hidden" || s.overflowY === "clip") && e.getBoundingClientRect().height > 0 && e.scrollHeight > e.clientHeight + 1; })
|
|
229
245
|
.map((e) => name(e) + " " + e.scrollHeight + ">" + e.clientHeight);
|
|
246
|
+
const boxOf = (e) => {
|
|
247
|
+
for (let p = e; p && p !== document.body && p !== root; p = p.parentElement) {
|
|
248
|
+
const s = getComputedStyle(p);
|
|
249
|
+
if (s.overflowX === "clip" || (s.overflowX !== "visible" && p.scrollWidth > p.clientWidth + 1)) return p;
|
|
250
|
+
if (s.position === "fixed") return undefined;
|
|
251
|
+
}
|
|
252
|
+
return undefined;
|
|
253
|
+
};
|
|
254
|
+
const shown = (e) => {
|
|
255
|
+
if (e.closest(".sr,.sr-only,.visually-hidden,.skip,[aria-hidden=true],select,textarea,svg")) return false;
|
|
256
|
+
if (getComputedStyle(e).visibility !== "visible") return false;
|
|
257
|
+
for (let p = e; p && p !== root; p = p.parentElement) {
|
|
258
|
+
const s = getComputedStyle(p);
|
|
259
|
+
if (s.opacity === "0") return false;
|
|
260
|
+
if (s.display === "contents") continue;
|
|
261
|
+
const r = p.getBoundingClientRect();
|
|
262
|
+
if (r.width <= 1 || r.height <= 1) return false;
|
|
263
|
+
}
|
|
264
|
+
return true;
|
|
265
|
+
};
|
|
266
|
+
const cuts = new Map();
|
|
267
|
+
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
|
|
268
|
+
const range = document.createRange();
|
|
269
|
+
for (let t = walker.nextNode(); t; t = walker.nextNode()) {
|
|
270
|
+
const e = t.parentElement;
|
|
271
|
+
if (!e || !t.data.trim()) continue;
|
|
272
|
+
const box = boxOf(e);
|
|
273
|
+
if (!box || cuts.has(box)) continue;
|
|
274
|
+
const s = getComputedStyle(box);
|
|
275
|
+
if (s.textOverflow !== "clip") continue;
|
|
276
|
+
const scrolls = s.overflowX === "auto" || s.overflowX === "scroll";
|
|
277
|
+
const table = e.closest("table,[role=table],[role=grid]");
|
|
278
|
+
if (scrolls && !(table && box.contains(table))) continue;
|
|
279
|
+
const left = box.getBoundingClientRect().left + box.clientLeft;
|
|
280
|
+
range.selectNodeContents(t);
|
|
281
|
+
const end = Math.max(...[...range.getClientRects()].filter((r) => r.width > 0).map((r) => r.right));
|
|
282
|
+
if (end <= left + box.clientWidth + 1) continue;
|
|
283
|
+
if (e.closest("[data-overflow-ok]") || (scrolls && box.classList.contains("scroll-cue")) || !shown(e)) continue;
|
|
284
|
+
cuts.set(box, { kind: scrolls ? "table" : "clip", box: name(box), element: name(scrolls ? table : e), scrollWidth: Math.max(box.scrollWidth, Math.ceil(end - left)), clientWidth: box.clientWidth, text: t.data.trim().replace(/\\s+/g, " ").slice(0, 40) });
|
|
285
|
+
}
|
|
230
286
|
return {
|
|
231
287
|
innerWidth,
|
|
232
288
|
scrollWidth: root.scrollWidth,
|
|
@@ -234,6 +290,7 @@ const MEASURE = `(() => {
|
|
|
234
290
|
text: (document.body ? document.body.innerText : "").trim().length,
|
|
235
291
|
past: past.slice(0, 8),
|
|
236
292
|
clipped: clipped.slice(0, 8),
|
|
293
|
+
cut: [...cuts.values()].slice(0, 8),
|
|
237
294
|
};
|
|
238
295
|
})()`;
|
|
239
296
|
/**
|
|
@@ -360,17 +417,19 @@ async function serve(root, base) {
|
|
|
360
417
|
close: () => new Promise((done) => server.close(() => done()))
|
|
361
418
|
};
|
|
362
419
|
}
|
|
363
|
-
// src/check/check.ts
|
|
364
420
|
/**
|
|
365
|
-
*
|
|
366
|
-
*
|
|
367
|
-
*
|
|
368
|
-
* A view fails when the page is wider than the screen or an element goes past the right edge. It
|
|
369
|
-
* also fails when a text area hides its text or the page shows no text. A console error, an
|
|
370
|
-
* exception and a failed request of the site also fail it. A script that does not load and a failed check of the probe
|
|
371
|
-
* fail it too. The command
|
|
372
|
-
* writes a screenshot of each view.
|
|
421
|
+
* This function gives the problems of the cuts of a view with the given width. A box that clips text is a problem at each width.
|
|
422
|
+
* A table in a box that scrolls is a problem only on a phone. A phone shows no scroll bar, but a desktop shows one.
|
|
423
|
+
* The summary and the log give the page of each problem.
|
|
373
424
|
*/
|
|
425
|
+
function cutProblems(cuts, width) {
|
|
426
|
+
const widths = (cut) => `${cut.element} in ${cut.box}: content of ${cut.scrollWidth} px in a box of ${cut.clientWidth} px, "${cut.text}"`;
|
|
427
|
+
return cuts.flatMap((cut) => {
|
|
428
|
+
if (cut.kind === "clip") return [`a box cuts text at ${width} px: ${widths(cut)}. Make the content fit, or add data-overflow-ok to the box.`];
|
|
429
|
+
if (width < 768) return [`a table goes out of view at ${width} px: ${widths(cut)}. Make the table fit, give the box the class scroll-cue, or add data-overflow-ok to the box.`];
|
|
430
|
+
return [];
|
|
431
|
+
});
|
|
432
|
+
}
|
|
374
433
|
/** This function gives the file name part of a page: `/` is `home`, `/releases/` is `releases`. */
|
|
375
434
|
function pageSlug(page) {
|
|
376
435
|
const slug = page.replace(/^\/+|\/+$/g, "").replace(/[^A-Za-z0-9._-]+/g, "_");
|
|
@@ -517,6 +576,7 @@ async function checkView(page, events, input) {
|
|
|
517
576
|
if (measure.scrollWidth > width) view.problems.push(`the page is wider than the screen: ${measure.scrollWidth} > ${width}`);
|
|
518
577
|
for (const past of measure.past) view.problems.push(`past the right edge: ${past}`);
|
|
519
578
|
for (const clipped of measure.clipped) view.problems.push(`a text area hides its text: ${clipped}`);
|
|
579
|
+
view.problems.push(...cutProblems(measure.cut, width));
|
|
520
580
|
if (measure.text === 0) view.problems.push("the page shows no text");
|
|
521
581
|
if (options.shots) {
|
|
522
582
|
const file = join(options.shots, `${pageSlug(path)}-${width}-${scheme}.png`);
|
|
@@ -570,8 +630,10 @@ const CHECK_HELP = `Usage: site-pages check (--out <dir> | --url <url>) [options
|
|
|
570
630
|
|
|
571
631
|
Opens the pages of the site in a headless Chromium at phone and desktop widths, in the light and
|
|
572
632
|
the dark scheme, and fails when a page is wider than the screen, an element goes past the right
|
|
573
|
-
edge, a text area hides its text, a
|
|
574
|
-
|
|
633
|
+
edge, a text area hides its text, a box cuts text, a table goes out of view on a phone, a page
|
|
634
|
+
shows no text, or on a console error, an exception, a failed request of the site, a script that
|
|
635
|
+
does not load or a failed check of the probe. Add data-overflow-ok to a box that is meant to
|
|
636
|
+
scroll or to clip.
|
|
575
637
|
|
|
576
638
|
Options:
|
|
577
639
|
--out <dir> The built site folder. The check serves it at --base.
|
package/build/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","names":[],"sources":["../src/check/browser.ts","../src/check/measure.ts","../src/check/serve.ts","../src/check/check.ts","../src/cli.ts"],"sourcesContent":["/**\n * A small client of the Chrome DevTools protocol for the `check` command. It starts a Chromium\n * browser (Chrome, Edge or the Chromium of Playwright) in headless mode and drives one page. It\n * uses only Node.js, so the check needs no package install.\n */\n\nimport { spawn, type ChildProcess } from \"node:child_process\";\nimport { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync } from \"node:fs\";\nimport { homedir, tmpdir } from \"node:os\";\nimport { delimiter, join } from \"node:path\";\n\n/** The paths that `findBrowser` examines after the option and the Playwright folder. */\nfunction systemBrowsers(platform: NodeJS.Platform, env: NodeJS.ProcessEnv): string[] {\n if (platform === \"win32\") {\n const roots = [env[\"PROGRAMFILES\"], env[\"PROGRAMFILES(X86)\"], env[\"LOCALAPPDATA\"]].filter((root): root is string => !!root);\n return roots.flatMap((root) => [join(root, \"Google/Chrome/Application/chrome.exe\"), join(root, \"Microsoft/Edge/Application/msedge.exe\")]);\n }\n if (platform === \"darwin\") {\n return [\"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome\", \"/Applications/Chromium.app/Contents/MacOS/Chromium\", \"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge\"];\n }\n const names = [\"google-chrome\", \"google-chrome-stable\", \"chromium\", \"chromium-browser\", \"microsoft-edge\"];\n const dirs = (env[\"PATH\"] ?? \"\").split(delimiter).filter(Boolean);\n return names.flatMap((name) => dirs.map((dir) => join(dir, name)));\n}\n\n/** The Chromium of the newest `chromium-<n>` folder of Playwright, or undefined. */\nfunction playwrightChromium(platform: NodeJS.Platform, env: NodeJS.ProcessEnv): string | undefined {\n const root =\n env[\"PLAYWRIGHT_BROWSERS_PATH\"] ||\n (platform === \"win32\" ? join(env[\"LOCALAPPDATA\"] ?? homedir(), \"ms-playwright\") : platform === \"darwin\" ? join(homedir(), \"Library/Caches/ms-playwright\") : join(homedir(), \".cache/ms-playwright\"));\n if (!existsSync(root)) return undefined;\n const builds = readdirSync(root)\n .map((name) => /^chromium-(\\d+)$/.exec(name))\n .filter((match): match is RegExpExecArray => match !== null)\n .sort((a, b) => Number(b[1]) - Number(a[1]));\n for (const [folder] of builds) {\n for (const sub of readdirSync(join(root, folder))) {\n const candidates =\n platform === \"win32\"\n ? [join(root, folder, sub, \"chrome.exe\")]\n : platform === \"darwin\"\n ? [join(root, folder, sub, \"Chromium.app/Contents/MacOS/Chromium\"), join(root, folder, sub, \"Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing\")]\n : [join(root, folder, sub, \"chrome\")];\n const found = candidates.find((file) => existsSync(file));\n if (found) return found;\n }\n }\n return undefined;\n}\n\n/**\n * This function finds a Chromium browser. It examines the given path, then `$SITE_CHECK_BROWSER`,\n * then the newest Chromium of Playwright, then a Chrome or an Edge of the system.\n *\n * @returns the path of the executable, or undefined when there is none\n */\nexport function findBrowser(given?: string, env: NodeJS.ProcessEnv = process.env, platform: NodeJS.Platform = process.platform): string | undefined {\n const named = given || env[\"SITE_CHECK_BROWSER\"];\n if (named) return named;\n return playwrightChromium(platform, env) ?? systemBrowsers(platform, env).find((file) => existsSync(file));\n}\n\n/** A message of the DevTools protocol. */\ninterface Message {\n id?: number;\n method?: string;\n params?: Record<string, unknown>;\n result?: Record<string, unknown>;\n error?: { message: string };\n}\n\n/** An event handler of a page. */\ntype Listener = (params: Record<string, unknown>) => void;\n\n/** One page of a started browser. */\nexport interface Page {\n /** This function sends a command of the protocol and returns its result. */\n send(method: string, params?: Record<string, unknown>): Promise<Record<string, unknown>>;\n /** This function calls the listener on each event of the method. */\n on(method: string, listener: Listener): void;\n /** This function calls the listener on the next event of the method, or rejects after `ms`. */\n once(method: string, ms: number): Promise<Record<string, unknown>>;\n /** This function evaluates the expression in the page, and waits for a promise. */\n evaluate<T>(expression: string): Promise<T>;\n}\n\n/** A started browser. */\nexport interface Browser {\n page: Page;\n /** This function stops the browser and removes its profile. */\n close(): Promise<void>;\n}\n\nconst sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));\n\n/**\n * This function starts the browser in headless mode with a new profile, and connects to its page.\n * The browser picks a free port, so two checks can run at the same time.\n *\n * @throws Error when the browser does not start in `timeoutMs`\n */\nexport async function launch(executable: string, timeoutMs = 30_000): Promise<Browser> {\n const profile = mkdtempSync(join(tmpdir(), \"site-check-\"));\n const args = [\n \"--headless=new\",\n \"--remote-debugging-port=0\",\n `--user-data-dir=${profile}`,\n \"--no-first-run\",\n \"--no-default-browser-check\",\n \"--disable-gpu\",\n \"--disable-extensions\",\n \"--disable-background-networking\",\n \"--disable-sync\",\n \"--hide-scrollbars\",\n \"--mute-audio\",\n // The sandbox of Chromium needs user namespaces, which Ubuntu 24.04 refuses to a browser that the\n // system did not install. Playwright also starts Chromium with no sandbox.\n ...(process.platform === \"linux\" ? [\"--no-sandbox\"] : []),\n \"about:blank\",\n ];\n const child: ChildProcess = spawn(executable, args, { stdio: \"ignore\" });\n let exited: string | undefined;\n child.once(\"exit\", (code, signal) => (exited = `the browser stopped (code ${code}, signal ${signal})`));\n child.once(\"error\", (error) => (exited = `the browser did not start: ${error.message}`));\n const stop = async () => {\n if (child.exitCode === null && child.signalCode === null) {\n child.kill();\n await Promise.race([new Promise((resolve) => child.once(\"exit\", resolve)), sleep(5000)]);\n }\n rmSync(profile, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 });\n };\n try {\n const portFile = join(profile, \"DevToolsActivePort\");\n const end = Date.now() + timeoutMs;\n let port = \"\";\n while (port === \"\") {\n if (exited !== undefined) throw new Error(`${executable}: ${exited}`);\n if (Date.now() > end) throw new Error(`${executable}: no DevTools port after ${timeoutMs} ms`);\n if (existsSync(portFile)) port = readFileSync(portFile, \"utf8\").split(\"\\n\")[0]?.trim() ?? \"\";\n if (port === \"\") await sleep(100);\n }\n let target: { type: string; webSocketDebuggerUrl: string } | undefined;\n while (target === undefined) {\n const list = (await (await fetch(`http://127.0.0.1:${port}/json/list`)).json()) as { type: string; webSocketDebuggerUrl: string }[];\n target = list.find((entry) => entry.type === \"page\");\n if (target === undefined) {\n if (Date.now() > end) throw new Error(`${executable}: no page target after ${timeoutMs} ms`);\n await sleep(100);\n }\n }\n const page = await connect(target.webSocketDebuggerUrl);\n return {\n page: page.page,\n close: async () => {\n page.close();\n await stop();\n },\n };\n } catch (error) {\n await stop();\n throw error;\n }\n}\n\nasync function connect(url: string): Promise<{ page: Page; close: () => void }> {\n const socket = new WebSocket(url);\n await new Promise<void>((resolve, reject) => {\n socket.addEventListener(\"open\", () => resolve(), { once: true });\n socket.addEventListener(\"error\", () => reject(new Error(`no connection to ${url}`)), { once: true });\n });\n let sequence = 0;\n const pending = new Map<number, { resolve: (value: Record<string, unknown>) => void; reject: (error: Error) => void }>();\n const listeners = new Map<string, Listener[]>();\n socket.addEventListener(\"message\", (event) => {\n const message = JSON.parse(String(event.data)) as Message;\n if (message.id !== undefined) {\n const call = pending.get(message.id);\n pending.delete(message.id);\n if (message.error) call?.reject(new Error(message.error.message));\n else call?.resolve(message.result ?? {});\n } else if (message.method) {\n for (const listener of listeners.get(message.method) ?? []) listener(message.params ?? {});\n }\n });\n socket.addEventListener(\"close\", () => {\n for (const call of pending.values()) call.reject(new Error(\"the browser closed the connection\"));\n pending.clear();\n });\n const page: Page = {\n send: (method, params = {}) =>\n new Promise((resolve, reject) => {\n const id = ++sequence;\n pending.set(id, { resolve, reject });\n socket.send(JSON.stringify({ id, method, params }));\n }),\n on: (method, listener) => listeners.set(method, [...(listeners.get(method) ?? []), listener]),\n once: (method, ms) =>\n new Promise((resolve, reject) => {\n const timer = setTimeout(() => {\n remove();\n reject(new Error(`no ${method} after ${ms} ms`));\n }, ms);\n const listener: Listener = (params) => {\n clearTimeout(timer);\n remove();\n resolve(params);\n };\n const remove = () => listeners.set(method, (listeners.get(method) ?? []).filter((entry) => entry !== listener));\n page.on(method, listener);\n }),\n evaluate: async <T>(expression: string): Promise<T> => {\n const reply = (await page.send(\"Runtime.evaluate\", { expression, awaitPromise: true, returnByValue: true })) as {\n result?: { value?: unknown };\n exceptionDetails?: { text?: string; exception?: { description?: string } };\n };\n if (reply.exceptionDetails) throw new Error(reply.exceptionDetails.exception?.description ?? reply.exceptionDetails.text ?? \"the expression failed\");\n return reply.result?.value as T;\n },\n };\n return { page, close: () => socket.close() };\n}\n","/**\n * The measurements that the `check` command makes in the page. Each is a string of JavaScript that\n * the browser evaluates, so the code can use only the APIs of the page.\n */\n\n/** The result of `MEASURE`. */\nexport interface Measure {\n innerWidth: number;\n scrollWidth: number;\n /** The height of the document, for the screenshot. */\n height: number;\n /** The number of characters of visible text. */\n text: number;\n /** The outermost elements that go past the right edge of the screen, at most 8. */\n past: string[];\n /** The text areas that hide a part of their text, at most 8. */\n clipped: string[];\n}\n\n/**\n * This expression measures the page. An element counts as past the edge when its right side is\n * more than half a pixel past the width of the document. These elements do not count:\n * - an element in a container that scrolls or clips (`overflow-x` other than `visible`, below `body`),\n * - an element of zero size, a hidden element, or a fixed element that is fully past the edge (a closed drawer),\n * - an element in `.sr`, `.sr-only`, `.visually-hidden` or `.skip` (text for screen readers and skip links),\n * - an element in a `pre`, other than the `pre` itself.\n * Of a chain of elements past the edge, only the outermost one counts.\n */\nexport const MEASURE = `(() => {\n const root = document.documentElement;\n const w = root.clientWidth;\n const name = (e) => e.tagName.toLowerCase() + (e.id ? \"#\" + e.id : \"\") + (typeof e.className === \"string\" && e.className.trim() ? \".\" + e.className.trim().split(/\\\\s+/).slice(0, 3).join(\".\") : \"\");\n const inScroller = (e) => {\n for (let p = e.parentElement; p && p !== document.body && p !== root; p = p.parentElement) {\n if (getComputedStyle(p).overflowX !== \"visible\") return true;\n }\n return false;\n };\n const counts = (e) => {\n const r = e.getBoundingClientRect();\n if (r.width === 0 || r.height === 0 || r.right <= w + 0.5) return false;\n if (e.closest(\".sr,.sr-only,.visually-hidden,.skip\")) return false;\n if (e.closest(\"pre\") && e.tagName !== \"PRE\") return false;\n const s = getComputedStyle(e);\n if (s.visibility === \"hidden\" || s.display === \"none\") return false;\n if (s.position === \"fixed\" && r.left >= w) return false;\n return !inScroller(e);\n };\n const over = new Set([...document.querySelectorAll(\"body *\")].filter(counts));\n const past = [...over]\n .filter((e) => { for (let p = e.parentElement; p; p = p.parentElement) if (over.has(p)) return false; return true; })\n .map((e) => name(e) + \" right=\" + Math.round(e.getBoundingClientRect().right));\n const clipped = [...document.querySelectorAll(\"textarea\")]\n .filter((e) => { const s = getComputedStyle(e); return (s.overflowY === \"hidden\" || s.overflowY === \"clip\") && e.getBoundingClientRect().height > 0 && e.scrollHeight > e.clientHeight + 1; })\n .map((e) => name(e) + \" \" + e.scrollHeight + \">\" + e.clientHeight);\n return {\n innerWidth,\n scrollWidth: root.scrollWidth,\n height: Math.max(root.scrollHeight, document.body ? document.body.scrollHeight : 0),\n text: (document.body ? document.body.innerText : \"\").trim().length,\n past: past.slice(0, 8),\n clipped: clipped.slice(0, 8),\n };\n})()`;\n\n/** One check of a probe. */\nexport interface ProbeCheck {\n name: string;\n ok: boolean;\n detail?: string;\n}\n\n/**\n * This function wraps the code of a probe file into an expression. The code is the body of an async\n * function with these names:\n * - `check(name, ok, detail)` records a check,\n * - `wait(ms)` waits,\n * - `until(fn, ms)` waits until `fn()` gives a true value, at most `ms` (default 5000), and returns the value,\n * - `$(selector)` and `$$(selector)` find elements,\n * - `type(element, text)` writes the text into an input or a text area, with an `input` event,\n * - `click(element)` clicks an element, or the first element of a selector.\n * An exception stops the probe, and it counts as a failed check.\n */\nexport function probeExpression(code: string): string {\n return `(async () => {\n const __checks = [];\n const __text = (v) => { if (v === undefined) return undefined; if (typeof v === \"string\") return v; try { return JSON.stringify(v); } catch { return String(v); } };\n const check = (name, ok, detail) => { __checks.push({ name: String(name), ok: !!ok, ...(ok ? {} : { detail: __text(detail) }) }); return !!ok; };\n const wait = (ms) => new Promise((r) => setTimeout(r, ms));\n const until = async (fn, ms = 5000) => { const end = Date.now() + ms; for (;;) { const v = await fn(); if (v) return v; if (Date.now() > end) return v; await wait(50); } };\n const $ = (s) => document.querySelector(s);\n const $$ = (s) => [...document.querySelectorAll(s)];\n const __el = (e) => { const el = typeof e === \"string\" ? document.querySelector(e) : e; if (!el) throw new Error(\"no element: \" + e); return el; };\n const type = (e, text) => { const el = __el(e); el.focus(); el.value = text; el.dispatchEvent(new Event(\"input\", { bubbles: true })); el.dispatchEvent(new Event(\"change\", { bubbles: true })); };\n const click = (e) => __el(e).click();\n try {\n await (async () => {\n${code}\n })();\n } catch (e) {\n __checks.push({ name: \"the probe runs to its end\", ok: false, detail: String((e && e.stack) || e) });\n }\n return __checks;\n})()`;\n}\n","/**\n * A static file server for the `check` command. It serves a built site folder at the path of the\n * site home, as GitHub Pages does. Thus the links of the site work as on the live site.\n */\n\nimport { createReadStream, existsSync, statSync } from \"node:fs\";\nimport { createServer, type Server } from \"node:http\";\nimport { extname, join, normalize, resolve, sep } from \"node:path\";\n\nconst TYPES: Record<string, string> = {\n \".html\": \"text/html; charset=utf-8\",\n \".js\": \"text/javascript; charset=utf-8\",\n \".mjs\": \"text/javascript; charset=utf-8\",\n \".css\": \"text/css; charset=utf-8\",\n \".json\": \"application/json; charset=utf-8\",\n \".svg\": \"image/svg+xml\",\n \".png\": \"image/png\",\n \".jpg\": \"image/jpeg\",\n \".jpeg\": \"image/jpeg\",\n \".gif\": \"image/gif\",\n \".webp\": \"image/webp\",\n \".avif\": \"image/avif\",\n \".ico\": \"image/x-icon\",\n \".woff\": \"font/woff\",\n \".woff2\": \"font/woff2\",\n \".ttf\": \"font/ttf\",\n \".otf\": \"font/otf\",\n \".wasm\": \"application/wasm\",\n \".txt\": \"text/plain; charset=utf-8\",\n \".md\": \"text/markdown; charset=utf-8\",\n \".xml\": \"application/xml\",\n \".webmanifest\": \"application/manifest+json\",\n \".map\": \"application/json; charset=utf-8\",\n};\n\n/**\n * This function maps the path of a request to a file of the folder, as GitHub Pages does. A folder\n * gives its `index.html`. For a path with no extension, the function also tries `<path>.html`.\n *\n * @returns the absolute file, or undefined when the path is outside the base or the file is missing\n */\nexport function resolveFile(root: string, base: string, pathname: string): string | undefined {\n let decoded: string;\n try {\n decoded = decodeURIComponent(pathname);\n } catch {\n return undefined;\n }\n const home = base.endsWith(\"/\") ? base : `${base}/`;\n if (decoded === home.slice(0, -1)) decoded = home;\n if (!decoded.startsWith(home)) return undefined;\n const top = resolve(root);\n const file = normalize(join(top, decoded.slice(home.length)));\n if (file !== top && !file.startsWith(top + sep)) return undefined;\n const candidates = [file, join(file, \"index.html\"), `${file}.html`];\n return candidates.find((candidate) => existsSync(candidate) && statSync(candidate).isFile());\n}\n\n/** A started server. */\nexport interface StaticServer {\n /** The URL of the site home, for example `http://127.0.0.1:50123/client/`. */\n url: string;\n close(): Promise<void>;\n}\n\n/**\n * This function serves the folder at `base` on a free port of 127.0.0.1. A missing file gives the\n * status 404 with the `404.html` of the folder, when it has one.\n */\nexport async function serve(root: string, base: string): Promise<StaticServer> {\n const server: Server = createServer((request, response) => {\n const pathname = new URL(request.url ?? \"/\", \"http://localhost\").pathname;\n const home = base.endsWith(\"/\") ? base : `${base}/`;\n if (pathname === home.slice(0, -1) && home !== \"/\") {\n response.writeHead(301, { location: home }).end();\n return;\n }\n const file = resolveFile(root, base, pathname);\n const missing = join(resolve(root), \"404.html\");\n const send = file ?? (existsSync(missing) ? missing : undefined);\n if (send === undefined) {\n response.writeHead(404, { \"content-type\": \"text/plain; charset=utf-8\" }).end(`404: ${pathname}\\n`);\n return;\n }\n response.writeHead(file ? 200 : 404, { \"content-type\": TYPES[extname(send).toLowerCase()] ?? \"application/octet-stream\", \"cache-control\": \"no-store\" });\n createReadStream(send).pipe(response);\n });\n await new Promise<void>((done, fail) => {\n server.once(\"error\", fail);\n server.listen(0, \"127.0.0.1\", () => done());\n });\n const address = server.address();\n if (address === null || typeof address === \"string\") throw new Error(\"the server has no port\");\n return {\n url: `http://127.0.0.1:${address.port}${base.endsWith(\"/\") ? base : `${base}/`}`,\n close: () => new Promise((done) => server.close(() => done())),\n };\n}\n","/**\n * The `site-pages check` command. It opens the pages of a built site (or of a live URL) in a\n * headless Chromium at phone and desktop widths, in the light and the dark scheme.\n *\n * A view fails when the page is wider than the screen or an element goes past the right edge. It\n * also fails when a text area hides its text or the page shows no text. A console error, an\n * exception and a failed request of the site also fail it. A script that does not load and a failed check of the probe\n * fail it too. The command\n * writes a screenshot of each view.\n */\n\nimport { appendFileSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { isAbsolute, join, resolve } from \"node:path\";\nimport { parseArgs } from \"node:util\";\nimport { findBrowser, launch, type Page } from \"./browser.ts\";\nimport { MEASURE, probeExpression, type Measure, type ProbeCheck } from \"./measure.ts\";\nimport { probeScript, TYPESCRIPT_PROBE } from \"./probe.ts\";\nimport { serve } from \"./serve.ts\";\n\n/** The options of `checkSite`. */\nexport interface CheckOptions {\n /** The built site folder. Give `out` or `url`. */\n out?: string | undefined;\n /** The path of the site home, for `out`, for example `/client/`. */\n base?: string | undefined;\n /** The URL of a live site home. Give `out` or `url`. */\n url?: string | undefined;\n /** The pages, from the site home, for example `/` and `/releases/`. */\n pages: readonly string[];\n /** The widths in CSS pixels. A width under 768 is a phone. */\n widths: readonly number[];\n /** The color schemes. */\n schemes: readonly (\"light\" | \"dark\")[];\n /** The code of a probe: the body of an async function (see `probeExpression`). It runs on the first page, in the light scheme, at each width. */\n probe?: string | undefined;\n /** The folder for the screenshots, `report.json` and `summary.md`. */\n shots?: string | undefined;\n /** The browser executable. */\n browser?: string | undefined;\n /** The time to wait after the load event, for the scripts of the page. */\n settleMs: number;\n /** The time limit of a page load. */\n timeoutMs: number;\n log?: ((line: string) => void) | undefined;\n}\n\n/** The result of one page at one width and scheme. */\nexport interface ViewResult {\n page: string;\n width: number;\n scheme: \"light\" | \"dark\";\n ok: boolean;\n /** What failed. */\n problems: string[];\n measure?: Measure | undefined;\n probe?: ProbeCheck[] | undefined;\n /** The file of the screenshot. */\n shot?: string | undefined;\n}\n\n/** The result of `checkSite`. */\nexport interface CheckResult {\n ok: boolean;\n url: string;\n browser: string;\n views: ViewResult[];\n}\n\n/** This function gives the file name part of a page: `/` is `home`, `/releases/` is `releases`. */\nexport function pageSlug(page: string): string {\n const slug = page.replace(/^\\/+|\\/+$/g, \"\").replace(/[^A-Za-z0-9._-]+/g, \"_\");\n return slug === \"\" ? \"home\" : slug;\n}\n\n/**\n * This function checks the site. It starts one browser and, for `out`, one server, and stops both at\n * the end.\n *\n * @throws Error when there is no browser, or the browser does not start\n */\nexport async function checkSite(options: CheckOptions): Promise<CheckResult> {\n const log = options.log ?? (() => {});\n const executable = findBrowser(options.browser);\n if (executable === undefined) throw new Error(\"no Chromium browser was found: give --browser, set SITE_CHECK_BROWSER, or install the Chromium of Playwright\");\n if (options.pages.length === 0) throw new Error(\"no page to check\");\n const server = options.out !== undefined ? await serve(options.out, options.base ?? \"/\") : undefined;\n const home = server?.url ?? options.url;\n if (home === undefined) throw new Error(\"give out or url\");\n const homeUrl = new URL(home.endsWith(\"/\") ? home : `${home}/`);\n const browser = await launch(executable, options.timeoutMs);\n const views: ViewResult[] = [];\n try {\n const { page } = browser;\n const events = watch(page, homeUrl.origin);\n await Promise.all([page.send(\"Page.enable\"), page.send(\"Runtime.enable\"), page.send(\"Network.enable\")]);\n await page.send(\"Page.addScriptToEvaluateOnNewDocument\", { source: LOAD_WATCH });\n if (options.shots) mkdirSync(options.shots, { recursive: true });\n for (const [index, path] of options.pages.entries()) {\n const target = new URL(path.replace(/^\\/+/, \"\"), homeUrl).href;\n for (const width of options.widths) {\n for (const scheme of options.schemes) {\n const withProbe = options.probe !== undefined && index === 0 && scheme === options.schemes[0];\n const view = await checkView(page, events, { target, path, width, scheme, withProbe, options });\n views.push(view);\n log(`${view.ok ? \"PASS\" : \"FAIL\"} ${path} ${width} ${scheme}${view.measure ? ` scrollWidth=${view.measure.scrollWidth} innerWidth=${view.measure.innerWidth}` : \"\"}${view.probe ? ` probe ${view.probe.filter((c) => c.ok).length}/${view.probe.length}` : \"\"}${view.problems.length > 0 ? `\\n - ${view.problems.join(\"\\n - \")}` : \"\"}`);\n }\n }\n }\n } finally {\n await browser.close();\n await server?.close();\n }\n const result: CheckResult = { ok: views.length > 0 && views.every((view) => view.ok), url: homeUrl.href, browser: executable, views };\n if (options.shots) {\n writeFileSync(join(options.shots, \"report.json\"), `${JSON.stringify(result, null, 2)}\\n`);\n writeFileSync(join(options.shots, \"summary.md\"), summary(result));\n }\n return result;\n}\n\n/**\n * A script that each page starts before its own scripts. A script or a style sheet that does not load fires `error` on its\n * element. This script writes that event as a console error. Thus a module script whose imports do not load is a problem too.\n * Such a script throws no exception. Its failed request can go to another origin, for example `node:perf_hooks` in a page bundle.\n */\nexport const LOAD_WATCH = `addEventListener(\"error\", (event) => {\n const element = event.target;\n if (!(element instanceof HTMLScriptElement || element instanceof HTMLLinkElement)) return;\n console.error(\"failed to load \" + element.tagName.toLowerCase() + \": \" + (element.getAttribute(\"src\") ?? element.getAttribute(\"href\") ?? \"(inline)\"));\n}, true);`;\n\n/** The problems that the events of the page gave, since the last `reset`. */\ninterface Events {\n reset(): void;\n problems(): string[];\n}\n\nfunction watch(page: Page, origin: string): Events {\n let problems: string[] = [];\n const urls = new Map<string, string>();\n const own = (url: string) => {\n try {\n const parsed = new URL(url);\n // A browser asks for /favicon.ico on its own, also for a site under a path.\n return parsed.origin === origin && parsed.pathname !== \"/favicon.ico\";\n } catch {\n return false;\n }\n };\n page.on(\"Runtime.exceptionThrown\", (params) => {\n const details = params[\"exceptionDetails\"] as { text?: string; exception?: { description?: string } };\n problems.push(`exception: ${(details.exception?.description ?? details.text ?? \"\").split(\"\\n\")[0]}`);\n });\n page.on(\"Runtime.consoleAPICalled\", (params) => {\n if (params[\"type\"] !== \"error\" && params[\"type\"] !== \"assert\") return;\n const args = (params[\"args\"] as { value?: unknown; description?: string }[]) ?? [];\n problems.push(`console.error: ${args.map((arg) => String(arg.value ?? arg.description ?? \"\")).join(\" \").slice(0, 300)}`);\n });\n page.on(\"Network.requestWillBeSent\", (params) => {\n urls.set(String(params[\"requestId\"]), String((params[\"request\"] as { url: string }).url));\n });\n page.on(\"Network.responseReceived\", (params) => {\n const response = params[\"response\"] as { url: string; status: number };\n if (response.status >= 400 && own(response.url)) problems.push(`HTTP ${response.status}: ${response.url}`);\n });\n page.on(\"Network.loadingFailed\", (params) => {\n const url = urls.get(String(params[\"requestId\"])) ?? \"\";\n // A script of any origin counts: without it, the page does not work.\n if ((own(url) || params[\"type\"] === \"Script\") && params[\"canceled\"] !== true) problems.push(`request failed (${String(params[\"errorText\"])}): ${url}`);\n });\n return {\n reset: () => {\n problems = [];\n },\n problems: () => [...new Set(problems)],\n };\n}\n\ninterface ViewInput {\n target: string;\n path: string;\n width: number;\n scheme: \"light\" | \"dark\";\n withProbe: boolean;\n options: CheckOptions;\n}\n\nasync function checkView(page: Page, events: Events, input: ViewInput): Promise<ViewResult> {\n const { target, path, width, scheme, withProbe, options } = input;\n const view: ViewResult = { page: path, width, scheme, ok: false, problems: [] };\n try {\n await page.send(\"Emulation.setDeviceMetricsOverride\", { width, height: 900, deviceScaleFactor: 1, mobile: width < 768 });\n await page.send(\"Emulation.setEmulatedMedia\", { features: [{ name: \"prefers-color-scheme\", value: scheme }] });\n events.reset();\n const loaded = page.once(\"Page.loadEventFired\", options.timeoutMs);\n const navigation = (await page.send(\"Page.navigate\", { url: target })) as { errorText?: string };\n if (navigation.errorText) {\n loaded.catch(() => {});\n throw new Error(`the page did not open: ${navigation.errorText}`);\n }\n await loaded;\n await page.evaluate(\"document.fonts ? document.fonts.ready.then(() => true) : true\");\n await new Promise((done) => setTimeout(done, options.settleMs));\n const measure = await page.evaluate<Measure>(MEASURE);\n view.measure = measure;\n // The screen width, not innerWidth: a phone browser can zoom out a page that is too wide.\n if (measure.scrollWidth > width) view.problems.push(`the page is wider than the screen: ${measure.scrollWidth} > ${width}`);\n for (const past of measure.past) view.problems.push(`past the right edge: ${past}`);\n for (const clipped of measure.clipped) view.problems.push(`a text area hides its text: ${clipped}`);\n if (measure.text === 0) view.problems.push(\"the page shows no text\");\n if (options.shots) {\n const file = join(options.shots, `${pageSlug(path)}-${width}-${scheme}.png`);\n const height = Math.min(Math.max(measure.height, 1), 8000);\n const shot = (await page.send(\"Page.captureScreenshot\", { format: \"png\", captureBeyondViewport: true, clip: { x: 0, y: 0, width, height, scale: 1 } })) as { data: string };\n writeFileSync(file, Buffer.from(shot.data, \"base64\"));\n view.shot = file;\n }\n if (withProbe && options.probe !== undefined) {\n const checks = (await page.evaluate<ProbeCheck[] | undefined>(probeExpression(options.probe))) ?? [];\n view.probe = checks;\n if (checks.length === 0) view.problems.push(\"the probe made no check\");\n for (const check of checks.filter((entry) => !entry.ok)) view.problems.push(`probe: ${check.name}${check.detail ? ` (${check.detail})` : \"\"}`);\n }\n } catch (error) {\n view.problems.push(error instanceof Error ? error.message : String(error));\n }\n view.problems.push(...events.problems());\n view.ok = view.problems.length === 0;\n return view;\n}\n\n/** This function makes a text safe for a cell of a Markdown table. It escapes `\\`, `|`, `<` and `>`. It changes each new line to a space. */\nexport function cell(text: string): string {\n return text.replace(/\\\\/g, \"\\\\\\\\\").replace(/\\|/g, \"\\\\|\").replace(/</g, \"<\").replace(/>/g, \">\").replace(/\\r?\\n/g, \" \");\n}\n\n/** This function gives the Markdown summary of a result, for the job summary of a workflow. */\nexport function summary(result: CheckResult): string {\n const failed = result.views.filter((view) => !view.ok);\n const lines = [\n `### Site check: ${result.ok ? \"passed\" : \"failed\"}`,\n \"\",\n `${result.views.length - failed.length} of ${result.views.length} views passed, at ${result.url}.`,\n \"\",\n \"| Page | Width | Scheme | Result | Problems |\",\n \"| --- | ---: | --- | --- | --- |\",\n ...result.views.map(\n (view) =>\n `| \\`${view.page}\\` | ${view.width} | ${view.scheme} | ${view.ok ? \"pass\" : \"**fail**\"}${view.probe ? ` (probe ${view.probe.filter((c) => c.ok).length}/${view.probe.length})` : \"\"} | ${view.problems.map(cell).join(\"<br>\")} |`,\n ),\n \"\",\n ];\n return lines.join(\"\\n\");\n}\n\n/** The input and output of the CLI. */\nexport interface CheckIo {\n cwd: string;\n env: NodeJS.ProcessEnv;\n stdout: (text: string) => void;\n stderr: (text: string) => void;\n}\n\nexport const CHECK_HELP = `Usage: site-pages check (--out <dir> | --url <url>) [options]\n\nOpens the pages of the site in a headless Chromium at phone and desktop widths, in the light and\nthe dark scheme, and fails when a page is wider than the screen, an element goes past the right\nedge, a text area hides its text, a page shows no text, or on a console error, an exception, a\nfailed request of the site, a script that does not load or a failed check of the probe.\n\nOptions:\n --out <dir> The built site folder. The check serves it at --base.\n --url <url> The URL of a live site home, in place of --out\n --repo <owner/name> The GitHub repository, for the default base (default: $GITHUB_REPOSITORY)\n --base </name/> The path of the site home (default: \"/<name>/\", or \"/\" for <owner>.github.io)\n --pages <paths> The pages, from the site home, separated by commas (default: /)\n --widths <px> The widths, separated by commas (default: 360,400,1280)\n --schemes <list> light, dark or both (default: light,dark)\n --probe <file> A JavaScript file: the body of an async function with check(name, ok, detail),\n wait, until, $, $$, type and click. It runs on the first page, in the first\n scheme, at each width. A .ts, .mts or .cts file is TypeScript: the check\n removes its types and its line export {}.\n --shots <dir> The folder for the screenshots, report.json and summary.md\n --browser <file> The Chromium executable (default: $SITE_CHECK_BROWSER, the Chromium of\n Playwright, else Chrome or Edge)\n --settle <ms> The wait after the load event (default: 1500)\n --timeout <ms> The time limit of a page load (default: 30000)\n --help Shows this text\n\nWith $GITHUB_STEP_SUMMARY, the check adds its table to the job summary.\n`;\n\n/** This function runs the `check` command with its arguments, without the word `check`. */\nexport async function runCheckCli(argv: readonly string[], io: CheckIo): Promise<number> {\n let values: Record<string, string | boolean | undefined>;\n try {\n ({ values } = parseArgs({\n args: [...argv],\n allowPositionals: false,\n options: {\n out: { type: \"string\" },\n url: { type: \"string\" },\n repo: { type: \"string\" },\n base: { type: \"string\" },\n pages: { type: \"string\" },\n widths: { type: \"string\" },\n schemes: { type: \"string\" },\n probe: { type: \"string\" },\n shots: { type: \"string\" },\n browser: { type: \"string\" },\n settle: { type: \"string\" },\n timeout: { type: \"string\" },\n help: { type: \"boolean\" },\n },\n }));\n } catch (error) {\n io.stderr(`site-pages check: ${error instanceof Error ? error.message : String(error)}\\n\\n${CHECK_HELP}`);\n return 2;\n }\n if (values[\"help\"]) {\n io.stdout(CHECK_HELP);\n return 0;\n }\n const usage = (message: string) => {\n io.stderr(`site-pages check: ${message}\\n\\n${CHECK_HELP}`);\n return 2;\n };\n const text = (key: string) => (typeof values[key] === \"string\" ? (values[key] as string) : undefined);\n const list = (key: string, fallback: string) => (text(key) ?? fallback).split(\",\").map((entry) => entry.trim()).filter(Boolean);\n const out = text(\"out\");\n const url = text(\"url\");\n if ((out === undefined) === (url === undefined)) return usage(\"give --out or --url, not both\");\n const widths = list(\"widths\", \"360,400,1280\").map(Number);\n if (widths.length === 0 || widths.some((width) => !Number.isInteger(width) || width < 200 || width > 4000)) return usage(\"--widths must be whole numbers from 200 to 4000\");\n const schemes = list(\"schemes\", \"light,dark\");\n if (schemes.length === 0 || schemes.some((scheme) => scheme !== \"light\" && scheme !== \"dark\")) return usage(\"--schemes must be light, dark or both\");\n const pages = list(\"pages\", \"/\");\n if (pages.length === 0) return usage(\"--pages has no page\");\n // Git Bash changes a bare \"/\" into \"C:/Program Files/Git/\". A page is a path, with no scheme or drive.\n const notPath = pages.find((entry) => entry.includes(\":\") || entry.includes(\"\\\\\"));\n if (notPath !== undefined) return usage(`a page is a path from the site home, not \"${notPath}\" (in Git Bash, set MSYS_NO_PATHCONV=1)`);\n const settleMs = Number(text(\"settle\") ?? 1500);\n const timeoutMs = Number(text(\"timeout\") ?? 30000);\n if (!(settleMs >= 0) || !(timeoutMs > 0)) return usage(\"--settle and --timeout must be numbers of milliseconds\");\n let base = text(\"base\");\n if (out !== undefined && base === undefined) {\n const name = (text(\"repo\") ?? io.env[\"GITHUB_REPOSITORY\"] ?? \"\").split(\"/\")[1];\n if (!name) return usage(\"give --base, --repo or set GITHUB_REPOSITORY\");\n base = /\\.github\\.io$/i.test(name) ? \"/\" : `/${name}/`;\n }\n if (base !== undefined) base = `/${base.replace(/^\\/+|\\/+$/g, \"\")}/`.replace(/^\\/\\/$/, \"/\");\n const at = (file: string) => (isAbsolute(file) ? file : resolve(io.cwd, file));\n const probeFile = text(\"probe\");\n let probe: string | undefined;\n if (probeFile !== undefined) {\n try {\n probe = readFileSync(at(probeFile), \"utf8\");\n } catch (error) {\n return usage(`the probe file ${probeFile} cannot be read: ${error instanceof Error ? error.message : String(error)}`);\n }\n if (TYPESCRIPT_PROBE.test(probeFile)) {\n try {\n probe = probeScript(probe);\n } catch (error) {\n return usage(`the types of the probe file ${probeFile} cannot be removed: ${error instanceof Error ? error.message : String(error)}`);\n }\n }\n }\n const shots = text(\"shots\");\n const result = await checkSite({\n out: out === undefined ? undefined : at(out),\n base,\n url,\n pages,\n widths,\n schemes: schemes as (\"light\" | \"dark\")[],\n probe,\n shots: shots === undefined ? undefined : at(shots),\n browser: text(\"browser\"),\n settleMs,\n timeoutMs,\n log: (line) => io.stdout(`${line}\\n`),\n });\n const failed = result.views.filter((view) => !view.ok).length;\n io.stdout(`site-pages check: ${result.views.length - failed} of ${result.views.length} views passed (${result.browser}).\\n`);\n const step = io.env[\"GITHUB_STEP_SUMMARY\"];\n if (step) appendFileSync(step, summary(result));\n return result.ok ? 0 : 1;\n}\n","#!/usr/bin/env node\n// The bin `site-pages`. The work is in build.ts and check/check.ts, so a test can run the same CLI with its own fetch.\nimport { runCli } from \"./build.ts\";\nimport { runCheckCli } from \"./check/check.ts\";\n\nconst argv = process.argv.slice(2);\nconst io = {\n cwd: process.cwd(),\n env: process.env,\n stdout: (text: string) => process.stdout.write(text),\n stderr: (text: string) => process.stderr.write(text),\n};\n(argv[0] === \"check\" ? runCheckCli(argv.slice(1), io) : runCli(argv, io)).then(\n (code) => {\n process.exitCode = code;\n },\n (error: unknown) => {\n process.stderr.write(`site-pages: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}\\n`);\n process.exitCode = 1;\n },\n);\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cli.js","names":[],"sources":["../src/check/browser.ts","../src/check/measure.ts","../src/check/serve.ts","../src/check/check.ts","../src/cli.ts"],"sourcesContent":["/**\n * A small client of the Chrome DevTools protocol for the `check` command. It starts a Chromium\n * browser (Chrome, Edge or the Chromium of Playwright) in headless mode and drives one page. It\n * uses only Node.js, so the check needs no package install.\n */\n\nimport { spawn, type ChildProcess } from \"node:child_process\";\nimport { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync } from \"node:fs\";\nimport { homedir, tmpdir } from \"node:os\";\nimport { delimiter, join } from \"node:path\";\n\n/** The paths that `findBrowser` examines after the option and the Playwright folder. */\nfunction systemBrowsers(platform: NodeJS.Platform, env: NodeJS.ProcessEnv): string[] {\n if (platform === \"win32\") {\n const roots = [env[\"PROGRAMFILES\"], env[\"PROGRAMFILES(X86)\"], env[\"LOCALAPPDATA\"]].filter((root): root is string => !!root);\n return roots.flatMap((root) => [join(root, \"Google/Chrome/Application/chrome.exe\"), join(root, \"Microsoft/Edge/Application/msedge.exe\")]);\n }\n if (platform === \"darwin\") {\n return [\"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome\", \"/Applications/Chromium.app/Contents/MacOS/Chromium\", \"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge\"];\n }\n const names = [\"google-chrome\", \"google-chrome-stable\", \"chromium\", \"chromium-browser\", \"microsoft-edge\"];\n const dirs = (env[\"PATH\"] ?? \"\").split(delimiter).filter(Boolean);\n return names.flatMap((name) => dirs.map((dir) => join(dir, name)));\n}\n\n/** The Chromium of the newest `chromium-<n>` folder of Playwright, or undefined. */\nfunction playwrightChromium(platform: NodeJS.Platform, env: NodeJS.ProcessEnv): string | undefined {\n const root =\n env[\"PLAYWRIGHT_BROWSERS_PATH\"] ||\n (platform === \"win32\" ? join(env[\"LOCALAPPDATA\"] ?? homedir(), \"ms-playwright\") : platform === \"darwin\" ? join(homedir(), \"Library/Caches/ms-playwright\") : join(homedir(), \".cache/ms-playwright\"));\n if (!existsSync(root)) return undefined;\n const builds = readdirSync(root)\n .map((name) => /^chromium-(\\d+)$/.exec(name))\n .filter((match): match is RegExpExecArray => match !== null)\n .sort((a, b) => Number(b[1]) - Number(a[1]));\n for (const [folder] of builds) {\n for (const sub of readdirSync(join(root, folder))) {\n const candidates =\n platform === \"win32\"\n ? [join(root, folder, sub, \"chrome.exe\")]\n : platform === \"darwin\"\n ? [join(root, folder, sub, \"Chromium.app/Contents/MacOS/Chromium\"), join(root, folder, sub, \"Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing\")]\n : [join(root, folder, sub, \"chrome\")];\n const found = candidates.find((file) => existsSync(file));\n if (found) return found;\n }\n }\n return undefined;\n}\n\n/**\n * This function finds a Chromium browser. It examines the given path, then `$SITE_CHECK_BROWSER`,\n * then the newest Chromium of Playwright, then a Chrome or an Edge of the system.\n *\n * @returns the path of the executable, or undefined when there is none\n */\nexport function findBrowser(given?: string, env: NodeJS.ProcessEnv = process.env, platform: NodeJS.Platform = process.platform): string | undefined {\n const named = given || env[\"SITE_CHECK_BROWSER\"];\n if (named) return named;\n return playwrightChromium(platform, env) ?? systemBrowsers(platform, env).find((file) => existsSync(file));\n}\n\n/** A message of the DevTools protocol. */\ninterface Message {\n id?: number;\n method?: string;\n params?: Record<string, unknown>;\n result?: Record<string, unknown>;\n error?: { message: string };\n}\n\n/** An event handler of a page. */\ntype Listener = (params: Record<string, unknown>) => void;\n\n/** One page of a started browser. */\nexport interface Page {\n /** This function sends a command of the protocol and returns its result. */\n send(method: string, params?: Record<string, unknown>): Promise<Record<string, unknown>>;\n /** This function calls the listener on each event of the method. */\n on(method: string, listener: Listener): void;\n /** This function calls the listener on the next event of the method, or rejects after `ms`. */\n once(method: string, ms: number): Promise<Record<string, unknown>>;\n /** This function evaluates the expression in the page, and waits for a promise. */\n evaluate<T>(expression: string): Promise<T>;\n}\n\n/** A started browser. */\nexport interface Browser {\n page: Page;\n /** This function stops the browser and removes its profile. */\n close(): Promise<void>;\n}\n\nconst sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));\n\n/**\n * This function starts the browser in headless mode with a new profile, and connects to its page.\n * The browser picks a free port, so two checks can run at the same time.\n *\n * @throws Error when the browser does not start in `timeoutMs`\n */\nexport async function launch(executable: string, timeoutMs = 30_000): Promise<Browser> {\n const profile = mkdtempSync(join(tmpdir(), \"site-check-\"));\n const args = [\n \"--headless=new\",\n \"--remote-debugging-port=0\",\n `--user-data-dir=${profile}`,\n \"--no-first-run\",\n \"--no-default-browser-check\",\n \"--disable-gpu\",\n \"--disable-extensions\",\n \"--disable-background-networking\",\n \"--disable-sync\",\n \"--hide-scrollbars\",\n \"--mute-audio\",\n // The sandbox of Chromium needs user namespaces, which Ubuntu 24.04 refuses to a browser that the\n // system did not install. Playwright also starts Chromium with no sandbox.\n ...(process.platform === \"linux\" ? [\"--no-sandbox\"] : []),\n \"about:blank\",\n ];\n const child: ChildProcess = spawn(executable, args, { stdio: \"ignore\" });\n let exited: string | undefined;\n child.once(\"exit\", (code, signal) => (exited = `the browser stopped (code ${code}, signal ${signal})`));\n child.once(\"error\", (error) => (exited = `the browser did not start: ${error.message}`));\n const stop = async () => {\n if (child.exitCode === null && child.signalCode === null) {\n child.kill();\n await Promise.race([new Promise((resolve) => child.once(\"exit\", resolve)), sleep(5000)]);\n }\n rmSync(profile, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 });\n };\n try {\n const portFile = join(profile, \"DevToolsActivePort\");\n const end = Date.now() + timeoutMs;\n let port = \"\";\n while (port === \"\") {\n if (exited !== undefined) throw new Error(`${executable}: ${exited}`);\n if (Date.now() > end) throw new Error(`${executable}: no DevTools port after ${timeoutMs} ms`);\n if (existsSync(portFile)) port = readFileSync(portFile, \"utf8\").split(\"\\n\")[0]?.trim() ?? \"\";\n if (port === \"\") await sleep(100);\n }\n let target: { type: string; webSocketDebuggerUrl: string } | undefined;\n while (target === undefined) {\n const list = (await (await fetch(`http://127.0.0.1:${port}/json/list`)).json()) as { type: string; webSocketDebuggerUrl: string }[];\n target = list.find((entry) => entry.type === \"page\");\n if (target === undefined) {\n if (Date.now() > end) throw new Error(`${executable}: no page target after ${timeoutMs} ms`);\n await sleep(100);\n }\n }\n const page = await connect(target.webSocketDebuggerUrl);\n return {\n page: page.page,\n close: async () => {\n page.close();\n await stop();\n },\n };\n } catch (error) {\n await stop();\n throw error;\n }\n}\n\nasync function connect(url: string): Promise<{ page: Page; close: () => void }> {\n const socket = new WebSocket(url);\n await new Promise<void>((resolve, reject) => {\n socket.addEventListener(\"open\", () => resolve(), { once: true });\n socket.addEventListener(\"error\", () => reject(new Error(`no connection to ${url}`)), { once: true });\n });\n let sequence = 0;\n const pending = new Map<number, { resolve: (value: Record<string, unknown>) => void; reject: (error: Error) => void }>();\n const listeners = new Map<string, Listener[]>();\n socket.addEventListener(\"message\", (event) => {\n const message = JSON.parse(String(event.data)) as Message;\n if (message.id !== undefined) {\n const call = pending.get(message.id);\n pending.delete(message.id);\n if (message.error) call?.reject(new Error(message.error.message));\n else call?.resolve(message.result ?? {});\n } else if (message.method) {\n for (const listener of listeners.get(message.method) ?? []) listener(message.params ?? {});\n }\n });\n socket.addEventListener(\"close\", () => {\n for (const call of pending.values()) call.reject(new Error(\"the browser closed the connection\"));\n pending.clear();\n });\n const page: Page = {\n send: (method, params = {}) =>\n new Promise((resolve, reject) => {\n const id = ++sequence;\n pending.set(id, { resolve, reject });\n socket.send(JSON.stringify({ id, method, params }));\n }),\n on: (method, listener) => listeners.set(method, [...(listeners.get(method) ?? []), listener]),\n once: (method, ms) =>\n new Promise((resolve, reject) => {\n const timer = setTimeout(() => {\n remove();\n reject(new Error(`no ${method} after ${ms} ms`));\n }, ms);\n const listener: Listener = (params) => {\n clearTimeout(timer);\n remove();\n resolve(params);\n };\n const remove = () => listeners.set(method, (listeners.get(method) ?? []).filter((entry) => entry !== listener));\n page.on(method, listener);\n }),\n evaluate: async <T>(expression: string): Promise<T> => {\n const reply = (await page.send(\"Runtime.evaluate\", { expression, awaitPromise: true, returnByValue: true })) as {\n result?: { value?: unknown };\n exceptionDetails?: { text?: string; exception?: { description?: string } };\n };\n if (reply.exceptionDetails) throw new Error(reply.exceptionDetails.exception?.description ?? reply.exceptionDetails.text ?? \"the expression failed\");\n return reply.result?.value as T;\n },\n };\n return { page, close: () => socket.close() };\n}\n","/**\n * The measurements that the `check` command makes in the page. Each is a string of JavaScript that\n * the browser evaluates, so the code can use only the APIs of the page.\n */\n\n/** Text past the edge of a box that cuts it: an entry of `Measure.cut`. */\nexport interface Cut {\n /**\n * The kind of the box:\n * - `clip`: the box has `overflow-x: hidden` or `clip`, so a visitor cannot reach the text,\n * - `table`: the box scrolls, and the text is in a table.\n */\n kind: \"clip\" | \"table\";\n /** The box. */\n box: string;\n /** The table, or the element of the text. */\n element: string;\n /** The width of the content of the box, in CSS pixels. */\n scrollWidth: number;\n /** The visible width of the box, in CSS pixels. */\n clientWidth: number;\n /** The start of the first text past the edge. */\n text: string;\n}\n\n/** The result of `MEASURE`. */\nexport interface Measure {\n innerWidth: number;\n scrollWidth: number;\n /** The height of the document, for the screenshot. */\n height: number;\n /** The number of characters of visible text. */\n text: number;\n /** The outermost elements that go past the right edge of the screen, at most 8. */\n past: string[];\n /** The text areas that hide a part of their text, at most 8. */\n clipped: string[];\n /** The boxes that cut text at their right edge, at most 8. `cutProblems` of `check.ts` tells which ones are problems. */\n cut: Cut[];\n}\n\n/**\n * This expression measures the page. An element counts as past the edge when its right side is\n * more than half a pixel past the width of the document. These elements do not count:\n * - an element in a container that scrolls or clips (`overflow-x` other than `visible`, below `body`),\n * - an element of zero size, a hidden element, or a fixed element that is fully past the edge (a closed drawer),\n * - an element in `.sr`, `.sr-only`, `.visually-hidden` or `.skip` (text for screen readers and skip links),\n * - an element in a `pre`, other than the `pre` itself.\n * Of a chain of elements past the edge, only the outermost one counts.\n *\n * `past` skips each element in a container that scrolls or clips, so `cut` examines those containers.\n * A box is the nearest element around a text, below `body`, with `overflow-x` other than `visible`. Its content is wider\n * than the box by more than 1 px. An element with `overflow-x: clip` is always a box, because it can report no overflow.\n * The text counts when a line of it ends more than 1 px past the right edge of the box.\n *\n * A box that scrolls counts only for text in a table (`table`, `role=\"table\"` or `role=\"grid\"`).\n * A code block, a hex view, a row of tabs and a carousel in such a box are meant to scroll.\n * A box that clips counts for each text, because a visitor cannot reach that text. These texts do not count:\n * - a text in a box with `text-overflow` other than `clip` (a person chose the ellipsis),\n * - a text in an element with `data-overflow-ok`, or in that element itself (a person chose the overflow),\n * - a text in a box with the class `scroll-cue` that scrolls (a shade shows the visitor that it scrolls),\n * - a text in `.sr`, `.sr-only`, `.visually-hidden`, `.skip`, `[aria-hidden=\"true\"]`, `select`, `textarea` or `svg`,\n * - a text in a fixed element, which the box does not clip,\n * - a text that is not visible, or with an element around it of 1 px or less or with `opacity: 0`.\n * Only the first text of a box counts. An element with no text does not count. Vertical overflow does not count.\n */\nexport const MEASURE = `(() => {\n const root = document.documentElement;\n const w = root.clientWidth;\n const name = (e) => e.tagName.toLowerCase() + (e.id ? \"#\" + e.id : \"\") + (typeof e.className === \"string\" && e.className.trim() ? \".\" + e.className.trim().split(/\\\\s+/).slice(0, 3).join(\".\") : \"\");\n const inScroller = (e) => {\n for (let p = e.parentElement; p && p !== document.body && p !== root; p = p.parentElement) {\n if (getComputedStyle(p).overflowX !== \"visible\") return true;\n }\n return false;\n };\n const counts = (e) => {\n const r = e.getBoundingClientRect();\n if (r.width === 0 || r.height === 0 || r.right <= w + 0.5) return false;\n if (e.closest(\".sr,.sr-only,.visually-hidden,.skip\")) return false;\n if (e.closest(\"pre\") && e.tagName !== \"PRE\") return false;\n const s = getComputedStyle(e);\n if (s.visibility === \"hidden\" || s.display === \"none\") return false;\n if (s.position === \"fixed\" && r.left >= w) return false;\n return !inScroller(e);\n };\n const over = new Set([...document.querySelectorAll(\"body *\")].filter(counts));\n const past = [...over]\n .filter((e) => { for (let p = e.parentElement; p; p = p.parentElement) if (over.has(p)) return false; return true; })\n .map((e) => name(e) + \" right=\" + Math.round(e.getBoundingClientRect().right));\n const clipped = [...document.querySelectorAll(\"textarea\")]\n .filter((e) => { const s = getComputedStyle(e); return (s.overflowY === \"hidden\" || s.overflowY === \"clip\") && e.getBoundingClientRect().height > 0 && e.scrollHeight > e.clientHeight + 1; })\n .map((e) => name(e) + \" \" + e.scrollHeight + \">\" + e.clientHeight);\n const boxOf = (e) => {\n for (let p = e; p && p !== document.body && p !== root; p = p.parentElement) {\n const s = getComputedStyle(p);\n if (s.overflowX === \"clip\" || (s.overflowX !== \"visible\" && p.scrollWidth > p.clientWidth + 1)) return p;\n if (s.position === \"fixed\") return undefined;\n }\n return undefined;\n };\n const shown = (e) => {\n if (e.closest(\".sr,.sr-only,.visually-hidden,.skip,[aria-hidden=true],select,textarea,svg\")) return false;\n if (getComputedStyle(e).visibility !== \"visible\") return false;\n for (let p = e; p && p !== root; p = p.parentElement) {\n const s = getComputedStyle(p);\n if (s.opacity === \"0\") return false;\n if (s.display === \"contents\") continue;\n const r = p.getBoundingClientRect();\n if (r.width <= 1 || r.height <= 1) return false;\n }\n return true;\n };\n const cuts = new Map();\n const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);\n const range = document.createRange();\n for (let t = walker.nextNode(); t; t = walker.nextNode()) {\n const e = t.parentElement;\n if (!e || !t.data.trim()) continue;\n const box = boxOf(e);\n if (!box || cuts.has(box)) continue;\n const s = getComputedStyle(box);\n if (s.textOverflow !== \"clip\") continue;\n const scrolls = s.overflowX === \"auto\" || s.overflowX === \"scroll\";\n const table = e.closest(\"table,[role=table],[role=grid]\");\n if (scrolls && !(table && box.contains(table))) continue;\n const left = box.getBoundingClientRect().left + box.clientLeft;\n range.selectNodeContents(t);\n const end = Math.max(...[...range.getClientRects()].filter((r) => r.width > 0).map((r) => r.right));\n if (end <= left + box.clientWidth + 1) continue;\n if (e.closest(\"[data-overflow-ok]\") || (scrolls && box.classList.contains(\"scroll-cue\")) || !shown(e)) continue;\n cuts.set(box, { kind: scrolls ? \"table\" : \"clip\", box: name(box), element: name(scrolls ? table : e), scrollWidth: Math.max(box.scrollWidth, Math.ceil(end - left)), clientWidth: box.clientWidth, text: t.data.trim().replace(/\\\\s+/g, \" \").slice(0, 40) });\n }\n return {\n innerWidth,\n scrollWidth: root.scrollWidth,\n height: Math.max(root.scrollHeight, document.body ? document.body.scrollHeight : 0),\n text: (document.body ? document.body.innerText : \"\").trim().length,\n past: past.slice(0, 8),\n clipped: clipped.slice(0, 8),\n cut: [...cuts.values()].slice(0, 8),\n };\n})()`;\n\n/** One check of a probe. */\nexport interface ProbeCheck {\n name: string;\n ok: boolean;\n detail?: string;\n}\n\n/**\n * This function wraps the code of a probe file into an expression. The code is the body of an async\n * function with these names:\n * - `check(name, ok, detail)` records a check,\n * - `wait(ms)` waits,\n * - `until(fn, ms)` waits until `fn()` gives a true value, at most `ms` (default 5000), and returns the value,\n * - `$(selector)` and `$$(selector)` find elements,\n * - `type(element, text)` writes the text into an input or a text area, with an `input` event,\n * - `click(element)` clicks an element, or the first element of a selector.\n * An exception stops the probe, and it counts as a failed check.\n */\nexport function probeExpression(code: string): string {\n return `(async () => {\n const __checks = [];\n const __text = (v) => { if (v === undefined) return undefined; if (typeof v === \"string\") return v; try { return JSON.stringify(v); } catch { return String(v); } };\n const check = (name, ok, detail) => { __checks.push({ name: String(name), ok: !!ok, ...(ok ? {} : { detail: __text(detail) }) }); return !!ok; };\n const wait = (ms) => new Promise((r) => setTimeout(r, ms));\n const until = async (fn, ms = 5000) => { const end = Date.now() + ms; for (;;) { const v = await fn(); if (v) return v; if (Date.now() > end) return v; await wait(50); } };\n const $ = (s) => document.querySelector(s);\n const $$ = (s) => [...document.querySelectorAll(s)];\n const __el = (e) => { const el = typeof e === \"string\" ? document.querySelector(e) : e; if (!el) throw new Error(\"no element: \" + e); return el; };\n const type = (e, text) => { const el = __el(e); el.focus(); el.value = text; el.dispatchEvent(new Event(\"input\", { bubbles: true })); el.dispatchEvent(new Event(\"change\", { bubbles: true })); };\n const click = (e) => __el(e).click();\n try {\n await (async () => {\n${code}\n })();\n } catch (e) {\n __checks.push({ name: \"the probe runs to its end\", ok: false, detail: String((e && e.stack) || e) });\n }\n return __checks;\n})()`;\n}\n","/**\n * A static file server for the `check` command. It serves a built site folder at the path of the\n * site home, as GitHub Pages does. Thus the links of the site work as on the live site.\n */\n\nimport { createReadStream, existsSync, statSync } from \"node:fs\";\nimport { createServer, type Server } from \"node:http\";\nimport { extname, join, normalize, resolve, sep } from \"node:path\";\n\nconst TYPES: Record<string, string> = {\n \".html\": \"text/html; charset=utf-8\",\n \".js\": \"text/javascript; charset=utf-8\",\n \".mjs\": \"text/javascript; charset=utf-8\",\n \".css\": \"text/css; charset=utf-8\",\n \".json\": \"application/json; charset=utf-8\",\n \".svg\": \"image/svg+xml\",\n \".png\": \"image/png\",\n \".jpg\": \"image/jpeg\",\n \".jpeg\": \"image/jpeg\",\n \".gif\": \"image/gif\",\n \".webp\": \"image/webp\",\n \".avif\": \"image/avif\",\n \".ico\": \"image/x-icon\",\n \".woff\": \"font/woff\",\n \".woff2\": \"font/woff2\",\n \".ttf\": \"font/ttf\",\n \".otf\": \"font/otf\",\n \".wasm\": \"application/wasm\",\n \".txt\": \"text/plain; charset=utf-8\",\n \".md\": \"text/markdown; charset=utf-8\",\n \".xml\": \"application/xml\",\n \".webmanifest\": \"application/manifest+json\",\n \".map\": \"application/json; charset=utf-8\",\n};\n\n/**\n * This function maps the path of a request to a file of the folder, as GitHub Pages does. A folder\n * gives its `index.html`. For a path with no extension, the function also tries `<path>.html`.\n *\n * @returns the absolute file, or undefined when the path is outside the base or the file is missing\n */\nexport function resolveFile(root: string, base: string, pathname: string): string | undefined {\n let decoded: string;\n try {\n decoded = decodeURIComponent(pathname);\n } catch {\n return undefined;\n }\n const home = base.endsWith(\"/\") ? base : `${base}/`;\n if (decoded === home.slice(0, -1)) decoded = home;\n if (!decoded.startsWith(home)) return undefined;\n const top = resolve(root);\n const file = normalize(join(top, decoded.slice(home.length)));\n if (file !== top && !file.startsWith(top + sep)) return undefined;\n const candidates = [file, join(file, \"index.html\"), `${file}.html`];\n return candidates.find((candidate) => existsSync(candidate) && statSync(candidate).isFile());\n}\n\n/** A started server. */\nexport interface StaticServer {\n /** The URL of the site home, for example `http://127.0.0.1:50123/client/`. */\n url: string;\n close(): Promise<void>;\n}\n\n/**\n * This function serves the folder at `base` on a free port of 127.0.0.1. A missing file gives the\n * status 404 with the `404.html` of the folder, when it has one.\n */\nexport async function serve(root: string, base: string): Promise<StaticServer> {\n const server: Server = createServer((request, response) => {\n const pathname = new URL(request.url ?? \"/\", \"http://localhost\").pathname;\n const home = base.endsWith(\"/\") ? base : `${base}/`;\n if (pathname === home.slice(0, -1) && home !== \"/\") {\n response.writeHead(301, { location: home }).end();\n return;\n }\n const file = resolveFile(root, base, pathname);\n const missing = join(resolve(root), \"404.html\");\n const send = file ?? (existsSync(missing) ? missing : undefined);\n if (send === undefined) {\n response.writeHead(404, { \"content-type\": \"text/plain; charset=utf-8\" }).end(`404: ${pathname}\\n`);\n return;\n }\n response.writeHead(file ? 200 : 404, { \"content-type\": TYPES[extname(send).toLowerCase()] ?? \"application/octet-stream\", \"cache-control\": \"no-store\" });\n createReadStream(send).pipe(response);\n });\n await new Promise<void>((done, fail) => {\n server.once(\"error\", fail);\n server.listen(0, \"127.0.0.1\", () => done());\n });\n const address = server.address();\n if (address === null || typeof address === \"string\") throw new Error(\"the server has no port\");\n return {\n url: `http://127.0.0.1:${address.port}${base.endsWith(\"/\") ? base : `${base}/`}`,\n close: () => new Promise((done) => server.close(() => done())),\n };\n}\n","/**\n * The `site-pages check` command. It opens the pages of a built site (or of a live URL) in a\n * headless Chromium at phone and desktop widths, in the light and the dark scheme.\n *\n * A view fails when the page is wider than the screen or an element goes past the right edge. It\n * also fails when a text area hides its text, a box cuts text (`cutProblems`) or the page shows no text. A console error, an\n * exception and a failed request of the site also fail it. A script that does not load and a failed check of the probe\n * fail it too. The command\n * writes a screenshot of each view.\n */\n\nimport { appendFileSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { isAbsolute, join, resolve } from \"node:path\";\nimport { parseArgs } from \"node:util\";\nimport { findBrowser, launch, type Page } from \"./browser.ts\";\nimport { MEASURE, probeExpression, type Cut, type Measure, type ProbeCheck } from \"./measure.ts\";\nimport { probeScript, TYPESCRIPT_PROBE } from \"./probe.ts\";\nimport { serve } from \"./serve.ts\";\n\n/** A view narrower than this width in CSS pixels is a phone. */\nexport const PHONE_WIDTH = 768;\n\n/** The options of `checkSite`. */\nexport interface CheckOptions {\n /** The built site folder. Give `out` or `url`. */\n out?: string | undefined;\n /** The path of the site home, for `out`, for example `/client/`. */\n base?: string | undefined;\n /** The URL of a live site home. Give `out` or `url`. */\n url?: string | undefined;\n /** The pages, from the site home, for example `/` and `/releases/`. */\n pages: readonly string[];\n /** The widths in CSS pixels. A width under `PHONE_WIDTH` is a phone. */\n widths: readonly number[];\n /** The color schemes. */\n schemes: readonly (\"light\" | \"dark\")[];\n /** The code of a probe: the body of an async function (see `probeExpression`). It runs on the first page, in the light scheme, at each width. */\n probe?: string | undefined;\n /** The folder for the screenshots, `report.json` and `summary.md`. */\n shots?: string | undefined;\n /** The browser executable. */\n browser?: string | undefined;\n /** The time to wait after the load event, for the scripts of the page. */\n settleMs: number;\n /** The time limit of a page load. */\n timeoutMs: number;\n log?: ((line: string) => void) | undefined;\n}\n\n/** The result of one page at one width and scheme. */\nexport interface ViewResult {\n page: string;\n width: number;\n scheme: \"light\" | \"dark\";\n ok: boolean;\n /** What failed. */\n problems: string[];\n measure?: Measure | undefined;\n probe?: ProbeCheck[] | undefined;\n /** The file of the screenshot. */\n shot?: string | undefined;\n}\n\n/** The result of `checkSite`. */\nexport interface CheckResult {\n ok: boolean;\n url: string;\n browser: string;\n views: ViewResult[];\n}\n\n/**\n * This function gives the problems of the cuts of a view with the given width. A box that clips text is a problem at each width.\n * A table in a box that scrolls is a problem only on a phone. A phone shows no scroll bar, but a desktop shows one.\n * The summary and the log give the page of each problem.\n */\nexport function cutProblems(cuts: readonly Cut[], width: number): string[] {\n const widths = (cut: Cut) => `${cut.element} in ${cut.box}: content of ${cut.scrollWidth} px in a box of ${cut.clientWidth} px, \"${cut.text}\"`;\n return cuts.flatMap((cut) => {\n if (cut.kind === \"clip\") return [`a box cuts text at ${width} px: ${widths(cut)}. Make the content fit, or add data-overflow-ok to the box.`];\n if (width < PHONE_WIDTH) return [`a table goes out of view at ${width} px: ${widths(cut)}. Make the table fit, give the box the class scroll-cue, or add data-overflow-ok to the box.`];\n return [];\n });\n}\n\n/** This function gives the file name part of a page: `/` is `home`, `/releases/` is `releases`. */\nexport function pageSlug(page: string): string {\n const slug = page.replace(/^\\/+|\\/+$/g, \"\").replace(/[^A-Za-z0-9._-]+/g, \"_\");\n return slug === \"\" ? \"home\" : slug;\n}\n\n/**\n * This function checks the site. It starts one browser and, for `out`, one server, and stops both at\n * the end.\n *\n * @throws Error when there is no browser, or the browser does not start\n */\nexport async function checkSite(options: CheckOptions): Promise<CheckResult> {\n const log = options.log ?? (() => {});\n const executable = findBrowser(options.browser);\n if (executable === undefined) throw new Error(\"no Chromium browser was found: give --browser, set SITE_CHECK_BROWSER, or install the Chromium of Playwright\");\n if (options.pages.length === 0) throw new Error(\"no page to check\");\n const server = options.out !== undefined ? await serve(options.out, options.base ?? \"/\") : undefined;\n const home = server?.url ?? options.url;\n if (home === undefined) throw new Error(\"give out or url\");\n const homeUrl = new URL(home.endsWith(\"/\") ? home : `${home}/`);\n const browser = await launch(executable, options.timeoutMs);\n const views: ViewResult[] = [];\n try {\n const { page } = browser;\n const events = watch(page, homeUrl.origin);\n await Promise.all([page.send(\"Page.enable\"), page.send(\"Runtime.enable\"), page.send(\"Network.enable\")]);\n await page.send(\"Page.addScriptToEvaluateOnNewDocument\", { source: LOAD_WATCH });\n if (options.shots) mkdirSync(options.shots, { recursive: true });\n for (const [index, path] of options.pages.entries()) {\n const target = new URL(path.replace(/^\\/+/, \"\"), homeUrl).href;\n for (const width of options.widths) {\n for (const scheme of options.schemes) {\n const withProbe = options.probe !== undefined && index === 0 && scheme === options.schemes[0];\n const view = await checkView(page, events, { target, path, width, scheme, withProbe, options });\n views.push(view);\n log(`${view.ok ? \"PASS\" : \"FAIL\"} ${path} ${width} ${scheme}${view.measure ? ` scrollWidth=${view.measure.scrollWidth} innerWidth=${view.measure.innerWidth}` : \"\"}${view.probe ? ` probe ${view.probe.filter((c) => c.ok).length}/${view.probe.length}` : \"\"}${view.problems.length > 0 ? `\\n - ${view.problems.join(\"\\n - \")}` : \"\"}`);\n }\n }\n }\n } finally {\n await browser.close();\n await server?.close();\n }\n const result: CheckResult = { ok: views.length > 0 && views.every((view) => view.ok), url: homeUrl.href, browser: executable, views };\n if (options.shots) {\n writeFileSync(join(options.shots, \"report.json\"), `${JSON.stringify(result, null, 2)}\\n`);\n writeFileSync(join(options.shots, \"summary.md\"), summary(result));\n }\n return result;\n}\n\n/**\n * A script that each page starts before its own scripts. A script or a style sheet that does not load fires `error` on its\n * element. This script writes that event as a console error. Thus a module script whose imports do not load is a problem too.\n * Such a script throws no exception. Its failed request can go to another origin, for example `node:perf_hooks` in a page bundle.\n */\nexport const LOAD_WATCH = `addEventListener(\"error\", (event) => {\n const element = event.target;\n if (!(element instanceof HTMLScriptElement || element instanceof HTMLLinkElement)) return;\n console.error(\"failed to load \" + element.tagName.toLowerCase() + \": \" + (element.getAttribute(\"src\") ?? element.getAttribute(\"href\") ?? \"(inline)\"));\n}, true);`;\n\n/** The problems that the events of the page gave, since the last `reset`. */\ninterface Events {\n reset(): void;\n problems(): string[];\n}\n\nfunction watch(page: Page, origin: string): Events {\n let problems: string[] = [];\n const urls = new Map<string, string>();\n const own = (url: string) => {\n try {\n const parsed = new URL(url);\n // A browser asks for /favicon.ico on its own, also for a site under a path.\n return parsed.origin === origin && parsed.pathname !== \"/favicon.ico\";\n } catch {\n return false;\n }\n };\n page.on(\"Runtime.exceptionThrown\", (params) => {\n const details = params[\"exceptionDetails\"] as { text?: string; exception?: { description?: string } };\n problems.push(`exception: ${(details.exception?.description ?? details.text ?? \"\").split(\"\\n\")[0]}`);\n });\n page.on(\"Runtime.consoleAPICalled\", (params) => {\n if (params[\"type\"] !== \"error\" && params[\"type\"] !== \"assert\") return;\n const args = (params[\"args\"] as { value?: unknown; description?: string }[]) ?? [];\n problems.push(`console.error: ${args.map((arg) => String(arg.value ?? arg.description ?? \"\")).join(\" \").slice(0, 300)}`);\n });\n page.on(\"Network.requestWillBeSent\", (params) => {\n urls.set(String(params[\"requestId\"]), String((params[\"request\"] as { url: string }).url));\n });\n page.on(\"Network.responseReceived\", (params) => {\n const response = params[\"response\"] as { url: string; status: number };\n if (response.status >= 400 && own(response.url)) problems.push(`HTTP ${response.status}: ${response.url}`);\n });\n page.on(\"Network.loadingFailed\", (params) => {\n const url = urls.get(String(params[\"requestId\"])) ?? \"\";\n // A script of any origin counts: without it, the page does not work.\n if ((own(url) || params[\"type\"] === \"Script\") && params[\"canceled\"] !== true) problems.push(`request failed (${String(params[\"errorText\"])}): ${url}`);\n });\n return {\n reset: () => {\n problems = [];\n },\n problems: () => [...new Set(problems)],\n };\n}\n\ninterface ViewInput {\n target: string;\n path: string;\n width: number;\n scheme: \"light\" | \"dark\";\n withProbe: boolean;\n options: CheckOptions;\n}\n\nasync function checkView(page: Page, events: Events, input: ViewInput): Promise<ViewResult> {\n const { target, path, width, scheme, withProbe, options } = input;\n const view: ViewResult = { page: path, width, scheme, ok: false, problems: [] };\n try {\n await page.send(\"Emulation.setDeviceMetricsOverride\", { width, height: 900, deviceScaleFactor: 1, mobile: width < PHONE_WIDTH });\n await page.send(\"Emulation.setEmulatedMedia\", { features: [{ name: \"prefers-color-scheme\", value: scheme }] });\n events.reset();\n const loaded = page.once(\"Page.loadEventFired\", options.timeoutMs);\n const navigation = (await page.send(\"Page.navigate\", { url: target })) as { errorText?: string };\n if (navigation.errorText) {\n loaded.catch(() => {});\n throw new Error(`the page did not open: ${navigation.errorText}`);\n }\n await loaded;\n await page.evaluate(\"document.fonts ? document.fonts.ready.then(() => true) : true\");\n await new Promise((done) => setTimeout(done, options.settleMs));\n const measure = await page.evaluate<Measure>(MEASURE);\n view.measure = measure;\n // The screen width, not innerWidth: a phone browser can zoom out a page that is too wide.\n if (measure.scrollWidth > width) view.problems.push(`the page is wider than the screen: ${measure.scrollWidth} > ${width}`);\n for (const past of measure.past) view.problems.push(`past the right edge: ${past}`);\n for (const clipped of measure.clipped) view.problems.push(`a text area hides its text: ${clipped}`);\n view.problems.push(...cutProblems(measure.cut, width));\n if (measure.text === 0) view.problems.push(\"the page shows no text\");\n if (options.shots) {\n const file = join(options.shots, `${pageSlug(path)}-${width}-${scheme}.png`);\n const height = Math.min(Math.max(measure.height, 1), 8000);\n const shot = (await page.send(\"Page.captureScreenshot\", { format: \"png\", captureBeyondViewport: true, clip: { x: 0, y: 0, width, height, scale: 1 } })) as { data: string };\n writeFileSync(file, Buffer.from(shot.data, \"base64\"));\n view.shot = file;\n }\n if (withProbe && options.probe !== undefined) {\n const checks = (await page.evaluate<ProbeCheck[] | undefined>(probeExpression(options.probe))) ?? [];\n view.probe = checks;\n if (checks.length === 0) view.problems.push(\"the probe made no check\");\n for (const check of checks.filter((entry) => !entry.ok)) view.problems.push(`probe: ${check.name}${check.detail ? ` (${check.detail})` : \"\"}`);\n }\n } catch (error) {\n view.problems.push(error instanceof Error ? error.message : String(error));\n }\n view.problems.push(...events.problems());\n view.ok = view.problems.length === 0;\n return view;\n}\n\n/** This function makes a text safe for a cell of a Markdown table. It escapes `\\`, `|`, `<` and `>`. It changes each new line to a space. */\nexport function cell(text: string): string {\n return text.replace(/\\\\/g, \"\\\\\\\\\").replace(/\\|/g, \"\\\\|\").replace(/</g, \"<\").replace(/>/g, \">\").replace(/\\r?\\n/g, \" \");\n}\n\n/** This function gives the Markdown summary of a result, for the job summary of a workflow. */\nexport function summary(result: CheckResult): string {\n const failed = result.views.filter((view) => !view.ok);\n const lines = [\n `### Site check: ${result.ok ? \"passed\" : \"failed\"}`,\n \"\",\n `${result.views.length - failed.length} of ${result.views.length} views passed, at ${result.url}.`,\n \"\",\n \"| Page | Width | Scheme | Result | Problems |\",\n \"| --- | ---: | --- | --- | --- |\",\n ...result.views.map(\n (view) =>\n `| \\`${view.page}\\` | ${view.width} | ${view.scheme} | ${view.ok ? \"pass\" : \"**fail**\"}${view.probe ? ` (probe ${view.probe.filter((c) => c.ok).length}/${view.probe.length})` : \"\"} | ${view.problems.map(cell).join(\"<br>\")} |`,\n ),\n \"\",\n ];\n return lines.join(\"\\n\");\n}\n\n/** The input and output of the CLI. */\nexport interface CheckIo {\n cwd: string;\n env: NodeJS.ProcessEnv;\n stdout: (text: string) => void;\n stderr: (text: string) => void;\n}\n\nexport const CHECK_HELP = `Usage: site-pages check (--out <dir> | --url <url>) [options]\n\nOpens the pages of the site in a headless Chromium at phone and desktop widths, in the light and\nthe dark scheme, and fails when a page is wider than the screen, an element goes past the right\nedge, a text area hides its text, a box cuts text, a table goes out of view on a phone, a page\nshows no text, or on a console error, an exception, a failed request of the site, a script that\ndoes not load or a failed check of the probe. Add data-overflow-ok to a box that is meant to\nscroll or to clip.\n\nOptions:\n --out <dir> The built site folder. The check serves it at --base.\n --url <url> The URL of a live site home, in place of --out\n --repo <owner/name> The GitHub repository, for the default base (default: $GITHUB_REPOSITORY)\n --base </name/> The path of the site home (default: \"/<name>/\", or \"/\" for <owner>.github.io)\n --pages <paths> The pages, from the site home, separated by commas (default: /)\n --widths <px> The widths, separated by commas (default: 360,400,1280)\n --schemes <list> light, dark or both (default: light,dark)\n --probe <file> A JavaScript file: the body of an async function with check(name, ok, detail),\n wait, until, $, $$, type and click. It runs on the first page, in the first\n scheme, at each width. A .ts, .mts or .cts file is TypeScript: the check\n removes its types and its line export {}.\n --shots <dir> The folder for the screenshots, report.json and summary.md\n --browser <file> The Chromium executable (default: $SITE_CHECK_BROWSER, the Chromium of\n Playwright, else Chrome or Edge)\n --settle <ms> The wait after the load event (default: 1500)\n --timeout <ms> The time limit of a page load (default: 30000)\n --help Shows this text\n\nWith $GITHUB_STEP_SUMMARY, the check adds its table to the job summary.\n`;\n\n/** This function runs the `check` command with its arguments, without the word `check`. */\nexport async function runCheckCli(argv: readonly string[], io: CheckIo): Promise<number> {\n let values: Record<string, string | boolean | undefined>;\n try {\n ({ values } = parseArgs({\n args: [...argv],\n allowPositionals: false,\n options: {\n out: { type: \"string\" },\n url: { type: \"string\" },\n repo: { type: \"string\" },\n base: { type: \"string\" },\n pages: { type: \"string\" },\n widths: { type: \"string\" },\n schemes: { type: \"string\" },\n probe: { type: \"string\" },\n shots: { type: \"string\" },\n browser: { type: \"string\" },\n settle: { type: \"string\" },\n timeout: { type: \"string\" },\n help: { type: \"boolean\" },\n },\n }));\n } catch (error) {\n io.stderr(`site-pages check: ${error instanceof Error ? error.message : String(error)}\\n\\n${CHECK_HELP}`);\n return 2;\n }\n if (values[\"help\"]) {\n io.stdout(CHECK_HELP);\n return 0;\n }\n const usage = (message: string) => {\n io.stderr(`site-pages check: ${message}\\n\\n${CHECK_HELP}`);\n return 2;\n };\n const text = (key: string) => (typeof values[key] === \"string\" ? (values[key] as string) : undefined);\n const list = (key: string, fallback: string) => (text(key) ?? fallback).split(\",\").map((entry) => entry.trim()).filter(Boolean);\n const out = text(\"out\");\n const url = text(\"url\");\n if ((out === undefined) === (url === undefined)) return usage(\"give --out or --url, not both\");\n const widths = list(\"widths\", \"360,400,1280\").map(Number);\n if (widths.length === 0 || widths.some((width) => !Number.isInteger(width) || width < 200 || width > 4000)) return usage(\"--widths must be whole numbers from 200 to 4000\");\n const schemes = list(\"schemes\", \"light,dark\");\n if (schemes.length === 0 || schemes.some((scheme) => scheme !== \"light\" && scheme !== \"dark\")) return usage(\"--schemes must be light, dark or both\");\n const pages = list(\"pages\", \"/\");\n if (pages.length === 0) return usage(\"--pages has no page\");\n // Git Bash changes a bare \"/\" into \"C:/Program Files/Git/\". A page is a path, with no scheme or drive.\n const notPath = pages.find((entry) => entry.includes(\":\") || entry.includes(\"\\\\\"));\n if (notPath !== undefined) return usage(`a page is a path from the site home, not \"${notPath}\" (in Git Bash, set MSYS_NO_PATHCONV=1)`);\n const settleMs = Number(text(\"settle\") ?? 1500);\n const timeoutMs = Number(text(\"timeout\") ?? 30000);\n if (!(settleMs >= 0) || !(timeoutMs > 0)) return usage(\"--settle and --timeout must be numbers of milliseconds\");\n let base = text(\"base\");\n if (out !== undefined && base === undefined) {\n const name = (text(\"repo\") ?? io.env[\"GITHUB_REPOSITORY\"] ?? \"\").split(\"/\")[1];\n if (!name) return usage(\"give --base, --repo or set GITHUB_REPOSITORY\");\n base = /\\.github\\.io$/i.test(name) ? \"/\" : `/${name}/`;\n }\n if (base !== undefined) base = `/${base.replace(/^\\/+|\\/+$/g, \"\")}/`.replace(/^\\/\\/$/, \"/\");\n const at = (file: string) => (isAbsolute(file) ? file : resolve(io.cwd, file));\n const probeFile = text(\"probe\");\n let probe: string | undefined;\n if (probeFile !== undefined) {\n try {\n probe = readFileSync(at(probeFile), \"utf8\");\n } catch (error) {\n return usage(`the probe file ${probeFile} cannot be read: ${error instanceof Error ? error.message : String(error)}`);\n }\n if (TYPESCRIPT_PROBE.test(probeFile)) {\n try {\n probe = probeScript(probe);\n } catch (error) {\n return usage(`the types of the probe file ${probeFile} cannot be removed: ${error instanceof Error ? error.message : String(error)}`);\n }\n }\n }\n const shots = text(\"shots\");\n const result = await checkSite({\n out: out === undefined ? undefined : at(out),\n base,\n url,\n pages,\n widths,\n schemes: schemes as (\"light\" | \"dark\")[],\n probe,\n shots: shots === undefined ? undefined : at(shots),\n browser: text(\"browser\"),\n settleMs,\n timeoutMs,\n log: (line) => io.stdout(`${line}\\n`),\n });\n const failed = result.views.filter((view) => !view.ok).length;\n io.stdout(`site-pages check: ${result.views.length - failed} of ${result.views.length} views passed (${result.browser}).\\n`);\n const step = io.env[\"GITHUB_STEP_SUMMARY\"];\n if (step) appendFileSync(step, summary(result));\n return result.ok ? 0 : 1;\n}\n","#!/usr/bin/env node\n// The bin `site-pages`. The work is in build.ts and check/check.ts, so a test can run the same CLI with its own fetch.\nimport { runCli } from \"./build.ts\";\nimport { runCheckCli } from \"./check/check.ts\";\n\nconst argv = process.argv.slice(2);\nconst io = {\n cwd: process.cwd(),\n env: process.env,\n stdout: (text: string) => process.stdout.write(text),\n stderr: (text: string) => process.stderr.write(text),\n};\n(argv[0] === \"check\" ? runCheckCli(argv.slice(1), io) : runCli(argv, io)).then(\n (code) => {\n process.exitCode = code;\n },\n (error: unknown) => {\n process.stderr.write(`site-pages: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}\\n`);\n process.exitCode = 1;\n },\n);\n"],"mappings":"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"}
|
package/build/vite.d.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { BuildInfo } from "./chrome.js";
|
|
2
|
+
/** The environment variables that this module reads. */
|
|
3
|
+
export type SiteEnv = Readonly<Record<string, string | undefined>>;
|
|
4
|
+
/** The options of `siteShell`. */
|
|
5
|
+
export interface SiteShellOptions {
|
|
6
|
+
/** The `localStorage` key of the theme. Give the same key to `setupTheme` in the page. */
|
|
7
|
+
readonly themeKey: string;
|
|
8
|
+
/**
|
|
9
|
+
* The file with the version for `__BUILD__`, relative to the folder of the site. The default is
|
|
10
|
+
* `../package.json`. The value `false` gives a build with no version.
|
|
11
|
+
*/
|
|
12
|
+
readonly packageFile?: string | false | undefined;
|
|
13
|
+
}
|
|
14
|
+
/** The value of `build.target` in a Vite config. */
|
|
15
|
+
export type SiteTarget = string | string[] | false;
|
|
16
|
+
/** The part of the Vite config that `siteShell` reads. */
|
|
17
|
+
export interface SiteShellConfigInput {
|
|
18
|
+
readonly root?: string | undefined;
|
|
19
|
+
readonly base?: string | undefined;
|
|
20
|
+
readonly build?: {
|
|
21
|
+
readonly target?: SiteTarget | undefined;
|
|
22
|
+
} | null | undefined;
|
|
23
|
+
}
|
|
24
|
+
/** The part of the Vite config that `siteShell` sets. Vite merges it into the config of the site. */
|
|
25
|
+
export interface SiteShellConfig {
|
|
26
|
+
readonly base: string;
|
|
27
|
+
readonly build: {
|
|
28
|
+
readonly target: SiteTarget;
|
|
29
|
+
};
|
|
30
|
+
readonly define: Readonly<Record<string, string>>;
|
|
31
|
+
}
|
|
32
|
+
/** A tag for the `head` of each HTML page. */
|
|
33
|
+
export interface SiteShellTag {
|
|
34
|
+
readonly tag: "script";
|
|
35
|
+
readonly children: string;
|
|
36
|
+
readonly injectTo: "head";
|
|
37
|
+
}
|
|
38
|
+
/** The Vite plugin that `siteShell` gives. */
|
|
39
|
+
export interface SiteShellPlugin {
|
|
40
|
+
readonly name: string;
|
|
41
|
+
config(config: SiteShellConfigInput): SiteShellConfig;
|
|
42
|
+
transformIndexHtml(): SiteShellTag[];
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* This function gives the script that sets the saved theme before the first paint, so a dark page
|
|
46
|
+
* does not show white for a moment. The script reads `storageKey`, as `applyTheme` writes it.
|
|
47
|
+
*
|
|
48
|
+
* @remarks The text goes in an inline `script` element. Thus the function escapes each `<` of the
|
|
49
|
+
* key, so a key cannot close the element.
|
|
50
|
+
*/
|
|
51
|
+
export declare function themeScript(storageKey: string): string;
|
|
52
|
+
/**
|
|
53
|
+
* This function gives the `base` of Vite from `SITE_BASE`, with one `/` at the end. The Pages
|
|
54
|
+
* workflow sets that variable to `/<name>`. A local build has no such variable, and gets `/`.
|
|
55
|
+
*/
|
|
56
|
+
export declare function siteBase(env?: SiteEnv): string;
|
|
57
|
+
/** This function gives the data of a build. Outside CI, `GITHUB_SHA` is not set, and `sha` is `local`. */
|
|
58
|
+
export declare function buildInfo(version?: string, env?: SiteEnv, now?: Date): BuildInfo;
|
|
59
|
+
/**
|
|
60
|
+
* This function makes the Vite plugin of a site. The plugin does these four operations:
|
|
61
|
+
*
|
|
62
|
+
* - It sets `base` from `SITE_BASE`.
|
|
63
|
+
* - It sets `build.target` to `es2022`.
|
|
64
|
+
* - It defines `__BUILD__`, the value that `showBuild` shows.
|
|
65
|
+
* - It adds `themeScript` to the `head` of each HTML page.
|
|
66
|
+
*
|
|
67
|
+
* A `base` or a `build.target` in the config of the site has priority. The other values of `build`
|
|
68
|
+
* and `define` in that config stay, because Vite merges them.
|
|
69
|
+
*/
|
|
70
|
+
export declare function siteShell(options: SiteShellOptions): SiteShellPlugin;
|
|
71
|
+
export type { BuildInfo };
|
|
72
|
+
//# sourceMappingURL=vite.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vite.d.ts","names":[],"sources":["../src/vite.ts"],"mappings":";;YAeY,UAAU,SAAS;;iBAGd;;WAEN;;;;;WAKA;;;YAIC;;iBAGK;WACN;WACA;WACA;aAAmB,SAAS;;;;iBAItB;WACN;WACA;aAAkB,QAAQ;;WAC1B,QAAQ,SAAS;;;iBAIX;WACN;WACA;WACA;;;iBAIM;WACN;EACT,OAAO,QAAQ,uBAAuB;EACtC,sBAAsB;;;;;;;;;wBAUR,YAAY;;;;;wBASZ,SAAS,MAAK;;wBAMd,UAAU,kBAAkB,MAAK,SAAuB,MAAK,OAAoB;;;;;;;;;;;;wBAoBjF,UAAU,SAAS,mBAAmB"}
|
package/build/vite.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
// src/vite.ts
|
|
4
|
+
/**
|
|
5
|
+
* The build side of the shell of a site: a Vite plugin and its parts. Import this module from
|
|
6
|
+
* `@mark1russell7/site-pages/vite`. It is for `vite.config.ts`, not for a browser.
|
|
7
|
+
*
|
|
8
|
+
* This module does not import Vite. The types of the plugin are the small part that it uses, so the
|
|
9
|
+
* package keeps no dependency. A test examines that Vite accepts the plugin.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* This function gives the script that sets the saved theme before the first paint, so a dark page
|
|
13
|
+
* does not show white for a moment. The script reads `storageKey`, as `applyTheme` writes it.
|
|
14
|
+
*
|
|
15
|
+
* @remarks The text goes in an inline `script` element. Thus the function escapes each `<` of the
|
|
16
|
+
* key, so a key cannot close the element.
|
|
17
|
+
*/
|
|
18
|
+
function themeScript(storageKey) {
|
|
19
|
+
return `try{const t=localStorage.getItem(${JSON.stringify(storageKey).replaceAll("<", "\\u003c")});if(t==="light"||t==="dark")document.documentElement.dataset.theme=t}catch{}`;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* This function gives the `base` of Vite from `SITE_BASE`, with one `/` at the end. The Pages
|
|
23
|
+
* workflow sets that variable to `/<name>`. A local build has no such variable, and gets `/`.
|
|
24
|
+
*/
|
|
25
|
+
function siteBase(env = process.env) {
|
|
26
|
+
const base = env["SITE_BASE"];
|
|
27
|
+
return base ? `${base.replace(/\/$/, "")}/` : "/";
|
|
28
|
+
}
|
|
29
|
+
/** This function gives the data of a build. Outside CI, `GITHUB_SHA` is not set, and `sha` is `local`. */
|
|
30
|
+
function buildInfo(version, env = process.env, now = /* @__PURE__ */ new Date()) {
|
|
31
|
+
return {
|
|
32
|
+
sha: (env["GITHUB_SHA"] ?? "local").slice(0, 7),
|
|
33
|
+
date: now.toISOString().slice(0, 10),
|
|
34
|
+
version
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/** This function reads the field `version` of a `package.json` file. */
|
|
38
|
+
function versionIn(file) {
|
|
39
|
+
return JSON.parse(readFileSync(file, "utf8")).version;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* This function makes the Vite plugin of a site. The plugin does these four operations:
|
|
43
|
+
*
|
|
44
|
+
* - It sets `base` from `SITE_BASE`.
|
|
45
|
+
* - It sets `build.target` to `es2022`.
|
|
46
|
+
* - It defines `__BUILD__`, the value that `showBuild` shows.
|
|
47
|
+
* - It adds `themeScript` to the `head` of each HTML page.
|
|
48
|
+
*
|
|
49
|
+
* A `base` or a `build.target` in the config of the site has priority. The other values of `build`
|
|
50
|
+
* and `define` in that config stay, because Vite merges them.
|
|
51
|
+
*/
|
|
52
|
+
function siteShell(options) {
|
|
53
|
+
return {
|
|
54
|
+
name: "site-pages-shell",
|
|
55
|
+
config(config) {
|
|
56
|
+
const packageFile = options.packageFile ?? "../package.json";
|
|
57
|
+
const version = packageFile === false ? void 0 : versionIn(resolve(config.root ?? ".", packageFile));
|
|
58
|
+
return {
|
|
59
|
+
base: config.base ?? siteBase(),
|
|
60
|
+
build: { target: config.build?.target ?? "es2022" },
|
|
61
|
+
define: { __BUILD__: JSON.stringify(buildInfo(version)) }
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
transformIndexHtml() {
|
|
65
|
+
return [{
|
|
66
|
+
tag: "script",
|
|
67
|
+
children: themeScript(options.themeKey),
|
|
68
|
+
injectTo: "head"
|
|
69
|
+
}];
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export { buildInfo, siteBase, siteShell, themeScript };
|
|
74
|
+
|
|
75
|
+
//# sourceMappingURL=vite.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vite.js","names":[],"sources":["../src/vite.ts"],"sourcesContent":["/**\n * The build side of the shell of a site: a Vite plugin and its parts. Import this module from\n * `@mark1russell7/site-pages/vite`. It is for `vite.config.ts`, not for a browser.\n *\n * This module does not import Vite. The types of the plugin are the small part that it uses, so the\n * package keeps no dependency. A test examines that Vite accepts the plugin.\n */\n\nimport { readFileSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport type { BuildInfo } from \"./chrome.ts\";\n\nexport type { BuildInfo } from \"./chrome.ts\";\n\n/** The environment variables that this module reads. */\nexport type SiteEnv = Readonly<Record<string, string | undefined>>;\n\n/** The options of `siteShell`. */\nexport interface SiteShellOptions {\n /** The `localStorage` key of the theme. Give the same key to `setupTheme` in the page. */\n readonly themeKey: string;\n /**\n * The file with the version for `__BUILD__`, relative to the folder of the site. The default is\n * `../package.json`. The value `false` gives a build with no version.\n */\n readonly packageFile?: string | false | undefined;\n}\n\n/** The value of `build.target` in a Vite config. */\nexport type SiteTarget = string | string[] | false;\n\n/** The part of the Vite config that `siteShell` reads. */\nexport interface SiteShellConfigInput {\n readonly root?: string | undefined;\n readonly base?: string | undefined;\n readonly build?: { readonly target?: SiteTarget | undefined } | null | undefined;\n}\n\n/** The part of the Vite config that `siteShell` sets. Vite merges it into the config of the site. */\nexport interface SiteShellConfig {\n readonly base: string;\n readonly build: { readonly target: SiteTarget };\n readonly define: Readonly<Record<string, string>>;\n}\n\n/** A tag for the `head` of each HTML page. */\nexport interface SiteShellTag {\n readonly tag: \"script\";\n readonly children: string;\n readonly injectTo: \"head\";\n}\n\n/** The Vite plugin that `siteShell` gives. */\nexport interface SiteShellPlugin {\n readonly name: string;\n config(config: SiteShellConfigInput): SiteShellConfig;\n transformIndexHtml(): SiteShellTag[];\n}\n\n/**\n * This function gives the script that sets the saved theme before the first paint, so a dark page\n * does not show white for a moment. The script reads `storageKey`, as `applyTheme` writes it.\n *\n * @remarks The text goes in an inline `script` element. Thus the function escapes each `<` of the\n * key, so a key cannot close the element.\n */\nexport function themeScript(storageKey: string): string {\n const key = JSON.stringify(storageKey).replaceAll(\"<\", \"\\\\u003c\");\n return `try{const t=localStorage.getItem(${key});if(t===\"light\"||t===\"dark\")document.documentElement.dataset.theme=t}catch{}`;\n}\n\n/**\n * This function gives the `base` of Vite from `SITE_BASE`, with one `/` at the end. The Pages\n * workflow sets that variable to `/<name>`. A local build has no such variable, and gets `/`.\n */\nexport function siteBase(env: SiteEnv = process.env): string {\n const base = env[\"SITE_BASE\"];\n return base ? `${base.replace(/\\/$/, \"\")}/` : \"/\";\n}\n\n/** This function gives the data of a build. Outside CI, `GITHUB_SHA` is not set, and `sha` is `local`. */\nexport function buildInfo(version?: string, env: SiteEnv = process.env, now: Date = new Date()): BuildInfo {\n return { sha: (env[\"GITHUB_SHA\"] ?? \"local\").slice(0, 7), date: now.toISOString().slice(0, 10), version };\n}\n\n/** This function reads the field `version` of a `package.json` file. */\nfunction versionIn(file: string): string | undefined {\n return (JSON.parse(readFileSync(file, \"utf8\")) as { version?: string }).version;\n}\n\n/**\n * This function makes the Vite plugin of a site. The plugin does these four operations:\n *\n * - It sets `base` from `SITE_BASE`.\n * - It sets `build.target` to `es2022`.\n * - It defines `__BUILD__`, the value that `showBuild` shows.\n * - It adds `themeScript` to the `head` of each HTML page.\n *\n * A `base` or a `build.target` in the config of the site has priority. The other values of `build`\n * and `define` in that config stay, because Vite merges them.\n */\nexport function siteShell(options: SiteShellOptions): SiteShellPlugin {\n return {\n name: \"site-pages-shell\",\n config(config) {\n const packageFile = options.packageFile ?? \"../package.json\";\n const version = packageFile === false ? undefined : versionIn(resolve(config.root ?? \".\", packageFile));\n return {\n base: config.base ?? siteBase(),\n build: { target: config.build?.target ?? \"es2022\" },\n define: { __BUILD__: JSON.stringify(buildInfo(version)) },\n };\n },\n transformIndexHtml() {\n return [{ tag: \"script\", children: themeScript(options.themeKey), injectTo: \"head\" }];\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAkEA,SAAgB,YAAY,YAA4B;CAEtD,OAAO,oCADK,KAAK,UAAU,UAAU,CAAC,CAAC,WAAW,KAAK,SACV,EAAE;AACjD;;;;;AAMA,SAAgB,SAAS,MAAe,QAAQ,KAAa;CAC3D,MAAM,OAAO,IAAI;CACjB,OAAO,OAAO,GAAG,KAAK,QAAQ,OAAO,EAAE,EAAE,KAAK;AAChD;;AAGA,SAAgB,UAAU,SAAkB,MAAe,QAAQ,KAAK,sBAAY,IAAI,KAAK,GAAc;CACzG,OAAO;EAAE,MAAM,IAAI,iBAAiB,QAAA,CAAS,MAAM,GAAG,CAAC;EAAG,MAAM,IAAI,YAAY,CAAC,CAAC,MAAM,GAAG,EAAE;EAAG;CAAQ;AAC1G;;AAGA,SAAS,UAAU,MAAkC;CACnD,OAAQ,KAAK,MAAM,aAAa,MAAM,MAAM,CAAC,CAAC,CAA0B;AAC1E;;;;;;;;;;;;AAaA,SAAgB,UAAU,SAA4C;CACpE,OAAO;EACL,MAAM;EACN,OAAO,QAAQ;GACb,MAAM,cAAc,QAAQ,eAAe;GAC3C,MAAM,UAAU,gBAAgB,QAAQ,KAAA,IAAY,UAAU,QAAQ,OAAO,QAAQ,KAAK,WAAW,CAAC;GACtG,OAAO;IACL,MAAM,OAAO,QAAQ,SAAS;IAC9B,OAAO,EAAE,QAAQ,OAAO,OAAO,UAAU,SAAS;IAClD,QAAQ,EAAE,WAAW,KAAK,UAAU,UAAU,OAAO,CAAC,EAAE;GAC1D;EACF;EACA,qBAAqB;GACnB,OAAO,CAAC;IAAE,KAAK;IAAU,UAAU,YAAY,QAAQ,QAAQ;IAAG,UAAU;GAAO,CAAC;EACtF;CACF;AACF"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package",
|
|
3
3
|
"name": "@mark1russell7/site-pages",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.2.0",
|
|
5
5
|
"description": "A release timeline page and a live status page for the site of a GitHub repository: JSON data and standalone HTML, made at deploy time.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"github-pages",
|
|
@@ -31,7 +31,16 @@
|
|
|
31
31
|
"types": "./build/index.d.ts",
|
|
32
32
|
"import": "./build/index.js"
|
|
33
33
|
},
|
|
34
|
+
"./chrome": {
|
|
35
|
+
"types": "./build/chrome.d.ts",
|
|
36
|
+
"import": "./build/chrome.js"
|
|
37
|
+
},
|
|
38
|
+
"./vite": {
|
|
39
|
+
"types": "./build/vite.d.ts",
|
|
40
|
+
"import": "./build/vite.js"
|
|
41
|
+
},
|
|
34
42
|
"./scroll-cue.css": "./styles/scroll-cue.css",
|
|
43
|
+
"./shell.css": "./styles/shell.css",
|
|
35
44
|
"./package.json": "./package.json"
|
|
36
45
|
},
|
|
37
46
|
"bin": {
|
|
@@ -48,10 +57,11 @@
|
|
|
48
57
|
"node": ">=22"
|
|
49
58
|
},
|
|
50
59
|
"devDependencies": {
|
|
51
|
-
"@mark1russell7/cue": "^
|
|
60
|
+
"@mark1russell7/cue": "^2.0.0",
|
|
52
61
|
"@types/node": "^26.6.4",
|
|
53
62
|
"tsdown": "^0.23.1",
|
|
54
63
|
"typescript": "^7.0.2",
|
|
64
|
+
"vite": "^8.3.5",
|
|
55
65
|
"vitest": "^5.0.3"
|
|
56
66
|
},
|
|
57
67
|
"scripts": {
|
package/styles/shell.css
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @mark1russell7/site-pages/shell.css: the tokens and the rules of the shell of a site.
|
|
3
|
+
* The shell is the parts of the page that are not its content: the document, the skip link, the
|
|
4
|
+
* header, the links, the theme button, the main column and the footer.
|
|
5
|
+
*
|
|
6
|
+
* Use: import this file before the style sheet of the site, so the rules of the site come last.
|
|
7
|
+
* import "@mark1russell7/site-pages/shell.css";
|
|
8
|
+
* import "./style.css";
|
|
9
|
+
*
|
|
10
|
+
* A site changes a color with its own :root rule, after this file. To change a dark color, it
|
|
11
|
+
* changes the two dark rules. A site with its own look does not import this file.
|
|
12
|
+
* The rules set no look for the content: buttons, cards, code and tables stay in the site.
|
|
13
|
+
*/
|
|
14
|
+
:root {
|
|
15
|
+
color-scheme: light;
|
|
16
|
+
--bg: #f4f3ee;
|
|
17
|
+
--surface: #ffffff;
|
|
18
|
+
--sunken: #eceae3;
|
|
19
|
+
--ink: #191b23;
|
|
20
|
+
--muted: #5a5d6c;
|
|
21
|
+
--rule: #dad7cd;
|
|
22
|
+
--focus: #2346c4;
|
|
23
|
+
--font: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
24
|
+
--mono: ui-monospace, "Cascadia Mono", "SFMono-Regular", Menlo, Consolas, monospace;
|
|
25
|
+
--radius: 10px;
|
|
26
|
+
--width: 75rem;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@media (prefers-color-scheme: dark) {
|
|
30
|
+
:root:not([data-theme="light"]) {
|
|
31
|
+
color-scheme: dark;
|
|
32
|
+
--bg: #0f1117;
|
|
33
|
+
--surface: #171a23;
|
|
34
|
+
--sunken: #1f2330;
|
|
35
|
+
--ink: #e8e9ef;
|
|
36
|
+
--muted: #a0a4b6;
|
|
37
|
+
--rule: #2e3345;
|
|
38
|
+
--focus: #93abff;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
:root[data-theme="dark"] {
|
|
43
|
+
color-scheme: dark;
|
|
44
|
+
--bg: #0f1117;
|
|
45
|
+
--surface: #171a23;
|
|
46
|
+
--sunken: #1f2330;
|
|
47
|
+
--ink: #e8e9ef;
|
|
48
|
+
--muted: #a0a4b6;
|
|
49
|
+
--rule: #2e3345;
|
|
50
|
+
--focus: #93abff;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
*,
|
|
54
|
+
*::before,
|
|
55
|
+
*::after {
|
|
56
|
+
box-sizing: border-box;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
html {
|
|
60
|
+
-webkit-text-size-adjust: 100%;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
body {
|
|
64
|
+
margin: 0;
|
|
65
|
+
background: var(--bg);
|
|
66
|
+
color: var(--ink);
|
|
67
|
+
font-family: var(--font);
|
|
68
|
+
font-size: 1rem;
|
|
69
|
+
line-height: 1.5;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
:focus-visible {
|
|
73
|
+
outline: 2px solid var(--focus);
|
|
74
|
+
outline-offset: 2px;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.sr {
|
|
78
|
+
position: absolute;
|
|
79
|
+
width: 1px;
|
|
80
|
+
height: 1px;
|
|
81
|
+
overflow: hidden;
|
|
82
|
+
clip: rect(0 0 0 0);
|
|
83
|
+
white-space: nowrap;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.skip {
|
|
87
|
+
position: absolute;
|
|
88
|
+
left: 8px;
|
|
89
|
+
top: -40px;
|
|
90
|
+
background: var(--surface);
|
|
91
|
+
padding: 0.4rem 0.8rem;
|
|
92
|
+
border-radius: 8px;
|
|
93
|
+
z-index: 10;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.skip:focus {
|
|
97
|
+
top: 8px;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.top {
|
|
101
|
+
max-width: var(--width);
|
|
102
|
+
margin: 0 auto;
|
|
103
|
+
padding: 1.1rem 16px 0.6rem;
|
|
104
|
+
display: flex;
|
|
105
|
+
flex-wrap: wrap;
|
|
106
|
+
align-items: flex-end;
|
|
107
|
+
justify-content: space-between;
|
|
108
|
+
gap: 0.5rem 2rem;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.brand {
|
|
112
|
+
flex: 1 1 28rem;
|
|
113
|
+
min-width: 0;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.wordmark {
|
|
117
|
+
margin: 0;
|
|
118
|
+
font-size: 1.75rem;
|
|
119
|
+
letter-spacing: -0.02em;
|
|
120
|
+
display: flex;
|
|
121
|
+
align-items: center;
|
|
122
|
+
gap: 0.6rem;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.lead {
|
|
126
|
+
margin: 0.35rem 0 0;
|
|
127
|
+
color: var(--muted);
|
|
128
|
+
max-width: 46rem;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.links {
|
|
132
|
+
display: flex;
|
|
133
|
+
flex-wrap: wrap;
|
|
134
|
+
align-items: center;
|
|
135
|
+
gap: 0.25rem 1rem;
|
|
136
|
+
font-size: 0.95rem;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.links a {
|
|
140
|
+
text-decoration: none;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.links a:hover {
|
|
144
|
+
text-decoration: underline;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.theme {
|
|
148
|
+
min-height: 2rem;
|
|
149
|
+
padding: 0.15rem 0.6rem;
|
|
150
|
+
font-size: 0.875rem;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
main {
|
|
154
|
+
max-width: var(--width);
|
|
155
|
+
margin: 0 auto;
|
|
156
|
+
padding: 0 16px;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.foot {
|
|
160
|
+
max-width: var(--width);
|
|
161
|
+
margin: 3rem auto 0;
|
|
162
|
+
padding: 16px;
|
|
163
|
+
border-top: 1px solid var(--rule);
|
|
164
|
+
color: var(--muted);
|
|
165
|
+
font-size: 0.875rem;
|
|
166
|
+
}
|