webperf-snippets 1.11.0 → 1.13.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 +13 -4
- package/package.json +2 -1
- package/snippets/CoreWebVitals/INP.js +20 -4
- package/snippets/CoreWebVitals/LCP-Trail.js +52 -40
- package/snippets/Interaction/Input-Latency-Breakdown.js +3 -0
- package/snippets/Interaction/Interactions.js +4 -0
- package/snippets/Loading/CSS-Complexity.js +562 -0
- package/snippets/Loading/WORKFLOWS.md +11 -0
- package/snippets/SCHEMA.md +86 -0
- package/src/baseline.js +33 -5
- package/src/bin.js +8 -1
- package/src/mcp/queue.js +72 -0
- package/src/mcp/server.js +116 -45
- package/src/runner.js +29 -1
- package/src/snippet-names.js +90 -19
package/src/snippet-names.js
CHANGED
|
@@ -11,6 +11,12 @@ const SNIPPET_ALIASES = {
|
|
|
11
11
|
CLS: "CoreWebVitals/CLS",
|
|
12
12
|
INP: "CoreWebVitals/INP",
|
|
13
13
|
"LCP-Subparts": "CoreWebVitals/LCP-Subparts",
|
|
14
|
+
// The full names of the metrics, in any case: the words someone types who does not know the acronym
|
|
15
|
+
"Largest-Contentful-Paint": "CoreWebVitals/LCP",
|
|
16
|
+
"Cumulative-Layout-Shift": "CoreWebVitals/CLS",
|
|
17
|
+
"Interaction-to-Next-Paint": "CoreWebVitals/INP",
|
|
18
|
+
"First-Contentful-Paint": "Loading/FCP",
|
|
19
|
+
"Time-to-First-Byte": "Loading/TTFB",
|
|
14
20
|
fonts: "Loading/Fonts-Preloaded-Loaded-and-used-above-the-fold",
|
|
15
21
|
"Fonts-Preloaded-Loaded-and-used-above-the-fold":
|
|
16
22
|
"Loading/Fonts-Preloaded-Loaded-and-used-above-the-fold",
|
|
@@ -65,34 +71,99 @@ export function listSnippets() {
|
|
|
65
71
|
return cache;
|
|
66
72
|
}
|
|
67
73
|
|
|
74
|
+
// The edits between two strings. Swapping two neighbours counts as one: LPC is one edit from LCP.
|
|
68
75
|
function distance(a, b) {
|
|
69
|
-
|
|
76
|
+
let twoBack = [];
|
|
77
|
+
let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
70
78
|
for (let i = 1; i <= a.length; i++) {
|
|
71
|
-
|
|
72
|
-
row[0] = i;
|
|
79
|
+
const row = [i];
|
|
73
80
|
for (let j = 1; j <= b.length; j++) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
previous = current;
|
|
81
|
+
row[j] = Math.min(previous[j] + 1, row[j - 1] + 1, previous[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
82
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) row[j] = Math.min(row[j], twoBack[j - 2] + 1);
|
|
77
83
|
}
|
|
84
|
+
twoBack = previous;
|
|
85
|
+
previous = row;
|
|
78
86
|
}
|
|
79
|
-
return
|
|
87
|
+
return previous[b.length];
|
|
80
88
|
}
|
|
81
89
|
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
90
|
+
// The words of a name, without their endings: LongTask and long-tasks give the same two.
|
|
91
|
+
function wordsOf(name) {
|
|
92
|
+
return name
|
|
93
|
+
.replace(/([a-z])([A-Z])|([A-Z])([A-Z][a-z])/g, "$1$3-$2$4")
|
|
94
|
+
.toLowerCase()
|
|
95
|
+
.split(/[^a-z0-9]+/)
|
|
96
|
+
.filter(Boolean)
|
|
97
|
+
.map((word) => (word.length > 4 ? word.replace(/(ing|ed)?s?$/, "") : word));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// The words of a guess that say nothing of what a snippet looks at. They rank, but do not make a match.
|
|
101
|
+
const GENERIC_WORDS = new Set(wordsOf("and the of with by find audit analysis analyzer info detection validation performance"));
|
|
102
|
+
|
|
103
|
+
// Equal, inside a longer word of the name (font in webfont), or a typo apart. Words of three letters only
|
|
104
|
+
// count when equal, and a typo needs five letters: size and side are one edit apart.
|
|
105
|
+
function isSameWord(guessed, word) {
|
|
106
|
+
if (guessed === word) return true;
|
|
107
|
+
if (Math.min(guessed.length, word.length) < 4) return false;
|
|
108
|
+
return word.includes(guessed) || (Math.max(guessed.length, word.length) > 4 && distance(guessed, word) === 1);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// How close a guess is to a name or an alias of a snippet, from 0 to 1: the share of its words the name
|
|
112
|
+
// has, or how alike they are spelled, which catches what splits into other words, such as LCP-Sub-Parts.
|
|
113
|
+
// Under MIN_SPELLING the likeness is chance: any two short names share letters.
|
|
114
|
+
const MIN_SPELLING = 0.65;
|
|
115
|
+
const MIN_SCORE = 0.5;
|
|
116
|
+
const NEAR_THE_FIRST = 0.75;
|
|
117
|
+
const MAX_SUGGESTIONS = 5;
|
|
118
|
+
|
|
119
|
+
function closeness(guess, name) {
|
|
120
|
+
const [a, b] = [guess.toLowerCase(), name.toLowerCase()];
|
|
121
|
+
const spelling = 1 - distance(a, b) / Math.max(a.length, b.length);
|
|
122
|
+
const words = wordsOf(name);
|
|
123
|
+
const guessed = wordsOf(guess);
|
|
124
|
+
const found = guessed.filter((word) => words.some((other) => isSameWord(word, other)));
|
|
125
|
+
const shared = found.some((word) => !GENERIC_WORDS.has(word)) ? found.length / guessed.length : 0;
|
|
126
|
+
return Math.max(spelling >= MIN_SPELLING ? spelling : 0, shared);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// The snippets a guess may have meant, the closest first, and on a tie the shortest name, which says the
|
|
130
|
+
// least else: Script-Loading before Inline-Script-Info-and-Size.
|
|
131
|
+
function suggestionsFor(guess) {
|
|
132
|
+
const ranked = listSnippets()
|
|
133
|
+
.map((path) => {
|
|
134
|
+
const name = path.split("/")[1];
|
|
135
|
+
const score = Math.max(...[name, ...aliasesOf(path)].map((n) => closeness(guess, n)));
|
|
136
|
+
return { path, score, length: wordsOf(name).length };
|
|
137
|
+
})
|
|
138
|
+
.filter((snippet) => snippet.score >= MIN_SCORE)
|
|
139
|
+
.sort((a, b) => b.score - a.score || a.length - b.length);
|
|
140
|
+
return ranked
|
|
141
|
+
.filter((snippet) => snippet.score >= ranked[0].score * NEAR_THE_FIRST)
|
|
142
|
+
.slice(0, MAX_SUGGESTIONS)
|
|
143
|
+
.map((snippet) => snippet.path);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// An alias as written, then a snippet name in any case, then an alias in any case: TTFB is the snippet,
|
|
147
|
+
// and ttfb the alias of its sub-parts.
|
|
148
|
+
function findSnippet(name) {
|
|
85
149
|
if (Object.hasOwn(SNIPPET_ALIASES, name)) return SNIPPET_ALIASES[name];
|
|
86
|
-
const all = listSnippets();
|
|
87
150
|
const wanted = name.toLowerCase();
|
|
88
|
-
const
|
|
151
|
+
const snippet = listSnippets().find((path) => path.toLowerCase() === wanted || path.split("/")[1].toLowerCase() === wanted);
|
|
152
|
+
if (snippet) return snippet;
|
|
153
|
+
const alias = Object.keys(SNIPPET_ALIASES).find((key) => key.toLowerCase() === wanted);
|
|
154
|
+
return alias && SNIPPET_ALIASES[alias];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Turns an alias, a bare snippet name or a Category/Name path into a Category/Name path. The name decides
|
|
158
|
+
// and not the category: no two snippets share a name, so a wrong category can only mean that snippet.
|
|
159
|
+
// Only a listed snippet resolves, so a name can never point at a file outside the snippets.
|
|
160
|
+
export function resolveSnippetName(name) {
|
|
161
|
+
const [category, bare, ...rest] = name.split("/");
|
|
162
|
+
const inCategory = bare && rest.length === 0 && CATEGORIES.some((c) => c.toLowerCase() === category.toLowerCase());
|
|
163
|
+
const found = findSnippet(name) ?? (inCategory ? findSnippet(bare) : undefined);
|
|
89
164
|
if (found) return found;
|
|
90
165
|
|
|
91
|
-
const
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
.sort((a, b) => a.score - b.score)
|
|
95
|
-
.slice(0, 3)
|
|
96
|
-
.map((c) => c.path);
|
|
97
|
-
throw new Error(`Unknown snippet "${name}". Closest: ${closest.join(", ")}. Run with a snippet name such as Compression-Audit, or a Category/Name path.`);
|
|
166
|
+
const suggestions = suggestionsFor(name.split("/").pop());
|
|
167
|
+
const answer = suggestions.length > 0 ? `Closest: ${suggestions.join(", ")}.` : "No snippet has a close name.";
|
|
168
|
+
throw new Error(`Unknown snippet "${name}". ${answer} Every name is in the list_snippets tool of the MCP server and in https://webperf-snippets.net/llms.txt.`);
|
|
98
169
|
}
|