nfunc-mcp 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +121 -379
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/mappers/a11yDedupe.js +38 -7
- package/dist/mappers/a11yDedupe.js.map +1 -1
- package/dist/mappers/defectFormatter.d.ts +9 -1
- package/dist/mappers/defectFormatter.js +53 -13
- package/dist/mappers/defectFormatter.js.map +1 -1
- package/dist/mappers/labFieldComparator.d.ts +62 -0
- package/dist/mappers/labFieldComparator.js +134 -0
- package/dist/mappers/labFieldComparator.js.map +1 -0
- package/dist/mappers/priorityMapper.d.ts +42 -0
- package/dist/mappers/priorityMapper.js +58 -0
- package/dist/mappers/priorityMapper.js.map +1 -1
- package/dist/mappers/psiAggregator.d.ts +130 -0
- package/dist/mappers/psiAggregator.js +293 -0
- package/dist/mappers/psiAggregator.js.map +1 -0
- package/dist/mappers/runComparator.d.ts +85 -0
- package/dist/mappers/runComparator.js +165 -0
- package/dist/mappers/runComparator.js.map +1 -0
- package/dist/mappers/wcagLevels.d.ts +73 -0
- package/dist/mappers/wcagLevels.js +320 -0
- package/dist/mappers/wcagLevels.js.map +1 -0
- package/dist/mappers/webVitalsMapper.d.ts +52 -0
- package/dist/mappers/webVitalsMapper.js +131 -0
- package/dist/mappers/webVitalsMapper.js.map +1 -0
- package/dist/tools/accessibility.d.ts +1 -0
- package/dist/tools/accessibility.js +488 -63
- package/dist/tools/accessibility.js.map +1 -1
- package/dist/tools/lighthouse.js +370 -102
- package/dist/tools/lighthouse.js.map +1 -1
- package/dist/tools/performanceAudit.d.ts +2 -0
- package/dist/tools/performanceAudit.js +446 -0
- package/dist/tools/performanceAudit.js.map +1 -0
- package/dist/tools/performanceAuditPlan.d.ts +2 -0
- package/dist/tools/performanceAuditPlan.js +438 -0
- package/dist/tools/performanceAuditPlan.js.map +1 -0
- package/dist/utils/batchState.d.ts +75 -0
- package/dist/utils/batchState.js +128 -0
- package/dist/utils/batchState.js.map +1 -0
- package/dist/utils/csvReader.d.ts +20 -0
- package/dist/utils/csvReader.js +172 -0
- package/dist/utils/csvReader.js.map +1 -0
- package/dist/utils/httpClient.d.ts +84 -0
- package/dist/utils/httpClient.js +171 -0
- package/dist/utils/httpClient.js.map +1 -0
- package/dist/utils/outputParsers.js +26 -30
- package/dist/utils/outputParsers.js.map +1 -1
- package/dist/utils/psiAuth.d.ts +26 -0
- package/dist/utils/psiAuth.js +36 -0
- package/dist/utils/psiAuth.js.map +1 -0
- package/dist/utils/psiParser.d.ts +135 -0
- package/dist/utils/psiParser.js +200 -0
- package/dist/utils/psiParser.js.map +1 -0
- package/dist/utils/publicUrl.d.ts +17 -0
- package/dist/utils/publicUrl.js +115 -0
- package/dist/utils/publicUrl.js.map +1 -0
- package/dist/utils/sitemapReader.d.ts +27 -0
- package/dist/utils/sitemapReader.js +272 -0
- package/dist/utils/sitemapReader.js.map +1 -0
- package/dist/utils/urlClassifier.d.ts +45 -0
- package/dist/utils/urlClassifier.js +267 -0
- package/dist/utils/urlClassifier.js.map +1 -0
- package/dist/utils/urlInput.d.ts +30 -0
- package/dist/utils/urlInput.js +130 -0
- package/dist/utils/urlInput.js.map +1 -0
- package/docs/manual.md +769 -0
- package/docs/psi-report-spec.md +174 -0
- package/package.json +13 -3
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PSI API key resolution.
|
|
3
|
+
*
|
|
4
|
+
* Precedence: explicit tool input → PAGESPEED_API_KEY → keyless.
|
|
5
|
+
*
|
|
6
|
+
* The preferred setup is the `env` block in the MCP client config, because the
|
|
7
|
+
* client launches this server and we do not control its flags — `--env-file`
|
|
8
|
+
* is not available to us. An explicit input is supported for people who want
|
|
9
|
+
* to paste a key once, but it is the worst option: it lands in the
|
|
10
|
+
* conversation transcript and in any client-side logging, so it is never
|
|
11
|
+
* echoed back and every error that touches a request URL is redacted.
|
|
12
|
+
*/
|
|
13
|
+
export const KEY_ENV_VAR = "PAGESPEED_API_KEY";
|
|
14
|
+
/**
|
|
15
|
+
* Keyless PSI is rate-limited hard enough that a batch will 429 partway
|
|
16
|
+
* through, leaving a half-finished audit and a confusing error. Refusing up
|
|
17
|
+
* front with instructions is a better failure than discovering it at run 12.
|
|
18
|
+
*/
|
|
19
|
+
export const KEYLESS_RUN_CAP = 4;
|
|
20
|
+
export function resolveApiKey(explicit) {
|
|
21
|
+
const trimmed = explicit?.trim();
|
|
22
|
+
if (trimmed)
|
|
23
|
+
return { key: trimmed, source: "input" };
|
|
24
|
+
const fromEnv = process.env[KEY_ENV_VAR]?.trim();
|
|
25
|
+
if (fromEnv)
|
|
26
|
+
return { key: fromEnv, source: "env" };
|
|
27
|
+
return { key: null, source: "none" };
|
|
28
|
+
}
|
|
29
|
+
export function keylessWarning(runs) {
|
|
30
|
+
return (`No ${KEY_ENV_VAR} set — running unauthenticated against a shared, ` +
|
|
31
|
+
`heavily rate-limited quota. This is fine for ${KEYLESS_RUN_CAP} runs or ` +
|
|
32
|
+
`fewer; ${runs} runs will almost certainly hit HTTP 429 partway through. ` +
|
|
33
|
+
`Get a key from the Google Cloud console (enable the PageSpeed Insights ` +
|
|
34
|
+
`API) and set ${KEY_ENV_VAR} in your MCP client config.`);
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=psiAuth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"psiAuth.js","sourceRoot":"","sources":["../../src/utils/psiAuth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AASH,MAAM,CAAC,MAAM,WAAW,GAAG,mBAAmB,CAAC;AAE/C;;;;GAIG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,CAAC;AAEjC,MAAM,UAAU,aAAa,CAAC,QAAiB;IAC7C,MAAM,OAAO,GAAG,QAAQ,EAAE,IAAI,EAAE,CAAC;IACjC,IAAI,OAAO;QAAE,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;IAEtD,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,IAAI,EAAE,CAAC;IACjD,IAAI,OAAO;QAAE,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;IAEpD,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;AACvC,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,IAAY;IACzC,OAAO,CACL,MAAM,WAAW,mDAAmD;QACpE,gDAAgD,eAAe,WAAW;QAC1E,UAAU,IAAI,4DAA4D;QAC1E,yEAAyE;QACzE,gBAAgB,WAAW,6BAA6B,CACzD,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PageSpeed Insights response parsing.
|
|
3
|
+
*
|
|
4
|
+
* One PSI call returns two unrelated datasets: `lighthouseResult` (a lab run
|
|
5
|
+
* on Google's hardware) and `loadingExperience` (CrUX field data from real
|
|
6
|
+
* Chrome users). They are measured differently, they disagree routinely, and
|
|
7
|
+
* conflating them produces confidently wrong reports — so they stay separate
|
|
8
|
+
* all the way through this module and only meet in the comparator.
|
|
9
|
+
*
|
|
10
|
+
* The lab half is the same LHR schema `parseLighthouseJSON` already handles
|
|
11
|
+
* and is delegated to it unchanged. Everything here is about the field half
|
|
12
|
+
* and about pulling numeric lab metrics that survive aggregation.
|
|
13
|
+
*/
|
|
14
|
+
import { type ParsedLighthouse } from "./outputParsers.js";
|
|
15
|
+
export type CruxCategory = "FAST" | "AVERAGE" | "SLOW" | "NONE";
|
|
16
|
+
/** The five field metrics PSI reports. `fid` is deliberately absent — see below. */
|
|
17
|
+
export type WebVital = "lcp" | "inp" | "cls" | "fcp" | "ttfb";
|
|
18
|
+
export type FieldSource = "url" | "origin";
|
|
19
|
+
export interface CruxMetric {
|
|
20
|
+
/**
|
|
21
|
+
* 75th percentile across the collection window. Milliseconds for every
|
|
22
|
+
* metric except `cls`, which is unitless.
|
|
23
|
+
*/
|
|
24
|
+
p75: number;
|
|
25
|
+
category: CruxCategory;
|
|
26
|
+
/** Share of real users in each bucket. Sums to ~1. */
|
|
27
|
+
distribution: {
|
|
28
|
+
good: number;
|
|
29
|
+
needsImprovement: number;
|
|
30
|
+
poor: number;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Whether this specific metric came from the URL or from origin-wide data.
|
|
34
|
+
* Per metric, not per response — see the note on `parseCrux`.
|
|
35
|
+
*/
|
|
36
|
+
source: FieldSource;
|
|
37
|
+
}
|
|
38
|
+
export interface ParsedCrux {
|
|
39
|
+
/**
|
|
40
|
+
* Always 28. PSI does not return the collection window, but CrUX in PSI is
|
|
41
|
+
* defined as a trailing 28-day aggregate, so this is a documented constant
|
|
42
|
+
* rather than a parsed value. Stated explicitly because reports must not
|
|
43
|
+
* describe field data as real-time.
|
|
44
|
+
*/
|
|
45
|
+
collectionPeriodDays: 28;
|
|
46
|
+
overallCategory: CruxCategory | null;
|
|
47
|
+
/** Which source the majority of metrics came from, for a one-line summary. */
|
|
48
|
+
primarySource: FieldSource;
|
|
49
|
+
metrics: Partial<Record<WebVital, CruxMetric>>;
|
|
50
|
+
}
|
|
51
|
+
export interface LabMetrics {
|
|
52
|
+
lcpMs: number | null;
|
|
53
|
+
fcpMs: number | null;
|
|
54
|
+
cls: number | null;
|
|
55
|
+
tbtMs: number | null;
|
|
56
|
+
speedIndexMs: number | null;
|
|
57
|
+
ttiMs: number | null;
|
|
58
|
+
ttfbMs: number | null;
|
|
59
|
+
}
|
|
60
|
+
export interface ParsedPsi {
|
|
61
|
+
requestedUrl: string;
|
|
62
|
+
finalUrl: string;
|
|
63
|
+
strategy: string;
|
|
64
|
+
fetchTime: string | null;
|
|
65
|
+
lighthouseVersion: string | null;
|
|
66
|
+
scores: Record<string, number>;
|
|
67
|
+
lab: LabMetrics;
|
|
68
|
+
/** Null when the URL has too little real-user traffic and origin fallback is off or empty. */
|
|
69
|
+
field: ParsedCrux | null;
|
|
70
|
+
lighthouse: ParsedLighthouse;
|
|
71
|
+
/** Lighthouse audits that failed, for the existing defect formatter. */
|
|
72
|
+
runWarnings: string[];
|
|
73
|
+
}
|
|
74
|
+
/** PSI returned a 200 whose Lighthouse run did not actually complete. */
|
|
75
|
+
export declare class PsiRuntimeError extends Error {
|
|
76
|
+
readonly code: string;
|
|
77
|
+
constructor(code: string, message: string);
|
|
78
|
+
}
|
|
79
|
+
interface RawCruxMetric {
|
|
80
|
+
percentile?: number;
|
|
81
|
+
category?: string;
|
|
82
|
+
distributions?: Array<{
|
|
83
|
+
min?: number;
|
|
84
|
+
max?: number;
|
|
85
|
+
proportion?: number;
|
|
86
|
+
}>;
|
|
87
|
+
}
|
|
88
|
+
interface RawLoadingExperience {
|
|
89
|
+
id?: string;
|
|
90
|
+
overall_category?: string;
|
|
91
|
+
metrics?: Record<string, RawCruxMetric>;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Merge URL-level and origin-level CrUX into one metric set.
|
|
95
|
+
*
|
|
96
|
+
* Field availability is **per metric, not per URL** — a page can have enough
|
|
97
|
+
* traffic for CrUX to report CLS but not LCP. A real response in the Five
|
|
98
|
+
* Below batch carried two of the five metrics at URL level while the origin
|
|
99
|
+
* carried all five, which is why the manual audit shows "No field data" for
|
|
100
|
+
* one metric and a category for another on the same row.
|
|
101
|
+
*
|
|
102
|
+
* So the fallback runs per metric, and each metric records where it came from.
|
|
103
|
+
* A single response-level `field_source` flag would have to either discard the
|
|
104
|
+
* URL-level metrics that do exist or silently label origin data as page data,
|
|
105
|
+
* and origin CrUX for a homepage says nothing about a checkout page.
|
|
106
|
+
*/
|
|
107
|
+
export declare function parseCrux(urlLevel: RawLoadingExperience | undefined, originLevel: RawLoadingExperience | undefined, originFallback: boolean): ParsedCrux | null;
|
|
108
|
+
/**
|
|
109
|
+
* Lab metrics as numbers.
|
|
110
|
+
*
|
|
111
|
+
* `numericValue`, never `displayValue`. The Five Below CSV recorded TTFB as
|
|
112
|
+
* "Root document took 930 ms" — a localised string containing a non-breaking
|
|
113
|
+
* space — because it read displayValue, which made the whole column
|
|
114
|
+
* unaggregatable while `numericValue: 929` sat in the same audit object.
|
|
115
|
+
* Formatting is a report-time concern; nothing upstream of the report should
|
|
116
|
+
* hold a number as text.
|
|
117
|
+
*/
|
|
118
|
+
export declare function extractLabMetrics(auditsJson: string): LabMetrics;
|
|
119
|
+
export interface ParsePsiOptions {
|
|
120
|
+
strategy: string;
|
|
121
|
+
/** Fall back to origin-wide CrUX per metric when URL-level data is absent. */
|
|
122
|
+
originFallback?: boolean;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Parse a full PSI v5 response.
|
|
126
|
+
*
|
|
127
|
+
* Throws `PsiRuntimeError` when Lighthouse itself failed inside a 200
|
|
128
|
+
* response. PSI does this routinely — a page that times out or refuses the
|
|
129
|
+
* fetch comes back as HTTP 200 with `lighthouseResult.runtimeError` set and
|
|
130
|
+
* every score null. Parsing it anyway records a real page as scoring zero
|
|
131
|
+
* across the board, which is worse than a failed run, because a zero looks
|
|
132
|
+
* like data. Callers should treat this as retryable.
|
|
133
|
+
*/
|
|
134
|
+
export declare function parsePsiResponse(rawJson: string, options: ParsePsiOptions): ParsedPsi;
|
|
135
|
+
export {};
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PageSpeed Insights response parsing.
|
|
3
|
+
*
|
|
4
|
+
* One PSI call returns two unrelated datasets: `lighthouseResult` (a lab run
|
|
5
|
+
* on Google's hardware) and `loadingExperience` (CrUX field data from real
|
|
6
|
+
* Chrome users). They are measured differently, they disagree routinely, and
|
|
7
|
+
* conflating them produces confidently wrong reports — so they stay separate
|
|
8
|
+
* all the way through this module and only meet in the comparator.
|
|
9
|
+
*
|
|
10
|
+
* The lab half is the same LHR schema `parseLighthouseJSON` already handles
|
|
11
|
+
* and is delegated to it unchanged. Everything here is about the field half
|
|
12
|
+
* and about pulling numeric lab metrics that survive aggregation.
|
|
13
|
+
*/
|
|
14
|
+
import { parseLighthouseJSON } from "./outputParsers.js";
|
|
15
|
+
/** PSI returned a 200 whose Lighthouse run did not actually complete. */
|
|
16
|
+
export class PsiRuntimeError extends Error {
|
|
17
|
+
code;
|
|
18
|
+
constructor(code, message) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.code = code;
|
|
21
|
+
this.name = "PsiRuntimeError";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* CrUX metric key → our vital name.
|
|
26
|
+
*
|
|
27
|
+
* FIRST_INPUT_DELAY_MS is intentionally not mapped. FID was retired in March
|
|
28
|
+
* 2024 and replaced by INP; it still appears in some responses, and treating
|
|
29
|
+
* it as a current metric would report a defect against a standard Google no
|
|
30
|
+
* longer measures. Read it only if a caller ever needs historical data.
|
|
31
|
+
*/
|
|
32
|
+
const CRUX_KEYS = {
|
|
33
|
+
LARGEST_CONTENTFUL_PAINT_MS: "lcp",
|
|
34
|
+
INTERACTION_TO_NEXT_PAINT: "inp",
|
|
35
|
+
CUMULATIVE_LAYOUT_SHIFT_SCORE: "cls",
|
|
36
|
+
FIRST_CONTENTFUL_PAINT_MS: "fcp",
|
|
37
|
+
EXPERIMENTAL_TIME_TO_FIRST_BYTE: "ttfb",
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* CLS is the one field metric PSI returns scaled by 100: a `percentile` of 55
|
|
41
|
+
* means a CLS of 0.55, and the distribution bucket boundaries (0-10, 10-25)
|
|
42
|
+
* are scaled the same way. Comparing the raw integer against the 0.25 "poor"
|
|
43
|
+
* threshold marks every site on earth as catastrophic; forgetting the scaling
|
|
44
|
+
* entirely turns a 0.55 into a 55. Verified against real responses.
|
|
45
|
+
*/
|
|
46
|
+
function scaleCruxValue(vital, percentile) {
|
|
47
|
+
return vital === "cls" ? percentile / 100 : percentile;
|
|
48
|
+
}
|
|
49
|
+
function toCategory(raw) {
|
|
50
|
+
return raw === "FAST" || raw === "AVERAGE" || raw === "SLOW" ? raw : "NONE";
|
|
51
|
+
}
|
|
52
|
+
function toDistribution(raw) {
|
|
53
|
+
const share = (i) => {
|
|
54
|
+
const p = raw?.[i]?.proportion;
|
|
55
|
+
return typeof p === "number" ? Number(p.toFixed(4)) : 0;
|
|
56
|
+
};
|
|
57
|
+
// PSI always emits exactly three buckets, ordered good → poor.
|
|
58
|
+
return { good: share(0), needsImprovement: share(1), poor: share(2) };
|
|
59
|
+
}
|
|
60
|
+
function readMetric(raw, vital, source) {
|
|
61
|
+
if (!raw || typeof raw.percentile !== "number")
|
|
62
|
+
return null;
|
|
63
|
+
return {
|
|
64
|
+
p75: scaleCruxValue(vital, raw.percentile),
|
|
65
|
+
category: toCategory(raw.category),
|
|
66
|
+
distribution: toDistribution(raw.distributions),
|
|
67
|
+
source,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Merge URL-level and origin-level CrUX into one metric set.
|
|
72
|
+
*
|
|
73
|
+
* Field availability is **per metric, not per URL** — a page can have enough
|
|
74
|
+
* traffic for CrUX to report CLS but not LCP. A real response in the Five
|
|
75
|
+
* Below batch carried two of the five metrics at URL level while the origin
|
|
76
|
+
* carried all five, which is why the manual audit shows "No field data" for
|
|
77
|
+
* one metric and a category for another on the same row.
|
|
78
|
+
*
|
|
79
|
+
* So the fallback runs per metric, and each metric records where it came from.
|
|
80
|
+
* A single response-level `field_source` flag would have to either discard the
|
|
81
|
+
* URL-level metrics that do exist or silently label origin data as page data,
|
|
82
|
+
* and origin CrUX for a homepage says nothing about a checkout page.
|
|
83
|
+
*/
|
|
84
|
+
export function parseCrux(urlLevel, originLevel, originFallback) {
|
|
85
|
+
const metrics = {};
|
|
86
|
+
let fromUrl = 0;
|
|
87
|
+
let fromOrigin = 0;
|
|
88
|
+
/**
|
|
89
|
+
* PSI performs its own origin fallback, and does it silently.
|
|
90
|
+
*
|
|
91
|
+
* When a URL has too little traffic, `loadingExperience` comes back populated
|
|
92
|
+
* with **origin-level** numbers and the only signal is its `id`, which holds
|
|
93
|
+
* the origin instead of the URL. Trusting the block's position in the
|
|
94
|
+
* response therefore labels origin data as page data: two different paginated
|
|
95
|
+
* URLs came back with byte-identical p75 values, both marked `source: "url"`.
|
|
96
|
+
*
|
|
97
|
+
* Comparing the two blocks' ids catches it without needing the requested URL.
|
|
98
|
+
*/
|
|
99
|
+
const psiSubstitutedOrigin = Boolean(urlLevel?.id) && Boolean(originLevel?.id) && urlLevel?.id === originLevel?.id;
|
|
100
|
+
const urlLevelSource = psiSubstitutedOrigin ? "origin" : "url";
|
|
101
|
+
for (const [cruxKey, vital] of Object.entries(CRUX_KEYS)) {
|
|
102
|
+
const atUrl = readMetric(urlLevel?.metrics?.[cruxKey], vital, urlLevelSource);
|
|
103
|
+
if (atUrl) {
|
|
104
|
+
metrics[vital] = atUrl;
|
|
105
|
+
if (urlLevelSource === "url")
|
|
106
|
+
fromUrl++;
|
|
107
|
+
else
|
|
108
|
+
fromOrigin++;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (!originFallback)
|
|
112
|
+
continue;
|
|
113
|
+
const atOrigin = readMetric(originLevel?.metrics?.[cruxKey], vital, "origin");
|
|
114
|
+
if (atOrigin) {
|
|
115
|
+
metrics[vital] = atOrigin;
|
|
116
|
+
fromOrigin++;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (fromUrl === 0 && fromOrigin === 0)
|
|
120
|
+
return null;
|
|
121
|
+
// Only trust the URL-level verdict when the URL actually supplied data;
|
|
122
|
+
// otherwise the origin's verdict is the one describing these numbers.
|
|
123
|
+
const overallRaw = fromUrl > 0 ? urlLevel?.overall_category : originLevel?.overall_category;
|
|
124
|
+
return {
|
|
125
|
+
collectionPeriodDays: 28,
|
|
126
|
+
overallCategory: overallRaw ? toCategory(overallRaw) : null,
|
|
127
|
+
primarySource: fromUrl >= fromOrigin ? "url" : "origin",
|
|
128
|
+
metrics,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Lab metrics as numbers.
|
|
133
|
+
*
|
|
134
|
+
* `numericValue`, never `displayValue`. The Five Below CSV recorded TTFB as
|
|
135
|
+
* "Root document took 930 ms" — a localised string containing a non-breaking
|
|
136
|
+
* space — because it read displayValue, which made the whole column
|
|
137
|
+
* unaggregatable while `numericValue: 929` sat in the same audit object.
|
|
138
|
+
* Formatting is a report-time concern; nothing upstream of the report should
|
|
139
|
+
* hold a number as text.
|
|
140
|
+
*/
|
|
141
|
+
export function extractLabMetrics(auditsJson) {
|
|
142
|
+
const audits = JSON.parse(auditsJson);
|
|
143
|
+
const num = (id) => {
|
|
144
|
+
const v = audits.audits?.[id]?.numericValue;
|
|
145
|
+
return typeof v === "number" ? v : null;
|
|
146
|
+
};
|
|
147
|
+
const ms = (id) => {
|
|
148
|
+
const v = num(id);
|
|
149
|
+
return v === null ? null : Math.round(v);
|
|
150
|
+
};
|
|
151
|
+
return {
|
|
152
|
+
lcpMs: ms("largest-contentful-paint"),
|
|
153
|
+
fcpMs: ms("first-contentful-paint"),
|
|
154
|
+
// CLS is unitless and small; rounding it to an integer would destroy it.
|
|
155
|
+
cls: num("cumulative-layout-shift"),
|
|
156
|
+
tbtMs: ms("total-blocking-time"),
|
|
157
|
+
speedIndexMs: ms("speed-index"),
|
|
158
|
+
ttiMs: ms("interactive"),
|
|
159
|
+
ttfbMs: ms("server-response-time"),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Parse a full PSI v5 response.
|
|
164
|
+
*
|
|
165
|
+
* Throws `PsiRuntimeError` when Lighthouse itself failed inside a 200
|
|
166
|
+
* response. PSI does this routinely — a page that times out or refuses the
|
|
167
|
+
* fetch comes back as HTTP 200 with `lighthouseResult.runtimeError` set and
|
|
168
|
+
* every score null. Parsing it anyway records a real page as scoring zero
|
|
169
|
+
* across the board, which is worse than a failed run, because a zero looks
|
|
170
|
+
* like data. Callers should treat this as retryable.
|
|
171
|
+
*/
|
|
172
|
+
export function parsePsiResponse(rawJson, options) {
|
|
173
|
+
const { strategy, originFallback = true } = options;
|
|
174
|
+
const response = JSON.parse(rawJson);
|
|
175
|
+
const lhr = response.lighthouseResult;
|
|
176
|
+
if (!lhr) {
|
|
177
|
+
throw new PsiRuntimeError("NO_LIGHTHOUSE_RESULT", "PSI response contained no lighthouseResult.");
|
|
178
|
+
}
|
|
179
|
+
if (lhr.runtimeError?.code) {
|
|
180
|
+
throw new PsiRuntimeError(lhr.runtimeError.code, lhr.runtimeError.message ?? "Lighthouse reported a runtime error.");
|
|
181
|
+
}
|
|
182
|
+
// parseLighthouseJSON takes the serialised LHR. Re-serialising the object we
|
|
183
|
+
// already hold is a little wasteful, but it keeps the Phase 2-13 parser and
|
|
184
|
+
// its priority mapping untouched, which is worth more than the milliseconds.
|
|
185
|
+
const lhrJson = JSON.stringify(lhr);
|
|
186
|
+
const lighthouse = parseLighthouseJSON(lhrJson);
|
|
187
|
+
return {
|
|
188
|
+
requestedUrl: lhr.requestedUrl ?? "",
|
|
189
|
+
finalUrl: lhr.finalDisplayedUrl ?? lhr.finalUrl ?? lhr.requestedUrl ?? "",
|
|
190
|
+
strategy,
|
|
191
|
+
fetchTime: lhr.fetchTime ?? null,
|
|
192
|
+
lighthouseVersion: lhr.lighthouseVersion ?? null,
|
|
193
|
+
scores: lighthouse.categoryScores,
|
|
194
|
+
lab: extractLabMetrics(lhrJson),
|
|
195
|
+
field: parseCrux(response.loadingExperience, response.originLoadingExperience, originFallback),
|
|
196
|
+
lighthouse,
|
|
197
|
+
runWarnings: lhr.runWarnings ?? [],
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
//# sourceMappingURL=psiParser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"psiParser.js","sourceRoot":"","sources":["../../src/utils/psiParser.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether PSI can reach a URL at all.
|
|
3
|
+
*
|
|
4
|
+
* PSI fetches the target from Google's own infrastructure, so "can I open this
|
|
5
|
+
* in my browser" is not the test — the page has to be reachable from the
|
|
6
|
+
* public internet. Localhost and RFC1918 addresses are not degraded cases that
|
|
7
|
+
* return partial data; they cannot be audited, full stop, and the only useful
|
|
8
|
+
* response is to say so before spending a request finding out.
|
|
9
|
+
*/
|
|
10
|
+
export type ReachVerdict = "public" | "loopback" | "private" | "non_http" | "malformed";
|
|
11
|
+
export interface ReachResult {
|
|
12
|
+
verdict: ReachVerdict;
|
|
13
|
+
auditable: boolean;
|
|
14
|
+
reason?: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function checkPublicReachability(rawUrl: string): ReachResult;
|
|
17
|
+
export declare function isSessionGated(rawUrl: string): boolean;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether PSI can reach a URL at all.
|
|
3
|
+
*
|
|
4
|
+
* PSI fetches the target from Google's own infrastructure, so "can I open this
|
|
5
|
+
* in my browser" is not the test — the page has to be reachable from the
|
|
6
|
+
* public internet. Localhost and RFC1918 addresses are not degraded cases that
|
|
7
|
+
* return partial data; they cannot be audited, full stop, and the only useful
|
|
8
|
+
* response is to say so before spending a request finding out.
|
|
9
|
+
*/
|
|
10
|
+
/** IPv4 in a private or link-local range, per RFC1918 / RFC3927. */
|
|
11
|
+
function isPrivateIPv4(host) {
|
|
12
|
+
const parts = host.split(".");
|
|
13
|
+
if (parts.length !== 4)
|
|
14
|
+
return false;
|
|
15
|
+
const [a, b] = parts.map(Number);
|
|
16
|
+
if (parts.some((p) => !/^\d+$/.test(p)) || [a, b].some(Number.isNaN))
|
|
17
|
+
return false;
|
|
18
|
+
if (a === 10)
|
|
19
|
+
return true;
|
|
20
|
+
if (a === 172 && b >= 16 && b <= 31)
|
|
21
|
+
return true;
|
|
22
|
+
if (a === 192 && b === 168)
|
|
23
|
+
return true;
|
|
24
|
+
if (a === 169 && b === 254)
|
|
25
|
+
return true; // link-local
|
|
26
|
+
if (a === 100 && b >= 64 && b <= 127)
|
|
27
|
+
return true; // CGNAT
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
function isLoopback(host) {
|
|
31
|
+
if (host === "localhost" || host.endsWith(".localhost"))
|
|
32
|
+
return true;
|
|
33
|
+
if (host === "::1" || host === "[::1]")
|
|
34
|
+
return true;
|
|
35
|
+
return /^127\./.test(host);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Hostnames that never resolve on the public internet. `.local` is mDNS,
|
|
39
|
+
* `.internal` and `.test`/`.invalid` are reserved, and a bare hostname with no
|
|
40
|
+
* dot is a LAN name.
|
|
41
|
+
*/
|
|
42
|
+
function isNonPublicName(host) {
|
|
43
|
+
if (/\.(local|internal|localdomain|test|invalid|example)$/.test(host))
|
|
44
|
+
return true;
|
|
45
|
+
return !host.includes(".") && !/^\d+$/.test(host);
|
|
46
|
+
}
|
|
47
|
+
export function checkPublicReachability(rawUrl) {
|
|
48
|
+
let url;
|
|
49
|
+
try {
|
|
50
|
+
url = new URL(rawUrl);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return { verdict: "malformed", auditable: false, reason: `"${rawUrl}" is not a valid URL.` };
|
|
54
|
+
}
|
|
55
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
56
|
+
return {
|
|
57
|
+
verdict: "non_http",
|
|
58
|
+
auditable: false,
|
|
59
|
+
reason: `PSI only audits http(s) URLs; got "${url.protocol}".`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const host = url.hostname.toLowerCase();
|
|
63
|
+
if (isLoopback(host)) {
|
|
64
|
+
return {
|
|
65
|
+
verdict: "loopback",
|
|
66
|
+
auditable: false,
|
|
67
|
+
reason: "PSI fetches pages from Google's infrastructure, so it cannot reach " +
|
|
68
|
+
"localhost. Use run_lighthouse, which runs Chrome on this machine.",
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
if (isPrivateIPv4(host)) {
|
|
72
|
+
return {
|
|
73
|
+
verdict: "private",
|
|
74
|
+
auditable: false,
|
|
75
|
+
reason: `${host} is a private-network address and is not reachable from ` +
|
|
76
|
+
"Google's infrastructure. Use run_lighthouse for internal hosts.",
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
if (isNonPublicName(host)) {
|
|
80
|
+
return {
|
|
81
|
+
verdict: "private",
|
|
82
|
+
auditable: false,
|
|
83
|
+
reason: `"${host}" is not a publicly resolvable hostname. Use run_lighthouse ` +
|
|
84
|
+
"for hosts that only resolve on your network.",
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return { verdict: "public", auditable: true };
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Paths that PSI can fetch but cannot meaningfully audit, because an anonymous
|
|
91
|
+
* request does not see the real page.
|
|
92
|
+
*
|
|
93
|
+
* PSI has no session: it audits an empty cart, a redirect to a login form, or
|
|
94
|
+
* a search page with no query, and reports the result as if it were the page
|
|
95
|
+
* someone asked about. A confidently wrong number is worse than a gap, so
|
|
96
|
+
* these are flagged rather than run. `run_lighthouse` can carry cookies and is
|
|
97
|
+
* the right tool for them.
|
|
98
|
+
*/
|
|
99
|
+
const SESSION_GATED = [
|
|
100
|
+
/(^|\/)(cart|basket|bag)(\/|$)/,
|
|
101
|
+
/(^|\/)(checkout|payment|order-confirmation)(\/|$)/,
|
|
102
|
+
/(^|\/)(account|my-account|profile|dashboard|orders)(\/|$)/,
|
|
103
|
+
/(^|\/)(login|signin|sign-in|register|signup|sign-up|logout)(\/|$)/,
|
|
104
|
+
/(^|\/)(wishlist|favorites|favourites|saved)(\/|$)/,
|
|
105
|
+
];
|
|
106
|
+
export function isSessionGated(rawUrl) {
|
|
107
|
+
try {
|
|
108
|
+
const path = new URL(rawUrl).pathname.toLowerCase();
|
|
109
|
+
return SESSION_GATED.some((re) => re.test(path));
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=publicUrl.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"publicUrl.js","sourceRoot":"","sources":["../../src/utils/publicUrl.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sitemap discovery.
|
|
3
|
+
*
|
|
4
|
+
* The cheapest honest way to learn which pages a site has. A sitemap is
|
|
5
|
+
* authoritative (the site published it), instant (one or two requests), and
|
|
6
|
+
* needs no crawling, no robots.txt politeness budget and no HTML parsing. A
|
|
7
|
+
* crawler is the fallback for sites without one, and is deliberately not built
|
|
8
|
+
* yet — everything downstream works without it.
|
|
9
|
+
*
|
|
10
|
+
* Sitemaps are parsed with regular expressions rather than an XML library.
|
|
11
|
+
* That is normally a mistake, but a sitemap is a machine-generated document
|
|
12
|
+
* with a fixed two-element vocabulary, and the alternative is a dependency
|
|
13
|
+
* carried by every install of this server for one code path. The parser reads
|
|
14
|
+
* <loc> only and ignores everything else, so malformed markup degrades to
|
|
15
|
+
* fewer URLs rather than to wrong ones.
|
|
16
|
+
*/
|
|
17
|
+
export interface SitemapResult {
|
|
18
|
+
urls: string[];
|
|
19
|
+
/** Every sitemap document actually fetched, in order. */
|
|
20
|
+
sitemapsRead: string[];
|
|
21
|
+
/** True when a cap stopped the walk early — the URL list is a subset. */
|
|
22
|
+
truncated: boolean;
|
|
23
|
+
/** Every location tried, so a failure can say what was ruled out. */
|
|
24
|
+
attempted: string[];
|
|
25
|
+
warnings: string[];
|
|
26
|
+
}
|
|
27
|
+
export declare function readSitemap(origin: string): Promise<SitemapResult>;
|