@mark1russell7/site-pages 0.2.4 → 0.2.5
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 +31 -0
- package/build/chunks/build.js +200 -6
- package/build/chunks/build.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/README.md
CHANGED
|
@@ -126,6 +126,37 @@ The check runs the probe on the first page, in the first scheme, at each width.
|
|
|
126
126
|
|
|
127
127
|
The check finds a browser in this order: `--browser`, `$SITE_CHECK_BROWSER`, the newest Chromium of Playwright, and then Chrome or Edge. The folder `src/check` imports only Node.js. Thus Node.js 22.18 or later can start `src/check/main.ts` with no install. The workflow `reusable-pages.yml` of the template uses it this way.
|
|
128
128
|
|
|
129
|
+
## The freshness of a measured page
|
|
130
|
+
|
|
131
|
+
A measured page shows numbers from a run. The numbers become old when the run becomes old or when an engine has a new release. A site keeps one file of data stamps. Each stamp names the page, its runs, the date of each run and the engine versions of each run. The gate reads this file:
|
|
132
|
+
|
|
133
|
+
```sh
|
|
134
|
+
site-pages freshness --stamps data/stamps.json
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
```json
|
|
138
|
+
{
|
|
139
|
+
"schema": "data-stamp/1",
|
|
140
|
+
"pages": [
|
|
141
|
+
{
|
|
142
|
+
"page": "index.html",
|
|
143
|
+
"runs": [{ "id": "101", "url": "https://github.com/owner/repo/actions/runs/101", "date": "2026-11-02T10:00:00Z", "engines": { "@scope/engine": "0.7.0" } }],
|
|
144
|
+
"inFlight": { "id": "102", "url": "https://github.com/owner/repo/actions/runs/102", "date": "2026-11-04T10:00:00Z", "engines": { "@scope/engine": "0.8.0" } }
|
|
145
|
+
}
|
|
146
|
+
]
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
The gate has three rules. A fault of one rule gives the exit code 1.
|
|
151
|
+
|
|
152
|
+
1. Each page names a run, and each run has an id, a link, a date and its engine versions.
|
|
153
|
+
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.
|
|
154
|
+
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.
|
|
155
|
+
|
|
156
|
+
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.
|
|
157
|
+
|
|
158
|
+
`renderDataStamp(stamp)` gives the line that the page shows with its data: the run, its date, its engine versions and the run in flight.
|
|
159
|
+
|
|
129
160
|
## The releases
|
|
130
161
|
|
|
131
162
|
The command merges four sources by version:
|
package/build/chunks/build.js
CHANGED
|
@@ -1414,7 +1414,7 @@ function listText(parts) {
|
|
|
1414
1414
|
* The dates of the pages. The pages show each date in UTC, so the build and the browser agree.
|
|
1415
1415
|
*/
|
|
1416
1416
|
/** The milliseconds of one day. */
|
|
1417
|
-
const DAY_MS = 864e5;
|
|
1417
|
+
const DAY_MS$1 = 864e5;
|
|
1418
1418
|
const MONTHS = [
|
|
1419
1419
|
"Jan",
|
|
1420
1420
|
"Feb",
|
|
@@ -1431,7 +1431,7 @@ const MONTHS = [
|
|
|
1431
1431
|
];
|
|
1432
1432
|
/** This function gives the start of the UTC day of a time. */
|
|
1433
1433
|
function utcDay(time) {
|
|
1434
|
-
return Math.floor(time / DAY_MS) * DAY_MS;
|
|
1434
|
+
return Math.floor(time / DAY_MS$1) * DAY_MS$1;
|
|
1435
1435
|
}
|
|
1436
1436
|
/** This function gives "Oct 18": the UTC day of a time or of an ISO date. */
|
|
1437
1437
|
function shortDate(date) {
|
|
@@ -1688,13 +1688,13 @@ function timelineLayout(releases, options) {
|
|
|
1688
1688
|
counts.set(day, (counts.get(day) ?? 0) + 1);
|
|
1689
1689
|
}
|
|
1690
1690
|
const raw = [];
|
|
1691
|
-
for (let day = utcDay(Date.parse(first.date)); day <= utcDay(Date.parse(last.date)); day += DAY_MS) {
|
|
1691
|
+
for (let day = utcDay(Date.parse(first.date)); day <= utcDay(Date.parse(last.date)); day += DAY_MS$1) {
|
|
1692
1692
|
const count = counts.get(day) ?? 0;
|
|
1693
1693
|
const previous = raw[raw.length - 1];
|
|
1694
|
-
if (count === 0 && previous?.empty) previous.end = day + DAY_MS;
|
|
1694
|
+
if (count === 0 && previous?.empty) previous.end = day + DAY_MS$1;
|
|
1695
1695
|
else raw.push({
|
|
1696
1696
|
start: day,
|
|
1697
|
-
end: day + DAY_MS,
|
|
1697
|
+
end: day + DAY_MS$1,
|
|
1698
1698
|
count,
|
|
1699
1699
|
empty: count === 0,
|
|
1700
1700
|
weight: count === 0 ? emptyWeight : 1
|
|
@@ -2302,6 +2302,195 @@ const TYPESCRIPT_PROBE = /\.[cm]?ts$/i;
|
|
|
2302
2302
|
function probeScript(source) {
|
|
2303
2303
|
return stripTypeScriptTypes(source, { mode: "strip" }).replace(/^export \{\s*\};?[ \t]*$/m, "");
|
|
2304
2304
|
}
|
|
2305
|
+
// src/check/freshness.ts
|
|
2306
|
+
/**
|
|
2307
|
+
* The freshness gate of a measured page. A site keeps a file of data stamps.
|
|
2308
|
+
* Each stamp names the page, its runs, their dates and their engine versions.
|
|
2309
|
+
* The gate fails on a page with no versions or no run date, and on old data with no newer run in flight.
|
|
2310
|
+
* The gate also fails on a run that started behind the engines on npm.
|
|
2311
|
+
* This file imports only Node.js and the folder `data`, so `reusable-pages.yml` starts it with no npm release of the package.
|
|
2312
|
+
*/
|
|
2313
|
+
/** The schema of a file of data stamps. */
|
|
2314
|
+
const DATA_STAMP_SCHEMA = "data-stamp/1";
|
|
2315
|
+
/** The largest age of the data of a page, in days, when no newer run is in flight. */
|
|
2316
|
+
const MAX_DATA_AGE_DAYS = 3;
|
|
2317
|
+
/** The most minor versions that an engine of a run can be behind npm at the start of the run. */
|
|
2318
|
+
const MAX_MINORS_BEHIND = 1;
|
|
2319
|
+
const DAY_MS = 864e5;
|
|
2320
|
+
const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
2321
|
+
/** This function gives the faults of one run of a stamp. */
|
|
2322
|
+
function runFaults(raw, at) {
|
|
2323
|
+
const run = record(raw);
|
|
2324
|
+
if (run === null) return [`${at}: a run is not an object.`];
|
|
2325
|
+
const faults = [];
|
|
2326
|
+
if (typeof run["id"] !== "string" || run["id"] === "") faults.push(`${at}: a run has no id.`);
|
|
2327
|
+
const label = `${at}, run ${String(run["id"])}`;
|
|
2328
|
+
if (typeof run["url"] !== "string" || !run["url"].startsWith("https://")) faults.push(`${label}: the run has no https link.`);
|
|
2329
|
+
if (typeof run["date"] !== "string" || !Number.isFinite(Date.parse(run["date"]))) faults.push(`${label}: the run has no date.`);
|
|
2330
|
+
const engines = record(run["engines"]);
|
|
2331
|
+
if (engines === null || Object.keys(engines).length === 0) return [...faults, `${label}: the run names no engine versions.`];
|
|
2332
|
+
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.`);
|
|
2333
|
+
return faults;
|
|
2334
|
+
}
|
|
2335
|
+
/**
|
|
2336
|
+
* This function reads the content of a file of data stamps. It gives each fault of the content, so a page with no versions
|
|
2337
|
+
* or no run date fails here.
|
|
2338
|
+
*/
|
|
2339
|
+
function parseDataStamps(raw) {
|
|
2340
|
+
const file = record(raw);
|
|
2341
|
+
if (file === null) return {
|
|
2342
|
+
ok: false,
|
|
2343
|
+
faults: ["The file is not an object."]
|
|
2344
|
+
};
|
|
2345
|
+
if (file["schema"] !== "data-stamp/1") return {
|
|
2346
|
+
ok: false,
|
|
2347
|
+
faults: [`The schema is not ${DATA_STAMP_SCHEMA}.`]
|
|
2348
|
+
};
|
|
2349
|
+
const pages = file["pages"];
|
|
2350
|
+
if (!Array.isArray(pages) || pages.length === 0) return {
|
|
2351
|
+
ok: false,
|
|
2352
|
+
faults: ["The file has no pages."]
|
|
2353
|
+
};
|
|
2354
|
+
const faults = [];
|
|
2355
|
+
for (const item of pages) {
|
|
2356
|
+
const page = record(item);
|
|
2357
|
+
if (page === null || typeof page["page"] !== "string" || page["page"] === "") {
|
|
2358
|
+
faults.push("A stamp has no page.");
|
|
2359
|
+
continue;
|
|
2360
|
+
}
|
|
2361
|
+
const at = page["page"];
|
|
2362
|
+
const runs = page["runs"];
|
|
2363
|
+
if (!Array.isArray(runs) || runs.length === 0) faults.push(`${at}: the page names no run.`);
|
|
2364
|
+
else for (const run of runs) faults.push(...runFaults(run, at));
|
|
2365
|
+
if (page["inFlight"] !== void 0) faults.push(...runFaults(page["inFlight"], `${at}, in flight`));
|
|
2366
|
+
}
|
|
2367
|
+
return faults.length > 0 ? {
|
|
2368
|
+
ok: false,
|
|
2369
|
+
faults
|
|
2370
|
+
} : {
|
|
2371
|
+
ok: true,
|
|
2372
|
+
stamps: raw
|
|
2373
|
+
};
|
|
2374
|
+
}
|
|
2375
|
+
/** This function gives the count of minor versions from `used` to `latest`. A newer major version counts as no limit. */
|
|
2376
|
+
function minorsBehind(used, latest) {
|
|
2377
|
+
const a = parseVersion(used);
|
|
2378
|
+
const b = parseVersion(latest);
|
|
2379
|
+
if (a === null || b === null) return Number.POSITIVE_INFINITY;
|
|
2380
|
+
if (a.major !== b.major) return b.major > a.major ? Number.POSITIVE_INFINITY : 0;
|
|
2381
|
+
return Math.max(0, b.minor - a.minor);
|
|
2382
|
+
}
|
|
2383
|
+
/** This function gives the newest stable version of a package on npm at a time, or undefined when npm has none. */
|
|
2384
|
+
function latestAt(times, name, date) {
|
|
2385
|
+
const limit = Date.parse(date);
|
|
2386
|
+
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);
|
|
2387
|
+
}
|
|
2388
|
+
const ageInDays = (date, now) => (now - Date.parse(date)) / DAY_MS;
|
|
2389
|
+
/** This function tells if a run uses a newer version of an engine than another run. */
|
|
2390
|
+
function hasNewerEngine(run, than) {
|
|
2391
|
+
return Object.entries(run.engines).some(([name, version]) => {
|
|
2392
|
+
const old = than.engines[name];
|
|
2393
|
+
return old !== void 0 && compareVersions(version, old) > 0;
|
|
2394
|
+
});
|
|
2395
|
+
}
|
|
2396
|
+
/**
|
|
2397
|
+
* This function applies the rules of the gate to the stamps.
|
|
2398
|
+
*
|
|
2399
|
+
* - The data of each page is at most `maxAgeDays` old, or a run on newer engines is in flight.
|
|
2400
|
+
* - The engines of the newest run are at most `maxMinorsBehind` behind npm at the start of that run.
|
|
2401
|
+
* - A run in flight has the same age limit, so an old stamp does not hold the gate open.
|
|
2402
|
+
*/
|
|
2403
|
+
function checkFreshness(stamps, options) {
|
|
2404
|
+
const { times, maxAgeDays = 3, maxMinorsBehind = 1 } = options;
|
|
2405
|
+
const now = Date.parse(options.now);
|
|
2406
|
+
const faults = [];
|
|
2407
|
+
const notRun = [];
|
|
2408
|
+
for (const stamp of stamps.pages) {
|
|
2409
|
+
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.`);
|
|
2410
|
+
const flight = stamp.inFlight;
|
|
2411
|
+
const flying = flight !== void 0 && ageInDays(flight.date, now) <= maxAgeDays;
|
|
2412
|
+
if (flight !== void 0 && !flying) faults.push(`${stamp.page}: the run ${flight.id} is in flight for more than ${maxAgeDays} days.`);
|
|
2413
|
+
for (const run of stamp.runs) {
|
|
2414
|
+
const age = ageInDays(run.date, now);
|
|
2415
|
+
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.`);
|
|
2416
|
+
}
|
|
2417
|
+
const newest = flight ?? [...stamp.runs].sort((a, b) => Date.parse(a.date) - Date.parse(b.date)).at(-1);
|
|
2418
|
+
if (newest === void 0) continue;
|
|
2419
|
+
for (const [name, version] of Object.entries(newest.engines)) {
|
|
2420
|
+
const latest = latestAt(times, name, newest.date);
|
|
2421
|
+
if (latest === void 0) notRun.push(`${stamp.page}: npm gives no version of ${name} at the start of the run ${newest.id}.`);
|
|
2422
|
+
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.`);
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
return {
|
|
2426
|
+
faults,
|
|
2427
|
+
notRun
|
|
2428
|
+
};
|
|
2429
|
+
}
|
|
2430
|
+
/** The help of the command `site-pages freshness`. */
|
|
2431
|
+
const FRESHNESS_HELP = `Usage: site-pages freshness --stamps <file> [options]
|
|
2432
|
+
|
|
2433
|
+
Examine the data stamps of the measured pages of a site.
|
|
2434
|
+
|
|
2435
|
+
--stamps <file> The file of data stamps, schema ${DATA_STAMP_SCHEMA}. (necessary)
|
|
2436
|
+
--now <time> The ISO time of the check. The default is the time of the call.
|
|
2437
|
+
--help Show this text.
|
|
2438
|
+
|
|
2439
|
+
The exit code is 0 when the stamps pass, 1 for a fault, and 2 for an incorrect command line.
|
|
2440
|
+
`;
|
|
2441
|
+
/** This function is the command `site-pages freshness`. The `fetch` option is for a test. */
|
|
2442
|
+
async function runFreshnessCli(argv, io, fetcher) {
|
|
2443
|
+
let values;
|
|
2444
|
+
try {
|
|
2445
|
+
({values} = parseArgs({
|
|
2446
|
+
args: [...argv],
|
|
2447
|
+
allowPositionals: false,
|
|
2448
|
+
options: {
|
|
2449
|
+
stamps: { type: "string" },
|
|
2450
|
+
now: { type: "string" },
|
|
2451
|
+
help: { type: "boolean" }
|
|
2452
|
+
}
|
|
2453
|
+
}));
|
|
2454
|
+
} catch (error) {
|
|
2455
|
+
io.stderr(`site-pages freshness: ${error instanceof Error ? error.message : String(error)}\n\n${FRESHNESS_HELP}`);
|
|
2456
|
+
return 2;
|
|
2457
|
+
}
|
|
2458
|
+
if (values.help === true) {
|
|
2459
|
+
io.stdout(FRESHNESS_HELP);
|
|
2460
|
+
return 0;
|
|
2461
|
+
}
|
|
2462
|
+
if (values.stamps === void 0) {
|
|
2463
|
+
io.stderr(`site-pages freshness: --stamps is necessary.\n\n${FRESHNESS_HELP}`);
|
|
2464
|
+
return 2;
|
|
2465
|
+
}
|
|
2466
|
+
const path = isAbsolute(values.stamps) ? values.stamps : join(io.cwd, values.stamps);
|
|
2467
|
+
let raw;
|
|
2468
|
+
try {
|
|
2469
|
+
raw = JSON.parse(readFileSync(path, "utf8"));
|
|
2470
|
+
} catch (error) {
|
|
2471
|
+
io.stderr(`site-pages freshness: the file ${values.stamps} cannot be read: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
2472
|
+
return 1;
|
|
2473
|
+
}
|
|
2474
|
+
const parsed = parseDataStamps(raw);
|
|
2475
|
+
if (!parsed.ok) {
|
|
2476
|
+
for (const fault of parsed.faults) io.stderr(`${fault}\n`);
|
|
2477
|
+
return 1;
|
|
2478
|
+
}
|
|
2479
|
+
const engines = parsed.stamps.pages.flatMap((p) => [...p.runs, ...p.inFlight === void 0 ? [] : [p.inFlight]]).flatMap((r) => Object.keys(r.engines));
|
|
2480
|
+
const npm = await npmPublishTimes({
|
|
2481
|
+
packages: [...new Set(engines)],
|
|
2482
|
+
...fetcher === void 0 ? {} : { fetch: fetcher }
|
|
2483
|
+
});
|
|
2484
|
+
const result = checkFreshness(parsed.stamps, {
|
|
2485
|
+
now: values.now ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
2486
|
+
times: npm.times
|
|
2487
|
+
});
|
|
2488
|
+
for (const problem of [...npm.problems, ...result.notRun]) io.stdout(`Not run: ${problem}\n`);
|
|
2489
|
+
for (const fault of result.faults) io.stderr(`${fault}\n`);
|
|
2490
|
+
if (result.faults.length > 0) return 1;
|
|
2491
|
+
io.stdout(`The data stamps of ${parsed.stamps.pages.length} pages pass.\n`);
|
|
2492
|
+
return 0;
|
|
2493
|
+
}
|
|
2305
2494
|
// src/build.ts
|
|
2306
2495
|
/**
|
|
2307
2496
|
* The `site-pages build` command. It reads the sources. Then it writes the JSON data and the two
|
|
@@ -2402,6 +2591,11 @@ Usage: site-pages check (--out <dir> | --url <url>) [options]
|
|
|
2402
2591
|
|
|
2403
2592
|
Opens the pages of the site in a headless Chromium at phone and desktop widths. Start
|
|
2404
2593
|
"site-pages check --help" for its options.
|
|
2594
|
+
|
|
2595
|
+
Usage: site-pages freshness --stamps <file> [options]
|
|
2596
|
+
|
|
2597
|
+
Examine the data stamps of the measured pages of a site. See
|
|
2598
|
+
"site-pages freshness --help" for its options.
|
|
2405
2599
|
`;
|
|
2406
2600
|
/**
|
|
2407
2601
|
* This function reads the command line into the options of `build`.
|
|
@@ -2505,6 +2699,6 @@ async function runCli(argv, io) {
|
|
|
2505
2699
|
throw error;
|
|
2506
2700
|
}
|
|
2507
2701
|
}
|
|
2508
|
-
export {
|
|
2702
|
+
export { DEFAULT_CHANGELOGS as $, BASE_CSS as A, safeUrl as B, RELEASES_SCRIPT as C, oldestFirst as D, laneCenter as E, utcDay as F, linksOf as G, RoadmapParseError as H, escapeHtml as I, plainText as J, markdownTables as K, inlineMarkdown as L, dayRange as M, longDate as N, releaseSummary as O, shortDate as P, statusClassOf as Q, link as R, statusLead as S, renderReleasesPage as T, STATUS_CLASSES as U, OPEN_CLASSES as V, buildStatusBoard as W, roadmapItems as X, readStatusBoard as Y, splitRow as Z, classCounts as _, kindOf as _t, runCli as a, NPM_REGISTRY as at, sectionsOf as b, MAX_MINORS_BEHIND as c, GITHUB_GRAPHQL as ct, minorsBehind as d, githubReleases as dt, ReleaseDataError as et, parseDataStamps as f, firstSentence as ft, STATUS_SCRIPT as g, compareVersions as gt, probeScript as h, releaseBodyEntries as ht, parseBuildArgs as i, globFiles as it, pageShell as j, timelineLayout as k, checkFreshness as l, checkStateOf as lt, TYPESCRIPT_PROBE as m, parseChangelog as mt, UsageError as n, readChangelogs as nt, DATA_STAMP_SCHEMA as o, npmPublishTimes as ot, runFreshnessCli as p, headingAnchor as pt, parseRoadmap as q, build as r, readReleases as rt, MAX_DATA_AGE_DAYS as s, GITHUB_API as st, HELP as t, mergeReleases as tt, latestAt as u, githubOpenWork as ut, openGroups as v, parseVersion as vt, releasesLead as w, slugOf as x, renderStatusPage as y, versionOfTag as yt, listText as z };
|
|
2509
2703
|
|
|
2510
2704
|
//# sourceMappingURL=build.js.map
|