@mark1russell7/site-pages 0.2.8 → 0.2.9
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 +20 -0
- package/build/chunks/build.js +21 -423
- package/build/chunks/build.js.map +1 -1
- package/build/chunks/data-stamp.js +23 -0
- package/build/chunks/data-stamp.js.map +1 -0
- package/build/chunks/{kpi.d.ts → freshness.d.ts} +94 -2
- package/build/chunks/freshness.d.ts.map +1 -0
- package/build/chunks/freshness.js +741 -0
- package/build/chunks/freshness.js.map +1 -0
- package/build/cli.js +2 -1
- package/build/cli.js.map +1 -1
- package/build/index.d.ts +2 -73
- package/build/index.d.ts.map +1 -1
- package/build/index.js +4 -19
- package/build/vite.d.ts +29 -1
- package/build/vite.d.ts.map +1 -1
- package/build/vite.js +52 -2
- package/build/vite.js.map +1 -1
- package/package.json +1 -1
- package/build/chunks/kpi.d.ts.map +0 -1
- package/build/chunks/kpi.js +0 -226
- package/build/chunks/kpi.js.map +0 -1
- package/build/index.js.map +0 -1
package/README.md
CHANGED
|
@@ -161,10 +161,29 @@ The gate has three rules. A fault of one rule gives the exit code 1.
|
|
|
161
161
|
2. The data of a page is at most 3 days old (`MAX_DATA_AGE_DAYS`), or a run on newer engines is in flight. The stamp of that run is at most 3 days old, so an old stamp does not hold the gate open.
|
|
162
162
|
3. The engines of the newest run are at most 1 minor version behind npm at the start of the run (`MAX_MINORS_BEHIND`). A release during a long run does not fail the gate.
|
|
163
163
|
|
|
164
|
+
Two fields of a stamp change the age rule:
|
|
165
|
+
|
|
166
|
+
- `"study": true` marks a fixed study. The data has no age limit, and the rule 3 does not apply. The run names the method or the run, the date and the engine version. The stamp line tells in plain words that the page is a fixed study of that version. A page with a claim about the current engine is not a fixed study.
|
|
167
|
+
- `"limit"` sets a longer age limit: `{ "days": 7, "reason": "The scan is weekly.", "workflow": ".github/workflows/scan.yml" }`. The stamp line shows the limit and the reason. The schedule of that workflow must meet the limit, or the gate fails. A limit with no schedule fails. A limit over 14 days (`MAX_LIMIT_DAYS_WITHOUT_RULING`) also needs a `ruling`, a public https link for that site.
|
|
168
|
+
|
|
164
169
|
When npm does not answer, the gate prints the rule 3 under `Not run` and does not fail. A workflow turns the gate on with the input `data-stamps` of `reusable-pages.yml`. The gate then runs from the template commit, so it needs no install.
|
|
165
170
|
|
|
166
171
|
`renderDataStamp(stamp)` gives the line that the page shows with its data: the run, its date, its engine versions and the run in flight.
|
|
167
172
|
|
|
173
|
+
A Vite site uses the plugin `siteStamps` from `@mark1russell7/site-pages/vite`. Give the stamps, or a function that reads them from the record of the run. The plugin replaces `<!-- data-stamp -->` in each measured page. It also adds `data-stamps.json` to the built site. So the page and the gate read one record:
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
export default defineConfig({
|
|
177
|
+
plugins: [siteStamps(() => stampsOf(readRun()))],
|
|
178
|
+
});
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
```yaml
|
|
182
|
+
data-stamps: packages/site/dist/data-stamps.json
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
A fault stops the build. A fault is a bad stamp, a mark with no stamp or a stamp with no mark. A stamp of a page that the build does not hold is also a fault.
|
|
186
|
+
|
|
168
187
|
## The releases
|
|
169
188
|
|
|
170
189
|
The command merges four sources by version:
|
|
@@ -466,6 +485,7 @@ showBuild(byId("build"), __BUILD__, "owner/my-site");
|
|
|
466
485
|
| Export of `./vite` | Operation |
|
|
467
486
|
| --- | --- |
|
|
468
487
|
| `siteShell({ themeKey, packageFile })` | Makes the plugin. A `base` or a `build.target` in the config of the site has priority. |
|
|
488
|
+
| `siteStamps(input)` | Makes the plugin of the data stamps of a site. |
|
|
469
489
|
| `themeScript(storageKey)` | Gives the text of the theme script, for a page without the plugin. |
|
|
470
490
|
| `siteBase(env)` | Gives `SITE_BASE` with one `/` at the end, or `/`. |
|
|
471
491
|
| `buildInfo(version, env, now)` | Gives the data of `__BUILD__`: the short commit, the date and the version. |
|
package/build/chunks/build.js
CHANGED
|
@@ -1,70 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { A as npmPublishTimes, D as isShallow, E as headingDate, M as kindOf, N as parseVersion, O as originRepository, P as versionOfTag, S as readKpi, T as headCommit, d as inlineMarkdown, f as link, g as KPI_FILE, j as compareVersions, m as plural, p as listText, u as escapeHtml, v as KpiError, w as gitTagDates } from "./freshness.js";
|
|
2
2
|
import { stripTypeScriptTypes } from "node:module";
|
|
3
3
|
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { basename, dirname, isAbsolute, join, posix, resolve } from "node:path";
|
|
5
5
|
import { parseArgs } from "node:util";
|
|
6
|
-
// src/data/versions.ts
|
|
7
|
-
const SEMVER = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
8
|
-
/** This function gives the parts of a version, or null when the text is not a SemVer version. */
|
|
9
|
-
function parseVersion(version) {
|
|
10
|
-
const match = SEMVER.exec(version);
|
|
11
|
-
if (!match) return null;
|
|
12
|
-
return {
|
|
13
|
-
major: Number(match[1]),
|
|
14
|
-
minor: Number(match[2]),
|
|
15
|
-
patch: Number(match[3]),
|
|
16
|
-
prerelease: match[4] ? match[4].split(".") : []
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* This function compares two versions by the SemVer precedence. It gives a negative number when `a`
|
|
21
|
-
* is older, a positive number when `a` is newer, and 0 for the same version. A text that is not a
|
|
22
|
-
* version comes before each version.
|
|
23
|
-
*/
|
|
24
|
-
function compareVersions(a, b) {
|
|
25
|
-
const x = parseVersion(a);
|
|
26
|
-
const y = parseVersion(b);
|
|
27
|
-
if (!x || !y) return x ? 1 : y ? -1 : a.localeCompare(b);
|
|
28
|
-
const core = x.major - y.major || x.minor - y.minor || x.patch - y.patch;
|
|
29
|
-
if (core !== 0) return core;
|
|
30
|
-
if (x.prerelease.length === 0 || y.prerelease.length === 0) return y.prerelease.length - x.prerelease.length;
|
|
31
|
-
for (let index = 0; index < Math.max(x.prerelease.length, y.prerelease.length); index++) {
|
|
32
|
-
const p = x.prerelease[index];
|
|
33
|
-
const q = y.prerelease[index];
|
|
34
|
-
if (p === void 0) return -1;
|
|
35
|
-
if (q === void 0) return 1;
|
|
36
|
-
if (p === q) continue;
|
|
37
|
-
const pNumber = /^\d+$/.test(p);
|
|
38
|
-
const qNumber = /^\d+$/.test(q);
|
|
39
|
-
if (pNumber && qNumber) return Number(p) - Number(q);
|
|
40
|
-
if (pNumber !== qNumber) return pNumber ? -1 : 1;
|
|
41
|
-
return p < q ? -1 : 1;
|
|
42
|
-
}
|
|
43
|
-
return 0;
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
46
|
-
* This function gives the kind of a version. The oldest version of the list is "first". The caller
|
|
47
|
-
* tells if the version is the oldest.
|
|
48
|
-
*/
|
|
49
|
-
function kindOf(version, oldest) {
|
|
50
|
-
if (oldest) return "first";
|
|
51
|
-
const parsed = parseVersion(version);
|
|
52
|
-
if (!parsed) return "patch";
|
|
53
|
-
if (parsed.major > 0 && parsed.minor === 0 && parsed.patch === 0) return "major";
|
|
54
|
-
return parsed.patch === 0 ? "minor" : "patch";
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* This function reads the version of a git tag. It knows `1.2.3`, `v1.2.3` and the tags of Changesets:
|
|
58
|
-
* `name@1.2.3` and `@scope/name@1.2.3`. It gives null for another tag.
|
|
59
|
-
*/
|
|
60
|
-
function versionOfTag(tag) {
|
|
61
|
-
const match = /^(?:(.+)@)?v?(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.+-]*)?)$/.exec(tag.trim());
|
|
62
|
-
if (!match || match[2] === void 0 || !parseVersion(match[2])) return null;
|
|
63
|
-
return {
|
|
64
|
-
version: match[2],
|
|
65
|
-
package: match[1] ?? null
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
6
|
// src/data/changelog.ts
|
|
69
7
|
const VERSION_HEADING = /^##\s+\[?v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)\]?(?:\s.*)?$/;
|
|
70
8
|
const COMMIT_ENTRY = /^([0-9a-f]{7,40}):\s+([\s\S]*)$/;
|
|
@@ -235,13 +173,13 @@ function checkStateOf(rollup) {
|
|
|
235
173
|
default: return "none";
|
|
236
174
|
}
|
|
237
175
|
}
|
|
238
|
-
const object
|
|
176
|
+
const object = (value) => value !== null && typeof value === "object" ? value : {};
|
|
239
177
|
const list = (value) => Array.isArray(value) ? value : [];
|
|
240
178
|
const text = (value, fallback = "") => typeof value === "string" ? value : fallback;
|
|
241
|
-
const labelsOf = (node) => list(object
|
|
242
|
-
const authorOf = (node) => text(object
|
|
179
|
+
const labelsOf = (node) => list(object(node["labels"])["nodes"]).map((label) => object(label)["name"]).filter((name) => typeof name === "string");
|
|
180
|
+
const authorOf = (node) => text(object(node["author"])["login"], "ghost");
|
|
243
181
|
function failure(error, timeoutMs) {
|
|
244
|
-
if (object
|
|
182
|
+
if (object(error)["name"] === "TimeoutError") return {
|
|
245
183
|
available: false,
|
|
246
184
|
reason: `GitHub did not answer in ${timeoutMs / 1e3} s`
|
|
247
185
|
};
|
|
@@ -297,22 +235,22 @@ async function githubOpenWork(options) {
|
|
|
297
235
|
available: false,
|
|
298
236
|
reason: `GitHub answered with HTTP ${response.status}`
|
|
299
237
|
};
|
|
300
|
-
const body = object
|
|
238
|
+
const body = object(await response.json());
|
|
301
239
|
const errors = list(body["errors"]);
|
|
302
240
|
if (errors.length > 0) return {
|
|
303
241
|
available: false,
|
|
304
|
-
reason: `GitHub gave an error: ${text(object
|
|
242
|
+
reason: `GitHub gave an error: ${text(object(errors[0])["message"], "no message")}`
|
|
305
243
|
};
|
|
306
|
-
const repository = object
|
|
244
|
+
const repository = object(body["data"])["repository"];
|
|
307
245
|
if (repository === null || typeof repository !== "object") return {
|
|
308
246
|
available: false,
|
|
309
247
|
reason: `GitHub has no repository ${owner}/${repo}`
|
|
310
248
|
};
|
|
311
|
-
const pulls = object
|
|
312
|
-
const open = object
|
|
249
|
+
const pulls = object(repository["pullRequests"]);
|
|
250
|
+
const open = object(repository["issues"]);
|
|
313
251
|
const pullRequests = list(pulls["nodes"]).filter((node) => node !== null && typeof node === "object").map((value) => {
|
|
314
|
-
const node = object
|
|
315
|
-
const lastCommit = object
|
|
252
|
+
const node = object(value);
|
|
253
|
+
const lastCommit = object(object(list(object(node["commits"])["nodes"])[0])["commit"]);
|
|
316
254
|
return {
|
|
317
255
|
number: Number(node["number"]),
|
|
318
256
|
title: text(node["title"]),
|
|
@@ -323,11 +261,11 @@ async function githubOpenWork(options) {
|
|
|
323
261
|
createdAt: text(node["createdAt"]),
|
|
324
262
|
updatedAt: text(node["updatedAt"]),
|
|
325
263
|
labels: labelsOf(node),
|
|
326
|
-
checks: checkStateOf(object
|
|
264
|
+
checks: checkStateOf(object(lastCommit["statusCheckRollup"]))
|
|
327
265
|
};
|
|
328
266
|
});
|
|
329
267
|
const issues = list(open["nodes"]).filter((node) => node !== null && typeof node === "object").map((value) => {
|
|
330
|
-
const node = object
|
|
268
|
+
const node = object(value);
|
|
331
269
|
return {
|
|
332
270
|
number: Number(node["number"]),
|
|
333
271
|
title: text(node["title"]),
|
|
@@ -400,7 +338,7 @@ async function githubReleases(options) {
|
|
|
400
338
|
reason: "GitHub gave no list of releases"
|
|
401
339
|
};
|
|
402
340
|
for (const value of body) {
|
|
403
|
-
const release = object
|
|
341
|
+
const release = object(value);
|
|
404
342
|
const tag = release["tag_name"];
|
|
405
343
|
if (release["draft"] === true || typeof tag !== "string") continue;
|
|
406
344
|
releases.push({
|
|
@@ -422,80 +360,6 @@ async function githubReleases(options) {
|
|
|
422
360
|
return failure(error, timeoutMs);
|
|
423
361
|
}
|
|
424
362
|
}
|
|
425
|
-
// src/data/npm.ts
|
|
426
|
-
/** The public npm registry. */
|
|
427
|
-
const NPM_REGISTRY = "https://registry.npmjs.org";
|
|
428
|
-
const object = (value) => value !== null && typeof value === "object" ? value : {};
|
|
429
|
-
function message(error) {
|
|
430
|
-
if (object(error)["name"] === "TimeoutError") return "npm did not answer in time";
|
|
431
|
-
return error instanceof Error ? error.message : String(error);
|
|
432
|
-
}
|
|
433
|
-
/**
|
|
434
|
-
* This function reads the publication time of each version of the packages from the npm registry.
|
|
435
|
-
* A scope (`@name`) gives each package of the scope. It does not throw. Each failure gives a
|
|
436
|
-
* message in `problems`.
|
|
437
|
-
*/
|
|
438
|
-
async function npmPublishTimes(options) {
|
|
439
|
-
const { registry = NPM_REGISTRY, timeoutMs = 15e3 } = options;
|
|
440
|
-
const fetcher = options.fetch ?? globalThis.fetch;
|
|
441
|
-
const problems = [];
|
|
442
|
-
const times = [];
|
|
443
|
-
if (typeof fetcher !== "function") return {
|
|
444
|
-
times,
|
|
445
|
-
problems: ["this Node has no fetch"]
|
|
446
|
-
};
|
|
447
|
-
const get = async (url) => {
|
|
448
|
-
const response = await fetcher(url, {
|
|
449
|
-
headers: { accept: "application/json" },
|
|
450
|
-
signal: AbortSignal.timeout(timeoutMs)
|
|
451
|
-
});
|
|
452
|
-
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
453
|
-
return response.json();
|
|
454
|
-
};
|
|
455
|
-
const names = /* @__PURE__ */ new Set();
|
|
456
|
-
for (const entry of options.packages.map((name) => name.trim()).filter(Boolean)) {
|
|
457
|
-
if (!/^@[^/]+$/.test(entry)) {
|
|
458
|
-
names.add(entry);
|
|
459
|
-
continue;
|
|
460
|
-
}
|
|
461
|
-
try {
|
|
462
|
-
const found = object(await get(`${registry}/-/v1/search?text=scope:${encodeURIComponent(entry.slice(1))}&size=250`));
|
|
463
|
-
const objects = Array.isArray(found["objects"]) ? found["objects"] : [];
|
|
464
|
-
const before = names.size;
|
|
465
|
-
for (const item of objects) {
|
|
466
|
-
const name = object(object(item)["package"])["name"];
|
|
467
|
-
if (typeof name === "string" && name.startsWith(`${entry}/`)) names.add(name);
|
|
468
|
-
}
|
|
469
|
-
if (names.size === before) problems.push(`npm has no package in the scope ${entry}`);
|
|
470
|
-
} catch (error) {
|
|
471
|
-
problems.push(`npm did not give the packages of ${entry}: ${message(error)}`);
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
const queue = [...names].sort();
|
|
475
|
-
const worker = async () => {
|
|
476
|
-
for (let name = queue.shift(); name !== void 0; name = queue.shift()) try {
|
|
477
|
-
const packument = object(await get(`${registry}/${name.replaceAll("/", "%2f")}`));
|
|
478
|
-
const published = object(packument["versions"]);
|
|
479
|
-
for (const [version, date] of Object.entries(object(packument["time"]))) {
|
|
480
|
-
if (version === "created" || version === "modified" || typeof date !== "string") continue;
|
|
481
|
-
if (!(version in published)) continue;
|
|
482
|
-
times.push({
|
|
483
|
-
name,
|
|
484
|
-
version,
|
|
485
|
-
date
|
|
486
|
-
});
|
|
487
|
-
}
|
|
488
|
-
} catch (error) {
|
|
489
|
-
problems.push(`npm did not give the versions of ${name}: ${message(error)}`);
|
|
490
|
-
}
|
|
491
|
-
};
|
|
492
|
-
await Promise.all(Array.from({ length: 4 }, worker));
|
|
493
|
-
times.sort((a, b) => a.name.localeCompare(b.name) || a.date.localeCompare(b.date));
|
|
494
|
-
return {
|
|
495
|
-
times,
|
|
496
|
-
problems
|
|
497
|
-
};
|
|
498
|
-
}
|
|
499
363
|
// src/data/glob.ts
|
|
500
364
|
/**
|
|
501
365
|
* A small glob for the changelog files. The syntax has `*` and `?` in one path segment, and `**` for
|
|
@@ -1332,89 +1196,12 @@ async function readStatusBoard(options) {
|
|
|
1332
1196
|
...options.generatedAt ? { generatedAt: options.generatedAt } : {}
|
|
1333
1197
|
});
|
|
1334
1198
|
}
|
|
1335
|
-
// src/render/html.ts
|
|
1336
|
-
/**
|
|
1337
|
-
* The HTML helpers of the pages: the escape functions and a small inline Markdown renderer.
|
|
1338
|
-
*/
|
|
1339
|
-
const ESCAPES = {
|
|
1340
|
-
"&": "&",
|
|
1341
|
-
"<": "<",
|
|
1342
|
-
">": ">",
|
|
1343
|
-
"\"": """,
|
|
1344
|
-
"'": "'"
|
|
1345
|
-
};
|
|
1346
|
-
/** This function escapes a text for HTML content and for an attribute value in double quotes. */
|
|
1347
|
-
function escapeHtml(text) {
|
|
1348
|
-
return String(text).replace(/[&<>"']/g, (char) => ESCAPES[char] ?? char);
|
|
1349
|
-
}
|
|
1350
|
-
/**
|
|
1351
|
-
* This function removes the control characters and the spaces (U+0000 to U+0020) at the two ends
|
|
1352
|
-
* of a text. A loop does it, because the regular expression `[\u0000- ]+$` backtracks. For a long
|
|
1353
|
-
* run of these characters, its time grows with the square of the length.
|
|
1354
|
-
*/
|
|
1355
|
-
function trimControl(text) {
|
|
1356
|
-
let start = 0;
|
|
1357
|
-
let end = text.length;
|
|
1358
|
-
while (start < end && text.charCodeAt(start) <= 32) start++;
|
|
1359
|
-
while (end > start && text.charCodeAt(end - 1) <= 32) end--;
|
|
1360
|
-
return text.slice(start, end);
|
|
1361
|
-
}
|
|
1362
|
-
/**
|
|
1363
|
-
* This function gives a URL that a link can hold, or null. It accepts `http:`, `https:` and
|
|
1364
|
-
* `mailto:` URLs, and relative URLs. It refuses each other scheme, for example `javascript:`.
|
|
1365
|
-
*
|
|
1366
|
-
* The URL parser of a browser removes each tab and line break, and the control characters and
|
|
1367
|
-
* spaces at the two ends. This function removes them before the scheme check, as the browser does.
|
|
1368
|
-
* Thus a control character before `javascript:` does not hide the scheme.
|
|
1369
|
-
*/
|
|
1370
|
-
function safeUrl(url) {
|
|
1371
|
-
const trimmed = trimControl(url.trim().replace(/[\t\n\r]/g, ""));
|
|
1372
|
-
if (/^(?:https?:|mailto:)/i.test(trimmed)) return trimmed;
|
|
1373
|
-
if (/^[a-z][a-z0-9+.-]*:/i.test(trimmed) || /^[/\\]{2}/.test(trimmed)) return null;
|
|
1374
|
-
return trimmed;
|
|
1375
|
-
}
|
|
1376
|
-
/** This function gives an `<a>` element, or the escaped text when the URL is not safe. */
|
|
1377
|
-
function link(url, html, attributes = "") {
|
|
1378
|
-
const href = safeUrl(url);
|
|
1379
|
-
return href === null ? html : `<a href="${escapeHtml(href)}"${attributes ? ` ${attributes}` : ""}>${html}</a>`;
|
|
1380
|
-
}
|
|
1381
|
-
/** The emphasis of a text without code spans or links. The text is already escaped. */
|
|
1382
|
-
function emphasis(escaped) {
|
|
1383
|
-
return escaped.replace(/\*\*([^*]+?)\*\*/g, "<strong>$1</strong>").replace(/(^|[^\w*])\*([^*\s][^*]*?)\*(?![\w*])/g, "$1<em>$2</em>").replace(/(^|[^\w])_([^_\s][^_]*?)_(?![\w])/g, "$1<em>$2</em>");
|
|
1384
|
-
}
|
|
1385
|
-
const INLINE = "`([^`]+)`|\\[((?:[^\\]`]|`[^`]*`)+)\\]\\(([^)\\s]+)\\)";
|
|
1386
|
-
/**
|
|
1387
|
-
* This function renders inline Markdown as HTML: code spans, links, strong text and emphasis. It
|
|
1388
|
-
* escapes all other text, so the result is safe.
|
|
1389
|
-
*/
|
|
1390
|
-
function inlineMarkdown(markdown) {
|
|
1391
|
-
const text = String(markdown ?? "");
|
|
1392
|
-
const pattern = new RegExp(INLINE, "g");
|
|
1393
|
-
let html = "";
|
|
1394
|
-
let last = 0;
|
|
1395
|
-
for (let match = pattern.exec(text); match; match = pattern.exec(text)) {
|
|
1396
|
-
html += emphasis(escapeHtml(text.slice(last, match.index)));
|
|
1397
|
-
if (match[1] !== void 0) html += `<code>${escapeHtml(match[1])}</code>`;
|
|
1398
|
-
else html += link(match[3] ?? "", inlineMarkdown(match[2] ?? ""));
|
|
1399
|
-
last = match.index + match[0].length;
|
|
1400
|
-
}
|
|
1401
|
-
return html + emphasis(escapeHtml(text.slice(last)));
|
|
1402
|
-
}
|
|
1403
|
-
/** This function gives "1 item" or "2 items". */
|
|
1404
|
-
function plural(count, word, many = `${word}s`) {
|
|
1405
|
-
return `${count} ${count === 1 ? word : many}`;
|
|
1406
|
-
}
|
|
1407
|
-
/** This function gives "a", "a and b" or "a, b and c". */
|
|
1408
|
-
function listText(parts) {
|
|
1409
|
-
if (parts.length <= 1) return parts.join("");
|
|
1410
|
-
return `${parts.slice(0, -1).join(", ")} and ${parts[parts.length - 1]}`;
|
|
1411
|
-
}
|
|
1412
1199
|
// src/render/dates.ts
|
|
1413
1200
|
/**
|
|
1414
1201
|
* The dates of the pages. The pages show each date in UTC, so the build and the browser agree.
|
|
1415
1202
|
*/
|
|
1416
1203
|
/** The milliseconds of one day. */
|
|
1417
|
-
const DAY_MS
|
|
1204
|
+
const DAY_MS = 864e5;
|
|
1418
1205
|
const MONTHS = [
|
|
1419
1206
|
"Jan",
|
|
1420
1207
|
"Feb",
|
|
@@ -1431,7 +1218,7 @@ const MONTHS = [
|
|
|
1431
1218
|
];
|
|
1432
1219
|
/** This function gives the start of the UTC day of a time. */
|
|
1433
1220
|
function utcDay(time) {
|
|
1434
|
-
return Math.floor(time / DAY_MS
|
|
1221
|
+
return Math.floor(time / DAY_MS) * DAY_MS;
|
|
1435
1222
|
}
|
|
1436
1223
|
/** This function gives "Oct 18": the UTC day of a time or of an ISO date. */
|
|
1437
1224
|
function shortDate(date) {
|
|
@@ -1688,13 +1475,13 @@ function timelineLayout(releases, options) {
|
|
|
1688
1475
|
counts.set(day, (counts.get(day) ?? 0) + 1);
|
|
1689
1476
|
}
|
|
1690
1477
|
const raw = [];
|
|
1691
|
-
for (let day = utcDay(Date.parse(first.date)); day <= utcDay(Date.parse(last.date)); day += DAY_MS
|
|
1478
|
+
for (let day = utcDay(Date.parse(first.date)); day <= utcDay(Date.parse(last.date)); day += DAY_MS) {
|
|
1692
1479
|
const count = counts.get(day) ?? 0;
|
|
1693
1480
|
const previous = raw[raw.length - 1];
|
|
1694
|
-
if (count === 0 && previous?.empty) previous.end = day + DAY_MS
|
|
1481
|
+
if (count === 0 && previous?.empty) previous.end = day + DAY_MS;
|
|
1695
1482
|
else raw.push({
|
|
1696
1483
|
start: day,
|
|
1697
|
-
end: day + DAY_MS
|
|
1484
|
+
end: day + DAY_MS,
|
|
1698
1485
|
count,
|
|
1699
1486
|
empty: count === 0,
|
|
1700
1487
|
weight: count === 0 ? emptyWeight : 1
|
|
@@ -2307,195 +2094,6 @@ const TYPESCRIPT_PROBE = /\.[cm]?ts$/i;
|
|
|
2307
2094
|
function probeScript(source) {
|
|
2308
2095
|
return stripTypeScriptTypes(source, { mode: "strip" }).replace(/^export \{\s*\};?[ \t]*$/m, "");
|
|
2309
2096
|
}
|
|
2310
|
-
// src/check/freshness.ts
|
|
2311
|
-
/**
|
|
2312
|
-
* The freshness gate of a measured page. A site keeps a file of data stamps.
|
|
2313
|
-
* Each stamp names the page, its runs, their dates and their engine versions.
|
|
2314
|
-
* The gate fails on a page with no versions or no run date, and on old data with no newer run in flight.
|
|
2315
|
-
* The gate also fails on a run that started behind the engines on npm.
|
|
2316
|
-
* This file imports only Node.js and the folder `data`, so `reusable-pages.yml` starts it with no npm release of the package.
|
|
2317
|
-
*/
|
|
2318
|
-
/** The schema of a file of data stamps. */
|
|
2319
|
-
const DATA_STAMP_SCHEMA = "data-stamp/1";
|
|
2320
|
-
/** The largest age of the data of a page, in days, when no newer run is in flight. */
|
|
2321
|
-
const MAX_DATA_AGE_DAYS = 3;
|
|
2322
|
-
/** The most minor versions that an engine of a run can be behind npm at the start of the run. */
|
|
2323
|
-
const MAX_MINORS_BEHIND = 1;
|
|
2324
|
-
const DAY_MS = 864e5;
|
|
2325
|
-
const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
2326
|
-
/** This function gives the faults of one run of a stamp. */
|
|
2327
|
-
function runFaults(raw, at) {
|
|
2328
|
-
const run = record(raw);
|
|
2329
|
-
if (run === null) return [`${at}: a run is not an object.`];
|
|
2330
|
-
const faults = [];
|
|
2331
|
-
if (typeof run["id"] !== "string" || run["id"] === "") faults.push(`${at}: a run has no id.`);
|
|
2332
|
-
const label = `${at}, run ${String(run["id"])}`;
|
|
2333
|
-
if (typeof run["url"] !== "string" || !run["url"].startsWith("https://")) faults.push(`${label}: the run has no https link.`);
|
|
2334
|
-
if (typeof run["date"] !== "string" || !Number.isFinite(Date.parse(run["date"]))) faults.push(`${label}: the run has no date.`);
|
|
2335
|
-
const engines = record(run["engines"]);
|
|
2336
|
-
if (engines === null || Object.keys(engines).length === 0) return [...faults, `${label}: the run names no engine versions.`];
|
|
2337
|
-
for (const [name, version] of Object.entries(engines)) if (typeof version !== "string" || parseVersion(version) === null) faults.push(`${label}: the version of ${name} is not a version.`);
|
|
2338
|
-
return faults;
|
|
2339
|
-
}
|
|
2340
|
-
/**
|
|
2341
|
-
* This function reads the content of a file of data stamps. It gives each fault of the content, so a page with no versions
|
|
2342
|
-
* or no run date fails here.
|
|
2343
|
-
*/
|
|
2344
|
-
function parseDataStamps(raw) {
|
|
2345
|
-
const file = record(raw);
|
|
2346
|
-
if (file === null) return {
|
|
2347
|
-
ok: false,
|
|
2348
|
-
faults: ["The file is not an object."]
|
|
2349
|
-
};
|
|
2350
|
-
if (file["schema"] !== "data-stamp/1") return {
|
|
2351
|
-
ok: false,
|
|
2352
|
-
faults: [`The schema is not ${DATA_STAMP_SCHEMA}.`]
|
|
2353
|
-
};
|
|
2354
|
-
const pages = file["pages"];
|
|
2355
|
-
if (!Array.isArray(pages) || pages.length === 0) return {
|
|
2356
|
-
ok: false,
|
|
2357
|
-
faults: ["The file has no pages."]
|
|
2358
|
-
};
|
|
2359
|
-
const faults = [];
|
|
2360
|
-
for (const item of pages) {
|
|
2361
|
-
const page = record(item);
|
|
2362
|
-
if (page === null || typeof page["page"] !== "string" || page["page"] === "") {
|
|
2363
|
-
faults.push("A stamp has no page.");
|
|
2364
|
-
continue;
|
|
2365
|
-
}
|
|
2366
|
-
const at = page["page"];
|
|
2367
|
-
const runs = page["runs"];
|
|
2368
|
-
if (!Array.isArray(runs) || runs.length === 0) faults.push(`${at}: the page names no run.`);
|
|
2369
|
-
else for (const run of runs) faults.push(...runFaults(run, at));
|
|
2370
|
-
if (page["inFlight"] !== void 0) faults.push(...runFaults(page["inFlight"], `${at}, in flight`));
|
|
2371
|
-
}
|
|
2372
|
-
return faults.length > 0 ? {
|
|
2373
|
-
ok: false,
|
|
2374
|
-
faults
|
|
2375
|
-
} : {
|
|
2376
|
-
ok: true,
|
|
2377
|
-
stamps: raw
|
|
2378
|
-
};
|
|
2379
|
-
}
|
|
2380
|
-
/** This function gives the count of minor versions from `used` to `latest`. A newer major version counts as no limit. */
|
|
2381
|
-
function minorsBehind(used, latest) {
|
|
2382
|
-
const a = parseVersion(used);
|
|
2383
|
-
const b = parseVersion(latest);
|
|
2384
|
-
if (a === null || b === null) return Number.POSITIVE_INFINITY;
|
|
2385
|
-
if (a.major !== b.major) return b.major > a.major ? Number.POSITIVE_INFINITY : 0;
|
|
2386
|
-
return Math.max(0, b.minor - a.minor);
|
|
2387
|
-
}
|
|
2388
|
-
/** This function gives the newest stable version of a package on npm at a time, or undefined when npm has none. */
|
|
2389
|
-
function latestAt(times, name, date) {
|
|
2390
|
-
const limit = Date.parse(date);
|
|
2391
|
-
return times.filter((t) => t.name === name && Date.parse(t.date) <= limit && parseVersion(t.version)?.prerelease.length === 0).map((t) => t.version).sort(compareVersions).at(-1);
|
|
2392
|
-
}
|
|
2393
|
-
const ageInDays = (date, now) => (now - Date.parse(date)) / DAY_MS;
|
|
2394
|
-
/** This function tells if a run uses a newer version of an engine than another run. */
|
|
2395
|
-
function hasNewerEngine(run, than) {
|
|
2396
|
-
return Object.entries(run.engines).some(([name, version]) => {
|
|
2397
|
-
const old = than.engines[name];
|
|
2398
|
-
return old !== void 0 && compareVersions(version, old) > 0;
|
|
2399
|
-
});
|
|
2400
|
-
}
|
|
2401
|
-
/**
|
|
2402
|
-
* This function applies the rules of the gate to the stamps.
|
|
2403
|
-
*
|
|
2404
|
-
* - The data of each page is at most `maxAgeDays` old, or a run on newer engines is in flight.
|
|
2405
|
-
* - The engines of the newest run are at most `maxMinorsBehind` behind npm at the start of that run.
|
|
2406
|
-
* - A run in flight has the same age limit, so an old stamp does not hold the gate open.
|
|
2407
|
-
*/
|
|
2408
|
-
function checkFreshness(stamps, options) {
|
|
2409
|
-
const { times, maxAgeDays = 3, maxMinorsBehind = 1 } = options;
|
|
2410
|
-
const now = Date.parse(options.now);
|
|
2411
|
-
const faults = [];
|
|
2412
|
-
const notRun = [];
|
|
2413
|
-
for (const stamp of stamps.pages) {
|
|
2414
|
-
for (const run of [...stamp.runs, ...stamp.inFlight === void 0 ? [] : [stamp.inFlight]]) if (ageInDays(run.date, now) < 0) faults.push(`${stamp.page}: the run ${run.id} has a date after the check.`);
|
|
2415
|
-
const flight = stamp.inFlight;
|
|
2416
|
-
const flying = flight !== void 0 && ageInDays(flight.date, now) <= maxAgeDays;
|
|
2417
|
-
if (flight !== void 0 && !flying) faults.push(`${stamp.page}: the run ${flight.id} is in flight for more than ${maxAgeDays} days.`);
|
|
2418
|
-
for (const run of stamp.runs) {
|
|
2419
|
-
const age = ageInDays(run.date, now);
|
|
2420
|
-
if (age > maxAgeDays && !(flying && hasNewerEngine(flight, run))) faults.push(`${stamp.page}: the run ${run.id} is ${age.toFixed(1)} days old, over the limit of ${maxAgeDays}, and no run on newer engines is in flight.`);
|
|
2421
|
-
}
|
|
2422
|
-
const newest = flight ?? [...stamp.runs].sort((a, b) => Date.parse(a.date) - Date.parse(b.date)).at(-1);
|
|
2423
|
-
if (newest === void 0) continue;
|
|
2424
|
-
for (const [name, version] of Object.entries(newest.engines)) {
|
|
2425
|
-
const latest = latestAt(times, name, newest.date);
|
|
2426
|
-
if (latest === void 0) notRun.push(`${stamp.page}: npm gives no version of ${name} at the start of the run ${newest.id}.`);
|
|
2427
|
-
else if (minorsBehind(version, latest) > maxMinorsBehind) faults.push(`${stamp.page}: the run ${newest.id} used ${name} ${version}, and npm had ${latest} at its start. The limit is ${maxMinorsBehind} minor version behind.`);
|
|
2428
|
-
}
|
|
2429
|
-
}
|
|
2430
|
-
return {
|
|
2431
|
-
faults,
|
|
2432
|
-
notRun
|
|
2433
|
-
};
|
|
2434
|
-
}
|
|
2435
|
-
/** The help of the command `site-pages freshness`. */
|
|
2436
|
-
const FRESHNESS_HELP = `Usage: site-pages freshness --stamps <file> [options]
|
|
2437
|
-
|
|
2438
|
-
Examine the data stamps of the measured pages of a site.
|
|
2439
|
-
|
|
2440
|
-
--stamps <file> The file of data stamps, schema ${DATA_STAMP_SCHEMA}. (necessary)
|
|
2441
|
-
--now <time> The ISO time of the check. The default is the time of the call.
|
|
2442
|
-
--help Show this text.
|
|
2443
|
-
|
|
2444
|
-
The exit code is 0 when the stamps pass, 1 for a fault, and 2 for an incorrect command line.
|
|
2445
|
-
`;
|
|
2446
|
-
/** This function is the command `site-pages freshness`. The `fetch` option is for a test. */
|
|
2447
|
-
async function runFreshnessCli(argv, io, fetcher) {
|
|
2448
|
-
let values;
|
|
2449
|
-
try {
|
|
2450
|
-
({values} = parseArgs({
|
|
2451
|
-
args: [...argv],
|
|
2452
|
-
allowPositionals: false,
|
|
2453
|
-
options: {
|
|
2454
|
-
stamps: { type: "string" },
|
|
2455
|
-
now: { type: "string" },
|
|
2456
|
-
help: { type: "boolean" }
|
|
2457
|
-
}
|
|
2458
|
-
}));
|
|
2459
|
-
} catch (error) {
|
|
2460
|
-
io.stderr(`site-pages freshness: ${error instanceof Error ? error.message : String(error)}\n\n${FRESHNESS_HELP}`);
|
|
2461
|
-
return 2;
|
|
2462
|
-
}
|
|
2463
|
-
if (values.help === true) {
|
|
2464
|
-
io.stdout(FRESHNESS_HELP);
|
|
2465
|
-
return 0;
|
|
2466
|
-
}
|
|
2467
|
-
if (values.stamps === void 0) {
|
|
2468
|
-
io.stderr(`site-pages freshness: --stamps is necessary.\n\n${FRESHNESS_HELP}`);
|
|
2469
|
-
return 2;
|
|
2470
|
-
}
|
|
2471
|
-
const path = isAbsolute(values.stamps) ? values.stamps : join(io.cwd, values.stamps);
|
|
2472
|
-
let raw;
|
|
2473
|
-
try {
|
|
2474
|
-
raw = JSON.parse(readFileSync(path, "utf8"));
|
|
2475
|
-
} catch (error) {
|
|
2476
|
-
io.stderr(`site-pages freshness: the file ${values.stamps} cannot be read: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
2477
|
-
return 1;
|
|
2478
|
-
}
|
|
2479
|
-
const parsed = parseDataStamps(raw);
|
|
2480
|
-
if (!parsed.ok) {
|
|
2481
|
-
for (const fault of parsed.faults) io.stderr(`${fault}\n`);
|
|
2482
|
-
return 1;
|
|
2483
|
-
}
|
|
2484
|
-
const engines = parsed.stamps.pages.flatMap((p) => [...p.runs, ...p.inFlight === void 0 ? [] : [p.inFlight]]).flatMap((r) => Object.keys(r.engines));
|
|
2485
|
-
const npm = await npmPublishTimes({
|
|
2486
|
-
packages: [...new Set(engines)],
|
|
2487
|
-
...fetcher === void 0 ? {} : { fetch: fetcher }
|
|
2488
|
-
});
|
|
2489
|
-
const result = checkFreshness(parsed.stamps, {
|
|
2490
|
-
now: values.now ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
2491
|
-
times: npm.times
|
|
2492
|
-
});
|
|
2493
|
-
for (const problem of [...npm.problems, ...result.notRun]) io.stdout(`Not run: ${problem}\n`);
|
|
2494
|
-
for (const fault of result.faults) io.stderr(`${fault}\n`);
|
|
2495
|
-
if (result.faults.length > 0) return 1;
|
|
2496
|
-
io.stdout(`The data stamps of ${parsed.stamps.pages.length} pages pass.\n`);
|
|
2497
|
-
return 0;
|
|
2498
|
-
}
|
|
2499
2097
|
// src/build.ts
|
|
2500
2098
|
/**
|
|
2501
2099
|
* The `site-pages build` command. It reads the sources. Then it writes the JSON data and the two
|
|
@@ -2704,6 +2302,6 @@ async function runCli(argv, io) {
|
|
|
2704
2302
|
throw error;
|
|
2705
2303
|
}
|
|
2706
2304
|
}
|
|
2707
|
-
export {
|
|
2305
|
+
export { parseChangelog as $, STATUS_CLASSES as A, DEFAULT_CHANGELOGS as B, pageShell as C, utcDay as D, shortDate as E, plainText as F, globFiles as G, mergeReleases as H, readStatusBoard as I, checkStateOf as J, GITHUB_API as K, roadmapItems as L, linksOf as M, markdownTables as N, OPEN_CLASSES as O, parseRoadmap as P, headingAnchor as Q, splitRow as R, BASE_CSS as S, longDate as T, readChangelogs as U, ReleaseDataError as V, readReleases as W, githubReleases as X, githubOpenWork as Y, firstSentence as Z, renderReleasesPage as _, runCli as a, releaseSummary as b, STATUS_SCRIPT as c, renderStatusPage as d, releaseBodyEntries as et, sectionsOf as f, releasesLead as g, RELEASES_SCRIPT as h, parseBuildArgs as i, buildStatusBoard as j, RoadmapParseError as k, classCounts as l, statusLead as m, UsageError as n, TYPESCRIPT_PROBE as o, slugOf as p, GITHUB_GRAPHQL as q, build as r, probeScript as s, HELP as t, openGroups as u, laneCenter as v, dayRange as w, timelineLayout as x, oldestFirst as y, statusClassOf as z };
|
|
2708
2306
|
|
|
2709
2307
|
//# sourceMappingURL=build.js.map
|