@mark1russell7/site-pages 0.2.4 → 0.2.6
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 +34 -2
- package/build/chunks/build.js +208 -9
- package/build/chunks/build.js.map +1 -1
- package/build/chunks/kpi.d.ts +2 -0
- package/build/chunks/kpi.d.ts.map +1 -1
- package/build/chunks/kpi.js +15 -11
- package/build/chunks/kpi.js.map +1 -1
- package/build/cli.js +2 -2
- package/build/cli.js.map +1 -1
- package/build/index.d.ts +75 -2
- package/build/index.d.ts.map +1 -1
- package/build/index.js +18 -2
- package/build/index.js.map +1 -0
- package/package.json +1 -1
package/build/chunks/kpi.d.ts
CHANGED
|
@@ -127,6 +127,8 @@ interface KpiMeasure {
|
|
|
127
127
|
id: string;
|
|
128
128
|
value: number;
|
|
129
129
|
unit?: string;
|
|
130
|
+
/** The ISO time of the data of this measure in UTC. Without it, the measure has the time of the file. */
|
|
131
|
+
at?: string;
|
|
130
132
|
}
|
|
131
133
|
/** One engine that made the data of `kpi.json`. */
|
|
132
134
|
interface KpiEngine {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"kpi.d.ts","names":[],"sources":["../../src/data/types.ts","../../src/data/kpi.ts"],"mappings":";;;;;UAMiB;EACf;EACA;;;;;;KAWU;;UAGK;;EAEf;;EAEA;;EAEA;;;UAIe;EACf;;EAEA;EACA,MAAM;;EAEN,SAAS;;EAET,OAAO;;;UAIQ;EACf;EACA;;EAEA;;EAEA;EACA;EACA;;;KAIU;EAAmB;EAAiB,UAAU;;EAAsB;EAAkB;;;UAGjF;EACf;EACA;EACA;;;UAIe;EACf,OAAO;EACP;;;KAQU;;UAGK;EACf;EACA;;EAEA;EACA,aAAa;EACb;EACA;;EAEA;;EAEA;;EAEA;EACA,OAAO;;;KAIG;;UAGK;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,QAAQ;;;UAIO;EACf;EACA;EACA;EACA;EACA;EACA;EACA;;;UAIe;EACf;EACA;;;KAIU;EACN;EAAiB,cAAc;EAAoB,QAAQ;EAAc,QAAQ;;EACjF;EAAkB;;;UAGP;;EAEf;EACA;EACA;;;UAIe;;EAEf;EACA;;;UAIe;EACf;;EAEA;;EAEA;;EAEA;EACA,UAAU;;EAEV,UAAU;;;UAIK;;EAEf;;EAEA;;EAEA;IAAU;IAAwB;;EAClC,OAAO;EACP,cAAc;EACd,QAAQ;;EAER;IAAU;IAAoB;IAAiB,SAAS;;;EAExD,MAAM;;;
|
|
1
|
+
{"version":3,"file":"kpi.d.ts","names":[],"sources":["../../src/data/types.ts","../../src/data/kpi.ts"],"mappings":";;;;;UAMiB;EACf;EACA;;;;;;KAWU;;UAGK;;EAEf;;EAEA;;EAEA;;;UAIe;EACf;;EAEA;EACA,MAAM;;EAEN,SAAS;;EAET,OAAO;;;UAIQ;EACf;EACA;;EAEA;;EAEA;EACA;EACA;;;KAIU;EAAmB;EAAiB,UAAU;;EAAsB;EAAkB;;;UAGjF;EACf;EACA;EACA;;;UAIe;EACf,OAAO;EACP;;;KAQU;;UAGK;EACf;EACA;;EAEA;EACA,aAAa;EACb;EACA;;EAEA;;EAEA;;EAEA;EACA,OAAO;;;KAIG;;UAGK;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,QAAQ;;;UAIO;EACf;EACA;EACA;EACA;EACA;EACA;EACA;;;UAIe;EACf;EACA;;;KAIU;EACN;EAAiB,cAAc;EAAoB,QAAQ;EAAc,QAAQ;;EACjF;EAAkB;;;UAGP;;EAEf;EACA;EACA;;EAEA;;;UAIe;;EAEf;EACA;;;UAIe;EACf;;EAEA;;EAEA;;EAEA;EACA,UAAU;;EAEV,UAAU;;;UAIK;;EAEf;;EAEA;;EAEA;IAAU;IAAwB;;EAClC,OAAO;EACP,cAAc;EACd,QAAQ;;EAER;IAAU;IAAoB;IAAiB,SAAS;;;EAExD,MAAM;;;cCvKK;;cAGA;;KAGD,WAAW,KAAK;;cAef,iBAAiB;WACnB;EAET,YAAY;;;;;;;;iBA4CE,YAAY,gBAAgB;;;;;;iBAgC5B,QAAQ,OAAO,UAAU,eAAe;;iBASxC,QAAQ,MAAM;;;;;;iBASd,QAAQ,cAAc,eAAe;;;;;;iBAkBrC,SAAS,aAAa,OAAO,UAAU"}
|
package/build/chunks/kpi.js
CHANGED
|
@@ -66,6 +66,7 @@ function originRepository(repoDir) {
|
|
|
66
66
|
* from this file. Thus a number on a progress page comes from a record and not from a person.
|
|
67
67
|
*
|
|
68
68
|
* The field `at` is the time of the data and not the time of the build. Thus the writer has no default for it.
|
|
69
|
+
* A measure can have its own `at` when its data is from another time than the rest of the file.
|
|
69
70
|
*/
|
|
70
71
|
/** The name of the format. A new format gets a new name, so that an old reader refuses it. */
|
|
71
72
|
const KPI_FORMAT = "kpi/1";
|
|
@@ -90,16 +91,22 @@ var KpiError = class extends Error {
|
|
|
90
91
|
};
|
|
91
92
|
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
92
93
|
const isText = (value) => typeof value === "string" && value.trim() !== "";
|
|
94
|
+
/** This function gives the fault of a time of the data, or an empty list. The name tells which field it is. */
|
|
95
|
+
function timeProblems(at, name, now) {
|
|
96
|
+
if (typeof at !== "string" || !UTC_TIME.test(at) || Number.isNaN(Date.parse(at))) return [`${name} is not a time in UTC.`];
|
|
97
|
+
return Date.parse(at) > now + CLOCK_MARGIN ? [`The time of the data ${at} is after the time of the check.`] : [];
|
|
98
|
+
}
|
|
93
99
|
/** This function gives the faults of one measure. */
|
|
94
|
-
function measureProblems(value, index, seen) {
|
|
100
|
+
function measureProblems(value, index, seen, now) {
|
|
95
101
|
if (!isRecord(value)) return [`Measure ${index} is not an object.`];
|
|
96
|
-
const { id, value: amount, unit } = value;
|
|
102
|
+
const { id, value: amount, unit, at } = value;
|
|
97
103
|
const problems = [];
|
|
98
104
|
if (typeof id !== "string" || !SLUG.test(id)) problems.push(`Measure ${index} has no id of lowercase words with single hyphens.`);
|
|
99
105
|
else if (seen.has(id)) problems.push(`The id ${id} occurs two times.`);
|
|
100
106
|
else seen.add(id);
|
|
101
107
|
if (typeof amount !== "number" || !Number.isFinite(amount)) problems.push(`Measure ${index} has no finite number as its value.`);
|
|
102
108
|
if (unit !== void 0 && !isText(unit)) problems.push(`Measure ${index} has a unit that is not a text.`);
|
|
109
|
+
if (at !== void 0) problems.push(...timeProblems(at, `The field at of measure ${index}`, now));
|
|
103
110
|
return problems;
|
|
104
111
|
}
|
|
105
112
|
/** This function gives the faults of the list of engines. */
|
|
@@ -120,26 +127,23 @@ function kpiProblems(value, now = Date.now()) {
|
|
|
120
127
|
if (value["format"] !== "kpi/1") problems.push(`The format is not ${KPI_FORMAT}.`);
|
|
121
128
|
const { project, at, commit, measures } = value;
|
|
122
129
|
if (typeof project !== "string" || !SLUG.test(project)) problems.push("The project is not lowercase words with single hyphens.");
|
|
123
|
-
|
|
124
|
-
else if (Date.parse(at) > now + CLOCK_MARGIN) problems.push(`The time of the data ${at} is after the time of the check.`);
|
|
130
|
+
problems.push(...timeProblems(at, "The field at", now));
|
|
125
131
|
if (typeof commit !== "string" || !COMMIT.test(commit)) problems.push("The commit is not a full commit.");
|
|
126
132
|
if (!Array.isArray(measures) || measures.length === 0) problems.push("The file has no measures.");
|
|
127
133
|
else {
|
|
128
134
|
const seen = /* @__PURE__ */ new Set();
|
|
129
|
-
problems.push(...measures.flatMap((measure, index) => measureProblems(measure, index, seen)));
|
|
135
|
+
problems.push(...measures.flatMap((measure, index) => measureProblems(measure, index, seen, now)));
|
|
130
136
|
}
|
|
131
137
|
problems.push(...engineProblems(value["engines"]));
|
|
132
138
|
return problems;
|
|
133
139
|
}
|
|
134
140
|
/** This function gives a copy of a measure with only its known fields. */
|
|
135
|
-
function cleanMeasure({ id, value, unit }) {
|
|
136
|
-
return
|
|
137
|
-
id,
|
|
138
|
-
value
|
|
139
|
-
} : {
|
|
141
|
+
function cleanMeasure({ id, value, unit, at }) {
|
|
142
|
+
return {
|
|
140
143
|
id,
|
|
141
144
|
value,
|
|
142
|
-
unit
|
|
145
|
+
...unit === void 0 ? {} : { unit },
|
|
146
|
+
...at === void 0 ? {} : { at }
|
|
143
147
|
};
|
|
144
148
|
}
|
|
145
149
|
/** This function gives a copy of an engine with only its known fields. */
|
package/build/chunks/kpi.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"kpi.js","names":[],"sources":["../../src/data/git.ts","../../src/data/kpi.ts"],"sourcesContent":["/**\n * The git source: the tags with their dates, the commit of the build and the GitHub remote. Each\n * function gives an empty result when git or the repository is not available.\n */\n\nimport { execFileSync } from \"node:child_process\";\n\n/** A git tag and the ISO date of its creation. */\nexport interface TagDate {\n tag: string;\n date: string;\n}\n\n/** This function starts git in a folder and gives its output, or null when git fails. */\nexport function git(repoDir: string, ...args: string[]): string | null {\n try {\n return execFileSync(\"git\", args, { cwd: repoDir, encoding: \"utf8\", stdio: [\"ignore\", \"pipe\", \"ignore\"], windowsHide: true });\n } catch {\n return null;\n }\n}\n\n/** This function tells if the clone is shallow. A shallow clone has not all the tags and commits. */\nexport function isShallow(repoDir: string): boolean {\n return git(repoDir, \"rev-parse\", \"--is-shallow-repository\")?.trim() === \"true\";\n}\n\n/** This function gives each tag of the repository with its date. An annotated tag has its own date. */\nexport function gitTagDates(repoDir: string): TagDate[] {\n const output = git(repoDir, \"for-each-ref\", \"--format=%(refname:strip=2) %(creatordate:iso-strict)\", \"refs/tags\");\n if (!output) return [];\n const tags: TagDate[] = [];\n for (const line of output.split(/\\r?\\n/)) {\n const match = /^(\\S+) (\\S+)$/.exec(line.trim());\n if (match?.[1] && match[2]) tags.push({ tag: match[1], date: match[2] });\n }\n return tags;\n}\n\n/**\n * This function gives the date of the oldest commit that added a version heading to a changelog,\n * or null. The path of the changelog starts at `repoDir`.\n */\nexport function headingDate(repoDir: string, version: string, file: string): string | null {\n const output = git(repoDir, \"log\", \"--format=%cI\", `-S## ${version}`, \"--\", file);\n const dates = (output ?? \"\").trim().split(/\\r?\\n/).filter(Boolean);\n return dates[dates.length - 1] ?? null;\n}\n\n/** This function gives the full SHA of `HEAD`, or null. */\nexport function headCommit(repoDir: string): string | null {\n const sha = git(repoDir, \"rev-parse\", \"HEAD\")?.trim();\n return sha && /^[0-9a-f]{40}$/.test(sha) ? sha : null;\n}\n\n/** This function gives \"owner/name\" of the GitHub remote \"origin\", or null. */\nexport function originRepository(repoDir: string): string | null {\n const url = git(repoDir, \"remote\", \"get-url\", \"origin\")?.trim() ?? \"\";\n const match = /github\\.com[:/]([^/]+)\\/(.+?)(?:\\.git)?\\/?$/.exec(url);\n return match ? `${match[1]}/${match[2]}` : null;\n}\n","/**\n * The file `kpi.json` at the root of a site, format `kpi/1`. A reader outside the site takes the measures of the project\n * from this file. Thus a number on a progress page comes from a record and not from a person.\n *\n * The field `at` is the time of the data and not the time of the build. Thus the writer has no default for it.\n */\n\nimport { mkdirSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { KpiEngine, KpiFile, KpiMeasure } from \"./types.ts\";\n\n/** The name of the format. A new format gets a new name, so that an old reader refuses it. */\nexport const KPI_FORMAT = \"kpi/1\";\n\n/** The name of the file at the root of the built site. */\nexport const KPI_FILE = \"kpi.json\";\n\n/** The data of `kpi.json`, without the name of the format. */\nexport type KpiInput = Omit<KpiFile, \"format\">;\n\n/** The form of a project and a measure. A double hyphen separates fields in the file name of a reading. */\nconst SLUG = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;\n\n/** The form of a time in UTC, with or without seconds. */\nconst UTC_TIME = /^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}(?::\\d{2}(?:\\.\\d+)?)?Z$/;\n\n/** The form of a full commit. */\nconst COMMIT = /^[0-9a-f]{40}$/;\n\n/** The clock difference between two computers that the check accepts, in milliseconds. */\nconst CLOCK_MARGIN = 5 * 60 * 1000;\n\n/** An error in the data of `kpi.json`. The field `problems` has one line for each fault. */\nexport class KpiError extends Error {\n readonly problems: readonly string[];\n\n constructor(problems: readonly string[]) {\n super(`${KPI_FILE} is not correct: ${problems.join(\" \")}`);\n this.name = \"KpiError\";\n this.problems = problems;\n }\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> => typeof value === \"object\" && value !== null && !Array.isArray(value);\n\nconst isText = (value: unknown): value is string => typeof value === \"string\" && value.trim() !== \"\";\n\n/** This function gives the faults of one measure. */\nfunction measureProblems(value: unknown, index: number, seen: Set<string>): string[] {\n if (!isRecord(value)) return [`Measure ${index} is not an object.`];\n const { id, value: amount, unit } = value;\n const problems: string[] = [];\n if (typeof id !== \"string\" || !SLUG.test(id)) problems.push(`Measure ${index} has no id of lowercase words with single hyphens.`);\n else if (seen.has(id)) problems.push(`The id ${id} occurs two times.`);\n else seen.add(id);\n if (typeof amount !== \"number\" || !Number.isFinite(amount)) problems.push(`Measure ${index} has no finite number as its value.`);\n if (unit !== undefined && !isText(unit)) problems.push(`Measure ${index} has a unit that is not a text.`);\n return problems;\n}\n\n/** This function gives the faults of the list of engines. */\nfunction engineProblems(value: unknown): string[] {\n if (value === undefined) return [];\n if (!Array.isArray(value)) return [\"The field engines is not a list.\"];\n return value.flatMap((engine: unknown, index) => (isRecord(engine) && isText(engine[\"name\"]) && isText(engine[\"version\"]) ? [] : [`Engine ${index} has no name and version.`]));\n}\n\n/**\n * This function gives the faults of a parsed `kpi.json`, or an empty list.\n *\n * @param value - The parsed file.\n * @param now - The time of the check, in milliseconds. A time of the data after this time is a fault.\n */\nexport function kpiProblems(value: unknown, now: number = Date.now()): string[] {\n if (!isRecord(value)) return [\"The file is not an object.\"];\n const problems: string[] = [];\n if (value[\"format\"] !== KPI_FORMAT) problems.push(`The format is not ${KPI_FORMAT}.`);\n const { project, at, commit, measures } = value;\n if (typeof project !== \"string\" || !SLUG.test(project)) problems.push(\"The project is not lowercase words with single hyphens.\");\n if (typeof at !== \"string\" || !UTC_TIME.test(at) || Number.isNaN(Date.parse(at))) problems.push(\"The field at is not a time in UTC.\");\n else if (Date.parse(at) > now + CLOCK_MARGIN) problems.push(`The time of the data ${at} is after the time of the check.`);\n if (typeof commit !== \"string\" || !COMMIT.test(commit)) problems.push(\"The commit is not a full commit.\");\n if (!Array.isArray(measures) || measures.length === 0) problems.push(\"The file has no measures.\");\n else {\n const seen = new Set<string>();\n problems.push(...measures.flatMap((measure: unknown, index) => measureProblems(measure, index, seen)));\n }\n problems.push(...engineProblems(value[\"engines\"]));\n return problems;\n}\n\n/** This function gives a copy of a measure with only its known fields. */\nfunction cleanMeasure({ id, value, unit }: KpiMeasure): KpiMeasure {\n return unit === undefined ? { id, value } : { id, value, unit };\n}\n\n/** This function gives a copy of an engine with only its known fields. */\nfunction cleanEngine({ name, version }: KpiEngine): KpiEngine {\n return { name, version };\n}\n\n/**\n * This function gives the checked content of `kpi.json`. The file has only its known fields, in a fixed order.\n *\n * @throws KpiError when the data is not correct\n */\nexport function kpiFile(input: KpiInput, now?: number): KpiFile {\n const problems = kpiProblems({ format: KPI_FORMAT, ...input }, now);\n if (problems.length > 0) throw new KpiError(problems);\n const { project, at, commit, measures, engines } = input;\n const file: KpiFile = { format: KPI_FORMAT, project, at, commit, measures: measures.map(cleanMeasure) };\n return engines === undefined ? file : { ...file, engines: engines.map(cleanEngine) };\n}\n\n/** This function gives the text of `kpi.json`. */\nexport function kpiText(file: KpiFile): string {\n return `${JSON.stringify(file, null, 2)}\\n`;\n}\n\n/**\n * This function reads the text of a `kpi.json`.\n *\n * @throws KpiError when the text is not correct\n */\nexport function readKpi(text: string, now?: number): KpiFile {\n let value: unknown;\n try {\n value = JSON.parse(text);\n } catch (error) {\n throw new KpiError([`The file is not JSON: ${error instanceof Error ? error.message : String(error)}`]);\n }\n const problems = kpiProblems(value, now);\n if (problems.length > 0) throw new KpiError(problems);\n const { project, at, commit, measures, engines } = value as KpiFile;\n return kpiFile(engines === undefined ? { project, at, commit, measures } : { project, at, commit, measures, engines }, now);\n}\n\n/**\n * This function writes `kpi.json` into the built output folder of a site, and gives the path of the file.\n *\n * @throws KpiError when the data is not correct\n */\nexport function writeKpi(out: string, input: KpiInput, now?: number): string {\n const text = kpiText(kpiFile(input, now));\n mkdirSync(out, { recursive: true });\n const file = join(out, KPI_FILE);\n writeFileSync(file, text);\n return file;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"kpi.js","names":[],"sources":["../../src/data/git.ts","../../src/data/kpi.ts"],"sourcesContent":["/**\n * The git source: the tags with their dates, the commit of the build and the GitHub remote. Each\n * function gives an empty result when git or the repository is not available.\n */\n\nimport { execFileSync } from \"node:child_process\";\n\n/** A git tag and the ISO date of its creation. */\nexport interface TagDate {\n tag: string;\n date: string;\n}\n\n/** This function starts git in a folder and gives its output, or null when git fails. */\nexport function git(repoDir: string, ...args: string[]): string | null {\n try {\n return execFileSync(\"git\", args, { cwd: repoDir, encoding: \"utf8\", stdio: [\"ignore\", \"pipe\", \"ignore\"], windowsHide: true });\n } catch {\n return null;\n }\n}\n\n/** This function tells if the clone is shallow. A shallow clone has not all the tags and commits. */\nexport function isShallow(repoDir: string): boolean {\n return git(repoDir, \"rev-parse\", \"--is-shallow-repository\")?.trim() === \"true\";\n}\n\n/** This function gives each tag of the repository with its date. An annotated tag has its own date. */\nexport function gitTagDates(repoDir: string): TagDate[] {\n const output = git(repoDir, \"for-each-ref\", \"--format=%(refname:strip=2) %(creatordate:iso-strict)\", \"refs/tags\");\n if (!output) return [];\n const tags: TagDate[] = [];\n for (const line of output.split(/\\r?\\n/)) {\n const match = /^(\\S+) (\\S+)$/.exec(line.trim());\n if (match?.[1] && match[2]) tags.push({ tag: match[1], date: match[2] });\n }\n return tags;\n}\n\n/**\n * This function gives the date of the oldest commit that added a version heading to a changelog,\n * or null. The path of the changelog starts at `repoDir`.\n */\nexport function headingDate(repoDir: string, version: string, file: string): string | null {\n const output = git(repoDir, \"log\", \"--format=%cI\", `-S## ${version}`, \"--\", file);\n const dates = (output ?? \"\").trim().split(/\\r?\\n/).filter(Boolean);\n return dates[dates.length - 1] ?? null;\n}\n\n/** This function gives the full SHA of `HEAD`, or null. */\nexport function headCommit(repoDir: string): string | null {\n const sha = git(repoDir, \"rev-parse\", \"HEAD\")?.trim();\n return sha && /^[0-9a-f]{40}$/.test(sha) ? sha : null;\n}\n\n/** This function gives \"owner/name\" of the GitHub remote \"origin\", or null. */\nexport function originRepository(repoDir: string): string | null {\n const url = git(repoDir, \"remote\", \"get-url\", \"origin\")?.trim() ?? \"\";\n const match = /github\\.com[:/]([^/]+)\\/(.+?)(?:\\.git)?\\/?$/.exec(url);\n return match ? `${match[1]}/${match[2]}` : null;\n}\n","/**\n * The file `kpi.json` at the root of a site, format `kpi/1`. A reader outside the site takes the measures of the project\n * from this file. Thus a number on a progress page comes from a record and not from a person.\n *\n * The field `at` is the time of the data and not the time of the build. Thus the writer has no default for it.\n * A measure can have its own `at` when its data is from another time than the rest of the file.\n */\n\nimport { mkdirSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { KpiEngine, KpiFile, KpiMeasure } from \"./types.ts\";\n\n/** The name of the format. A new format gets a new name, so that an old reader refuses it. */\nexport const KPI_FORMAT = \"kpi/1\";\n\n/** The name of the file at the root of the built site. */\nexport const KPI_FILE = \"kpi.json\";\n\n/** The data of `kpi.json`, without the name of the format. */\nexport type KpiInput = Omit<KpiFile, \"format\">;\n\n/** The form of a project and a measure. A double hyphen separates fields in the file name of a reading. */\nconst SLUG = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;\n\n/** The form of a time in UTC, with or without seconds. */\nconst UTC_TIME = /^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}(?::\\d{2}(?:\\.\\d+)?)?Z$/;\n\n/** The form of a full commit. */\nconst COMMIT = /^[0-9a-f]{40}$/;\n\n/** The clock difference between two computers that the check accepts, in milliseconds. */\nconst CLOCK_MARGIN = 5 * 60 * 1000;\n\n/** An error in the data of `kpi.json`. The field `problems` has one line for each fault. */\nexport class KpiError extends Error {\n readonly problems: readonly string[];\n\n constructor(problems: readonly string[]) {\n super(`${KPI_FILE} is not correct: ${problems.join(\" \")}`);\n this.name = \"KpiError\";\n this.problems = problems;\n }\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> => typeof value === \"object\" && value !== null && !Array.isArray(value);\n\nconst isText = (value: unknown): value is string => typeof value === \"string\" && value.trim() !== \"\";\n\n/** This function gives the fault of a time of the data, or an empty list. The name tells which field it is. */\nfunction timeProblems(at: unknown, name: string, now: number): string[] {\n if (typeof at !== \"string\" || !UTC_TIME.test(at) || Number.isNaN(Date.parse(at))) return [`${name} is not a time in UTC.`];\n return Date.parse(at) > now + CLOCK_MARGIN ? [`The time of the data ${at} is after the time of the check.`] : [];\n}\n\n/** This function gives the faults of one measure. */\nfunction measureProblems(value: unknown, index: number, seen: Set<string>, now: number): string[] {\n if (!isRecord(value)) return [`Measure ${index} is not an object.`];\n const { id, value: amount, unit, at } = value;\n const problems: string[] = [];\n if (typeof id !== \"string\" || !SLUG.test(id)) problems.push(`Measure ${index} has no id of lowercase words with single hyphens.`);\n else if (seen.has(id)) problems.push(`The id ${id} occurs two times.`);\n else seen.add(id);\n if (typeof amount !== \"number\" || !Number.isFinite(amount)) problems.push(`Measure ${index} has no finite number as its value.`);\n if (unit !== undefined && !isText(unit)) problems.push(`Measure ${index} has a unit that is not a text.`);\n if (at !== undefined) problems.push(...timeProblems(at, `The field at of measure ${index}`, now));\n return problems;\n}\n\n/** This function gives the faults of the list of engines. */\nfunction engineProblems(value: unknown): string[] {\n if (value === undefined) return [];\n if (!Array.isArray(value)) return [\"The field engines is not a list.\"];\n return value.flatMap((engine: unknown, index) => (isRecord(engine) && isText(engine[\"name\"]) && isText(engine[\"version\"]) ? [] : [`Engine ${index} has no name and version.`]));\n}\n\n/**\n * This function gives the faults of a parsed `kpi.json`, or an empty list.\n *\n * @param value - The parsed file.\n * @param now - The time of the check, in milliseconds. A time of the data after this time is a fault.\n */\nexport function kpiProblems(value: unknown, now: number = Date.now()): string[] {\n if (!isRecord(value)) return [\"The file is not an object.\"];\n const problems: string[] = [];\n if (value[\"format\"] !== KPI_FORMAT) problems.push(`The format is not ${KPI_FORMAT}.`);\n const { project, at, commit, measures } = value;\n if (typeof project !== \"string\" || !SLUG.test(project)) problems.push(\"The project is not lowercase words with single hyphens.\");\n problems.push(...timeProblems(at, \"The field at\", now));\n if (typeof commit !== \"string\" || !COMMIT.test(commit)) problems.push(\"The commit is not a full commit.\");\n if (!Array.isArray(measures) || measures.length === 0) problems.push(\"The file has no measures.\");\n else {\n const seen = new Set<string>();\n problems.push(...measures.flatMap((measure: unknown, index) => measureProblems(measure, index, seen, now)));\n }\n problems.push(...engineProblems(value[\"engines\"]));\n return problems;\n}\n\n/** This function gives a copy of a measure with only its known fields. */\nfunction cleanMeasure({ id, value, unit, at }: KpiMeasure): KpiMeasure {\n return { id, value, ...(unit === undefined ? {} : { unit }), ...(at === undefined ? {} : { at }) };\n}\n\n/** This function gives a copy of an engine with only its known fields. */\nfunction cleanEngine({ name, version }: KpiEngine): KpiEngine {\n return { name, version };\n}\n\n/**\n * This function gives the checked content of `kpi.json`. The file has only its known fields, in a fixed order.\n *\n * @throws KpiError when the data is not correct\n */\nexport function kpiFile(input: KpiInput, now?: number): KpiFile {\n const problems = kpiProblems({ format: KPI_FORMAT, ...input }, now);\n if (problems.length > 0) throw new KpiError(problems);\n const { project, at, commit, measures, engines } = input;\n const file: KpiFile = { format: KPI_FORMAT, project, at, commit, measures: measures.map(cleanMeasure) };\n return engines === undefined ? file : { ...file, engines: engines.map(cleanEngine) };\n}\n\n/** This function gives the text of `kpi.json`. */\nexport function kpiText(file: KpiFile): string {\n return `${JSON.stringify(file, null, 2)}\\n`;\n}\n\n/**\n * This function reads the text of a `kpi.json`.\n *\n * @throws KpiError when the text is not correct\n */\nexport function readKpi(text: string, now?: number): KpiFile {\n let value: unknown;\n try {\n value = JSON.parse(text);\n } catch (error) {\n throw new KpiError([`The file is not JSON: ${error instanceof Error ? error.message : String(error)}`]);\n }\n const problems = kpiProblems(value, now);\n if (problems.length > 0) throw new KpiError(problems);\n const { project, at, commit, measures, engines } = value as KpiFile;\n return kpiFile(engines === undefined ? { project, at, commit, measures } : { project, at, commit, measures, engines }, now);\n}\n\n/**\n * This function writes `kpi.json` into the built output folder of a site, and gives the path of the file.\n *\n * @throws KpiError when the data is not correct\n */\nexport function writeKpi(out: string, input: KpiInput, now?: number): string {\n const text = kpiText(kpiFile(input, now));\n mkdirSync(out, { recursive: true });\n const file = join(out, KPI_FILE);\n writeFileSync(file, text);\n return file;\n}\n"],"mappings":"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"}
|
package/build/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { a as runCli,
|
|
2
|
+
import { a as runCli, h as probeScript, m as TYPESCRIPT_PROBE, p as runFreshnessCli } from "./chunks/build.js";
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
4
|
import { appendFileSync, createReadStream, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { delimiter, extname, isAbsolute, join, normalize, resolve, sep } from "node:path";
|
|
@@ -758,7 +758,7 @@ const io = {
|
|
|
758
758
|
stdout: (text) => process.stdout.write(text),
|
|
759
759
|
stderr: (text) => process.stderr.write(text)
|
|
760
760
|
};
|
|
761
|
-
(argv[0] === "check" ? runCheckCli(argv.slice(1), io) : runCli(argv, io)).then((code) => {
|
|
761
|
+
(argv[0] === "check" ? runCheckCli(argv.slice(1), io) : argv[0] === "freshness" ? runFreshnessCli(argv.slice(1), io) : runCli(argv, io)).then((code) => {
|
|
762
762
|
process.exitCode = code;
|
|
763
763
|
}, (error) => {
|
|
764
764
|
process.stderr.write(`site-pages: ${error instanceof Error ? error.stack ?? error.message : String(error)}\n`);
|
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/** 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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"}
|
|
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, check/check.ts and check/freshness.ts, so a test can run the same CLI with its own fetch.\nimport { runCli } from \"./build.ts\";\nimport { runCheckCli } from \"./check/check.ts\";\nimport { runFreshnessCli } from \"./check/freshness.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) : argv[0] === \"freshness\" ? runFreshnessCli(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/index.d.ts
CHANGED
|
@@ -513,6 +513,79 @@ export declare const TYPESCRIPT_PROBE: RegExp;
|
|
|
513
513
|
* that is not only types (an `enum`, a `namespace`) causes an error.
|
|
514
514
|
*/
|
|
515
515
|
export declare function probeScript(source: string): string;
|
|
516
|
+
/** The schema of a file of data stamps. */
|
|
517
|
+
export declare const DATA_STAMP_SCHEMA = "data-stamp/1";
|
|
518
|
+
/** The largest age of the data of a page, in days, when no newer run is in flight. */
|
|
519
|
+
export declare const MAX_DATA_AGE_DAYS = 3;
|
|
520
|
+
/** The most minor versions that an engine of a run can be behind npm at the start of the run. */
|
|
521
|
+
export declare const MAX_MINORS_BEHIND = 1;
|
|
522
|
+
/** One run that gives the data of a page. */
|
|
523
|
+
interface DataRun {
|
|
524
|
+
readonly id: string;
|
|
525
|
+
readonly url: string;
|
|
526
|
+
/** The ISO time of the start of the run. */
|
|
527
|
+
readonly date: string;
|
|
528
|
+
/** The version of each engine of the run, by its npm name. */
|
|
529
|
+
readonly engines: Readonly<Record<string, string>>;
|
|
530
|
+
}
|
|
531
|
+
/** The stamp of one measured page. */
|
|
532
|
+
interface PageStamp {
|
|
533
|
+
readonly page: string;
|
|
534
|
+
/** Each run that gives data to the page. */
|
|
535
|
+
readonly runs: readonly DataRun[];
|
|
536
|
+
/** The run on newer engines that is in progress. */
|
|
537
|
+
readonly inFlight?: DataRun;
|
|
538
|
+
}
|
|
539
|
+
/** The content of a file of data stamps. */
|
|
540
|
+
interface DataStamps {
|
|
541
|
+
readonly schema: typeof DATA_STAMP_SCHEMA;
|
|
542
|
+
readonly pages: readonly PageStamp[];
|
|
543
|
+
}
|
|
544
|
+
/** The result of `parseDataStamps`. */
|
|
545
|
+
type DataStampsParse = {
|
|
546
|
+
readonly ok: true;
|
|
547
|
+
readonly stamps: DataStamps;
|
|
548
|
+
} | {
|
|
549
|
+
readonly ok: false;
|
|
550
|
+
readonly faults: readonly string[];
|
|
551
|
+
};
|
|
552
|
+
/** The options of `checkFreshness`. */
|
|
553
|
+
interface FreshnessOptions {
|
|
554
|
+
/** The ISO time of the check. */
|
|
555
|
+
readonly now: string;
|
|
556
|
+
/** The publication times of the engines on npm. */
|
|
557
|
+
readonly times: readonly NpmVersionTime[];
|
|
558
|
+
/** The limit of the age. The default is `MAX_DATA_AGE_DAYS`. */
|
|
559
|
+
readonly maxAgeDays?: number;
|
|
560
|
+
/** The limit of the minor versions. The default is `MAX_MINORS_BEHIND`. */
|
|
561
|
+
readonly maxMinorsBehind?: number;
|
|
562
|
+
}
|
|
563
|
+
/** The result of `checkFreshness`. */
|
|
564
|
+
interface FreshnessCheck {
|
|
565
|
+
/** Each break of a rule. One fault fails the gate. */
|
|
566
|
+
readonly faults: readonly string[];
|
|
567
|
+
/** Each rule that the gate could not run, with the reason. */
|
|
568
|
+
readonly notRun: readonly string[];
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* This function reads the content of a file of data stamps. It gives each fault of the content, so a page with no versions
|
|
572
|
+
* or no run date fails here.
|
|
573
|
+
*/
|
|
574
|
+
export declare function parseDataStamps(raw: unknown): DataStampsParse;
|
|
575
|
+
/** This function gives the count of minor versions from `used` to `latest`. A newer major version counts as no limit. */
|
|
576
|
+
export declare function minorsBehind(used: string, latest: string): number;
|
|
577
|
+
/** This function gives the newest stable version of a package on npm at a time, or undefined when npm has none. */
|
|
578
|
+
export declare function latestAt(times: readonly NpmVersionTime[], name: string, date: string): string | undefined;
|
|
579
|
+
/**
|
|
580
|
+
* This function applies the rules of the gate to the stamps.
|
|
581
|
+
*
|
|
582
|
+
* - The data of each page is at most `maxAgeDays` old, or a run on newer engines is in flight.
|
|
583
|
+
* - The engines of the newest run are at most `maxMinorsBehind` behind npm at the start of that run.
|
|
584
|
+
* - A run in flight has the same age limit, so an old stamp does not hold the gate open.
|
|
585
|
+
*/
|
|
586
|
+
export declare function checkFreshness(stamps: DataStamps, options: FreshnessOptions): FreshnessCheck;
|
|
587
|
+
/** This function gives the html line of the stamp of a page. A page shows this line with its data. */
|
|
588
|
+
export declare function renderDataStamp(stamp: PageStamp): string;
|
|
516
589
|
/** An error in the command line. The CLI shows the help and exits with code 2. */
|
|
517
590
|
export declare class UsageError extends Error {}
|
|
518
591
|
/** The options of `build`. */
|
|
@@ -563,7 +636,7 @@ interface BuildResult {
|
|
|
563
636
|
*/
|
|
564
637
|
export declare function build(options: BuildOptions): Promise<BuildResult>;
|
|
565
638
|
/** The help of the CLI. */
|
|
566
|
-
export declare const HELP = "Usage: site-pages build --out <dir> [options]\n\nWrites <out>/data/releases.json, <out>/data/status.json, <out>/releases/index.html\nand <out>/status/index.html. Start it in the repository, after the build of the site.\n\nOptions:\n --out <dir> The built output folder of the site (necessary)\n --repo <owner/name> The GitHub repository (default: $GITHUB_REPOSITORY, else the remote \"origin\")\n --base </name/> The path of the site home (default: \"/<name>/\", or \"/\" for <owner>.github.io)\n --roadmap <file> The roadmap file (default: none, the status page has only the GitHub part)\n --packages <names> npm names or a @scope, separated by commas, for the npm dates\n --changesets <glob> The changelog files (default: packages/*/CHANGELOG.md, else CHANGELOG.md)\n --group <name> The npm name of the package whose changelog gives the versions\n --token <token> A GitHub token with read access (default: $GITHUB_TOKEN)\n --css <file> A CSS file for the pages, after the default CSS\n --site-name <name> The name of the site in the header (default: the repository name)\n --data-only Write only the JSON data, not the pages\n -h, --help Show this help\n\nUsage: site-pages check (--out <dir> | --url <url>) [options]\n\nOpens the pages of the site in a headless Chromium at phone and desktop widths. Start\n\"site-pages check --help\" for its options.\n";
|
|
639
|
+
export declare const HELP = "Usage: site-pages build --out <dir> [options]\n\nWrites <out>/data/releases.json, <out>/data/status.json, <out>/releases/index.html\nand <out>/status/index.html. Start it in the repository, after the build of the site.\n\nOptions:\n --out <dir> The built output folder of the site (necessary)\n --repo <owner/name> The GitHub repository (default: $GITHUB_REPOSITORY, else the remote \"origin\")\n --base </name/> The path of the site home (default: \"/<name>/\", or \"/\" for <owner>.github.io)\n --roadmap <file> The roadmap file (default: none, the status page has only the GitHub part)\n --packages <names> npm names or a @scope, separated by commas, for the npm dates\n --changesets <glob> The changelog files (default: packages/*/CHANGELOG.md, else CHANGELOG.md)\n --group <name> The npm name of the package whose changelog gives the versions\n --token <token> A GitHub token with read access (default: $GITHUB_TOKEN)\n --css <file> A CSS file for the pages, after the default CSS\n --site-name <name> The name of the site in the header (default: the repository name)\n --data-only Write only the JSON data, not the pages\n -h, --help Show this help\n\nUsage: site-pages check (--out <dir> | --url <url>) [options]\n\nOpens the pages of the site in a headless Chromium at phone and desktop widths. Start\n\"site-pages check --help\" for its options.\n\nUsage: site-pages freshness --stamps <file> [options]\n\nExamine the data stamps of the measured pages of a site. See\n\"site-pages freshness --help\" for its options.\n";
|
|
567
640
|
/** The input and output of `runCli`. A test gives its own values. */
|
|
568
641
|
interface CliIo {
|
|
569
642
|
cwd: string;
|
|
@@ -584,5 +657,5 @@ export declare function parseBuildArgs(argv: readonly string[], io: Pick<CliIo,
|
|
|
584
657
|
* source is not correct, and 2 for an incorrect command line.
|
|
585
658
|
*/
|
|
586
659
|
export declare function runCli(argv: readonly string[], io: CliIo): Promise<number>;
|
|
587
|
-
export { type BoardIssue, type BoardItem, type BoardPullRequest, type BoardSection, type BuildOptions, type BuildResult, type ChangelogEntry, type ChangelogSection, type ChangelogSource, type CheckState, type CliIo, type GitHubOpenWork, type GitHubOptions, type GitHubRelease, type GitHubReleases, type ItemGroup, KPI_FILE, KPI_FORMAT, type KpiEngine, KpiError, type KpiFile, type KpiInput, type KpiMeasure, type Link, type LinkOptions, type MarkdownTable, type NpmOptions, type NpmPublishTimes, type NpmVersionTime, type OpenWorkTotals, type PageOptions, type PageParts, type ParsedVersion, type ReadReleasesOptions, type ReadStatusBoardOptions, type Release, type ReleaseChange, type ReleaseKind, type ReleasePoint, type ReleaseSources, type ReleaseSummary, type StatusBoardData, type StatusBoardInput, type StatusClass, type TagDate, type TagVersion, type TimelineBand, type TimelineLayout, type TimelineMark, type TimelineOptions, kpiFile, kpiProblems, kpiText, readKpi, writeKpi };
|
|
660
|
+
export { type BoardIssue, type BoardItem, type BoardPullRequest, type BoardSection, type BuildOptions, type BuildResult, type ChangelogEntry, type ChangelogSection, type ChangelogSource, type CheckState, type CliIo, type DataRun, type DataStamps, type DataStampsParse, type FreshnessCheck, type FreshnessOptions, type GitHubOpenWork, type GitHubOptions, type GitHubRelease, type GitHubReleases, type ItemGroup, KPI_FILE, KPI_FORMAT, type KpiEngine, KpiError, type KpiFile, type KpiInput, type KpiMeasure, type Link, type LinkOptions, type MarkdownTable, type NpmOptions, type NpmPublishTimes, type NpmVersionTime, type OpenWorkTotals, type PageOptions, type PageParts, type PageStamp, type ParsedVersion, type ReadReleasesOptions, type ReadStatusBoardOptions, type Release, type ReleaseChange, type ReleaseKind, type ReleasePoint, type ReleaseSources, type ReleaseSummary, type StatusBoardData, type StatusBoardInput, type StatusClass, type TagDate, type TagVersion, type TimelineBand, type TimelineLayout, type TimelineMark, type TimelineOptions, kpiFile, kpiProblems, kpiText, readKpi, writeKpi };
|
|
588
661
|
//# sourceMappingURL=index.d.ts.map
|