vagejs 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +244 -2
- package/dist/audit/a11y.d.ts +13 -0
- package/dist/audit/a11y.js +33 -0
- package/dist/audit/audit.d.ts +31 -0
- package/dist/audit/audit.js +143 -0
- package/dist/audit/budgets.d.ts +22 -0
- package/dist/audit/budgets.js +106 -0
- package/dist/audit/lighthouse.d.ts +3 -0
- package/dist/audit/lighthouse.js +48 -0
- package/dist/audit/remote.d.ts +65 -0
- package/dist/audit/remote.js +123 -0
- package/dist/audit/vitals.d.ts +28 -0
- package/dist/audit/vitals.js +18 -0
- package/dist/checks/check.d.ts +31 -0
- package/dist/checks/check.js +434 -0
- package/dist/checks/integrity.d.ts +36 -0
- package/dist/checks/integrity.js +145 -0
- package/dist/checks/links.d.ts +18 -0
- package/dist/checks/links.js +47 -0
- package/dist/checks/verify.d.ts +35 -0
- package/dist/checks/verify.js +167 -0
- package/dist/cli/create.d.ts +10 -0
- package/dist/cli/create.js +39 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +710 -0
- package/dist/cli/info.d.ts +82 -0
- package/dist/cli/info.js +100 -0
- package/dist/content/images.d.ts +23 -0
- package/dist/content/images.js +96 -0
- package/dist/content/markdown.d.ts +28 -0
- package/dist/content/markdown.js +56 -0
- package/dist/content/sanitize.d.ts +16 -0
- package/dist/content/sanitize.js +57 -0
- package/dist/core/build.d.ts +25 -0
- package/dist/core/build.js +54 -0
- package/dist/core/clone.d.ts +6 -0
- package/dist/core/clone.js +16 -0
- package/dist/core/config.d.ts +180 -0
- package/dist/core/config.js +260 -0
- package/dist/core/errors.d.ts +7 -0
- package/dist/core/errors.js +12 -0
- package/dist/core/html.d.ts +24 -0
- package/dist/core/html.js +42 -0
- package/dist/core/loader.d.ts +10 -0
- package/dist/core/loader.js +16 -0
- package/dist/core/pages.d.ts +40 -0
- package/dist/core/pages.js +93 -0
- package/dist/core/project-config.d.ts +3 -0
- package/dist/core/project-config.js +14 -0
- package/dist/core/render.d.ts +53 -0
- package/dist/core/render.js +104 -0
- package/dist/core/routes.d.ts +24 -0
- package/dist/core/routes.js +70 -0
- package/dist/core/site.d.ts +42 -0
- package/dist/core/site.js +182 -0
- package/dist/core/validate.d.ts +12 -0
- package/dist/core/validate.js +36 -0
- package/dist/deploy/cloudflare-pages.d.ts +20 -0
- package/dist/deploy/cloudflare-pages.js +42 -0
- package/dist/deploy/cloudflare-workers.d.ts +59 -0
- package/dist/deploy/cloudflare-workers.js +223 -0
- package/dist/deploy/git.d.ts +20 -0
- package/dist/deploy/git.js +55 -0
- package/dist/deploy/github-pages.d.ts +33 -0
- package/dist/deploy/github-pages.js +99 -0
- package/dist/deploy/headers.d.ts +11 -0
- package/dist/deploy/headers.js +103 -0
- package/dist/deploy/publish.d.ts +45 -0
- package/dist/deploy/publish.js +148 -0
- package/dist/deploy/redirects.d.ts +7 -0
- package/dist/deploy/redirects.js +18 -0
- package/dist/deploy/secrets.d.ts +6 -0
- package/dist/deploy/secrets.js +66 -0
- package/dist/dev/guard.d.ts +9 -0
- package/dist/dev/guard.js +57 -0
- package/dist/dev/listen.d.ts +6 -0
- package/dist/dev/listen.js +13 -0
- package/dist/dev/preview.d.ts +6 -0
- package/dist/dev/preview.js +72 -0
- package/dist/dev/server.d.ts +8 -0
- package/dist/dev/server.js +104 -0
- package/dist/discovery/crawlers.d.ts +13 -0
- package/dist/discovery/crawlers.js +2 -0
- package/dist/discovery/llms.d.ts +12 -0
- package/dist/discovery/llms.js +19 -0
- package/dist/discovery/robots.d.ts +8 -0
- package/dist/discovery/robots.js +40 -0
- package/dist/discovery/sitemap.d.ts +8 -0
- package/dist/discovery/sitemap.js +11 -0
- package/dist/guidelines/monitor.d.ts +55 -0
- package/dist/guidelines/monitor.js +126 -0
- package/dist/guidelines/pack.d.ts +41 -0
- package/dist/guidelines/pack.js +13 -0
- package/dist/guidelines/rule-pack.json +611 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +12 -0
- package/dist/integrations/api-keys.d.ts +26 -0
- package/dist/integrations/api-keys.js +65 -0
- package/dist/integrations/ga4-data.d.ts +13 -0
- package/dist/integrations/ga4-data.js +39 -0
- package/dist/integrations/google-auth.d.ts +47 -0
- package/dist/integrations/google-auth.js +157 -0
- package/dist/integrations/http.d.ts +15 -0
- package/dist/integrations/http.js +40 -0
- package/dist/integrations/indexnow.d.ts +18 -0
- package/dist/integrations/indexnow.js +38 -0
- package/dist/integrations/search-console.d.ts +25 -0
- package/dist/integrations/search-console.js +39 -0
- package/dist/integrations/tags.d.ts +22 -0
- package/dist/integrations/tags.js +67 -0
- package/dist/seo/head.d.ts +20 -0
- package/dist/seo/head.js +85 -0
- package/dist/seo/jsonld.d.ts +35 -0
- package/dist/seo/jsonld.js +31 -0
- package/dist/seo/urls.d.ts +3 -0
- package/dist/seo/urls.js +10 -0
- package/package.json +81 -4
- package/templates/starter/.env.example +12 -0
- package/templates/starter/.github/dependabot.yml +11 -0
- package/templates/starter/.github/workflows/ci.yml +30 -0
- package/templates/starter/.github/workflows/verify.yml +32 -0
- package/templates/starter/AGENTS.md +166 -0
- package/templates/starter/CLAUDE.md +3 -0
- package/templates/starter/_gitignore +8 -0
- package/templates/starter/links.lock.json +4 -0
- package/templates/starter/package.json +16 -0
- package/templates/starter/public/apple-touch-icon.png +0 -0
- package/templates/starter/public/favicon.ico +0 -0
- package/templates/starter/public/favicon.svg +1 -0
- package/templates/starter/src/layouts/blog.ts +13 -0
- package/templates/starter/src/layouts/default.ts +38 -0
- package/templates/starter/src/pages/404.md +7 -0
- package/templates/starter/src/pages/about.md +8 -0
- package/templates/starter/src/pages/blog/first-post.md +10 -0
- package/templates/starter/src/pages/blog/index.md +7 -0
- package/templates/starter/src/pages/index.md +8 -0
- package/templates/starter/vage.config.ts +38 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
3
|
+
import { VageError } from "../core/errors.js";
|
|
4
|
+
import { labReport } from "./remote.js";
|
|
5
|
+
/**
|
|
6
|
+
* Loads Lighthouse from the user's project. It is an optional peer dependency
|
|
7
|
+
* (large, and needs Chrome), so it is never required for build or check.
|
|
8
|
+
*/
|
|
9
|
+
async function loadLighthouse(root) {
|
|
10
|
+
const require = createRequire(pathToFileURL(`${root}/package.json`));
|
|
11
|
+
let entry;
|
|
12
|
+
try {
|
|
13
|
+
entry = require.resolve("lighthouse");
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
throw new VageError('Lighthouse is not installed. Run "npm install --save-dev lighthouse" (requires Chrome), or audit without --lighthouse.');
|
|
17
|
+
}
|
|
18
|
+
const mod = await import(pathToFileURL(entry).href);
|
|
19
|
+
// chrome-launcher is a Lighthouse dependency; resolve it from there.
|
|
20
|
+
const launcher = await import(pathToFileURL(createRequire(entry).resolve("chrome-launcher")).href);
|
|
21
|
+
return { lighthouse: mod.default, desktopConfig: mod.desktopConfig, launcher };
|
|
22
|
+
}
|
|
23
|
+
/** Runs Lighthouse (lab) against already-served URLs. */
|
|
24
|
+
export async function runLighthouse(root, urls, strategies) {
|
|
25
|
+
const { lighthouse, desktopConfig, launcher } = await loadLighthouse(root);
|
|
26
|
+
let chrome;
|
|
27
|
+
try {
|
|
28
|
+
chrome = await launcher.launch({ chromeFlags: ["--headless=new"] });
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
throw new VageError(`Could not start Chrome for Lighthouse: ${err.message}. Install Chrome or set CHROME_PATH.`);
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const reports = [];
|
|
35
|
+
for (const url of urls) {
|
|
36
|
+
for (const strategy of strategies) {
|
|
37
|
+
const result = await lighthouse(url, { port: chrome.port, output: "json", logLevel: "error", onlyCategories: ["performance", "accessibility", "seo", "best-practices"] }, strategy === "desktop" ? desktopConfig : undefined);
|
|
38
|
+
if (!result)
|
|
39
|
+
throw new VageError(`Lighthouse returned no result for ${url}.`);
|
|
40
|
+
reports.push(labReport(result.lhr, "lighthouse", url, strategy));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return reports;
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
chrome.kill();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { redact, type HttpOptions } from "../integrations/http.js";
|
|
3
|
+
import { type Rating, type VitalName } from "./vitals.js";
|
|
4
|
+
export type Strategy = "mobile" | "desktop";
|
|
5
|
+
export type RemoteOptions = HttpOptions;
|
|
6
|
+
export { redact };
|
|
7
|
+
declare const CATEGORY_IDS: readonly ["performance", "accessibility", "seo", "best-practices"];
|
|
8
|
+
declare const psiSchema: z.ZodObject<{
|
|
9
|
+
lighthouseResult: z.ZodObject<{
|
|
10
|
+
lighthouseVersion: z.ZodString;
|
|
11
|
+
fetchTime: z.ZodString;
|
|
12
|
+
finalDisplayedUrl: z.ZodOptional<z.ZodString>;
|
|
13
|
+
categories: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
14
|
+
score: z.ZodNullable<z.ZodNumber>;
|
|
15
|
+
}, z.core.$strip>>;
|
|
16
|
+
audits: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
17
|
+
title: z.ZodString;
|
|
18
|
+
score: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
19
|
+
numericValue: z.ZodOptional<z.ZodNumber>;
|
|
20
|
+
displayValue: z.ZodOptional<z.ZodString>;
|
|
21
|
+
}, z.core.$strip>>;
|
|
22
|
+
}, z.core.$strip>;
|
|
23
|
+
}, z.core.$strip>;
|
|
24
|
+
export interface LabReport {
|
|
25
|
+
source: "pagespeed-insights" | "lighthouse";
|
|
26
|
+
url: string;
|
|
27
|
+
strategy: Strategy;
|
|
28
|
+
fetchTime: string;
|
|
29
|
+
toolVersion: string;
|
|
30
|
+
/** 0-100, or null when the tool returned no score. */
|
|
31
|
+
scores: Record<(typeof CATEGORY_IDS)[number], number | null>;
|
|
32
|
+
metrics: {
|
|
33
|
+
lcpMs?: number;
|
|
34
|
+
cls?: number;
|
|
35
|
+
tbtMs?: number;
|
|
36
|
+
fcpMs?: number;
|
|
37
|
+
speedIndexMs?: number;
|
|
38
|
+
};
|
|
39
|
+
/** Lowest-scoring audits, most impactful first. */
|
|
40
|
+
opportunities: {
|
|
41
|
+
id: string;
|
|
42
|
+
title: string;
|
|
43
|
+
displayValue?: string;
|
|
44
|
+
}[];
|
|
45
|
+
}
|
|
46
|
+
/** Normalizes a Lighthouse result (from PSI or a local run). */
|
|
47
|
+
export declare function labReport(lhr: z.infer<typeof psiSchema>["lighthouseResult"], source: LabReport["source"], url: string, strategy: Strategy): LabReport;
|
|
48
|
+
export declare function pageSpeed(url: string, strategy: Strategy, key: string | undefined, opts?: RemoteOptions): Promise<LabReport>;
|
|
49
|
+
export interface FieldReport {
|
|
50
|
+
source: "crux";
|
|
51
|
+
/** "url" when page-level data exists, otherwise "origin". */
|
|
52
|
+
scope: "url" | "origin";
|
|
53
|
+
formFactor: "PHONE" | "DESKTOP";
|
|
54
|
+
collectionPeriod?: string;
|
|
55
|
+
metrics: Partial<Record<VitalName, {
|
|
56
|
+
p75: number;
|
|
57
|
+
rating: Rating;
|
|
58
|
+
}>>;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Queries the CrUX API for the page, falling back to the origin. Returns
|
|
62
|
+
* undefined when neither has data. A 404 is treated as "no data"; the API
|
|
63
|
+
* docs do not specify this response, so it is an observed-behavior assumption.
|
|
64
|
+
*/
|
|
65
|
+
export declare function cruxField(url: string, strategy: Strategy, key: string, opts?: RemoteOptions): Promise<FieldReport | undefined>;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { VageError } from "../core/errors.js";
|
|
3
|
+
import { apiError, redact, requestWithRetry } from "../integrations/http.js";
|
|
4
|
+
import { rate } from "./vitals.js";
|
|
5
|
+
const PSI_ENDPOINT = "https://www.googleapis.com/pagespeedonline/v5/runPagespeed";
|
|
6
|
+
const CRUX_ENDPOINT = "https://chromeuxreport.googleapis.com/v1/records:queryRecord";
|
|
7
|
+
const KEY_HINTS = {
|
|
8
|
+
400: "Check the URL is public and the API key has this API enabled.",
|
|
9
|
+
403: "Check the URL is public and the API key has this API enabled.",
|
|
10
|
+
429: "Rate limited: wait and retry, or use an API key with quota.",
|
|
11
|
+
};
|
|
12
|
+
/** The API key goes in the x-goog-api-key header, never in the URL. */
|
|
13
|
+
function withKey(init, key) {
|
|
14
|
+
return { ...init, headers: { ...init.headers, ...(key ? { "x-goog-api-key": key } : {}) } };
|
|
15
|
+
}
|
|
16
|
+
export { redact };
|
|
17
|
+
// ---------------------------------------------------------------- PageSpeed Insights (lab)
|
|
18
|
+
const CATEGORY_IDS = ["performance", "accessibility", "seo", "best-practices"];
|
|
19
|
+
const psiSchema = z.object({
|
|
20
|
+
lighthouseResult: z.object({
|
|
21
|
+
lighthouseVersion: z.string(),
|
|
22
|
+
fetchTime: z.string(),
|
|
23
|
+
finalDisplayedUrl: z.string().optional(),
|
|
24
|
+
categories: z.record(z.string(), z.object({ score: z.number().nullable() })),
|
|
25
|
+
audits: z.record(z.string(), z.object({ title: z.string(), score: z.number().nullable().optional(), numericValue: z.number().optional(), displayValue: z.string().optional() })),
|
|
26
|
+
}),
|
|
27
|
+
});
|
|
28
|
+
/** Normalizes a Lighthouse result (from PSI or a local run). */
|
|
29
|
+
export function labReport(lhr, source, url, strategy) {
|
|
30
|
+
const score = (id) => {
|
|
31
|
+
const s = lhr.categories[id]?.score;
|
|
32
|
+
return s === null || s === undefined ? null : Math.round(s * 100);
|
|
33
|
+
};
|
|
34
|
+
const num = (id) => lhr.audits[id]?.numericValue;
|
|
35
|
+
const opportunities = Object.entries(lhr.audits)
|
|
36
|
+
.filter(([, a]) => typeof a.score === "number" && a.score < 0.9 && a.displayValue)
|
|
37
|
+
.sort(([, a], [, b]) => (a.score ?? 1) - (b.score ?? 1))
|
|
38
|
+
.slice(0, 8)
|
|
39
|
+
.map(([id, a]) => ({ id, title: a.title, displayValue: a.displayValue }));
|
|
40
|
+
return {
|
|
41
|
+
source,
|
|
42
|
+
url,
|
|
43
|
+
strategy,
|
|
44
|
+
fetchTime: lhr.fetchTime,
|
|
45
|
+
toolVersion: `lighthouse ${lhr.lighthouseVersion}`,
|
|
46
|
+
scores: {
|
|
47
|
+
performance: score("performance"),
|
|
48
|
+
accessibility: score("accessibility"),
|
|
49
|
+
seo: score("seo"),
|
|
50
|
+
"best-practices": score("best-practices"),
|
|
51
|
+
},
|
|
52
|
+
metrics: {
|
|
53
|
+
lcpMs: num("largest-contentful-paint"),
|
|
54
|
+
cls: num("cumulative-layout-shift"),
|
|
55
|
+
tbtMs: num("total-blocking-time"),
|
|
56
|
+
fcpMs: num("first-contentful-paint"),
|
|
57
|
+
speedIndexMs: num("speed-index"),
|
|
58
|
+
},
|
|
59
|
+
opportunities,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
export async function pageSpeed(url, strategy, key, opts = {}) {
|
|
63
|
+
const params = new URLSearchParams({ url, strategy: strategy.toUpperCase() });
|
|
64
|
+
for (const c of CATEGORY_IDS)
|
|
65
|
+
params.append("category", c.toUpperCase().replace("-", "_"));
|
|
66
|
+
const res = await requestWithRetry("PageSpeed Insights", `${PSI_ENDPOINT}?${params}`, withKey({ method: "GET" }, key), [key], opts);
|
|
67
|
+
if (!res.ok)
|
|
68
|
+
throw await apiError("PageSpeed Insights", res, [key], KEY_HINTS);
|
|
69
|
+
const parsed = psiSchema.safeParse(await res.json().catch(() => undefined));
|
|
70
|
+
if (!parsed.success)
|
|
71
|
+
throw new VageError("PageSpeed Insights returned an unexpected response (no lighthouseResult).");
|
|
72
|
+
return labReport(parsed.data.lighthouseResult, "pagespeed-insights", url, strategy);
|
|
73
|
+
}
|
|
74
|
+
// ---------------------------------------------------------------- CrUX (field)
|
|
75
|
+
const cruxSchema = z.object({
|
|
76
|
+
record: z.object({
|
|
77
|
+
metrics: z.record(z.string(), z.object({ percentiles: z.object({ p75: z.union([z.number(), z.string()]) }) })),
|
|
78
|
+
collectionPeriod: z
|
|
79
|
+
.object({
|
|
80
|
+
firstDate: z.object({ year: z.number(), month: z.number(), day: z.number() }),
|
|
81
|
+
lastDate: z.object({ year: z.number(), month: z.number(), day: z.number() }),
|
|
82
|
+
})
|
|
83
|
+
.optional(),
|
|
84
|
+
}),
|
|
85
|
+
});
|
|
86
|
+
const CRUX_METRICS = {
|
|
87
|
+
largest_contentful_paint: "lcp",
|
|
88
|
+
interaction_to_next_paint: "inp",
|
|
89
|
+
cumulative_layout_shift: "cls",
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* Queries the CrUX API for the page, falling back to the origin. Returns
|
|
93
|
+
* undefined when neither has data. A 404 is treated as "no data"; the API
|
|
94
|
+
* docs do not specify this response, so it is an observed-behavior assumption.
|
|
95
|
+
*/
|
|
96
|
+
export async function cruxField(url, strategy, key, opts = {}) {
|
|
97
|
+
const formFactor = strategy === "mobile" ? "PHONE" : "DESKTOP";
|
|
98
|
+
for (const scope of ["url", "origin"]) {
|
|
99
|
+
const body = { [scope]: scope === "url" ? url : new URL(url).origin, formFactor, metrics: Object.keys(CRUX_METRICS) };
|
|
100
|
+
const init = withKey({ method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) }, key);
|
|
101
|
+
const res = await requestWithRetry("CrUX API", CRUX_ENDPOINT, init, [key], opts);
|
|
102
|
+
if (res.status === 404)
|
|
103
|
+
continue;
|
|
104
|
+
if (!res.ok)
|
|
105
|
+
throw await apiError("CrUX API", res, [key], KEY_HINTS);
|
|
106
|
+
const parsed = cruxSchema.safeParse(await res.json().catch(() => undefined));
|
|
107
|
+
if (!parsed.success)
|
|
108
|
+
throw new VageError("CrUX API returned an unexpected response (no record.metrics).");
|
|
109
|
+
const metrics = {};
|
|
110
|
+
for (const [apiName, name] of Object.entries(CRUX_METRICS)) {
|
|
111
|
+
const raw = parsed.data.record.metrics[apiName]?.percentiles.p75;
|
|
112
|
+
if (raw === undefined)
|
|
113
|
+
continue;
|
|
114
|
+
const p75 = Number(raw); // CLS is a string-encoded double
|
|
115
|
+
if (Number.isFinite(p75))
|
|
116
|
+
metrics[name] = { p75, rating: rate(name, p75) };
|
|
117
|
+
}
|
|
118
|
+
const period = parsed.data.record.collectionPeriod;
|
|
119
|
+
const date = (d) => `${d.year}-${String(d.month).padStart(2, "0")}-${String(d.day).padStart(2, "0")}`;
|
|
120
|
+
return { source: "crux", scope, formFactor, metrics, collectionPeriod: period && `${date(period.firstDate)} to ${date(period.lastDate)}` };
|
|
121
|
+
}
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core Web Vitals thresholds, assessed at the 75th percentile of real-user
|
|
3
|
+
* page loads (rule pack "vitals.thresholds", source: https://web.dev/articles/vitals).
|
|
4
|
+
*/
|
|
5
|
+
export declare const VITALS: {
|
|
6
|
+
readonly lcp: {
|
|
7
|
+
readonly good: number;
|
|
8
|
+
readonly poor: number;
|
|
9
|
+
readonly label: "LCP";
|
|
10
|
+
readonly unit: "ms";
|
|
11
|
+
};
|
|
12
|
+
readonly inp: {
|
|
13
|
+
readonly good: number;
|
|
14
|
+
readonly poor: number;
|
|
15
|
+
readonly label: "INP";
|
|
16
|
+
readonly unit: "ms";
|
|
17
|
+
};
|
|
18
|
+
readonly cls: {
|
|
19
|
+
readonly good: number;
|
|
20
|
+
readonly poor: number;
|
|
21
|
+
readonly label: "CLS";
|
|
22
|
+
readonly unit: "";
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
export type VitalName = keyof typeof VITALS;
|
|
26
|
+
export type Rating = "good" | "needs-improvement" | "poor";
|
|
27
|
+
export declare function rate(metric: VitalName, p75: number): Rating;
|
|
28
|
+
export declare const LAB_VS_FIELD: string;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ruleData } from "../guidelines/pack.js";
|
|
2
|
+
const T = ruleData("vitals.thresholds");
|
|
3
|
+
/**
|
|
4
|
+
* Core Web Vitals thresholds, assessed at the 75th percentile of real-user
|
|
5
|
+
* page loads (rule pack "vitals.thresholds", source: https://web.dev/articles/vitals).
|
|
6
|
+
*/
|
|
7
|
+
export const VITALS = {
|
|
8
|
+
lcp: { label: "LCP", unit: "ms", ...T.lcp },
|
|
9
|
+
inp: { label: "INP", unit: "ms", ...T.inp },
|
|
10
|
+
cls: { label: "CLS", unit: "", ...T.cls },
|
|
11
|
+
};
|
|
12
|
+
export function rate(metric, p75) {
|
|
13
|
+
const t = VITALS[metric];
|
|
14
|
+
return p75 <= t.good ? "good" : p75 <= t.poor ? "needs-improvement" : "poor";
|
|
15
|
+
}
|
|
16
|
+
export const LAB_VS_FIELD = "Lab results (Lighthouse/PageSpeed) are a single simulated load and vary between runs. " +
|
|
17
|
+
"Field data (CrUX) reflects real Chrome users at the 75th percentile over 28 days and is " +
|
|
18
|
+
"unavailable for pages or sites without enough traffic. Google's Core Web Vitals assessment uses field data.";
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type SiteModel } from "../core/site.js";
|
|
2
|
+
export type Severity = "error" | "warning";
|
|
3
|
+
export interface Finding {
|
|
4
|
+
rule: string;
|
|
5
|
+
severity: Severity;
|
|
6
|
+
message: string;
|
|
7
|
+
/** Route of the page the finding is about, if any. */
|
|
8
|
+
page?: string;
|
|
9
|
+
/** Source file relative to the project root, if any. */
|
|
10
|
+
source?: string;
|
|
11
|
+
/** Official documentation backing the rule, where one exists. */
|
|
12
|
+
docs?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface CheckResult {
|
|
15
|
+
ok: boolean;
|
|
16
|
+
errors: number;
|
|
17
|
+
warnings: number;
|
|
18
|
+
pagesChecked: number;
|
|
19
|
+
findings: Finding[];
|
|
20
|
+
}
|
|
21
|
+
/** Paths of frontmatter values that came out empty (null), e.g. "faq.items[3].Claude and other AI search?". */
|
|
22
|
+
export declare function emptyValues(value: unknown, at?: string): string[];
|
|
23
|
+
/**
|
|
24
|
+
* Static checks on the rendered site (nothing is written to disk).
|
|
25
|
+
* Passing checks mean the output is technically sound; they do not guarantee
|
|
26
|
+
* indexing, rankings, rich results or AI citations.
|
|
27
|
+
*/
|
|
28
|
+
export declare function check(root: string, options?: {
|
|
29
|
+
strict?: boolean;
|
|
30
|
+
}): Promise<CheckResult>;
|
|
31
|
+
export declare function checkSite(root: string, model: SiteModel): Finding[];
|