@svelte-vitals/core 0.47.0 → 0.47.2
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/dist/index.d.ts +2 -1
- package/dist/index.js +2 -16
- package/dist/internal.d.ts +379 -265
- package/dist/internal.js +978 -1071
- package/dist/json-OdgY5Bpw.d.ts +1287 -0
- package/dist/markdown-CUNuKq3Z.js +9895 -0
- package/package.json +3 -3
- package/dist/chunk-25TAKBFU.js +0 -8057
- package/dist/index-Qrw_f9HP.d.ts +0 -1283
package/dist/internal.js
CHANGED
|
@@ -1,956 +1,1036 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
LANDMARK_ROLES,
|
|
5
|
-
SEVERITY_RANK,
|
|
6
|
-
a11yAccessibleName,
|
|
7
|
-
a11yDeprecatedAria,
|
|
8
|
-
a11yDeprecatedAttr,
|
|
9
|
-
a11yDeprecatedElement,
|
|
10
|
-
a11yDisallowedAriaProps,
|
|
11
|
-
a11yDisallowedElement,
|
|
12
|
-
a11yDoctype,
|
|
13
|
-
a11yDuplicateLandmark,
|
|
14
|
-
a11yIdDuplication,
|
|
15
|
-
a11yInteractiveNesting,
|
|
16
|
-
a11yInvalidAriaValue,
|
|
17
|
-
a11yInvalidRole,
|
|
18
|
-
a11yLabelHasControl,
|
|
19
|
-
a11yNoMissingIdRef,
|
|
20
|
-
a11yPermittedContents,
|
|
21
|
-
a11yPlaceholderLabelOption,
|
|
22
|
-
a11yRequireDatetime,
|
|
23
|
-
a11yRequiredAriaProps,
|
|
24
|
-
a11yRequiredElement,
|
|
25
|
-
a11yTopLevelLandmark,
|
|
26
|
-
a11yUnknownAriaAttribute,
|
|
27
|
-
a11yUnverifiedIdRef,
|
|
28
|
-
a11yUseList,
|
|
29
|
-
allRules,
|
|
30
|
-
applyOverrides,
|
|
31
|
-
applyRuleSeverities,
|
|
32
|
-
architectureComponentSize,
|
|
33
|
-
architectureDirectoryNaming,
|
|
34
|
-
architectureDocLinkTarget,
|
|
35
|
-
architecturePrivateScopeImport,
|
|
36
|
-
architecturePropCount,
|
|
37
|
-
architectureReservedDirectoryNames,
|
|
38
|
-
architectureReservedNamePlacement,
|
|
39
|
-
architectureRouteComponentImport,
|
|
40
|
-
architectureUnitEntryFile,
|
|
41
|
-
attrText,
|
|
42
|
-
attrTextOf,
|
|
43
|
-
attrValue,
|
|
44
|
-
attrValueOf,
|
|
45
|
-
buildJsonReport,
|
|
46
|
-
classify,
|
|
47
|
-
collectNamedImportAliases,
|
|
48
|
-
collectSuppressions,
|
|
49
|
-
collectTopLevelBindings,
|
|
50
|
-
compileOverrides,
|
|
51
|
-
computeHealth,
|
|
52
|
-
computeScore,
|
|
53
|
-
correctnessBasePathNavigation,
|
|
54
|
-
correctnessCheckableBindValue,
|
|
55
|
-
correctnessEachIndexKey,
|
|
56
|
-
correctnessEachKey,
|
|
57
|
-
correctnessEffectAsDerived,
|
|
58
|
-
correctnessEffectAsOnMount,
|
|
59
|
-
correctnessInstanceBrowserGlobal,
|
|
60
|
-
correctnessNonreactiveBuiltinState,
|
|
61
|
-
correctnessOrphanEffect,
|
|
62
|
-
correctnessOrphanLifecycle,
|
|
63
|
-
correctnessPropMutation,
|
|
64
|
-
correctnessServerBrowserGlobal,
|
|
65
|
-
correctnessStalePropDerivation,
|
|
66
|
-
correctnessUnmutatedState,
|
|
67
|
-
decodeFragmentId,
|
|
68
|
-
defaultConfig,
|
|
69
|
-
defaultProject,
|
|
70
|
-
docsUrlFor,
|
|
71
|
-
effectiveSeverity,
|
|
72
|
-
explainRule,
|
|
73
|
-
findAttr,
|
|
74
|
-
foldOccurrences,
|
|
75
|
-
formatFailedRuleWarning,
|
|
76
|
-
formatGithubReport,
|
|
77
|
-
formatJsonReport,
|
|
78
|
-
formatMarkdownReport,
|
|
79
|
-
hasFailureAtOrAbove,
|
|
80
|
-
headTagRule,
|
|
81
|
-
imageRule,
|
|
82
|
-
intOption,
|
|
83
|
-
isMentionedAnywhere,
|
|
84
|
-
isPenalized,
|
|
85
|
-
isTopFragment,
|
|
86
|
-
lineOf,
|
|
87
|
-
linkRule,
|
|
88
|
-
listOption,
|
|
89
|
-
mapOption,
|
|
90
|
-
mdEscape,
|
|
91
|
-
messageText,
|
|
92
|
-
overrideMatches,
|
|
93
|
-
parseComponentFacts,
|
|
94
|
-
parseInMemoryExports,
|
|
95
|
-
parseKitModuleFacts,
|
|
96
|
-
parseModuleProgram,
|
|
97
|
-
parseSvelte,
|
|
98
|
-
performanceFontPreloadCrossorigin,
|
|
99
|
-
performanceHeavyImport,
|
|
100
|
-
performanceImageDimensions,
|
|
101
|
-
performanceImageLoadingHint,
|
|
102
|
-
performanceLcpImage,
|
|
103
|
-
performanceLoadWaterfall,
|
|
104
|
-
performanceMinifyDisabled,
|
|
105
|
-
performanceNamespaceImport,
|
|
106
|
-
performancePreconnect,
|
|
107
|
-
performancePreloadMissingAs,
|
|
108
|
-
performanceRenderBlockingScript,
|
|
109
|
-
performanceResponsiveImage,
|
|
110
|
-
performanceSequentialAwaits,
|
|
111
|
-
performanceStateRaw,
|
|
112
|
-
resolveRepoLocalPath,
|
|
113
|
-
resolveRuleOptions,
|
|
114
|
-
resolveRunesModuleSpecifier,
|
|
115
|
-
ruleMetaById,
|
|
116
|
-
scoresByCategory,
|
|
117
|
-
securityHandlerStateWrite,
|
|
118
|
-
securityJavascriptUrl,
|
|
119
|
-
securityRawHtml,
|
|
120
|
-
securityServerModuleState,
|
|
121
|
-
securitySharedStateImport,
|
|
122
|
-
selectRules,
|
|
123
|
-
seoCanonicalUrl,
|
|
124
|
-
seoCharset,
|
|
125
|
-
seoDescriptionLength,
|
|
126
|
-
seoDescriptionPresence,
|
|
127
|
-
seoDuplicateDescription,
|
|
128
|
-
seoDuplicateTitle,
|
|
129
|
-
seoHeadingLevelSkip,
|
|
130
|
-
seoHreflang,
|
|
131
|
-
seoHtmlLang,
|
|
132
|
-
seoImageAlt,
|
|
133
|
-
seoIndexability,
|
|
134
|
-
seoJsonLd,
|
|
135
|
-
seoJsonLdDateFormat,
|
|
136
|
-
seoJsonLdDeprecatedType,
|
|
137
|
-
seoJsonLdPlaceholder,
|
|
138
|
-
seoJsonLdRelativeUrl,
|
|
139
|
-
seoJsonLdRequiredProps,
|
|
140
|
-
seoJsonLdValidity,
|
|
141
|
-
seoOgDescription,
|
|
142
|
-
seoOgImage,
|
|
143
|
-
seoOgTitle,
|
|
144
|
-
seoOgUrl,
|
|
145
|
-
seoRobotsTxt,
|
|
146
|
-
seoSingleH1,
|
|
147
|
-
seoSitemapInRobots,
|
|
148
|
-
seoSitemapXml,
|
|
149
|
-
seoSsrDisabled,
|
|
150
|
-
seoTitleLength,
|
|
151
|
-
seoTitlePresence,
|
|
152
|
-
seoTwitterCard,
|
|
153
|
-
seoViewport,
|
|
154
|
-
settingOptions,
|
|
155
|
-
settingSeverity,
|
|
156
|
-
severityToSarifLevel,
|
|
157
|
-
shouldSkipRangeCheck,
|
|
158
|
-
splitTokens,
|
|
159
|
-
stripTextDirective,
|
|
160
|
-
summarize,
|
|
161
|
-
terminalSafe,
|
|
162
|
-
textFromNodes,
|
|
163
|
-
unwrapTs,
|
|
164
|
-
validateRuleOptions,
|
|
165
|
-
validateRuleSetting,
|
|
166
|
-
valueFromNodes,
|
|
167
|
-
withFailedRulesOff
|
|
168
|
-
} from "./chunk-25TAKBFU.js";
|
|
169
|
-
|
|
170
|
-
// src/inline-directives.ts
|
|
1
|
+
import { $ as parseInMemoryExports, $n as hasFailureAtOrAbove, $t as seoCharset, A as a11yPlaceholderLabelOption, An as settingSeverity, At as securityJavascriptUrl, B as performanceSequentialAwaits, Bn as imageRule, Bt as correctnessPropMutation, C as a11yPermittedContents, Cn as applyOverrides, Ct as textFromNodes, D as a11yDeprecatedAttr, Dn as overrideMatches, Dt as securitySharedStateImport, E as a11yDisallowedAriaProps, En as formatFailedRuleWarning, Et as architectureComponentSize, F as a11yInvalidAriaValue, Fn as performancePreloadMissingAs, Ft as correctnessOrphanLifecycle, G as architectureDocLinkTarget, Gn as seoOgTitle, Gt as correctnessEachKey, H as performanceMinifyDisabled, Hn as seoSitemapXml, Ht as correctnessEffectAsOnMount, I as a11yRequiredAriaProps, In as linkRule, It as correctnessOrphanEffect, J as architectureReservedDirectoryNames, Jn as seoDescriptionPresence, Jt as seoDuplicateDescription, K as architectureRouteComponentImport, Kn as seoOgImage, Kt as seoSsrDisabled, L as a11yUnknownAriaAttribute, Ln as performanceResponsiveImage, Lt as correctnessCheckableBindValue, M as a11yLabelHasControl, Mn as performanceRenderBlockingScript, Mt as correctnessInstanceBrowserGlobal, N as a11yAccessibleName, Nn as performanceLcpImage, Nt as correctnessServerBrowserGlobal, O as a11yDeprecatedElement, On as selectRules, Ot as securityServerModuleState, P as a11yInteractiveNesting, Pn as performanceFontPreloadCrossorigin, Pt as correctnessBasePathNavigation, Q as collectTopLevelBindings, Qn as effectiveSeverity, Qt as seoImageAlt, R as a11yInvalidRole, Rn as performanceImageLoadingHint, Rt as correctnessNonreactiveBuiltinState, S as a11yRequiredElement, Sn as validateRuleSetting, St as parseSvelte, T as a11yDeprecatedAria, Tn as compileOverrides, Tt as architecturePropCount, U as performanceNamespaceImport, Un as seoRobotsTxt, Ut as correctnessEffectAsDerived, V as performanceLoadWaterfall, Vn as seoHtmlLang, Vt as correctnessUnmutatedState, W as performanceHeavyImport, Wn as seoJsonLd, Wt as correctnessEachIndexKey, X as architectureUnitEntryFile, Xn as seoTitlePresence, Xt as seoSingleH1, Y as architectureDirectoryNaming, Yn as headTagRule, Yt as seoDuplicateTitle, Z as architecturePrivateScopeImport, Zn as classify, Zt as seoHreflang, _ as a11yNoMissingIdRef, _n as listOption, _t as attrTextOf, a as formatJsonReport, an as seoJsonLdRelativeUrl, ar as defaultProject, at as parseComponentFacts, b as a11yDuplicateLandmark, bn as shouldSkipRangeCheck, bt as findAttr, c as scoresByCategory, cn as seoSitemapInRobots, ct as IDREF_ATTRS, d as messageText, dn as seoOgDescription, dt as foldOccurrences, en as seoDescriptionLength, er as summarize, et as parseKitModuleFacts, f as ruleMetaById, fn as seoTwitterCard, ft as isTopFragment, g as a11yUnverifiedIdRef, gn as isMentionedAnywhere, gt as attrText, h as explainRule, hn as intOption, ht as CHILD_NODE_KEYS, i as buildJsonReport, in as seoJsonLdDateFormat, ir as defaultConfig, it as collectSuppressions, j as a11yUseList, jn as withFailedRulesOff, jt as securityRawHtml, k as a11yRequireDatetime, kn as settingOptions, kt as securityHandlerStateWrite, l as formatGithubReport, ln as seoViewport, lt as LANDMARK_ROLES, m as allRules, mn as performancePreconnect, mt as stripTextDirective, n as mdEscape, nn as seoJsonLdRequiredProps, nr as isPenalized, nt as resolveRunesModuleSpecifier, o as computeHealth, on as seoJsonLdDeprecatedType, ot as parseModuleProgram, p as severityToSarifLevel, pn as seoIndexability, pt as splitTokens, q as architectureReservedNamePlacement, qn as seoCanonicalUrl, qt as seoHeadingLevelSkip, r as terminalSafe, rn as seoJsonLdPlaceholder, rt as collectNamedImportAliases, s as computeScore, sn as seoJsonLdValidity, st as unwrapTs, t as formatMarkdownReport, tn as seoTitleLength, tr as docsUrlFor, tt as resolveRepoLocalPath, u as SEVERITY_RANK, un as seoOgUrl, ut as decodeFragmentId, v as a11yIdDuplication, vn as mapOption, vt as attrValue, w as a11yDisallowedElement, wn as applyRuleSeverities, wt as valueFromNodes, x as a11yDoctype, xn as validateRuleOptions, xt as lineOf, y as a11yTopLevelLandmark, yn as resolveRuleOptions, yt as attrValueOf, z as performanceStateRaw, zn as performanceImageDimensions, zt as correctnessStalePropDerivation } from "./markdown-CUNuKq3Z.js";
|
|
2
|
+
//#region src/inline-directives.ts
|
|
3
|
+
/** The directive silencing `r`, if one sits on its line and names its rule (or names nothing). */
|
|
171
4
|
function directiveFor(index, r) {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
5
|
+
const line = r.line;
|
|
6
|
+
if (r.location === void 0 || line === void 0 || line <= 0) return void 0;
|
|
7
|
+
return (index.get(r.location) ?? []).find((d) => d.line === line && (!d.ruleIds || d.ruleIds.includes(r.id)));
|
|
175
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* Apply inline `svelte-vitals-disable-next-line` directives to a finished result set — one
|
|
11
|
+
* mechanism, over `Result`s, so any rule that emits a line-anchored finding is covered by
|
|
12
|
+
* construction rather than by per-family wiring (design 2026-08-17).
|
|
13
|
+
*
|
|
14
|
+
* Runs on the output of `applyOverrides(applyRuleSeverities(...))`, so a directive silences
|
|
15
|
+
* whatever survived config, and every consumer downstream — scoring, `--fail-on`, the suppressions
|
|
16
|
+
* file, the reporters — sees the same suppressed set. `fileRule` keeps its own earlier filter and
|
|
17
|
+
* is unaffected: a result it already suppressed never arrives here, and its PASS is not penalized.
|
|
18
|
+
*
|
|
19
|
+
* A rule+route whose every penalized result was suppressed gains one PASS, located at the first
|
|
20
|
+
* suppressed result in emission order — the anchor the rules themselves use. Its message is the
|
|
21
|
+
* rule's `passLabel` when it declares one, else its `title`.
|
|
22
|
+
*/
|
|
176
23
|
function applyInlineDirectives(results, index, rules, config) {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
24
|
+
const labels = new Map(rules.map((r) => [r.id, r.passLabel ?? r.title]));
|
|
25
|
+
const kept = [];
|
|
26
|
+
/** rule+route → the first suppressed result, for the PASS this pair may need. */
|
|
27
|
+
const silenced = /* @__PURE__ */ new Map();
|
|
28
|
+
const survived = /* @__PURE__ */ new Set();
|
|
29
|
+
for (const r of results) {
|
|
30
|
+
if (!isPenalized(r.detection, config.treatDynamicAs)) {
|
|
31
|
+
kept.push(r);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const key = `${r.id} ${r.route ?? ""}`;
|
|
35
|
+
if (directiveFor(index, r) !== void 0) {
|
|
36
|
+
if (!silenced.has(key)) silenced.set(key, r);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
survived.add(key);
|
|
40
|
+
kept.push(r);
|
|
41
|
+
}
|
|
42
|
+
for (const [key, r] of silenced) {
|
|
43
|
+
if (survived.has(key)) continue;
|
|
44
|
+
kept.push({
|
|
45
|
+
id: r.id,
|
|
46
|
+
...r.category ? { category: r.category } : {},
|
|
47
|
+
severity: r.severity,
|
|
48
|
+
detection: {
|
|
49
|
+
presence: "own",
|
|
50
|
+
value: "static"
|
|
51
|
+
},
|
|
52
|
+
...r.route !== void 0 ? { route: r.route } : {},
|
|
53
|
+
...r.location !== void 0 ? { location: r.location } : {},
|
|
54
|
+
message: labels.get(r.id) ?? r.id
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return kept;
|
|
207
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Directives naming a rule id that no rule declares. Unlike a directive that matches no finding —
|
|
61
|
+
* legitimate whenever the code was fixed, the rule turned off, or the run scoped — a misspelled id
|
|
62
|
+
* can never be right, so it is reported rather than silently suppressing nothing.
|
|
63
|
+
*
|
|
64
|
+
* Compared against every registered rule, not the selected ones: disabling a rule in config must
|
|
65
|
+
* not turn its directives into errors.
|
|
66
|
+
*/
|
|
208
67
|
function unknownDirectiveIds(index, rules) {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
for (const id of d.ruleIds ?? []) {
|
|
214
|
-
if (!known.has(id)) seen.add(`${file}:${d.line - 1} disables unknown rule "${id}"`);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
return [...seen].sort();
|
|
68
|
+
const known = new Set(rules.map((r) => r.id));
|
|
69
|
+
const seen = /* @__PURE__ */ new Set();
|
|
70
|
+
for (const [file, directives] of index) for (const d of directives) for (const id of d.ruleIds ?? []) if (!known.has(id)) seen.add(`${file}:${d.line - 1} disables unknown rule "${id}" — run \`svelte-vitals explain --list\` for the ids.`);
|
|
71
|
+
return [...seen].sort();
|
|
219
72
|
}
|
|
220
|
-
|
|
221
|
-
|
|
73
|
+
//#endregion
|
|
74
|
+
//#region src/component.ts
|
|
75
|
+
/**
|
|
76
|
+
* Warnings for files a collector could not read or parse: the file contributes empty facts, so any
|
|
77
|
+
* findings it would have produced are simply missing rather than reported as fixed. The two causes
|
|
78
|
+
* are reported separately — an unreadable file is an environment problem (permissions, a descriptor
|
|
79
|
+
* limit) and a malformed one is the author's, and sharing a message is how a descriptor limit once
|
|
80
|
+
* read as hundreds of broken components. Capped at 10 inline paths so one badly-broken directory
|
|
81
|
+
* cannot flood the terminal.
|
|
82
|
+
*/
|
|
222
83
|
function skippedFileWarnings(facts) {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
"this is an environment problem, not a code one \u2014 check file permissions and the open-file limit (`ulimit -n`)."
|
|
235
|
-
);
|
|
236
|
-
}
|
|
237
|
-
if (unparsed.length > 0) {
|
|
238
|
-
out.push(
|
|
239
|
-
`skipped ${unparsed.length} file(s) that could not be parsed: ${list(unparsed)}`,
|
|
240
|
-
"findings for these files are unavailable until they parse."
|
|
241
|
-
);
|
|
242
|
-
}
|
|
243
|
-
return out;
|
|
84
|
+
const list = (files) => {
|
|
85
|
+
const shown = files.slice(0, 10);
|
|
86
|
+
return files.length > shown.length ? `${shown.join(", ")}, … and ${files.length - shown.length} more` : shown.join(", ");
|
|
87
|
+
};
|
|
88
|
+
const names = (pick) => [...new Set(facts.filter(pick).map((f) => f.file))].sort();
|
|
89
|
+
const unread = names((f) => f.readFailed === true);
|
|
90
|
+
const unparsed = names((f) => f.parseFailed === true && f.readFailed !== true);
|
|
91
|
+
const out = [];
|
|
92
|
+
if (unread.length > 0) out.push(`skipped ${unread.length} file(s) that could not be read: ${list(unread)}`, "this is an environment problem, not a code one — check file permissions and the open-file limit (`ulimit -n`).");
|
|
93
|
+
if (unparsed.length > 0) out.push(`skipped ${unparsed.length} file(s) that could not be parsed: ${list(unparsed)}`, "findings for these files are unavailable until they parse.");
|
|
94
|
+
return out;
|
|
244
95
|
}
|
|
245
|
-
|
|
246
|
-
|
|
96
|
+
//#endregion
|
|
97
|
+
//#region src/component-collect.ts
|
|
98
|
+
/**
|
|
99
|
+
* Fallback facts for a file that fails to read or parse (dev tooling must never
|
|
100
|
+
* throw). This is the single source of truth for the empty-facts shape — add new
|
|
101
|
+
* `ComponentFacts` fields HERE so TypeScript catches every call site that still
|
|
102
|
+
* needs updating.
|
|
103
|
+
*/
|
|
247
104
|
function emptyComponentFacts(file) {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
105
|
+
return {
|
|
106
|
+
file,
|
|
107
|
+
eachBlocks: [],
|
|
108
|
+
effects: [],
|
|
109
|
+
htmlTags: [],
|
|
110
|
+
javascriptUrls: [],
|
|
111
|
+
loc: 0,
|
|
112
|
+
propCount: 0,
|
|
113
|
+
imports: [],
|
|
114
|
+
importSpans: [],
|
|
115
|
+
namespaceImports: [],
|
|
116
|
+
constableStates: [],
|
|
117
|
+
mutatedProps: [],
|
|
118
|
+
stalePropDerivations: [],
|
|
119
|
+
rawableStates: [],
|
|
120
|
+
nonreactiveBuiltinStates: [],
|
|
121
|
+
checkableBindValues: [],
|
|
122
|
+
basePathLinks: [],
|
|
123
|
+
orphanEffects: [],
|
|
124
|
+
orphanLifecycleCalls: [],
|
|
125
|
+
browserGlobalRefs: [],
|
|
126
|
+
moduleStateDecls: [],
|
|
127
|
+
suppressions: [],
|
|
128
|
+
commentLinks: []
|
|
129
|
+
};
|
|
273
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* Scan every `.svelte` component and `.svelte.ts`/`.svelte.js` runes module under `src/`
|
|
133
|
+
* for Correctness/Security/Architecture/Bundle-Performance/Accessibility facts. Independent
|
|
134
|
+
* of route resolution — covers `$lib` and non-route components too. A file that fails to
|
|
135
|
+
* read or parse contributes empty facts instead of aborting the whole scan (dev tooling
|
|
136
|
+
* must never throw).
|
|
137
|
+
*/
|
|
274
138
|
async function collectComponentFacts(rt, cwd) {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
139
|
+
const files = await rt.glob("src/**/*.svelte{,.ts,.js}", cwd);
|
|
140
|
+
return Promise.all(files.sort().map(async (rel) => {
|
|
141
|
+
let source;
|
|
142
|
+
try {
|
|
143
|
+
source = await rt.readFile(rt.join(cwd, rel));
|
|
144
|
+
} catch {
|
|
145
|
+
return {
|
|
146
|
+
...emptyComponentFacts(rel),
|
|
147
|
+
parseFailed: true,
|
|
148
|
+
readFailed: true
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
return {
|
|
153
|
+
file: rel,
|
|
154
|
+
...parseComponentFacts(source, rel)
|
|
155
|
+
};
|
|
156
|
+
} catch {
|
|
157
|
+
return {
|
|
158
|
+
...emptyComponentFacts(rel),
|
|
159
|
+
parseFailed: true
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
}));
|
|
291
163
|
}
|
|
292
|
-
|
|
293
|
-
|
|
164
|
+
//#endregion
|
|
165
|
+
//#region src/source-files.ts
|
|
166
|
+
/**
|
|
167
|
+
* Every file under `src/`, as project-relative paths, sorted. Paths only — nothing is
|
|
168
|
+
* read, so this is the cheaper of the two passes over `src/` (the component collector
|
|
169
|
+
* already walks the same tree and reads every `.svelte`).
|
|
170
|
+
*
|
|
171
|
+
* Directory-shaped rules derive their directory set from these paths' ancestor prefixes
|
|
172
|
+
* rather than globbing a second time; see `architecture/unit-entry-file`. The list is
|
|
173
|
+
* sorted so anything that picks "the first file under a directory" is deterministic.
|
|
174
|
+
*
|
|
175
|
+
* Two properties of the result the directory-shaped rules depend on: a directory containing no file
|
|
176
|
+
* at any depth does not appear among these paths' ancestor prefixes and so does not exist as far as
|
|
177
|
+
* those rules are concerned, and dot directories never appear at all (see `Runtime.glob`).
|
|
178
|
+
*/
|
|
294
179
|
async function collectSourceFiles(rt, cwd) {
|
|
295
|
-
|
|
296
|
-
return files.slice().sort();
|
|
180
|
+
return (await rt.glob("src/**/*", cwd)).slice().sort();
|
|
297
181
|
}
|
|
298
|
-
|
|
299
|
-
|
|
182
|
+
//#endregion
|
|
183
|
+
//#region src/kit-module-collect.ts
|
|
184
|
+
/** Fallback facts for a Kit file that fails to read or parse (dev tooling must never throw). */
|
|
300
185
|
function emptyKitModuleFacts(file, kind) {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
186
|
+
return {
|
|
187
|
+
file,
|
|
188
|
+
kind,
|
|
189
|
+
moduleStateReassignments: [],
|
|
190
|
+
importedStateWrites: [],
|
|
191
|
+
importedStateWritesOutsideHandlers: [],
|
|
192
|
+
pendingServerStoreWrites: [],
|
|
193
|
+
runesModuleImports: [],
|
|
194
|
+
lifecycleCalls: [],
|
|
195
|
+
browserGlobalRefs: [],
|
|
196
|
+
basePathLinks: [],
|
|
197
|
+
suppressions: []
|
|
198
|
+
};
|
|
314
199
|
}
|
|
200
|
+
/** 'server' when the file only ever runs on the server; 'universal' for +page.ts/+layout.ts. */
|
|
315
201
|
function kindOf(file) {
|
|
316
|
-
|
|
317
|
-
|
|
202
|
+
const base = file.split("/").pop() ?? file;
|
|
203
|
+
return base.includes(".server.") || base.startsWith("+server.") ? "server" : "universal";
|
|
318
204
|
}
|
|
205
|
+
/**
|
|
206
|
+
* Scan SvelteKit route/hooks files for SSR shared-state facts (the security kit-module rules): route
|
|
207
|
+
* `+page`/`+layout` server and universal modules, `+server` endpoints, and
|
|
208
|
+
* `src/hooks.server`. `src/lib/server/**` is deliberately NOT scanned — legitimate
|
|
209
|
+
* module singletons (DB connections, clients) live there (design). A file that
|
|
210
|
+
* fails to read or parse contributes empty facts instead of aborting the scan.
|
|
211
|
+
*
|
|
212
|
+
* `aliases` is the project's compiled alias list (`Project.kitAliases`); omitted,
|
|
213
|
+
* specifiers resolve through `$lib` → `src/lib` only.
|
|
214
|
+
*/
|
|
319
215
|
async function collectKitModuleFacts(rt, cwd, aliases) {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
216
|
+
const lists = await Promise.all([
|
|
217
|
+
"src/routes/**/+{page,layout}.server.{ts,js}",
|
|
218
|
+
"src/routes/**/+{page,layout}.{ts,js}",
|
|
219
|
+
"src/routes/**/+server.{ts,js}",
|
|
220
|
+
"src/hooks.server.{ts,js}"
|
|
221
|
+
].map((p) => rt.glob(p, cwd)));
|
|
222
|
+
const files = [...new Set(lists.flat())];
|
|
223
|
+
return arbitrateServerStoreWrites(rt, cwd, await Promise.all(files.sort().map(async (rel) => {
|
|
224
|
+
const kind = kindOf(rel);
|
|
225
|
+
let source;
|
|
226
|
+
try {
|
|
227
|
+
source = await rt.readFile(rt.join(cwd, rel));
|
|
228
|
+
} catch {
|
|
229
|
+
return {
|
|
230
|
+
...emptyKitModuleFacts(rel, kind),
|
|
231
|
+
parseFailed: true,
|
|
232
|
+
readFailed: true
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
try {
|
|
236
|
+
return {
|
|
237
|
+
file: rel,
|
|
238
|
+
kind,
|
|
239
|
+
...parseKitModuleFacts(source, rel, aliases)
|
|
240
|
+
};
|
|
241
|
+
} catch {
|
|
242
|
+
return {
|
|
243
|
+
...emptyKitModuleFacts(rel, kind),
|
|
244
|
+
parseFailed: true
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
})));
|
|
345
248
|
}
|
|
249
|
+
/**
|
|
250
|
+
* The files a resolved specifier may name, in resolution order. An extensionless path takes an
|
|
251
|
+
* extension or an index file; a path that already ends in `.js` is checked as written and then
|
|
252
|
+
* remapped to `.ts`, which is how a NodeNext/ESM TypeScript project spells an import of its own
|
|
253
|
+
* `.ts` source (`from '$lib/server/store.js'` -> `src/lib/server/store.ts`).
|
|
254
|
+
*/
|
|
346
255
|
function moduleCandidates(repoPath) {
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
256
|
+
if (repoPath.endsWith(".js")) return [repoPath, `${repoPath.slice(0, -3)}.ts`];
|
|
257
|
+
if (repoPath.endsWith(".ts")) return [repoPath];
|
|
258
|
+
return [
|
|
259
|
+
`${repoPath}.ts`,
|
|
260
|
+
`${repoPath}.js`,
|
|
261
|
+
`${repoPath}/index.ts`,
|
|
262
|
+
`${repoPath}/index.js`
|
|
263
|
+
];
|
|
350
264
|
}
|
|
265
|
+
/**
|
|
266
|
+
* Read the module a pending write targets and return the names it exports as an in-memory
|
|
267
|
+
* container. Returns an empty set when the file cannot be found or read: unresolvable means
|
|
268
|
+
* unarbitrated, which leaves the write exempt.
|
|
269
|
+
*/
|
|
351
270
|
async function inMemoryExportsOf(rt, cwd, repoPath) {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
}
|
|
360
|
-
return /* @__PURE__ */ new Set();
|
|
271
|
+
for (const rel of moduleCandidates(repoPath)) try {
|
|
272
|
+
if (!await rt.exists(rt.join(cwd, rel))) continue;
|
|
273
|
+
return parseInMemoryExports(await rt.readFile(rt.join(cwd, rel)), rel);
|
|
274
|
+
} catch {
|
|
275
|
+
return /* @__PURE__ */ new Set();
|
|
276
|
+
}
|
|
277
|
+
return /* @__PURE__ */ new Set();
|
|
361
278
|
}
|
|
279
|
+
/**
|
|
280
|
+
* Decide the `.set()`/`.update()` calls the parse could not: a call on a binding exported from
|
|
281
|
+
* under the `$lib` server root is shared module state when that binding is an in-memory
|
|
282
|
+
* container, and persistence when it is anything else. Only the modules actually targeted are
|
|
283
|
+
* read, so a project whose handlers never write to `$lib/server` pays nothing.
|
|
284
|
+
*/
|
|
362
285
|
async function arbitrateServerStoreWrites(rt, cwd, facts) {
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
286
|
+
const targets = [...new Set(facts.flatMap((f) => f.pendingServerStoreWrites.map((w) => w.resolved)))];
|
|
287
|
+
if (targets.length === 0) return facts;
|
|
288
|
+
const byPath = new Map(await Promise.all(targets.map(async (t) => [t, await inMemoryExportsOf(rt, cwd, t)])));
|
|
289
|
+
return facts.map((f) => {
|
|
290
|
+
const promoted = f.pendingServerStoreWrites.filter((w) => byPath.get(w.resolved)?.has(w.imported)).map((w) => ({
|
|
291
|
+
name: w.name,
|
|
292
|
+
line: w.line,
|
|
293
|
+
via: "set-call"
|
|
294
|
+
}));
|
|
295
|
+
if (promoted.length === 0) return f;
|
|
296
|
+
return {
|
|
297
|
+
...f,
|
|
298
|
+
importedStateWrites: [...f.importedStateWrites, ...promoted].sort((a, b) => a.line - b.line)
|
|
299
|
+
};
|
|
300
|
+
});
|
|
376
301
|
}
|
|
377
|
-
|
|
378
|
-
|
|
302
|
+
//#endregion
|
|
303
|
+
//#region src/config-object.ts
|
|
304
|
+
/**
|
|
305
|
+
* Non-computed property of an object literal, by key name (`build` or `'build'`).
|
|
306
|
+
* Returns the LAST matching property, honoring JavaScript object semantics where
|
|
307
|
+
* duplicate keys are resolved to the rightmost value — UNLESS a `SpreadElement`
|
|
308
|
+
* appears after that match in the same object literal, in which case the spread
|
|
309
|
+
* could re-introduce or overwrite the key at runtime and the effective value is
|
|
310
|
+
* unknowable, so this conservatively returns undefined. A spread BEFORE the
|
|
311
|
+
* match doesn't matter — the literal property still wins.
|
|
312
|
+
*/
|
|
379
313
|
function propOf(obj, name) {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
314
|
+
let found;
|
|
315
|
+
for (const p of obj.properties) {
|
|
316
|
+
if (p.type === "SpreadElement") {
|
|
317
|
+
if (found) found = void 0;
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (p.type !== "Property" || p.computed) continue;
|
|
321
|
+
if (p.key.type === "Identifier" && p.key.name === name) found = p;
|
|
322
|
+
else if (p.key.type === "Literal" && p.key.value === name) found = p;
|
|
323
|
+
}
|
|
324
|
+
return found;
|
|
391
325
|
}
|
|
326
|
+
/**
|
|
327
|
+
* Unwrap an expression to an object literal, resolving up to 4 steps of: TS
|
|
328
|
+
* wrappers (`satisfies`/`as`), an `Identifier` through same-file top-level
|
|
329
|
+
* `bindings`, and a `CallExpression`'s first argument (so both
|
|
330
|
+
* `defineConfig({…})` and `defineConfig(config)` — a same-file identifier
|
|
331
|
+
* argument — resolve, and so does either nested inside the other). Stops as
|
|
332
|
+
* soon as an `ObjectExpression` is reached, or when a step can't make further
|
|
333
|
+
* progress (an unresolvable identifier, or anything else that isn't a wrapper,
|
|
334
|
+
* identifier, or call).
|
|
335
|
+
*/
|
|
392
336
|
function unwrapToObjectExpression(expr, bindings) {
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
337
|
+
let current = expr;
|
|
338
|
+
for (let i = 0; i < 4 && current; i++) {
|
|
339
|
+
const e = unwrapTs(current);
|
|
340
|
+
if (e.type === "ObjectExpression") return e;
|
|
341
|
+
if (e.type === "Identifier") {
|
|
342
|
+
current = bindings.get(e.name);
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
if (e.type === "CallExpression") {
|
|
346
|
+
current = e.arguments[0];
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
const final = current ? unwrapTs(current) : void 0;
|
|
352
|
+
return final?.type === "ObjectExpression" ? final : void 0;
|
|
409
353
|
}
|
|
354
|
+
/**
|
|
355
|
+
* The exported config expression's raw (un-unwrapped) node: an ESM
|
|
356
|
+
* `export default …` declaration, or — when no default export exists — the
|
|
357
|
+
* RHS of the LAST top-level CJS `module.exports = …` assignment (mirrors
|
|
358
|
+
* JavaScript's last-assignment-wins semantics, same rationale as `propOf`'s
|
|
359
|
+
* last-wins). Undefined when neither form is present.
|
|
360
|
+
*/
|
|
410
361
|
function findExportedExpression(program) {
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
cjsExported = expr.right;
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
return cjsExported;
|
|
362
|
+
let exported;
|
|
363
|
+
for (const stmt of program.body) if (stmt.type === "ExportDefaultDeclaration") exported = stmt.declaration;
|
|
364
|
+
if (exported) return exported;
|
|
365
|
+
let cjsExported;
|
|
366
|
+
for (const stmt of program.body) {
|
|
367
|
+
if (stmt.type !== "ExpressionStatement") continue;
|
|
368
|
+
const expr = stmt.expression;
|
|
369
|
+
if (expr.type !== "AssignmentExpression" || expr.operator !== "=") continue;
|
|
370
|
+
const left = expr.left;
|
|
371
|
+
if (left.type === "MemberExpression" && !left.computed && left.object.type === "Identifier" && left.object.name === "module" && left.property.type === "Identifier" && left.property.name === "exports") cjsExported = expr.right;
|
|
372
|
+
}
|
|
373
|
+
return cjsExported;
|
|
427
374
|
}
|
|
375
|
+
/**
|
|
376
|
+
* Resolve the exported config expression to an object literal: ESM
|
|
377
|
+
* `export default {…}`, `export default defineConfig({…})` (any call's first
|
|
378
|
+
* argument — the callee name is not verified), a same-file alias
|
|
379
|
+
* (`const config = {…}; export default config`) including one passed as
|
|
380
|
+
* `defineConfig`'s argument, or CJS `module.exports = {…}` in the same forms —
|
|
381
|
+
* with `satisfies`/`as` unwrapped at every step. Function-form configs and
|
|
382
|
+
* anything else resolve to undefined — the CLI channel is deliberately
|
|
383
|
+
* literal-only; the Vite plugin channel sees the resolved value instead.
|
|
384
|
+
*/
|
|
428
385
|
function resolveConfigObject(program) {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
386
|
+
const exported = findExportedExpression(program);
|
|
387
|
+
if (!exported) return void 0;
|
|
388
|
+
return unwrapToObjectExpression(exported, collectTopLevelBindings(program));
|
|
432
389
|
}
|
|
433
|
-
|
|
434
|
-
|
|
390
|
+
//#endregion
|
|
391
|
+
//#region src/vite-config-parse.ts
|
|
392
|
+
/**
|
|
393
|
+
* The `build: { minify: false }` override, when present as a literal: returns the
|
|
394
|
+
* `minify` property's 1-based line in the ORIGINAL source. Undefined for clean,
|
|
395
|
+
* dynamic, or unparsable configs (never throws).
|
|
396
|
+
*/
|
|
435
397
|
function findMinifyDisabled(source) {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
398
|
+
let program;
|
|
399
|
+
let wrapped;
|
|
400
|
+
try {
|
|
401
|
+
({program, wrapped} = parseModuleProgram(source, "vite.config.ts"));
|
|
402
|
+
} catch {
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
if (!program) return void 0;
|
|
406
|
+
const config = resolveConfigObject(program);
|
|
407
|
+
if (!config) return void 0;
|
|
408
|
+
const build = propOf(config, "build");
|
|
409
|
+
const buildValue = build ? unwrapTs(build.value) : void 0;
|
|
410
|
+
if (buildValue?.type !== "ObjectExpression") return void 0;
|
|
411
|
+
const minify = propOf(buildValue, "minify");
|
|
412
|
+
const minifyValue = minify ? unwrapTs(minify.value) : void 0;
|
|
413
|
+
if (!minify || minifyValue?.type !== "Literal" || minifyValue.value !== false) return void 0;
|
|
414
|
+
return { line: Math.max(0, lineOf(wrapped, minify.start) - 1) };
|
|
453
415
|
}
|
|
454
|
-
|
|
455
|
-
|
|
416
|
+
//#endregion
|
|
417
|
+
//#region src/svelte-config-parse.ts
|
|
418
|
+
/**
|
|
419
|
+
* `paths.base` off a resolved Kit-config object: `{ value }` for a non-empty string literal,
|
|
420
|
+
* `{}` for any other expression (base exists, value unknowable — the `dev ? '' : '/repo'`
|
|
421
|
+
* deploy form), and undefined when absent or an explicit empty string.
|
|
422
|
+
*/
|
|
456
423
|
function basePathOf(kitConfig, bindings) {
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
}
|
|
466
|
-
return {};
|
|
424
|
+
const paths = propOf(kitConfig, "paths");
|
|
425
|
+
const pathsObj = paths ? unwrapToObjectExpression(paths.value, bindings) : void 0;
|
|
426
|
+
if (!pathsObj) return void 0;
|
|
427
|
+
const base = propOf(pathsObj, "base");
|
|
428
|
+
if (!base) return void 0;
|
|
429
|
+
const value = unwrapTs(base.value);
|
|
430
|
+
if (value.type === "Literal") return typeof value.value === "string" && value.value !== "" ? { value: value.value } : void 0;
|
|
431
|
+
return {};
|
|
467
432
|
}
|
|
433
|
+
/** A property's key when it is a plain (non-computed) string or identifier key. */
|
|
468
434
|
function keyNameOf(p) {
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
return void 0;
|
|
435
|
+
if (p.computed) return void 0;
|
|
436
|
+
if (p.key.type === "Identifier") return p.key.name;
|
|
437
|
+
if (p.key.type === "Literal" && typeof p.key.value === "string") return p.key.value;
|
|
473
438
|
}
|
|
439
|
+
/** A property's value when it is a string literal, else undefined. */
|
|
474
440
|
function stringValueOf(p) {
|
|
475
|
-
|
|
476
|
-
|
|
441
|
+
const v = unwrapTs(p.value);
|
|
442
|
+
return v.type === "Literal" && typeof v.value === "string" ? v.value : void 0;
|
|
477
443
|
}
|
|
444
|
+
/**
|
|
445
|
+
* `kit.alias`'s entries in source order. Undefined when the key set cannot be known (see
|
|
446
|
+
* `RawKitAliases.entries`); `[]` when `propOf` finds no `alias` property — which also covers the
|
|
447
|
+
* case where a spread in the `kit` object could have supplied one, matching how every other
|
|
448
|
+
* fact in this file treats that unknowability rather than inventing a stricter rule for alias
|
|
449
|
+
* alone. Duplicate literal keys collapse the way `Object.entries` does — FIRST position, LAST
|
|
450
|
+
* value — because that is the object Kit iterates.
|
|
451
|
+
*/
|
|
478
452
|
function aliasEntriesOf(kitConfig, bindings) {
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
453
|
+
const alias = propOf(kitConfig, "alias");
|
|
454
|
+
if (!alias) return [];
|
|
455
|
+
const obj = unwrapToObjectExpression(alias.value, bindings);
|
|
456
|
+
if (!obj) return void 0;
|
|
457
|
+
const out = [];
|
|
458
|
+
const at = /* @__PURE__ */ new Map();
|
|
459
|
+
for (const p of obj.properties) {
|
|
460
|
+
if (p.type !== "Property") return void 0;
|
|
461
|
+
const key = keyNameOf(p);
|
|
462
|
+
if (key === void 0) return void 0;
|
|
463
|
+
const entry = {
|
|
464
|
+
key,
|
|
465
|
+
value: stringValueOf(p) ?? null
|
|
466
|
+
};
|
|
467
|
+
const seen = at.get(key);
|
|
468
|
+
if (seen === void 0) {
|
|
469
|
+
at.set(key, out.length);
|
|
470
|
+
out.push(entry);
|
|
471
|
+
} else out[seen] = entry;
|
|
472
|
+
}
|
|
473
|
+
return out;
|
|
497
474
|
}
|
|
475
|
+
/**
|
|
476
|
+
* `kit.files.lib` in the three states documented on `RawKitAliases.filesLib`: `undefined` when
|
|
477
|
+
* there is no `lib` property to read (including when `files` itself doesn't resolve to an object
|
|
478
|
+
* literal), `null` when `lib` exists but is not statically a string, else the string.
|
|
479
|
+
*/
|
|
498
480
|
function filesLibOf(kitConfig, bindings) {
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
481
|
+
const files = propOf(kitConfig, "files");
|
|
482
|
+
const obj = files ? unwrapToObjectExpression(files.value, bindings) : void 0;
|
|
483
|
+
const lib = obj ? propOf(obj, "lib") : void 0;
|
|
484
|
+
if (!lib) return void 0;
|
|
485
|
+
return stringValueOf(lib) ?? null;
|
|
504
486
|
}
|
|
487
|
+
/** `kit.alias` and `kit.files.lib` from a `svelte.config.{js,ts}` source. */
|
|
505
488
|
function findKitAliasesInSvelteConfig(source) {
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
489
|
+
const program = programOf(source, "svelte.config.js");
|
|
490
|
+
const config = program ? resolveConfigObject(program) : void 0;
|
|
491
|
+
if (!program || !config) return { entries: [] };
|
|
492
|
+
const bindings = collectTopLevelBindings(program);
|
|
493
|
+
const kit = propOf(config, "kit");
|
|
494
|
+
const kitObj = kit ? unwrapToObjectExpression(kit.value, bindings) : void 0;
|
|
495
|
+
if (!kitObj) return { entries: [] };
|
|
496
|
+
const filesLib = filesLibOf(kitObj, bindings);
|
|
497
|
+
return {
|
|
498
|
+
entries: aliasEntriesOf(kitObj, bindings),
|
|
499
|
+
...filesLib !== void 0 ? { filesLib } : {}
|
|
500
|
+
};
|
|
515
501
|
}
|
|
502
|
+
/**
|
|
503
|
+
* The normalisation Kit applies to an alias value — `posixify`, then a trailing `/*` stripped —
|
|
504
|
+
* plus the trailing-slash trim that Kit gets for free from `path.resolve` and this parser does
|
|
505
|
+
* not (it works in project-relative strings and never resolves). Applied to the `$lib` entry
|
|
506
|
+
* too, which Kit builds with neither step: `kit.files.lib` is a user-written string with the
|
|
507
|
+
* same irregularities available to it.
|
|
508
|
+
*/
|
|
516
509
|
function normalizeAliasValue(value) {
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
return noStar.replace(/\/+$/, "");
|
|
510
|
+
const posix = value.replace(/\\/g, "/");
|
|
511
|
+
return (posix.endsWith("/*") ? posix.slice(0, -2) : posix).replace(/\/+$/, "");
|
|
520
512
|
}
|
|
513
|
+
/**
|
|
514
|
+
* Compile raw config values into Kit's ordered entry list: `$lib` first (from `kit.files.lib`,
|
|
515
|
+
* else `src/lib`), then the user's entries in declaration order. Modes come from the DECLARED
|
|
516
|
+
* key set — `key + '/*'` present makes the plain key exact — never from the subset whose values
|
|
517
|
+
* happened to be readable. `raw.filesLib === null` (a `kit.files.lib` present but not statically a
|
|
518
|
+
* string) compiles to an opaque `$lib` entry, `replacement: null` — `??` would otherwise collapse
|
|
519
|
+
* that into the `src/lib` default, which is a wrong answer, not a missing one, for a project that
|
|
520
|
+
* moved `$lib` to something this parser couldn't read.
|
|
521
|
+
*/
|
|
521
522
|
function compileKitAliases(raw) {
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
523
|
+
const out = [{
|
|
524
|
+
find: "$lib",
|
|
525
|
+
replacement: raw.filesLib === null ? null : normalizeAliasValue(raw.filesLib ?? "src/lib"),
|
|
526
|
+
match: "prefix"
|
|
527
|
+
}];
|
|
528
|
+
const entries = raw.entries ?? [];
|
|
529
|
+
const declared = new Set(entries.map((e) => e.key));
|
|
530
|
+
for (const { key, value } of entries) {
|
|
531
|
+
const star = key.endsWith("/*");
|
|
532
|
+
out.push({
|
|
533
|
+
find: star ? key.slice(0, -2) : key,
|
|
534
|
+
replacement: value === null ? null : normalizeAliasValue(value),
|
|
535
|
+
match: star ? "contents" : declared.has(`${key}/*`) ? "exact" : "prefix"
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
return out;
|
|
535
539
|
}
|
|
540
|
+
/**
|
|
541
|
+
* The project's compiled alias list, following SvelteKit's config precedence: options passed to
|
|
542
|
+
* the `sveltekit()` Vite plugin make `svelte.config` irrelevant (Kit logs "svelte.config.js is
|
|
543
|
+
* ignored when options are passed via your Vite config"), so aliases are read from
|
|
544
|
+
* `svelte.config` only when the Vite config carries no plugin config. Reading `kit.alias` out of
|
|
545
|
+
* a plugin config is deliberately not done — that costs reach, not correctness, and such a
|
|
546
|
+
* project keeps the resolver's default `$lib` behaviour. Undefined means "no config was read".
|
|
547
|
+
*/
|
|
536
548
|
function resolveKitAliases(viteConfig, svelteConfig) {
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
549
|
+
if (viteConfig && findKitPathsBaseInViteConfig(viteConfig.source).kind !== "no-plugin-config") return void 0;
|
|
550
|
+
if (!svelteConfig) return void 0;
|
|
551
|
+
return compileKitAliases(findKitAliasesInSvelteConfig(svelteConfig.source));
|
|
540
552
|
}
|
|
553
|
+
/** Parse a config source to a program, or undefined when it cannot be parsed. */
|
|
541
554
|
function programOf(source, filename) {
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
555
|
+
try {
|
|
556
|
+
return parseModuleProgram(source, filename).program ?? void 0;
|
|
557
|
+
} catch {
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
547
560
|
}
|
|
561
|
+
/** `kit.paths.base` from a `svelte.config.{js,ts}` source. */
|
|
548
562
|
function findKitPathsBaseInSvelteConfig(source) {
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
563
|
+
const program = programOf(source, "svelte.config.js");
|
|
564
|
+
if (!program) return void 0;
|
|
565
|
+
const config = resolveConfigObject(program);
|
|
566
|
+
if (!config) return void 0;
|
|
567
|
+
const bindings = collectTopLevelBindings(program);
|
|
568
|
+
const kit = propOf(config, "kit");
|
|
569
|
+
const kitObj = kit ? unwrapToObjectExpression(kit.value, bindings) : void 0;
|
|
570
|
+
return kitObj ? basePathOf(kitObj, bindings) : void 0;
|
|
557
571
|
}
|
|
572
|
+
/**
|
|
573
|
+
* Local names bound to `sveltekit` imported from '@sveltejs/kit/vite'. When no such import is
|
|
574
|
+
* found (an unusual or unparsed import form), the bare name `sveltekit` is assumed — the call
|
|
575
|
+
* shape is distinctive enough that a false match is not a realistic concern.
|
|
576
|
+
*/
|
|
558
577
|
function sveltekitLocalNames(program) {
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
578
|
+
const out = collectNamedImportAliases(program, "@sveltejs/kit/vite", /* @__PURE__ */ new Set(["sveltekit"]));
|
|
579
|
+
if (out.size === 0) out.add("sveltekit");
|
|
580
|
+
return out;
|
|
562
581
|
}
|
|
582
|
+
/** SvelteKit config passed to the `sveltekit()` plugin in a Vite config source (since Kit 2.62). */
|
|
563
583
|
function findKitPathsBaseInViteConfig(source) {
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
584
|
+
const none = { kind: "no-plugin-config" };
|
|
585
|
+
const program = programOf(source, "vite.config.ts");
|
|
586
|
+
if (!program) return none;
|
|
587
|
+
const config = resolveConfigObject(program);
|
|
588
|
+
if (!config) return none;
|
|
589
|
+
const bindings = collectTopLevelBindings(program);
|
|
590
|
+
const plugins = propOf(config, "plugins");
|
|
591
|
+
const pluginsValue = plugins ? unwrapTs(plugins.value) : void 0;
|
|
592
|
+
if (pluginsValue?.type !== "ArrayExpression") return none;
|
|
593
|
+
const locals = sveltekitLocalNames(program);
|
|
594
|
+
for (const el of pluginsValue.elements) {
|
|
595
|
+
if (!el || el.type === "SpreadElement") continue;
|
|
596
|
+
const call = unwrapTs(el);
|
|
597
|
+
if (call.type !== "CallExpression") continue;
|
|
598
|
+
if (call.callee.type !== "Identifier" || !locals.has(call.callee.name)) continue;
|
|
599
|
+
const arg = call.arguments[0];
|
|
600
|
+
if (arg === void 0) return none;
|
|
601
|
+
const kitConfig = unwrapToObjectExpression(arg, bindings);
|
|
602
|
+
if (!kitConfig) return { kind: "unresolvable" };
|
|
603
|
+
const base = basePathOf(kitConfig, bindings);
|
|
604
|
+
return base ? {
|
|
605
|
+
kind: "resolved",
|
|
606
|
+
base
|
|
607
|
+
} : { kind: "resolved" };
|
|
608
|
+
}
|
|
609
|
+
return none;
|
|
587
610
|
}
|
|
611
|
+
/**
|
|
612
|
+
* The project's effective `kit.paths.base`, following SvelteKit's precedence: the `sveltekit()`
|
|
613
|
+
* plugin config when it carries one, otherwise `svelte.config`. `file` is the config the base
|
|
614
|
+
* came from (as passed in by the caller). Undefined means "no base path" — the gate stays shut.
|
|
615
|
+
*/
|
|
588
616
|
function resolveKitPathsBase(viteConfig, svelteConfig) {
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
617
|
+
if (viteConfig) {
|
|
618
|
+
const result = findKitPathsBaseInViteConfig(viteConfig.source);
|
|
619
|
+
if (result.kind === "unresolvable") return void 0;
|
|
620
|
+
if (result.kind === "resolved") return result.base ? {
|
|
621
|
+
...result.base,
|
|
622
|
+
file: viteConfig.file
|
|
623
|
+
} : void 0;
|
|
624
|
+
}
|
|
625
|
+
if (!svelteConfig) return void 0;
|
|
626
|
+
const base = findKitPathsBaseInSvelteConfig(svelteConfig.source);
|
|
627
|
+
return base ? {
|
|
628
|
+
...base,
|
|
629
|
+
file: svelteConfig.file
|
|
630
|
+
} : void 0;
|
|
599
631
|
}
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
632
|
+
//#endregion
|
|
633
|
+
//#region src/project-paths.ts
|
|
634
|
+
/**
|
|
635
|
+
* Source-file locations that satisfy the project-scope rules, shared by every
|
|
636
|
+
* mode so the static (CLI) and rendered (plugin) collectors never drift. This
|
|
637
|
+
* module is pure data: no I/O, no `node:` imports (design §8).
|
|
638
|
+
*/
|
|
639
|
+
/** Locations that satisfy the robots.txt project rule (seo/robots-txt). */
|
|
640
|
+
const ROBOTS_SOURCE_PATHS = [
|
|
641
|
+
"static/robots.txt",
|
|
642
|
+
"src/routes/robots.txt/+server.ts",
|
|
643
|
+
"src/routes/robots.txt/+server.js"
|
|
606
644
|
];
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
645
|
+
/** Locations that satisfy the sitemap.xml project rule (seo/sitemap-xml). */
|
|
646
|
+
const SITEMAP_SOURCE_PATHS = [
|
|
647
|
+
"static/sitemap.xml",
|
|
648
|
+
"src/routes/sitemap.xml/+server.ts",
|
|
649
|
+
"src/routes/sitemap.xml/+server.js"
|
|
611
650
|
];
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
651
|
+
/** Vite's own config resolution order — only the first existing file is the one Vite loads. */
|
|
652
|
+
const VITE_CONFIG_FILES = [
|
|
653
|
+
"vite.config.js",
|
|
654
|
+
"vite.config.mjs",
|
|
655
|
+
"vite.config.ts",
|
|
656
|
+
"vite.config.cjs",
|
|
657
|
+
"vite.config.mts",
|
|
658
|
+
"vite.config.cts"
|
|
619
659
|
];
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
660
|
+
/** SvelteKit's config resolution order (`@sveltejs/kit` checks js before ts). */
|
|
661
|
+
const SVELTE_CONFIG_FILES = ["svelte.config.js", "svelte.config.ts"];
|
|
662
|
+
//#endregion
|
|
663
|
+
//#region src/runtime.ts
|
|
664
|
+
/**
|
|
665
|
+
* How many file reads may be in flight at once. Analysis reads every `.svelte` file in a project
|
|
666
|
+
* in parallel, which on a large project opens more descriptors than the process is allowed: at
|
|
667
|
+
* `ulimit -n 1024` — a common container default — a 1 681-route project raised `EMFILE`, and
|
|
668
|
+
* because a failed read lands in the same `catch` as a malformed component, 682 files were dropped
|
|
669
|
+
* and the run still reported a normal score. The cap is what keeps the analysis whole.
|
|
670
|
+
*
|
|
671
|
+
* 64 is chosen to sit well under the stock 256 on macOS while leaving descriptors for everything
|
|
672
|
+
* else the process holds open. It is not a throughput knob: reads are a few percent of the work.
|
|
673
|
+
*/
|
|
674
|
+
const READ_CONCURRENCY = 64;
|
|
675
|
+
/**
|
|
676
|
+
* `readFile` with at most `limit` reads in flight. A plain counter plus a queue of waiters —
|
|
677
|
+
* deliberately not a dependency, and pure enough to live in core.
|
|
678
|
+
*/
|
|
679
|
+
function withReadLimit(readFile, limit = 64) {
|
|
680
|
+
let active = 0;
|
|
681
|
+
const waiting = [];
|
|
682
|
+
const release = () => {
|
|
683
|
+
active--;
|
|
684
|
+
waiting.shift()?.();
|
|
685
|
+
};
|
|
686
|
+
return async (path) => {
|
|
687
|
+
if (active >= limit) await new Promise((resolve) => waiting.push(resolve));
|
|
688
|
+
active++;
|
|
689
|
+
try {
|
|
690
|
+
return await readFile(path);
|
|
691
|
+
} finally {
|
|
692
|
+
release();
|
|
693
|
+
}
|
|
694
|
+
};
|
|
640
695
|
}
|
|
641
|
-
|
|
642
|
-
|
|
696
|
+
//#endregion
|
|
697
|
+
//#region src/engine.ts
|
|
698
|
+
/**
|
|
699
|
+
* Run a set of rules against a shared context and collect their findings.
|
|
700
|
+
* Rules are independent, so they run concurrently; results are flattened in
|
|
701
|
+
* rule order for stable output. A rule that throws (sync or async) contributes
|
|
702
|
+
* no results instead of taking the whole run down with it — dev tooling must
|
|
703
|
+
* never throw — and is reported in `failedRules` instead.
|
|
704
|
+
*/
|
|
643
705
|
async function runRules(rules, ctx) {
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
706
|
+
const examined = {};
|
|
707
|
+
const perRule = await Promise.all(rules.map(async (rule) => {
|
|
708
|
+
try {
|
|
709
|
+
return await rule.check({
|
|
710
|
+
...ctx,
|
|
711
|
+
recordExamined: (counts) => void (examined[rule.id] = counts)
|
|
712
|
+
});
|
|
713
|
+
} catch (err) {
|
|
714
|
+
return {
|
|
715
|
+
id: rule.id,
|
|
716
|
+
message: err instanceof Error ? err.message : String(err)
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
}));
|
|
720
|
+
const results = [];
|
|
721
|
+
const failedRules = [];
|
|
722
|
+
for (const outcome of perRule) if (Array.isArray(outcome)) results.push(...outcome);
|
|
723
|
+
else failedRules.push(outcome);
|
|
724
|
+
return {
|
|
725
|
+
results,
|
|
726
|
+
examined,
|
|
727
|
+
failedRules
|
|
728
|
+
};
|
|
661
729
|
}
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
730
|
+
//#endregion
|
|
731
|
+
//#region src/reporter/palette.ts
|
|
732
|
+
/** Default: no decoration (identity) — output is byte-identical to plain text. */
|
|
733
|
+
const noColorPalette = {
|
|
734
|
+
bold: (s) => s,
|
|
735
|
+
dim: (s) => s,
|
|
736
|
+
red: (s) => s,
|
|
737
|
+
yellow: (s) => s,
|
|
738
|
+
green: (s) => s,
|
|
739
|
+
cyan: (s) => s
|
|
671
740
|
};
|
|
741
|
+
/** Green ≥ 90, yellow ≥ 70, red otherwise — for a 0–100 score. */
|
|
672
742
|
function scoreColor(p, score) {
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
743
|
+
if (score >= 90) return p.green;
|
|
744
|
+
if (score >= 70) return p.yellow;
|
|
745
|
+
return p.red;
|
|
676
746
|
}
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
747
|
+
//#endregion
|
|
748
|
+
//#region src/reporter/console.ts
|
|
749
|
+
const RULE = "────────────────────────";
|
|
750
|
+
const SEVERITY_TITLE = {
|
|
751
|
+
critical: "Critical",
|
|
752
|
+
warning: "Warnings",
|
|
753
|
+
info: "Info"
|
|
684
754
|
};
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
755
|
+
const CATEGORY_LABEL = {
|
|
756
|
+
seo: "SEO",
|
|
757
|
+
performance: "Performance",
|
|
758
|
+
correctness: "Correctness",
|
|
759
|
+
security: "Security",
|
|
760
|
+
architecture: "Architecture",
|
|
761
|
+
a11y: "Accessibility"
|
|
692
762
|
};
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
763
|
+
const CATEGORY_ORDER = Object.keys(CATEGORY_LABEL);
|
|
764
|
+
const categoryLabel = (c) => CATEGORY_LABEL[c];
|
|
765
|
+
const MAX_RULE_GROUPS_PER_BUCKET = 5;
|
|
766
|
+
/** Groups results by rule id, ranked by descending group size (most-affected rule first); ties broken by id for determinism. */
|
|
696
767
|
function groupByRule(results) {
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
768
|
+
const groups = /* @__PURE__ */ new Map();
|
|
769
|
+
for (const r of results) {
|
|
770
|
+
const bucket = groups.get(r.id);
|
|
771
|
+
if (bucket) bucket.push(r);
|
|
772
|
+
else groups.set(r.id, [r]);
|
|
773
|
+
}
|
|
774
|
+
return [...groups.entries()].map(([id, rs]) => ({
|
|
775
|
+
id,
|
|
776
|
+
results: rs
|
|
777
|
+
})).sort((a, b) => b.results.length - a.results.length || a.id.localeCompare(b.id));
|
|
704
778
|
}
|
|
705
779
|
function scoreLine(p, label, { score, scoreModel }) {
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
780
|
+
const parts = [`route avg ${scoreModel.routeAverage}`];
|
|
781
|
+
if (scoreModel.sitePenalty > 0) parts.push(`site −${scoreModel.sitePenalty}`);
|
|
782
|
+
if (scoreModel.criticalCap !== null) parts.push(`capped at ${scoreModel.criticalCap}: critical present`);
|
|
783
|
+
return `${label} Score: ${scoreColor(p, score)(`${score}/100`)} ${p.dim(`(${parts.join(" · ")})`)}`;
|
|
710
784
|
}
|
|
711
|
-
|
|
785
|
+
const MAX_ROUTES_BY_ROUTE = 10;
|
|
712
786
|
function byRouteTree(p, results, config, verbose) {
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
`\u2026and ${remaining.length} more route${remaining.length > 1 ? "s" : ""} (avg score ${avgScore}) \u2014 run with --verbose to see all`
|
|
739
|
-
)
|
|
740
|
-
);
|
|
741
|
-
}
|
|
742
|
-
lines.push("");
|
|
743
|
-
return lines;
|
|
787
|
+
const routes = /* @__PURE__ */ new Map();
|
|
788
|
+
for (const r of results) {
|
|
789
|
+
if (r.route === void 0) continue;
|
|
790
|
+
if (!routes.has(r.route)) routes.set(r.route, []);
|
|
791
|
+
routes.get(r.route).push(r);
|
|
792
|
+
}
|
|
793
|
+
const scored = [...routes.entries()].map(([route, rs]) => ({
|
|
794
|
+
route,
|
|
795
|
+
rs,
|
|
796
|
+
score: computeScore(rs, config, { applyCriticalCap: false }).score
|
|
797
|
+
}));
|
|
798
|
+
scored.sort((a, b) => a.score - b.score || a.route.localeCompare(b.route));
|
|
799
|
+
const shown = verbose ? scored : scored.slice(0, MAX_ROUTES_BY_ROUTE);
|
|
800
|
+
const lines = [p.bold("By route"), p.dim(RULE)];
|
|
801
|
+
for (const { route, rs, score } of shown) {
|
|
802
|
+
lines.push(`${terminalSafe(route).padEnd(28)} ${scoreColor(p, score)(`${score}`)}`);
|
|
803
|
+
for (const r of rs.filter((x) => classify(x, config) === "fail")) lines.push(` ${p.red("✗")} ${r.id} ${terminalSafe(r.message)}`);
|
|
804
|
+
}
|
|
805
|
+
if (!verbose && scored.length > MAX_ROUTES_BY_ROUTE) {
|
|
806
|
+
const remaining = scored.slice(MAX_ROUTES_BY_ROUTE);
|
|
807
|
+
const avgScore = Math.floor(remaining.reduce((sum, r) => sum + r.score, 0) / remaining.length);
|
|
808
|
+
lines.push(p.dim(`…and ${remaining.length} more route${remaining.length > 1 ? "s" : ""} (avg score ${avgScore}) — run with --verbose to see all`));
|
|
809
|
+
}
|
|
810
|
+
lines.push("");
|
|
811
|
+
return lines;
|
|
744
812
|
}
|
|
813
|
+
/**
|
|
814
|
+
* Render results as a console report string (design §7). Pure: returns a string,
|
|
815
|
+
* the caller is responsible for printing. Prepends a score header; when byRoute is
|
|
816
|
+
* set, adds a per-route score tree.
|
|
817
|
+
*/
|
|
745
818
|
function formatConsoleReport(results, config, options = {}) {
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
const marker = classify(r, config) === "dynamic" ? p.cyan(" \u21AF dynamic") : "";
|
|
805
|
-
const where = r.location ?? r.route;
|
|
806
|
-
const suffix = where ? ` ${terminalSafe(where)}` : "";
|
|
807
|
-
lines.push(`${p.green("\u2713")} ${r.id} ${terminalSafe(r.message)}${marker}${suffix}`);
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
|
-
lines.push("");
|
|
811
|
-
}
|
|
812
|
-
if (options.byRoute) lines.push(...byRouteTree(p, results, config, options.verbose ?? false));
|
|
813
|
-
if (options.verbose && summary.dynamic > 0) lines.push(p.dim("\u21AF = set dynamically (verified at runtime)."));
|
|
814
|
-
return lines.join("\n").replace(/\n+$/, "\n");
|
|
819
|
+
const p = options.palette ?? noColorPalette;
|
|
820
|
+
const summary = summarize(results, config);
|
|
821
|
+
const { health, categories: byCat } = computeHealth(results, config);
|
|
822
|
+
const present = CATEGORY_ORDER.filter((c) => byCat[c] !== void 0);
|
|
823
|
+
const lines = [];
|
|
824
|
+
if (!options.omitHeader) lines.push(p.bold(`Svelte Vitals · ${options.mode ?? "static mode"}`), "", `${p.bold("Health:")} ${scoreColor(p, health)(`${health}/100`)}`);
|
|
825
|
+
for (const c of present) lines.push(scoreLine(p, categoryLabel(c), byCat[c]));
|
|
826
|
+
lines.push("");
|
|
827
|
+
const SEVERITY_COLOR = {
|
|
828
|
+
critical: (s) => p.red(p.bold(s)),
|
|
829
|
+
warning: (s) => p.yellow(p.bold(s)),
|
|
830
|
+
info: (s) => p.dim(s)
|
|
831
|
+
};
|
|
832
|
+
const failures = results.filter((r) => classify(r, config) === "fail");
|
|
833
|
+
for (const severity of [
|
|
834
|
+
"critical",
|
|
835
|
+
"warning",
|
|
836
|
+
"info"
|
|
837
|
+
]) {
|
|
838
|
+
const bucket = failures.filter((r) => effectiveSeverity(r, config) === severity);
|
|
839
|
+
if (bucket.length === 0) continue;
|
|
840
|
+
lines.push(SEVERITY_COLOR[severity](`${SEVERITY_TITLE[severity]} (${bucket.length})`), p.dim(RULE));
|
|
841
|
+
if (options.verbose) for (const r of bucket) {
|
|
842
|
+
lines.push(`${p.red("✗")} ${r.id} ${terminalSafe(r.message)}`);
|
|
843
|
+
if (r.route) lines.push(p.dim(` ${terminalSafe(r.route)}`));
|
|
844
|
+
if (r.location) lines.push(p.dim(` ${terminalSafe(r.location)}${r.line ? `:${r.line}` : ""}`));
|
|
845
|
+
}
|
|
846
|
+
else {
|
|
847
|
+
const groups = groupByRule(bucket);
|
|
848
|
+
const shownGroups = groups.slice(0, MAX_RULE_GROUPS_PER_BUCKET);
|
|
849
|
+
for (const group of shownGroups) {
|
|
850
|
+
const r = group.results[0];
|
|
851
|
+
lines.push(`${p.red("✗")} ${r.id} ${terminalSafe(r.message)}`);
|
|
852
|
+
if (r.route) lines.push(p.dim(` ${terminalSafe(r.route)}`));
|
|
853
|
+
if (r.location) lines.push(p.dim(` ${terminalSafe(r.location)}${r.line ? `:${r.line}` : ""}`));
|
|
854
|
+
if (group.results.length > 1) lines.push(p.dim(` …and ${group.results.length - 1} more`));
|
|
855
|
+
}
|
|
856
|
+
if (groups.length > MAX_RULE_GROUPS_PER_BUCKET) {
|
|
857
|
+
const remaining = groups.length - MAX_RULE_GROUPS_PER_BUCKET;
|
|
858
|
+
lines.push(p.dim(`…and ${remaining} more rule${remaining > 1 ? "s" : ""} affected — run with --verbose to see all`));
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
lines.push("");
|
|
862
|
+
}
|
|
863
|
+
const passed = results.filter((r) => classify(r, config) !== "fail");
|
|
864
|
+
if (passed.length > 0) {
|
|
865
|
+
lines.push(p.bold(`Passed (${passed.length})`), p.dim(RULE));
|
|
866
|
+
if (options.verbose) for (const r of passed) {
|
|
867
|
+
const marker = classify(r, config) === "dynamic" ? p.cyan(" ↯ dynamic") : "";
|
|
868
|
+
const where = r.location ?? r.route;
|
|
869
|
+
const suffix = where ? ` ${terminalSafe(where)}` : "";
|
|
870
|
+
lines.push(`${p.green("✓")} ${r.id} ${terminalSafe(r.message)}${marker}${suffix}`);
|
|
871
|
+
}
|
|
872
|
+
lines.push("");
|
|
873
|
+
}
|
|
874
|
+
if (options.byRoute) lines.push(...byRouteTree(p, results, config, options.verbose ?? false));
|
|
875
|
+
if (options.verbose && summary.dynamic > 0) lines.push(p.dim("↯ = set dynamically (verified at runtime)."));
|
|
876
|
+
return lines.join("\n").replace(/\n+$/, "\n");
|
|
815
877
|
}
|
|
816
|
-
|
|
817
|
-
|
|
878
|
+
//#endregion
|
|
879
|
+
//#region src/reporter/agent.ts
|
|
880
|
+
/** Render failing findings as an agent-actionable Markdown remediation document (issue #18). */
|
|
818
881
|
function formatAgentReport(results, config) {
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
}
|
|
857
|
-
return lines.join("\n").replace(/\n+$/, "\n");
|
|
882
|
+
const failing = results.filter((r) => classify(r, config) === "fail");
|
|
883
|
+
const { health } = computeHealth(results, config);
|
|
884
|
+
const lines = [
|
|
885
|
+
"# svelte-vitals — fixes",
|
|
886
|
+
"",
|
|
887
|
+
`Health: ${health}/100`,
|
|
888
|
+
""
|
|
889
|
+
];
|
|
890
|
+
if (failing.length === 0) {
|
|
891
|
+
lines.push("No issues to fix.", "");
|
|
892
|
+
return lines.join("\n").replace(/\n+$/, "\n");
|
|
893
|
+
}
|
|
894
|
+
lines.push(`${failing.length} issue(s) to fix, ordered most-severe first. Fix critical issues first; warning and info items improve SEO but do not fail the default build. Apply each fix below, then re-run \`svelte-vitals\` (or the build) to confirm each rule passes. Run \`svelte-vitals explain <rule-id>\` for any rule's rationale and options (works offline).`, "");
|
|
895
|
+
const groups = /* @__PURE__ */ new Map();
|
|
896
|
+
for (const r of failing) {
|
|
897
|
+
const key = r.location ?? r.route ?? "(project)";
|
|
898
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
899
|
+
groups.get(key).push(r);
|
|
900
|
+
}
|
|
901
|
+
const groupSeverity = (rs) => Math.min(...rs.map((r) => SEVERITY_RANK[effectiveSeverity(r, config)]));
|
|
902
|
+
const orderedGroups = [...groups.entries()].sort((a, b) => groupSeverity(a[1]) - groupSeverity(b[1]) || a[0].localeCompare(b[0]));
|
|
903
|
+
for (const [loc, rs] of orderedGroups) {
|
|
904
|
+
rs.sort((x, y) => SEVERITY_RANK[effectiveSeverity(x, config)] - SEVERITY_RANK[effectiveSeverity(y, config)] || x.id.localeCompare(y.id));
|
|
905
|
+
lines.push(`## ${mdEscape(loc)}`, "");
|
|
906
|
+
for (const r of rs) {
|
|
907
|
+
const at = r.line == null ? "" : `, line ${r.line}`;
|
|
908
|
+
lines.push(`### ${r.id} · ${mdEscape(r.message)} (${effectiveSeverity(r, config)}${at})`);
|
|
909
|
+
if (r.fix) {
|
|
910
|
+
lines.push(`- Fix: ${mdEscape(r.fix.description)}`);
|
|
911
|
+
if (r.fix.snippet) lines.push("", "```" + (r.fix.lang ?? "svelte"), r.fix.snippet, "```");
|
|
912
|
+
} else if (r.recommendation) lines.push(`- Fix: ${mdEscape(r.recommendation)}`);
|
|
913
|
+
if (r.docsUrl) lines.push(`- Docs: ${r.docsUrl}`);
|
|
914
|
+
const byReview = r.line == null ? "" : ` If the code is right as written, a reviewed \`svelte-vitals-disable-next-line ${r.id}\` comment directly above line ${r.line} resolves it instead.`;
|
|
915
|
+
lines.push(`- Accept: re-run svelte-vitals; ${r.id} passes${r.route ? ` for ${mdEscape(r.route)}` : ""}.${byReview}`, "");
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
return lines.join("\n").replace(/\n+$/, "\n");
|
|
858
919
|
}
|
|
859
|
-
|
|
860
|
-
|
|
920
|
+
//#endregion
|
|
921
|
+
//#region src/reporter/sarif.ts
|
|
922
|
+
/** Render penalized findings as a SARIF 2.1.0 log string (issue #18, design slice 5). */
|
|
861
923
|
function formatSarifReport(results, config, meta) {
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
driver: {
|
|
906
|
-
name: "svelte-vitals",
|
|
907
|
-
informationUri: "https://oekazuma.github.io/svelte-vitals",
|
|
908
|
-
version: meta.version,
|
|
909
|
-
rules
|
|
910
|
-
}
|
|
911
|
-
},
|
|
912
|
-
results: sarifResults
|
|
913
|
-
}
|
|
914
|
-
]
|
|
915
|
-
};
|
|
916
|
-
return JSON.stringify(log, null, 2);
|
|
924
|
+
const penalized = results.filter((r) => isPenalized(r.detection, config.treatDynamicAs));
|
|
925
|
+
const rules = [];
|
|
926
|
+
const ruleIndex = /* @__PURE__ */ new Map();
|
|
927
|
+
const sarifResults = penalized.map((r) => {
|
|
928
|
+
if (!ruleIndex.has(r.id)) {
|
|
929
|
+
const m = ruleMetaById(r.id);
|
|
930
|
+
const name = m?.title ?? r.id;
|
|
931
|
+
ruleIndex.set(r.id, rules.length);
|
|
932
|
+
rules.push({
|
|
933
|
+
id: r.id,
|
|
934
|
+
name,
|
|
935
|
+
shortDescription: { text: name },
|
|
936
|
+
helpUri: r.docsUrl ?? m?.docsUrl ?? docsUrlFor(r.id),
|
|
937
|
+
defaultConfiguration: { level: severityToSarifLevel(m?.severity ?? r.severity) }
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
const result = {
|
|
941
|
+
ruleId: r.id,
|
|
942
|
+
ruleIndex: ruleIndex.get(r.id),
|
|
943
|
+
level: severityToSarifLevel(effectiveSeverity(r, config)),
|
|
944
|
+
message: { text: messageText(r) },
|
|
945
|
+
partialFingerprints: { "svelteVitals/v1": `${r.id}:${r.route ?? "project"}${r.line !== void 0 ? `:${r.location ?? ""}:${r.line}` : ""}` }
|
|
946
|
+
};
|
|
947
|
+
if (r.location) result.locations = [{ physicalLocation: {
|
|
948
|
+
artifactLocation: { uri: r.location },
|
|
949
|
+
...r.line !== void 0 ? { region: { startLine: r.line } } : {}
|
|
950
|
+
} }];
|
|
951
|
+
return result;
|
|
952
|
+
});
|
|
953
|
+
const log = {
|
|
954
|
+
$schema: "https://json.schemastore.org/sarif-2.1.0.json",
|
|
955
|
+
version: "2.1.0",
|
|
956
|
+
runs: [{
|
|
957
|
+
tool: { driver: {
|
|
958
|
+
name: "svelte-vitals",
|
|
959
|
+
informationUri: "https://oekazuma.github.io/svelte-vitals",
|
|
960
|
+
version: meta.version,
|
|
961
|
+
rules
|
|
962
|
+
} },
|
|
963
|
+
results: sarifResults
|
|
964
|
+
}]
|
|
965
|
+
};
|
|
966
|
+
return JSON.stringify(log, null, 2);
|
|
917
967
|
}
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
968
|
+
//#endregion
|
|
969
|
+
//#region src/reporter/app-shell.ts
|
|
970
|
+
const BAND_COLOR = {
|
|
971
|
+
good: "#2FA968",
|
|
972
|
+
warn: "#E8A317",
|
|
973
|
+
poor: "#E5484D"
|
|
924
974
|
};
|
|
925
975
|
function scoreBand(score) {
|
|
926
|
-
|
|
976
|
+
return score >= 90 ? "good" : score >= 50 ? "warn" : "poor";
|
|
927
977
|
}
|
|
928
978
|
function escapeHtml(s) {
|
|
929
|
-
|
|
930
|
-
/[&<>"']/g,
|
|
931
|
-
(c) => c === "&" ? "&" : c === "<" ? "<" : c === ">" ? ">" : c === '"' ? """ : "'"
|
|
932
|
-
);
|
|
979
|
+
return s.replace(/[&<>"']/g, (c) => c === "&" ? "&" : c === "<" ? "<" : c === ">" ? ">" : c === "\"" ? """ : "'");
|
|
933
980
|
}
|
|
981
|
+
/**
|
|
982
|
+
* Return the URL only when it uses a safe http/https scheme, else null.
|
|
983
|
+
* Guards a finding's `docsUrl` against `javascript:`/`data:` hrefs — escapeHtml
|
|
984
|
+
* neutralizes attribute breakout but not a malicious scheme. Browsers strip
|
|
985
|
+
* ASCII whitespace (tab/newline/CR) from a URL before resolving its scheme (so
|
|
986
|
+
* `java\tscript:` runs as `javascript:`), so strip whitespace first; anything not
|
|
987
|
+
* plainly http(s):// afterward is rejected. Pure string work — no `URL` global,
|
|
988
|
+
* keeping core runtime-agnostic and lib-minimal.
|
|
989
|
+
*/
|
|
934
990
|
function safeHref(url) {
|
|
935
|
-
|
|
936
|
-
|
|
991
|
+
const normalized = url.replace(/\s/g, "").toLowerCase();
|
|
992
|
+
return /^https?:\/\//.test(normalized) ? url : null;
|
|
937
993
|
}
|
|
994
|
+
/**
|
|
995
|
+
* JSON.stringify escapes `"` inside string values but not `<` or the JS line terminators
|
|
996
|
+
* U+2028/U+2029 — all three matter once the result is embedded inside an inline
|
|
997
|
+
* <script type="application/json"> element: an unescaped `<\/script>` in any finding-derived
|
|
998
|
+
* string (route path, location, recommendation, fix snippet, title) would close the tag
|
|
999
|
+
* early, and U+2028/U+2029 can still break some script-parsing environments.
|
|
1000
|
+
*/
|
|
938
1001
|
function embedJson(value) {
|
|
939
|
-
|
|
1002
|
+
return JSON.stringify(value).replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
940
1003
|
}
|
|
1004
|
+
/**
|
|
1005
|
+
* The client script renders `docsUrl` straight into an <a href>, so a malicious scheme
|
|
1006
|
+
* (`javascript:`, `data:`) must never reach the embedded snapshot — `buildHtmlDocument`
|
|
1007
|
+
* is a public API fed loosely-typed reports. Sanitized server-side once, here.
|
|
1008
|
+
*/
|
|
941
1009
|
function sanitizeDocsUrl(issue) {
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
1010
|
+
if (issue.docsUrl === void 0) return issue;
|
|
1011
|
+
if (safeHref(issue.docsUrl) !== null) return issue;
|
|
1012
|
+
return {
|
|
1013
|
+
...issue,
|
|
1014
|
+
docsUrl: void 0
|
|
1015
|
+
};
|
|
945
1016
|
}
|
|
946
1017
|
function sanitizeReport(report) {
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
1018
|
+
return {
|
|
1019
|
+
...report,
|
|
1020
|
+
routes: report.routes.map((route) => ({
|
|
1021
|
+
...route,
|
|
1022
|
+
issues: route.issues.map(sanitizeDocsUrl)
|
|
1023
|
+
})),
|
|
1024
|
+
siteIssues: report.siteIssues.map(sanitizeDocsUrl)
|
|
1025
|
+
};
|
|
952
1026
|
}
|
|
953
|
-
|
|
1027
|
+
/**
|
|
1028
|
+
* Hand-authored CSS for the master/detail shell. Reuses the same design-token
|
|
1029
|
+
* names/values as the rest of the project, and adds a dark theme via
|
|
1030
|
+
* `:root[data-theme="dark"]` plus a `prefers-color-scheme` fallback for a
|
|
1031
|
+
* first-ever visit with no stored preference.
|
|
1032
|
+
*/
|
|
1033
|
+
const APP_STYLE = `
|
|
954
1034
|
:root{--ground:#f6f7f9;--panel:#fff;--ink:#0c1322;--muted:#5a6472;--faint:#8c95a3;--line:#e4e7ec;--line-strong:#d3d8e0;--accent:#ff3e00;--good:#2fa968;--warn:#e8a317;--poor:#e5484d;--code-bg:#0e1525;--code-ink:#e7ecf4;--active-bg:#0c1322;--active-ink:#fff;--mono:ui-monospace,"SF Mono","JetBrains Mono",Menlo,Consolas,monospace;--sans:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif}
|
|
955
1035
|
:root[data-theme="dark"]{--ground:#0b0e14;--panel:#12161f;--ink:#e7ecf4;--muted:#9aa4b2;--faint:#6b7484;--line:#232838;--line-strong:#2d3345;--code-bg:#05070c;--code-ink:#e7ecf4;--active-bg:#e7ecf4;--active-ink:#0b0e14}
|
|
956
1036
|
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]){--ground:#0b0e14;--panel:#12161f;--ink:#e7ecf4;--muted:#9aa4b2;--faint:#6b7484;--line:#232838;--line-strong:#2d3345;--code-bg:#05070c;--code-ink:#e7ecf4;--active-bg:#e7ecf4;--active-ink:#0b0e14}}
|
|
@@ -1040,7 +1120,7 @@ body{background:var(--ground);color:var(--ink);font-family:var(--sans);line-heig
|
|
|
1040
1120
|
.dv-ai-prompt{margin-top:12px;border:1px solid var(--line);border-radius:8px;overflow:hidden}
|
|
1041
1121
|
.dv-ai-prompt>summary{cursor:pointer;list-style:none;padding:8px 12px;font-size:12px;font-weight:600;color:var(--muted);display:flex;align-items:center;gap:6px;user-select:none}
|
|
1042
1122
|
.dv-ai-prompt>summary::-webkit-details-marker{display:none}
|
|
1043
|
-
.dv-ai-prompt>summary::before{content:"
|
|
1123
|
+
.dv-ai-prompt>summary::before{content:"▸";display:inline-block;transition:transform .15s ease}
|
|
1044
1124
|
.dv-ai-prompt[open]>summary::before{transform:rotate(90deg)}
|
|
1045
1125
|
.dv-ai-prompt-body{padding:0 12px 12px;display:flex;flex-direction:column;gap:8px}
|
|
1046
1126
|
.dv-ai-prompt-pre{margin:0;padding:10px 12px;background:var(--code-bg);color:var(--code-ink);border-radius:8px;font-family:var(--mono);font-size:12px;line-height:1.6;white-space:pre-wrap;word-break:break-word;max-height:280px;overflow-y:auto}
|
|
@@ -1051,13 +1131,22 @@ body{background:var(--ground);color:var(--ink);font-family:var(--sans);line-heig
|
|
|
1051
1131
|
@media (max-width:640px){.dv-app{grid-template-columns:1fr;grid-template-areas:"top" "main"}.dv-menu-toggle{display:inline-flex}.dv-sidebar{position:fixed;inset:0 20% 0 0;transform:translateX(-100%);transition:transform .2s ease;z-index:10}.dv-sidebar.open{transform:translateX(0)}}
|
|
1052
1132
|
@media (prefers-reduced-motion:reduce){*{transition:none!important}}
|
|
1053
1133
|
`;
|
|
1054
|
-
|
|
1134
|
+
/**
|
|
1135
|
+
* Hand-authored client script for the shell — no bundler, no framework. Parses the
|
|
1136
|
+
* AppSnapshot embedded by renderAppShell, then owns all rendering: sidebar
|
|
1137
|
+
* (search/sort/route list) and detail pane (Overview or a selected route). When the
|
|
1138
|
+
* snapshot says `live`, it additionally re-fetches /data.json on every SSE `update`
|
|
1139
|
+
* and on the EventSource's `open` event (covers the initial connection and every
|
|
1140
|
+
* auto-reconnect, since EventSource replays no missed events) — discarding any
|
|
1141
|
+
* response whose `sequence` isn't newer than what's already rendered.
|
|
1142
|
+
*/
|
|
1143
|
+
const APP_SCRIPT = `
|
|
1055
1144
|
(function(){
|
|
1056
1145
|
var BAND_COLOR = { good: '#2fa968', warn: '#e8a317', poor: '#e5484d' };
|
|
1057
1146
|
var CATEGORY_NAMES = ${JSON.stringify(CATEGORY_LABEL)};
|
|
1058
1147
|
function scoreBand(score) { return score >= 90 ? 'good' : score >= 50 ? 'warn' : 'poor'; }
|
|
1059
1148
|
|
|
1060
|
-
// Same mark as the docs site's hero wordmark (docs/public/wordmark.svg)
|
|
1149
|
+
// Same mark as the docs site's hero wordmark (docs/public/wordmark.svg) — an inline
|
|
1061
1150
|
// copy, not an <img src>, since the dashboard is a single self-contained HTML response
|
|
1062
1151
|
// with no other static assets to serve alongside it. Fixed brand colors (not CSS custom
|
|
1063
1152
|
// properties), matching the docs usage: the wordmark reads the same in both themes.
|
|
@@ -1158,12 +1247,12 @@ var APP_SCRIPT = `
|
|
|
1158
1247
|
return pre;
|
|
1159
1248
|
}
|
|
1160
1249
|
|
|
1161
|
-
// Plain-text, copy-pasteable prompt for a single finding
|
|
1250
|
+
// Plain-text, copy-pasteable prompt for a single finding — same ingredients as the
|
|
1162
1251
|
// agent reporter's per-finding block (rule id, location, recommendation, fix, docs),
|
|
1163
1252
|
// reshaped for a standalone request rather than a whole-project remediation doc.
|
|
1164
1253
|
function buildAiPrompt(issue, route) {
|
|
1165
1254
|
var lines = ['Fix this svelte-vitals finding:', ''];
|
|
1166
|
-
lines.push('- Rule: ' + issue.id + '
|
|
1255
|
+
lines.push('- Rule: ' + issue.id + ' — ' + issue.title + ' (' + issue.severity + ')');
|
|
1167
1256
|
if (route) lines.push('- Route: ' + route);
|
|
1168
1257
|
if (issue.location) {
|
|
1169
1258
|
lines.push('- Location: ' + issue.location + (issue.line !== undefined ? ':' + issue.line : ''));
|
|
@@ -1268,7 +1357,7 @@ var APP_SCRIPT = `
|
|
|
1268
1357
|
var s = state.snapshot;
|
|
1269
1358
|
var findings = s.report.routes.reduce(function (n, r) { return n + r.issues.length; }, 0) + s.report.siteIssues.length;
|
|
1270
1359
|
var kids = [
|
|
1271
|
-
h('button', { type: 'button', class: 'dv-menu-toggle', 'aria-label': 'Toggle route list', onclick: toggleSidebar, text: '
|
|
1360
|
+
h('button', { type: 'button', class: 'dv-menu-toggle', 'aria-label': 'Toggle route list', onclick: toggleSidebar, text: '≡' }, []),
|
|
1272
1361
|
brandEl(),
|
|
1273
1362
|
h('div', { class: 'dv-meta' }, [
|
|
1274
1363
|
h('span', { text: 'v' + s.meta.version }, []),
|
|
@@ -1277,9 +1366,9 @@ var APP_SCRIPT = `
|
|
|
1277
1366
|
h('span', { text: findings + ' findings' }, [])
|
|
1278
1367
|
].filter(Boolean)),
|
|
1279
1368
|
h('div', { class: 'dv-status' }, [
|
|
1280
|
-
s.live && s.analyzing ? h('span', { class: 'dv-analyzing', text: 'Analyzing
|
|
1369
|
+
s.live && s.analyzing ? h('span', { class: 'dv-analyzing', text: 'Analyzing…' }, []) : null,
|
|
1281
1370
|
s.live ? h('span', { class: 'dv-conn dv-conn-' + state.connection, title: state.connection }, []) : null,
|
|
1282
|
-
h('button', { type: 'button', class: 'dv-theme-toggle', 'aria-label': 'Toggle dark mode', onclick: toggleTheme, text: state.theme === 'dark' ? '
|
|
1371
|
+
h('button', { type: 'button', class: 'dv-theme-toggle', 'aria-label': 'Toggle dark mode', onclick: toggleTheme, text: state.theme === 'dark' ? '☀' : '☾' }, [])
|
|
1283
1372
|
].filter(Boolean))
|
|
1284
1373
|
];
|
|
1285
1374
|
mount('dv-topbar', h('div', { class: 'dv-topbar-inner' }, kids));
|
|
@@ -1325,7 +1414,7 @@ var APP_SCRIPT = `
|
|
|
1325
1414
|
kids.push(h('span', { class: 'dv-nav-meta' }, [
|
|
1326
1415
|
badge ? h('span', { class: 'dv-badge dv-badge-' + badge, text: badge }, []) : null,
|
|
1327
1416
|
h('span', { class: 'dv-nav-score', style: 'color:' + BAND_COLOR[band], text: String(route.score) }, []),
|
|
1328
|
-
h('span', { class: 'dv-nav-sum', text: summary.length ? summary.join('
|
|
1417
|
+
h('span', { class: 'dv-nav-sum', text: summary.length ? summary.join(' · ') : 'no issues' }, [])
|
|
1329
1418
|
].filter(Boolean)));
|
|
1330
1419
|
}
|
|
1331
1420
|
return h('div', {
|
|
@@ -1352,7 +1441,7 @@ var APP_SCRIPT = `
|
|
|
1352
1441
|
state.routeBySlug = {};
|
|
1353
1442
|
|
|
1354
1443
|
// mount() clears and rebuilds the whole sidebar, including the <input> itself, so
|
|
1355
|
-
// a naive re-render on every keystroke drops focus and cursor position
|
|
1444
|
+
// a naive re-render on every keystroke drops focus and cursor position — capture
|
|
1356
1445
|
// them before rebuilding and restore them on the freshly-created input afterward.
|
|
1357
1446
|
var prevSearch = document.querySelector('.dv-search');
|
|
1358
1447
|
var hadFocus = !!prevSearch && document.activeElement === prevSearch;
|
|
@@ -1362,7 +1451,7 @@ var APP_SCRIPT = `
|
|
|
1362
1451
|
var searchInput = h('input', {
|
|
1363
1452
|
type: 'search',
|
|
1364
1453
|
class: 'dv-search',
|
|
1365
|
-
placeholder: 'Search routes or rules
|
|
1454
|
+
placeholder: 'Search routes or rules…',
|
|
1366
1455
|
value: state.search,
|
|
1367
1456
|
oninput: function (e) { state.search = e.target.value; renderSidebar(); }
|
|
1368
1457
|
}, []);
|
|
@@ -1474,7 +1563,7 @@ var APP_SCRIPT = `
|
|
|
1474
1563
|
var band = scoreBand(c.score);
|
|
1475
1564
|
var weight = s.report.weights[cat];
|
|
1476
1565
|
var name = CATEGORY_NAMES[cat] || cat;
|
|
1477
|
-
// keys/affectedKeys are absent on hand-built snapshots (older fixtures, tests)
|
|
1566
|
+
// keys/affectedKeys are absent on hand-built snapshots (older fixtures, tests) —
|
|
1478
1567
|
// render nothing rather than "undefined of undefined". 0 affected of N keys is still
|
|
1479
1568
|
// rendered: on a real project that's the signal a thin score can't give, that the
|
|
1480
1569
|
// category is clean project-wide and not just on the one key it happened to look at
|
|
@@ -1506,7 +1595,7 @@ var APP_SCRIPT = `
|
|
|
1506
1595
|
});
|
|
1507
1596
|
var body = entries.length
|
|
1508
1597
|
? entries.map(function (e) { return renderFinding(e.issue, e.route); })
|
|
1509
|
-
: [h('p', { class: 'dv-empty', text: totalCount ? 'No issues match the current filter.' : 'No issues found
|
|
1598
|
+
: [h('p', { class: 'dv-empty', text: totalCount ? 'No issues match the current filter.' : 'No issues found — nice work!' }, [])];
|
|
1510
1599
|
var findings = h('section', { class: 'dv-section' }, [h('h2', { text: 'Findings' }, [])].concat(body));
|
|
1511
1600
|
return h('div', { class: 'dv-overview' }, [gauge, h('div', { class: 'dv-cats' }, cats), chips, findings].filter(Boolean));
|
|
1512
1601
|
}
|
|
@@ -1590,214 +1679,32 @@ var APP_SCRIPT = `
|
|
|
1590
1679
|
boot();
|
|
1591
1680
|
})();
|
|
1592
1681
|
`;
|
|
1682
|
+
/** The shell HTML: empty sidebar/detail/topbar containers, the stylesheet, the
|
|
1683
|
+
* client script, and the snapshot embedded as JSON for the client's first paint. */
|
|
1593
1684
|
function renderAppShell(snapshot) {
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1685
|
+
const safe = {
|
|
1686
|
+
...snapshot,
|
|
1687
|
+
report: sanitizeReport(snapshot.report)
|
|
1688
|
+
};
|
|
1689
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>${snapshot.live ? "svelte-vitals dashboard" : "svelte-vitals report"}</title><style>${APP_STYLE}</style></head><body><div class="dv-app" id="dv-app"><header class="dv-topbar" id="dv-topbar"></header><nav class="dv-sidebar" id="dv-sidebar"></nav><main class="dv-detail" id="dv-detail"></main></div><script type="application/json" id="svelte-vitals-data">${embedJson(safe)}<\/script><script>${APP_SCRIPT}<\/script></body></html>`;
|
|
1597
1690
|
}
|
|
1691
|
+
/**
|
|
1692
|
+
* Static (non-live) document over a prebuilt JsonReport — kept as the public name the
|
|
1693
|
+
* html reporter has always exported.
|
|
1694
|
+
*/
|
|
1598
1695
|
function buildHtmlDocument(report, meta) {
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1696
|
+
return renderAppShell({
|
|
1697
|
+
report,
|
|
1698
|
+
badges: {},
|
|
1699
|
+
analyzing: false,
|
|
1700
|
+
sequence: 0,
|
|
1701
|
+
live: false,
|
|
1702
|
+
meta
|
|
1703
|
+
});
|
|
1607
1704
|
}
|
|
1705
|
+
/** Render results as the self-contained HTML report (the CLI's `--reporter html`). */
|
|
1608
1706
|
function formatHtmlReport(results, config, meta) {
|
|
1609
|
-
|
|
1707
|
+
return buildHtmlDocument(buildJsonReport(results, config, meta), meta);
|
|
1610
1708
|
}
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
APP_STYLE,
|
|
1614
|
-
BAND_COLOR,
|
|
1615
|
-
CHILD_NODE_KEYS,
|
|
1616
|
-
IDREF_ATTRS,
|
|
1617
|
-
LANDMARK_ROLES,
|
|
1618
|
-
READ_CONCURRENCY,
|
|
1619
|
-
ROBOTS_SOURCE_PATHS,
|
|
1620
|
-
SITEMAP_SOURCE_PATHS,
|
|
1621
|
-
SVELTE_CONFIG_FILES,
|
|
1622
|
-
VITE_CONFIG_FILES,
|
|
1623
|
-
a11yAccessibleName,
|
|
1624
|
-
a11yDeprecatedAria,
|
|
1625
|
-
a11yDeprecatedAttr,
|
|
1626
|
-
a11yDeprecatedElement,
|
|
1627
|
-
a11yDisallowedAriaProps,
|
|
1628
|
-
a11yDisallowedElement,
|
|
1629
|
-
a11yDoctype,
|
|
1630
|
-
a11yDuplicateLandmark,
|
|
1631
|
-
a11yIdDuplication,
|
|
1632
|
-
a11yInteractiveNesting,
|
|
1633
|
-
a11yInvalidAriaValue,
|
|
1634
|
-
a11yInvalidRole,
|
|
1635
|
-
a11yLabelHasControl,
|
|
1636
|
-
a11yNoMissingIdRef,
|
|
1637
|
-
a11yPermittedContents,
|
|
1638
|
-
a11yPlaceholderLabelOption,
|
|
1639
|
-
a11yRequireDatetime,
|
|
1640
|
-
a11yRequiredAriaProps,
|
|
1641
|
-
a11yRequiredElement,
|
|
1642
|
-
a11yTopLevelLandmark,
|
|
1643
|
-
a11yUnknownAriaAttribute,
|
|
1644
|
-
a11yUnverifiedIdRef,
|
|
1645
|
-
a11yUseList,
|
|
1646
|
-
allRules,
|
|
1647
|
-
applyInlineDirectives,
|
|
1648
|
-
applyOverrides,
|
|
1649
|
-
applyRuleSeverities,
|
|
1650
|
-
architectureComponentSize,
|
|
1651
|
-
architectureDirectoryNaming,
|
|
1652
|
-
architectureDocLinkTarget,
|
|
1653
|
-
architecturePrivateScopeImport,
|
|
1654
|
-
architecturePropCount,
|
|
1655
|
-
architectureReservedDirectoryNames,
|
|
1656
|
-
architectureReservedNamePlacement,
|
|
1657
|
-
architectureRouteComponentImport,
|
|
1658
|
-
architectureUnitEntryFile,
|
|
1659
|
-
attrText,
|
|
1660
|
-
attrTextOf,
|
|
1661
|
-
attrValue,
|
|
1662
|
-
attrValueOf,
|
|
1663
|
-
buildHtmlDocument,
|
|
1664
|
-
buildJsonReport,
|
|
1665
|
-
classify,
|
|
1666
|
-
collectComponentFacts,
|
|
1667
|
-
collectKitModuleFacts,
|
|
1668
|
-
collectSourceFiles,
|
|
1669
|
-
collectSuppressions,
|
|
1670
|
-
compileOverrides,
|
|
1671
|
-
computeHealth,
|
|
1672
|
-
computeScore,
|
|
1673
|
-
correctnessBasePathNavigation,
|
|
1674
|
-
correctnessCheckableBindValue,
|
|
1675
|
-
correctnessEachIndexKey,
|
|
1676
|
-
correctnessEachKey,
|
|
1677
|
-
correctnessEffectAsDerived,
|
|
1678
|
-
correctnessEffectAsOnMount,
|
|
1679
|
-
correctnessInstanceBrowserGlobal,
|
|
1680
|
-
correctnessNonreactiveBuiltinState,
|
|
1681
|
-
correctnessOrphanEffect,
|
|
1682
|
-
correctnessOrphanLifecycle,
|
|
1683
|
-
correctnessPropMutation,
|
|
1684
|
-
correctnessServerBrowserGlobal,
|
|
1685
|
-
correctnessStalePropDerivation,
|
|
1686
|
-
correctnessUnmutatedState,
|
|
1687
|
-
decodeFragmentId,
|
|
1688
|
-
defaultConfig,
|
|
1689
|
-
defaultProject,
|
|
1690
|
-
docsUrlFor,
|
|
1691
|
-
effectiveSeverity,
|
|
1692
|
-
emptyComponentFacts,
|
|
1693
|
-
emptyKitModuleFacts,
|
|
1694
|
-
escapeHtml,
|
|
1695
|
-
explainRule,
|
|
1696
|
-
findAttr,
|
|
1697
|
-
findKitAliasesInSvelteConfig,
|
|
1698
|
-
findKitPathsBaseInSvelteConfig,
|
|
1699
|
-
findKitPathsBaseInViteConfig,
|
|
1700
|
-
findMinifyDisabled,
|
|
1701
|
-
foldOccurrences,
|
|
1702
|
-
formatAgentReport,
|
|
1703
|
-
formatConsoleReport,
|
|
1704
|
-
formatFailedRuleWarning,
|
|
1705
|
-
formatGithubReport,
|
|
1706
|
-
formatHtmlReport,
|
|
1707
|
-
formatJsonReport,
|
|
1708
|
-
formatMarkdownReport,
|
|
1709
|
-
formatSarifReport,
|
|
1710
|
-
hasFailureAtOrAbove,
|
|
1711
|
-
headTagRule,
|
|
1712
|
-
imageRule,
|
|
1713
|
-
intOption,
|
|
1714
|
-
isMentionedAnywhere,
|
|
1715
|
-
isPenalized,
|
|
1716
|
-
isTopFragment,
|
|
1717
|
-
lineOf,
|
|
1718
|
-
linkRule,
|
|
1719
|
-
listOption,
|
|
1720
|
-
mapOption,
|
|
1721
|
-
noColorPalette,
|
|
1722
|
-
overrideMatches,
|
|
1723
|
-
parseComponentFacts,
|
|
1724
|
-
parseKitModuleFacts,
|
|
1725
|
-
parseSvelte,
|
|
1726
|
-
performanceFontPreloadCrossorigin,
|
|
1727
|
-
performanceHeavyImport,
|
|
1728
|
-
performanceImageDimensions,
|
|
1729
|
-
performanceImageLoadingHint,
|
|
1730
|
-
performanceLcpImage,
|
|
1731
|
-
performanceLoadWaterfall,
|
|
1732
|
-
performanceMinifyDisabled,
|
|
1733
|
-
performanceNamespaceImport,
|
|
1734
|
-
performancePreconnect,
|
|
1735
|
-
performancePreloadMissingAs,
|
|
1736
|
-
performanceRenderBlockingScript,
|
|
1737
|
-
performanceResponsiveImage,
|
|
1738
|
-
performanceSequentialAwaits,
|
|
1739
|
-
performanceStateRaw,
|
|
1740
|
-
renderAppShell,
|
|
1741
|
-
resolveKitAliases,
|
|
1742
|
-
resolveKitPathsBase,
|
|
1743
|
-
resolveRepoLocalPath,
|
|
1744
|
-
resolveRuleOptions,
|
|
1745
|
-
resolveRunesModuleSpecifier,
|
|
1746
|
-
runRules,
|
|
1747
|
-
safeHref,
|
|
1748
|
-
scoreBand,
|
|
1749
|
-
scoreColor,
|
|
1750
|
-
scoresByCategory,
|
|
1751
|
-
securityHandlerStateWrite,
|
|
1752
|
-
securityJavascriptUrl,
|
|
1753
|
-
securityRawHtml,
|
|
1754
|
-
securityServerModuleState,
|
|
1755
|
-
securitySharedStateImport,
|
|
1756
|
-
selectRules,
|
|
1757
|
-
seoCanonicalUrl,
|
|
1758
|
-
seoCharset,
|
|
1759
|
-
seoDescriptionLength,
|
|
1760
|
-
seoDescriptionPresence,
|
|
1761
|
-
seoDuplicateDescription,
|
|
1762
|
-
seoDuplicateTitle,
|
|
1763
|
-
seoHeadingLevelSkip,
|
|
1764
|
-
seoHreflang,
|
|
1765
|
-
seoHtmlLang,
|
|
1766
|
-
seoImageAlt,
|
|
1767
|
-
seoIndexability,
|
|
1768
|
-
seoJsonLd,
|
|
1769
|
-
seoJsonLdDateFormat,
|
|
1770
|
-
seoJsonLdDeprecatedType,
|
|
1771
|
-
seoJsonLdPlaceholder,
|
|
1772
|
-
seoJsonLdRelativeUrl,
|
|
1773
|
-
seoJsonLdRequiredProps,
|
|
1774
|
-
seoJsonLdValidity,
|
|
1775
|
-
seoOgDescription,
|
|
1776
|
-
seoOgImage,
|
|
1777
|
-
seoOgTitle,
|
|
1778
|
-
seoOgUrl,
|
|
1779
|
-
seoRobotsTxt,
|
|
1780
|
-
seoSingleH1,
|
|
1781
|
-
seoSitemapInRobots,
|
|
1782
|
-
seoSitemapXml,
|
|
1783
|
-
seoSsrDisabled,
|
|
1784
|
-
seoTitleLength,
|
|
1785
|
-
seoTitlePresence,
|
|
1786
|
-
seoTwitterCard,
|
|
1787
|
-
seoViewport,
|
|
1788
|
-
settingOptions,
|
|
1789
|
-
settingSeverity,
|
|
1790
|
-
shouldSkipRangeCheck,
|
|
1791
|
-
skippedFileWarnings,
|
|
1792
|
-
splitTokens,
|
|
1793
|
-
stripTextDirective,
|
|
1794
|
-
summarize,
|
|
1795
|
-
terminalSafe,
|
|
1796
|
-
textFromNodes,
|
|
1797
|
-
unknownDirectiveIds,
|
|
1798
|
-
validateRuleOptions,
|
|
1799
|
-
validateRuleSetting,
|
|
1800
|
-
valueFromNodes,
|
|
1801
|
-
withFailedRulesOff,
|
|
1802
|
-
withReadLimit
|
|
1803
|
-
};
|
|
1709
|
+
//#endregion
|
|
1710
|
+
export { APP_SCRIPT, APP_STYLE, BAND_COLOR, CHILD_NODE_KEYS, IDREF_ATTRS, LANDMARK_ROLES, READ_CONCURRENCY, ROBOTS_SOURCE_PATHS, SITEMAP_SOURCE_PATHS, SVELTE_CONFIG_FILES, VITE_CONFIG_FILES, a11yAccessibleName, a11yDeprecatedAria, a11yDeprecatedAttr, a11yDeprecatedElement, a11yDisallowedAriaProps, a11yDisallowedElement, a11yDoctype, a11yDuplicateLandmark, a11yIdDuplication, a11yInteractiveNesting, a11yInvalidAriaValue, a11yInvalidRole, a11yLabelHasControl, a11yNoMissingIdRef, a11yPermittedContents, a11yPlaceholderLabelOption, a11yRequireDatetime, a11yRequiredAriaProps, a11yRequiredElement, a11yTopLevelLandmark, a11yUnknownAriaAttribute, a11yUnverifiedIdRef, a11yUseList, allRules, applyInlineDirectives, applyOverrides, applyRuleSeverities, architectureComponentSize, architectureDirectoryNaming, architectureDocLinkTarget, architecturePrivateScopeImport, architecturePropCount, architectureReservedDirectoryNames, architectureReservedNamePlacement, architectureRouteComponentImport, architectureUnitEntryFile, attrText, attrTextOf, attrValue, attrValueOf, buildHtmlDocument, buildJsonReport, classify, collectComponentFacts, collectKitModuleFacts, collectSourceFiles, collectSuppressions, compileOverrides, computeHealth, computeScore, correctnessBasePathNavigation, correctnessCheckableBindValue, correctnessEachIndexKey, correctnessEachKey, correctnessEffectAsDerived, correctnessEffectAsOnMount, correctnessInstanceBrowserGlobal, correctnessNonreactiveBuiltinState, correctnessOrphanEffect, correctnessOrphanLifecycle, correctnessPropMutation, correctnessServerBrowserGlobal, correctnessStalePropDerivation, correctnessUnmutatedState, decodeFragmentId, defaultConfig, defaultProject, docsUrlFor, effectiveSeverity, emptyComponentFacts, emptyKitModuleFacts, escapeHtml, explainRule, findAttr, findKitAliasesInSvelteConfig, findKitPathsBaseInSvelteConfig, findKitPathsBaseInViteConfig, findMinifyDisabled, foldOccurrences, formatAgentReport, formatConsoleReport, formatFailedRuleWarning, formatGithubReport, formatHtmlReport, formatJsonReport, formatMarkdownReport, formatSarifReport, hasFailureAtOrAbove, headTagRule, imageRule, intOption, isMentionedAnywhere, isPenalized, isTopFragment, lineOf, linkRule, listOption, mapOption, noColorPalette, overrideMatches, parseComponentFacts, parseKitModuleFacts, parseSvelte, performanceFontPreloadCrossorigin, performanceHeavyImport, performanceImageDimensions, performanceImageLoadingHint, performanceLcpImage, performanceLoadWaterfall, performanceMinifyDisabled, performanceNamespaceImport, performancePreconnect, performancePreloadMissingAs, performanceRenderBlockingScript, performanceResponsiveImage, performanceSequentialAwaits, performanceStateRaw, renderAppShell, resolveKitAliases, resolveKitPathsBase, resolveRepoLocalPath, resolveRuleOptions, resolveRunesModuleSpecifier, runRules, safeHref, scoreBand, scoreColor, scoresByCategory, securityHandlerStateWrite, securityJavascriptUrl, securityRawHtml, securityServerModuleState, securitySharedStateImport, selectRules, seoCanonicalUrl, seoCharset, seoDescriptionLength, seoDescriptionPresence, seoDuplicateDescription, seoDuplicateTitle, seoHeadingLevelSkip, seoHreflang, seoHtmlLang, seoImageAlt, seoIndexability, seoJsonLd, seoJsonLdDateFormat, seoJsonLdDeprecatedType, seoJsonLdPlaceholder, seoJsonLdRelativeUrl, seoJsonLdRequiredProps, seoJsonLdValidity, seoOgDescription, seoOgImage, seoOgTitle, seoOgUrl, seoRobotsTxt, seoSingleH1, seoSitemapInRobots, seoSitemapXml, seoSsrDisabled, seoTitleLength, seoTitlePresence, seoTwitterCard, seoViewport, settingOptions, settingSeverity, shouldSkipRangeCheck, skippedFileWarnings, splitTokens, stripTextDirective, summarize, terminalSafe, textFromNodes, unknownDirectiveIds, validateRuleOptions, validateRuleSetting, valueFromNodes, withFailedRulesOff, withReadLimit };
|