@svelte-vitals/core 0.46.0 → 0.47.1
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 +380 -264
- package/dist/internal.js +976 -1069
- package/dist/json-OdgY5Bpw.d.ts +1287 -0
- package/dist/markdown-CUNuKq3Z.js +9895 -0
- package/package.json +2 -2
- package/dist/chunk-M7RA6QQW.js +0 -7994
- package/dist/index-Cx3Mi_d4.d.ts +0 -1255
package/dist/internal.js
CHANGED
|
@@ -1,955 +1,1034 @@
|
|
|
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
|
-
a11yUseList,
|
|
28
|
-
allRules,
|
|
29
|
-
applyOverrides,
|
|
30
|
-
applyRuleSeverities,
|
|
31
|
-
architectureComponentSize,
|
|
32
|
-
architectureDirectoryNaming,
|
|
33
|
-
architectureDocLinkTarget,
|
|
34
|
-
architecturePrivateScopeImport,
|
|
35
|
-
architecturePropCount,
|
|
36
|
-
architectureReservedDirectoryNames,
|
|
37
|
-
architectureReservedNamePlacement,
|
|
38
|
-
architectureRouteComponentImport,
|
|
39
|
-
architectureUnitEntryFile,
|
|
40
|
-
attrText,
|
|
41
|
-
attrTextOf,
|
|
42
|
-
attrValue,
|
|
43
|
-
attrValueOf,
|
|
44
|
-
buildJsonReport,
|
|
45
|
-
classify,
|
|
46
|
-
collectNamedImportAliases,
|
|
47
|
-
collectSuppressions,
|
|
48
|
-
collectTopLevelBindings,
|
|
49
|
-
compileOverrides,
|
|
50
|
-
computeHealth,
|
|
51
|
-
computeScore,
|
|
52
|
-
correctnessBasePathNavigation,
|
|
53
|
-
correctnessCheckableBindValue,
|
|
54
|
-
correctnessEachIndexKey,
|
|
55
|
-
correctnessEachKey,
|
|
56
|
-
correctnessEffectAsDerived,
|
|
57
|
-
correctnessEffectAsOnMount,
|
|
58
|
-
correctnessInstanceBrowserGlobal,
|
|
59
|
-
correctnessNonreactiveBuiltinState,
|
|
60
|
-
correctnessOrphanEffect,
|
|
61
|
-
correctnessOrphanLifecycle,
|
|
62
|
-
correctnessPropMutation,
|
|
63
|
-
correctnessServerBrowserGlobal,
|
|
64
|
-
correctnessStalePropDerivation,
|
|
65
|
-
correctnessUnmutatedState,
|
|
66
|
-
decodeFragmentId,
|
|
67
|
-
defaultConfig,
|
|
68
|
-
defaultProject,
|
|
69
|
-
docsUrlFor,
|
|
70
|
-
effectiveSeverity,
|
|
71
|
-
explainRule,
|
|
72
|
-
findAttr,
|
|
73
|
-
foldOccurrences,
|
|
74
|
-
formatFailedRuleWarning,
|
|
75
|
-
formatGithubReport,
|
|
76
|
-
formatJsonReport,
|
|
77
|
-
formatMarkdownReport,
|
|
78
|
-
hasFailureAtOrAbove,
|
|
79
|
-
headTagRule,
|
|
80
|
-
imageRule,
|
|
81
|
-
intOption,
|
|
82
|
-
isMentionedAnywhere,
|
|
83
|
-
isPenalized,
|
|
84
|
-
isTopFragment,
|
|
85
|
-
lineOf,
|
|
86
|
-
linkRule,
|
|
87
|
-
listOption,
|
|
88
|
-
mapOption,
|
|
89
|
-
mdEscape,
|
|
90
|
-
messageText,
|
|
91
|
-
overrideMatches,
|
|
92
|
-
parseComponentFacts,
|
|
93
|
-
parseInMemoryExports,
|
|
94
|
-
parseKitModuleFacts,
|
|
95
|
-
parseModuleProgram,
|
|
96
|
-
parseSvelte,
|
|
97
|
-
performanceFontPreloadCrossorigin,
|
|
98
|
-
performanceHeavyImport,
|
|
99
|
-
performanceImageDimensions,
|
|
100
|
-
performanceImageLoadingHint,
|
|
101
|
-
performanceLcpImage,
|
|
102
|
-
performanceLoadWaterfall,
|
|
103
|
-
performanceMinifyDisabled,
|
|
104
|
-
performanceNamespaceImport,
|
|
105
|
-
performancePreconnect,
|
|
106
|
-
performancePreloadMissingAs,
|
|
107
|
-
performanceRenderBlockingScript,
|
|
108
|
-
performanceResponsiveImage,
|
|
109
|
-
performanceSequentialAwaits,
|
|
110
|
-
performanceStateRaw,
|
|
111
|
-
resolveRepoLocalPath,
|
|
112
|
-
resolveRuleOptions,
|
|
113
|
-
resolveRunesModuleSpecifier,
|
|
114
|
-
ruleMetaById,
|
|
115
|
-
scoresByCategory,
|
|
116
|
-
securityHandlerStateWrite,
|
|
117
|
-
securityJavascriptUrl,
|
|
118
|
-
securityRawHtml,
|
|
119
|
-
securityServerModuleState,
|
|
120
|
-
securitySharedStateImport,
|
|
121
|
-
selectRules,
|
|
122
|
-
seoCanonicalUrl,
|
|
123
|
-
seoCharset,
|
|
124
|
-
seoDescriptionLength,
|
|
125
|
-
seoDescriptionPresence,
|
|
126
|
-
seoDuplicateDescription,
|
|
127
|
-
seoDuplicateTitle,
|
|
128
|
-
seoHeadingLevelSkip,
|
|
129
|
-
seoHreflang,
|
|
130
|
-
seoHtmlLang,
|
|
131
|
-
seoImageAlt,
|
|
132
|
-
seoIndexability,
|
|
133
|
-
seoJsonLd,
|
|
134
|
-
seoJsonLdDateFormat,
|
|
135
|
-
seoJsonLdDeprecatedType,
|
|
136
|
-
seoJsonLdPlaceholder,
|
|
137
|
-
seoJsonLdRelativeUrl,
|
|
138
|
-
seoJsonLdRequiredProps,
|
|
139
|
-
seoJsonLdValidity,
|
|
140
|
-
seoOgDescription,
|
|
141
|
-
seoOgImage,
|
|
142
|
-
seoOgTitle,
|
|
143
|
-
seoOgUrl,
|
|
144
|
-
seoRobotsTxt,
|
|
145
|
-
seoSingleH1,
|
|
146
|
-
seoSitemapInRobots,
|
|
147
|
-
seoSitemapXml,
|
|
148
|
-
seoSsrDisabled,
|
|
149
|
-
seoTitleLength,
|
|
150
|
-
seoTitlePresence,
|
|
151
|
-
seoTwitterCard,
|
|
152
|
-
seoViewport,
|
|
153
|
-
settingOptions,
|
|
154
|
-
settingSeverity,
|
|
155
|
-
severityToSarifLevel,
|
|
156
|
-
shouldSkipRangeCheck,
|
|
157
|
-
splitTokens,
|
|
158
|
-
stripTextDirective,
|
|
159
|
-
summarize,
|
|
160
|
-
terminalSafe,
|
|
161
|
-
textFromNodes,
|
|
162
|
-
unwrapTs,
|
|
163
|
-
validateRuleOptions,
|
|
164
|
-
validateRuleSetting,
|
|
165
|
-
valueFromNodes,
|
|
166
|
-
withFailedRulesOff
|
|
167
|
-
} from "./chunk-M7RA6QQW.js";
|
|
168
|
-
|
|
169
|
-
// 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). */
|
|
170
4
|
function directiveFor(index, r) {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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)));
|
|
174
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
|
+
*/
|
|
175
23
|
function applyInlineDirectives(results, index, rules, config) {
|
|
176
|
-
|
|
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
|
-
|
|
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;
|
|
206
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
|
+
*/
|
|
207
67
|
function unknownDirectiveIds(index, rules) {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
for (const id of d.ruleIds ?? []) {
|
|
213
|
-
if (!known.has(id)) seen.add(`${file}:${d.line - 1} disables unknown rule "${id}"`);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
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}"`);
|
|
71
|
+
return [...seen].sort();
|
|
218
72
|
}
|
|
219
|
-
|
|
220
|
-
|
|
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
|
+
*/
|
|
221
83
|
function skippedFileWarnings(facts) {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
"this is an environment problem, not a code one \u2014 check file permissions and the open-file limit (`ulimit -n`)."
|
|
234
|
-
);
|
|
235
|
-
}
|
|
236
|
-
if (unparsed.length > 0) {
|
|
237
|
-
out.push(
|
|
238
|
-
`skipped ${unparsed.length} file(s) that could not be parsed: ${list(unparsed)}`,
|
|
239
|
-
"findings for these files are unavailable until they parse."
|
|
240
|
-
);
|
|
241
|
-
}
|
|
242
|
-
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;
|
|
243
95
|
}
|
|
244
|
-
|
|
245
|
-
|
|
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
|
+
*/
|
|
246
104
|
function emptyComponentFacts(file) {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
+
};
|
|
272
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
|
+
*/
|
|
273
138
|
async function collectComponentFacts(rt, cwd) {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
+
}));
|
|
290
163
|
}
|
|
291
|
-
|
|
292
|
-
|
|
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
|
+
*/
|
|
293
179
|
async function collectSourceFiles(rt, cwd) {
|
|
294
|
-
|
|
295
|
-
return files.slice().sort();
|
|
180
|
+
return (await rt.glob("src/**/*", cwd)).slice().sort();
|
|
296
181
|
}
|
|
297
|
-
|
|
298
|
-
|
|
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). */
|
|
299
185
|
function emptyKitModuleFacts(file, kind) {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
+
};
|
|
313
199
|
}
|
|
200
|
+
/** 'server' when the file only ever runs on the server; 'universal' for +page.ts/+layout.ts. */
|
|
314
201
|
function kindOf(file) {
|
|
315
|
-
|
|
316
|
-
|
|
202
|
+
const base = file.split("/").pop() ?? file;
|
|
203
|
+
return base.includes(".server.") || base.startsWith("+server.") ? "server" : "universal";
|
|
317
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
|
+
*/
|
|
318
215
|
async function collectKitModuleFacts(rt, cwd, aliases) {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
+
})));
|
|
344
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
|
+
*/
|
|
345
255
|
function moduleCandidates(repoPath) {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
+
];
|
|
349
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
|
+
*/
|
|
350
270
|
async function inMemoryExportsOf(rt, cwd, repoPath) {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
}
|
|
359
|
-
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();
|
|
360
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
|
+
*/
|
|
361
285
|
async function arbitrateServerStoreWrites(rt, cwd, facts) {
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
+
});
|
|
375
301
|
}
|
|
376
|
-
|
|
377
|
-
|
|
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
|
+
*/
|
|
378
313
|
function propOf(obj, name) {
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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;
|
|
390
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
|
+
*/
|
|
391
336
|
function unwrapToObjectExpression(expr, bindings) {
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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;
|
|
408
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
|
+
*/
|
|
409
361
|
function findExportedExpression(program) {
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
cjsExported = expr.right;
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
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;
|
|
426
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
|
+
*/
|
|
427
385
|
function resolveConfigObject(program) {
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
386
|
+
const exported = findExportedExpression(program);
|
|
387
|
+
if (!exported) return void 0;
|
|
388
|
+
return unwrapToObjectExpression(exported, collectTopLevelBindings(program));
|
|
431
389
|
}
|
|
432
|
-
|
|
433
|
-
|
|
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
|
+
*/
|
|
434
397
|
function findMinifyDisabled(source) {
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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) };
|
|
452
415
|
}
|
|
453
|
-
|
|
454
|
-
|
|
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
|
+
*/
|
|
455
423
|
function basePathOf(kitConfig, bindings) {
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
}
|
|
465
|
-
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 {};
|
|
466
432
|
}
|
|
433
|
+
/** A property's key when it is a plain (non-computed) string or identifier key. */
|
|
467
434
|
function keyNameOf(p) {
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
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;
|
|
472
438
|
}
|
|
439
|
+
/** A property's value when it is a string literal, else undefined. */
|
|
473
440
|
function stringValueOf(p) {
|
|
474
|
-
|
|
475
|
-
|
|
441
|
+
const v = unwrapTs(p.value);
|
|
442
|
+
return v.type === "Literal" && typeof v.value === "string" ? v.value : void 0;
|
|
476
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
|
+
*/
|
|
477
452
|
function aliasEntriesOf(kitConfig, bindings) {
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
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;
|
|
496
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
|
+
*/
|
|
497
480
|
function filesLibOf(kitConfig, bindings) {
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
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;
|
|
503
486
|
}
|
|
487
|
+
/** `kit.alias` and `kit.files.lib` from a `svelte.config.{js,ts}` source. */
|
|
504
488
|
function findKitAliasesInSvelteConfig(source) {
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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
|
+
};
|
|
514
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
|
+
*/
|
|
515
509
|
function normalizeAliasValue(value) {
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
return noStar.replace(/\/+$/, "");
|
|
510
|
+
const posix = value.replace(/\\/g, "/");
|
|
511
|
+
return (posix.endsWith("/*") ? posix.slice(0, -2) : posix).replace(/\/+$/, "");
|
|
519
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
|
+
*/
|
|
520
522
|
function compileKitAliases(raw) {
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
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;
|
|
534
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
|
+
*/
|
|
535
548
|
function resolveKitAliases(viteConfig, svelteConfig) {
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
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));
|
|
539
552
|
}
|
|
553
|
+
/** Parse a config source to a program, or undefined when it cannot be parsed. */
|
|
540
554
|
function programOf(source, filename) {
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
555
|
+
try {
|
|
556
|
+
return parseModuleProgram(source, filename).program ?? void 0;
|
|
557
|
+
} catch {
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
546
560
|
}
|
|
561
|
+
/** `kit.paths.base` from a `svelte.config.{js,ts}` source. */
|
|
547
562
|
function findKitPathsBaseInSvelteConfig(source) {
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
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;
|
|
556
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
|
+
*/
|
|
557
577
|
function sveltekitLocalNames(program) {
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
578
|
+
const out = collectNamedImportAliases(program, "@sveltejs/kit/vite", /* @__PURE__ */ new Set(["sveltekit"]));
|
|
579
|
+
if (out.size === 0) out.add("sveltekit");
|
|
580
|
+
return out;
|
|
561
581
|
}
|
|
582
|
+
/** SvelteKit config passed to the `sveltekit()` plugin in a Vite config source (since Kit 2.62). */
|
|
562
583
|
function findKitPathsBaseInViteConfig(source) {
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
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;
|
|
586
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
|
+
*/
|
|
587
616
|
function resolveKitPathsBase(viteConfig, svelteConfig) {
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
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;
|
|
598
631
|
}
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
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"
|
|
605
644
|
];
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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"
|
|
610
650
|
];
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
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"
|
|
618
659
|
];
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
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
|
+
};
|
|
639
695
|
}
|
|
640
|
-
|
|
641
|
-
|
|
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
|
+
*/
|
|
642
705
|
async function runRules(rules, ctx) {
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
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
|
+
};
|
|
660
729
|
}
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
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
|
|
670
740
|
};
|
|
741
|
+
/** Green ≥ 90, yellow ≥ 70, red otherwise — for a 0–100 score. */
|
|
671
742
|
function scoreColor(p, score) {
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
743
|
+
if (score >= 90) return p.green;
|
|
744
|
+
if (score >= 70) return p.yellow;
|
|
745
|
+
return p.red;
|
|
675
746
|
}
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
747
|
+
//#endregion
|
|
748
|
+
//#region src/reporter/console.ts
|
|
749
|
+
const RULE = "────────────────────────";
|
|
750
|
+
const SEVERITY_TITLE = {
|
|
751
|
+
critical: "Critical",
|
|
752
|
+
warning: "Warnings",
|
|
753
|
+
info: "Info"
|
|
683
754
|
};
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
755
|
+
const CATEGORY_LABEL = {
|
|
756
|
+
seo: "SEO",
|
|
757
|
+
performance: "Performance",
|
|
758
|
+
correctness: "Correctness",
|
|
759
|
+
security: "Security",
|
|
760
|
+
architecture: "Architecture",
|
|
761
|
+
a11y: "Accessibility"
|
|
691
762
|
};
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
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. */
|
|
695
767
|
function groupByRule(results) {
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
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));
|
|
703
778
|
}
|
|
704
779
|
function scoreLine(p, label, { score, scoreModel }) {
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
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(" · ")})`)}`;
|
|
709
784
|
}
|
|
710
|
-
|
|
785
|
+
const MAX_ROUTES_BY_ROUTE = 10;
|
|
711
786
|
function byRouteTree(p, results, config, verbose) {
|
|
712
|
-
|
|
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
|
-
`\u2026and ${remaining.length} more route${remaining.length > 1 ? "s" : ""} (avg score ${avgScore}) \u2014 run with --verbose to see all`
|
|
738
|
-
)
|
|
739
|
-
);
|
|
740
|
-
}
|
|
741
|
-
lines.push("");
|
|
742
|
-
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;
|
|
743
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
|
+
*/
|
|
744
818
|
function formatConsoleReport(results, config, options = {}) {
|
|
745
|
-
|
|
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
|
-
const marker = classify(r, config) === "dynamic" ? p.cyan(" \u21AF dynamic") : "";
|
|
804
|
-
const where = r.location ?? r.route;
|
|
805
|
-
const suffix = where ? ` ${terminalSafe(where)}` : "";
|
|
806
|
-
lines.push(`${p.green("\u2713")} ${r.id} ${terminalSafe(r.message)}${marker}${suffix}`);
|
|
807
|
-
}
|
|
808
|
-
}
|
|
809
|
-
lines.push("");
|
|
810
|
-
}
|
|
811
|
-
if (options.byRoute) lines.push(...byRouteTree(p, results, config, options.verbose ?? false));
|
|
812
|
-
if (options.verbose && summary.dynamic > 0) lines.push(p.dim("\u21AF = set dynamically (verified at runtime)."));
|
|
813
|
-
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");
|
|
814
877
|
}
|
|
815
|
-
|
|
816
|
-
|
|
878
|
+
//#endregion
|
|
879
|
+
//#region src/reporter/agent.ts
|
|
880
|
+
/** Render failing findings as an agent-actionable Markdown remediation document (issue #18). */
|
|
817
881
|
function formatAgentReport(results, config) {
|
|
818
|
-
|
|
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
|
-
lines.push(`- Accept: re-run svelte-vitals; ${r.id} passes${r.route ? ` for ${mdEscape(r.route)}` : ""}.`, "");
|
|
854
|
-
}
|
|
855
|
-
}
|
|
856
|
-
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
|
+
lines.push(`### ${r.id} · ${mdEscape(r.message)} (${effectiveSeverity(r, config)})`);
|
|
908
|
+
if (r.fix) {
|
|
909
|
+
lines.push(`- Fix: ${mdEscape(r.fix.description)}`);
|
|
910
|
+
if (r.fix.snippet) lines.push("", "```" + (r.fix.lang ?? "svelte"), r.fix.snippet, "```");
|
|
911
|
+
} else if (r.recommendation) lines.push(`- Fix: ${mdEscape(r.recommendation)}`);
|
|
912
|
+
if (r.docsUrl) lines.push(`- Docs: ${r.docsUrl}`);
|
|
913
|
+
lines.push(`- Accept: re-run svelte-vitals; ${r.id} passes${r.route ? ` for ${mdEscape(r.route)}` : ""}.`, "");
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
return lines.join("\n").replace(/\n+$/, "\n");
|
|
857
917
|
}
|
|
858
|
-
|
|
859
|
-
|
|
918
|
+
//#endregion
|
|
919
|
+
//#region src/reporter/sarif.ts
|
|
920
|
+
/** Render penalized findings as a SARIF 2.1.0 log string (issue #18, design slice 5). */
|
|
860
921
|
function formatSarifReport(results, config, meta) {
|
|
861
|
-
|
|
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
|
-
driver: {
|
|
905
|
-
name: "svelte-vitals",
|
|
906
|
-
informationUri: "https://oekazuma.github.io/svelte-vitals",
|
|
907
|
-
version: meta.version,
|
|
908
|
-
rules
|
|
909
|
-
}
|
|
910
|
-
},
|
|
911
|
-
results: sarifResults
|
|
912
|
-
}
|
|
913
|
-
]
|
|
914
|
-
};
|
|
915
|
-
return JSON.stringify(log, null, 2);
|
|
922
|
+
const penalized = results.filter((r) => isPenalized(r.detection, config.treatDynamicAs));
|
|
923
|
+
const rules = [];
|
|
924
|
+
const ruleIndex = /* @__PURE__ */ new Map();
|
|
925
|
+
const sarifResults = penalized.map((r) => {
|
|
926
|
+
if (!ruleIndex.has(r.id)) {
|
|
927
|
+
const m = ruleMetaById(r.id);
|
|
928
|
+
const name = m?.title ?? r.id;
|
|
929
|
+
ruleIndex.set(r.id, rules.length);
|
|
930
|
+
rules.push({
|
|
931
|
+
id: r.id,
|
|
932
|
+
name,
|
|
933
|
+
shortDescription: { text: name },
|
|
934
|
+
helpUri: r.docsUrl ?? m?.docsUrl ?? docsUrlFor(r.id),
|
|
935
|
+
defaultConfiguration: { level: severityToSarifLevel(m?.severity ?? r.severity) }
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
const result = {
|
|
939
|
+
ruleId: r.id,
|
|
940
|
+
ruleIndex: ruleIndex.get(r.id),
|
|
941
|
+
level: severityToSarifLevel(effectiveSeverity(r, config)),
|
|
942
|
+
message: { text: messageText(r) },
|
|
943
|
+
partialFingerprints: { "svelteVitals/v1": `${r.id}:${r.route ?? "project"}${r.line !== void 0 ? `:${r.location ?? ""}:${r.line}` : ""}` }
|
|
944
|
+
};
|
|
945
|
+
if (r.location) result.locations = [{ physicalLocation: {
|
|
946
|
+
artifactLocation: { uri: r.location },
|
|
947
|
+
...r.line !== void 0 ? { region: { startLine: r.line } } : {}
|
|
948
|
+
} }];
|
|
949
|
+
return result;
|
|
950
|
+
});
|
|
951
|
+
const log = {
|
|
952
|
+
$schema: "https://json.schemastore.org/sarif-2.1.0.json",
|
|
953
|
+
version: "2.1.0",
|
|
954
|
+
runs: [{
|
|
955
|
+
tool: { driver: {
|
|
956
|
+
name: "svelte-vitals",
|
|
957
|
+
informationUri: "https://oekazuma.github.io/svelte-vitals",
|
|
958
|
+
version: meta.version,
|
|
959
|
+
rules
|
|
960
|
+
} },
|
|
961
|
+
results: sarifResults
|
|
962
|
+
}]
|
|
963
|
+
};
|
|
964
|
+
return JSON.stringify(log, null, 2);
|
|
916
965
|
}
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
966
|
+
//#endregion
|
|
967
|
+
//#region src/reporter/app-shell.ts
|
|
968
|
+
const BAND_COLOR = {
|
|
969
|
+
good: "#2FA968",
|
|
970
|
+
warn: "#E8A317",
|
|
971
|
+
poor: "#E5484D"
|
|
923
972
|
};
|
|
924
973
|
function scoreBand(score) {
|
|
925
|
-
|
|
974
|
+
return score >= 90 ? "good" : score >= 50 ? "warn" : "poor";
|
|
926
975
|
}
|
|
927
976
|
function escapeHtml(s) {
|
|
928
|
-
|
|
929
|
-
/[&<>"']/g,
|
|
930
|
-
(c) => c === "&" ? "&" : c === "<" ? "<" : c === ">" ? ">" : c === '"' ? """ : "'"
|
|
931
|
-
);
|
|
977
|
+
return s.replace(/[&<>"']/g, (c) => c === "&" ? "&" : c === "<" ? "<" : c === ">" ? ">" : c === "\"" ? """ : "'");
|
|
932
978
|
}
|
|
979
|
+
/**
|
|
980
|
+
* Return the URL only when it uses a safe http/https scheme, else null.
|
|
981
|
+
* Guards a finding's `docsUrl` against `javascript:`/`data:` hrefs — escapeHtml
|
|
982
|
+
* neutralizes attribute breakout but not a malicious scheme. Browsers strip
|
|
983
|
+
* ASCII whitespace (tab/newline/CR) from a URL before resolving its scheme (so
|
|
984
|
+
* `java\tscript:` runs as `javascript:`), so strip whitespace first; anything not
|
|
985
|
+
* plainly http(s):// afterward is rejected. Pure string work — no `URL` global,
|
|
986
|
+
* keeping core runtime-agnostic and lib-minimal.
|
|
987
|
+
*/
|
|
933
988
|
function safeHref(url) {
|
|
934
|
-
|
|
935
|
-
|
|
989
|
+
const normalized = url.replace(/\s/g, "").toLowerCase();
|
|
990
|
+
return /^https?:\/\//.test(normalized) ? url : null;
|
|
936
991
|
}
|
|
992
|
+
/**
|
|
993
|
+
* JSON.stringify escapes `"` inside string values but not `<` or the JS line terminators
|
|
994
|
+
* U+2028/U+2029 — all three matter once the result is embedded inside an inline
|
|
995
|
+
* <script type="application/json"> element: an unescaped `<\/script>` in any finding-derived
|
|
996
|
+
* string (route path, location, recommendation, fix snippet, title) would close the tag
|
|
997
|
+
* early, and U+2028/U+2029 can still break some script-parsing environments.
|
|
998
|
+
*/
|
|
937
999
|
function embedJson(value) {
|
|
938
|
-
|
|
1000
|
+
return JSON.stringify(value).replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
939
1001
|
}
|
|
1002
|
+
/**
|
|
1003
|
+
* The client script renders `docsUrl` straight into an <a href>, so a malicious scheme
|
|
1004
|
+
* (`javascript:`, `data:`) must never reach the embedded snapshot — `buildHtmlDocument`
|
|
1005
|
+
* is a public API fed loosely-typed reports. Sanitized server-side once, here.
|
|
1006
|
+
*/
|
|
940
1007
|
function sanitizeDocsUrl(issue) {
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
1008
|
+
if (issue.docsUrl === void 0) return issue;
|
|
1009
|
+
if (safeHref(issue.docsUrl) !== null) return issue;
|
|
1010
|
+
return {
|
|
1011
|
+
...issue,
|
|
1012
|
+
docsUrl: void 0
|
|
1013
|
+
};
|
|
944
1014
|
}
|
|
945
1015
|
function sanitizeReport(report) {
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
1016
|
+
return {
|
|
1017
|
+
...report,
|
|
1018
|
+
routes: report.routes.map((route) => ({
|
|
1019
|
+
...route,
|
|
1020
|
+
issues: route.issues.map(sanitizeDocsUrl)
|
|
1021
|
+
})),
|
|
1022
|
+
siteIssues: report.siteIssues.map(sanitizeDocsUrl)
|
|
1023
|
+
};
|
|
951
1024
|
}
|
|
952
|
-
|
|
1025
|
+
/**
|
|
1026
|
+
* Hand-authored CSS for the master/detail shell. Reuses the same design-token
|
|
1027
|
+
* names/values as the rest of the project, and adds a dark theme via
|
|
1028
|
+
* `:root[data-theme="dark"]` plus a `prefers-color-scheme` fallback for a
|
|
1029
|
+
* first-ever visit with no stored preference.
|
|
1030
|
+
*/
|
|
1031
|
+
const APP_STYLE = `
|
|
953
1032
|
: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}
|
|
954
1033
|
: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}
|
|
955
1034
|
@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}}
|
|
@@ -1039,7 +1118,7 @@ body{background:var(--ground);color:var(--ink);font-family:var(--sans);line-heig
|
|
|
1039
1118
|
.dv-ai-prompt{margin-top:12px;border:1px solid var(--line);border-radius:8px;overflow:hidden}
|
|
1040
1119
|
.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}
|
|
1041
1120
|
.dv-ai-prompt>summary::-webkit-details-marker{display:none}
|
|
1042
|
-
.dv-ai-prompt>summary::before{content:"
|
|
1121
|
+
.dv-ai-prompt>summary::before{content:"▸";display:inline-block;transition:transform .15s ease}
|
|
1043
1122
|
.dv-ai-prompt[open]>summary::before{transform:rotate(90deg)}
|
|
1044
1123
|
.dv-ai-prompt-body{padding:0 12px 12px;display:flex;flex-direction:column;gap:8px}
|
|
1045
1124
|
.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}
|
|
@@ -1050,13 +1129,22 @@ body{background:var(--ground);color:var(--ink);font-family:var(--sans);line-heig
|
|
|
1050
1129
|
@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)}}
|
|
1051
1130
|
@media (prefers-reduced-motion:reduce){*{transition:none!important}}
|
|
1052
1131
|
`;
|
|
1053
|
-
|
|
1132
|
+
/**
|
|
1133
|
+
* Hand-authored client script for the shell — no bundler, no framework. Parses the
|
|
1134
|
+
* AppSnapshot embedded by renderAppShell, then owns all rendering: sidebar
|
|
1135
|
+
* (search/sort/route list) and detail pane (Overview or a selected route). When the
|
|
1136
|
+
* snapshot says `live`, it additionally re-fetches /data.json on every SSE `update`
|
|
1137
|
+
* and on the EventSource's `open` event (covers the initial connection and every
|
|
1138
|
+
* auto-reconnect, since EventSource replays no missed events) — discarding any
|
|
1139
|
+
* response whose `sequence` isn't newer than what's already rendered.
|
|
1140
|
+
*/
|
|
1141
|
+
const APP_SCRIPT = `
|
|
1054
1142
|
(function(){
|
|
1055
1143
|
var BAND_COLOR = { good: '#2fa968', warn: '#e8a317', poor: '#e5484d' };
|
|
1056
1144
|
var CATEGORY_NAMES = ${JSON.stringify(CATEGORY_LABEL)};
|
|
1057
1145
|
function scoreBand(score) { return score >= 90 ? 'good' : score >= 50 ? 'warn' : 'poor'; }
|
|
1058
1146
|
|
|
1059
|
-
// Same mark as the docs site's hero wordmark (docs/public/wordmark.svg)
|
|
1147
|
+
// Same mark as the docs site's hero wordmark (docs/public/wordmark.svg) — an inline
|
|
1060
1148
|
// copy, not an <img src>, since the dashboard is a single self-contained HTML response
|
|
1061
1149
|
// with no other static assets to serve alongside it. Fixed brand colors (not CSS custom
|
|
1062
1150
|
// properties), matching the docs usage: the wordmark reads the same in both themes.
|
|
@@ -1157,12 +1245,12 @@ var APP_SCRIPT = `
|
|
|
1157
1245
|
return pre;
|
|
1158
1246
|
}
|
|
1159
1247
|
|
|
1160
|
-
// Plain-text, copy-pasteable prompt for a single finding
|
|
1248
|
+
// Plain-text, copy-pasteable prompt for a single finding — same ingredients as the
|
|
1161
1249
|
// agent reporter's per-finding block (rule id, location, recommendation, fix, docs),
|
|
1162
1250
|
// reshaped for a standalone request rather than a whole-project remediation doc.
|
|
1163
1251
|
function buildAiPrompt(issue, route) {
|
|
1164
1252
|
var lines = ['Fix this svelte-vitals finding:', ''];
|
|
1165
|
-
lines.push('- Rule: ' + issue.id + '
|
|
1253
|
+
lines.push('- Rule: ' + issue.id + ' — ' + issue.title + ' (' + issue.severity + ')');
|
|
1166
1254
|
if (route) lines.push('- Route: ' + route);
|
|
1167
1255
|
if (issue.location) {
|
|
1168
1256
|
lines.push('- Location: ' + issue.location + (issue.line !== undefined ? ':' + issue.line : ''));
|
|
@@ -1267,7 +1355,7 @@ var APP_SCRIPT = `
|
|
|
1267
1355
|
var s = state.snapshot;
|
|
1268
1356
|
var findings = s.report.routes.reduce(function (n, r) { return n + r.issues.length; }, 0) + s.report.siteIssues.length;
|
|
1269
1357
|
var kids = [
|
|
1270
|
-
h('button', { type: 'button', class: 'dv-menu-toggle', 'aria-label': 'Toggle route list', onclick: toggleSidebar, text: '
|
|
1358
|
+
h('button', { type: 'button', class: 'dv-menu-toggle', 'aria-label': 'Toggle route list', onclick: toggleSidebar, text: '≡' }, []),
|
|
1271
1359
|
brandEl(),
|
|
1272
1360
|
h('div', { class: 'dv-meta' }, [
|
|
1273
1361
|
h('span', { text: 'v' + s.meta.version }, []),
|
|
@@ -1276,9 +1364,9 @@ var APP_SCRIPT = `
|
|
|
1276
1364
|
h('span', { text: findings + ' findings' }, [])
|
|
1277
1365
|
].filter(Boolean)),
|
|
1278
1366
|
h('div', { class: 'dv-status' }, [
|
|
1279
|
-
s.live && s.analyzing ? h('span', { class: 'dv-analyzing', text: 'Analyzing
|
|
1367
|
+
s.live && s.analyzing ? h('span', { class: 'dv-analyzing', text: 'Analyzing…' }, []) : null,
|
|
1280
1368
|
s.live ? h('span', { class: 'dv-conn dv-conn-' + state.connection, title: state.connection }, []) : null,
|
|
1281
|
-
h('button', { type: 'button', class: 'dv-theme-toggle', 'aria-label': 'Toggle dark mode', onclick: toggleTheme, text: state.theme === 'dark' ? '
|
|
1369
|
+
h('button', { type: 'button', class: 'dv-theme-toggle', 'aria-label': 'Toggle dark mode', onclick: toggleTheme, text: state.theme === 'dark' ? '☀' : '☾' }, [])
|
|
1282
1370
|
].filter(Boolean))
|
|
1283
1371
|
];
|
|
1284
1372
|
mount('dv-topbar', h('div', { class: 'dv-topbar-inner' }, kids));
|
|
@@ -1324,7 +1412,7 @@ var APP_SCRIPT = `
|
|
|
1324
1412
|
kids.push(h('span', { class: 'dv-nav-meta' }, [
|
|
1325
1413
|
badge ? h('span', { class: 'dv-badge dv-badge-' + badge, text: badge }, []) : null,
|
|
1326
1414
|
h('span', { class: 'dv-nav-score', style: 'color:' + BAND_COLOR[band], text: String(route.score) }, []),
|
|
1327
|
-
h('span', { class: 'dv-nav-sum', text: summary.length ? summary.join('
|
|
1415
|
+
h('span', { class: 'dv-nav-sum', text: summary.length ? summary.join(' · ') : 'no issues' }, [])
|
|
1328
1416
|
].filter(Boolean)));
|
|
1329
1417
|
}
|
|
1330
1418
|
return h('div', {
|
|
@@ -1351,7 +1439,7 @@ var APP_SCRIPT = `
|
|
|
1351
1439
|
state.routeBySlug = {};
|
|
1352
1440
|
|
|
1353
1441
|
// mount() clears and rebuilds the whole sidebar, including the <input> itself, so
|
|
1354
|
-
// a naive re-render on every keystroke drops focus and cursor position
|
|
1442
|
+
// a naive re-render on every keystroke drops focus and cursor position — capture
|
|
1355
1443
|
// them before rebuilding and restore them on the freshly-created input afterward.
|
|
1356
1444
|
var prevSearch = document.querySelector('.dv-search');
|
|
1357
1445
|
var hadFocus = !!prevSearch && document.activeElement === prevSearch;
|
|
@@ -1361,7 +1449,7 @@ var APP_SCRIPT = `
|
|
|
1361
1449
|
var searchInput = h('input', {
|
|
1362
1450
|
type: 'search',
|
|
1363
1451
|
class: 'dv-search',
|
|
1364
|
-
placeholder: 'Search routes or rules
|
|
1452
|
+
placeholder: 'Search routes or rules…',
|
|
1365
1453
|
value: state.search,
|
|
1366
1454
|
oninput: function (e) { state.search = e.target.value; renderSidebar(); }
|
|
1367
1455
|
}, []);
|
|
@@ -1473,7 +1561,7 @@ var APP_SCRIPT = `
|
|
|
1473
1561
|
var band = scoreBand(c.score);
|
|
1474
1562
|
var weight = s.report.weights[cat];
|
|
1475
1563
|
var name = CATEGORY_NAMES[cat] || cat;
|
|
1476
|
-
// keys/affectedKeys are absent on hand-built snapshots (older fixtures, tests)
|
|
1564
|
+
// keys/affectedKeys are absent on hand-built snapshots (older fixtures, tests) —
|
|
1477
1565
|
// render nothing rather than "undefined of undefined". 0 affected of N keys is still
|
|
1478
1566
|
// rendered: on a real project that's the signal a thin score can't give, that the
|
|
1479
1567
|
// category is clean project-wide and not just on the one key it happened to look at
|
|
@@ -1505,7 +1593,7 @@ var APP_SCRIPT = `
|
|
|
1505
1593
|
});
|
|
1506
1594
|
var body = entries.length
|
|
1507
1595
|
? entries.map(function (e) { return renderFinding(e.issue, e.route); })
|
|
1508
|
-
: [h('p', { class: 'dv-empty', text: totalCount ? 'No issues match the current filter.' : 'No issues found
|
|
1596
|
+
: [h('p', { class: 'dv-empty', text: totalCount ? 'No issues match the current filter.' : 'No issues found — nice work!' }, [])];
|
|
1509
1597
|
var findings = h('section', { class: 'dv-section' }, [h('h2', { text: 'Findings' }, [])].concat(body));
|
|
1510
1598
|
return h('div', { class: 'dv-overview' }, [gauge, h('div', { class: 'dv-cats' }, cats), chips, findings].filter(Boolean));
|
|
1511
1599
|
}
|
|
@@ -1589,213 +1677,32 @@ var APP_SCRIPT = `
|
|
|
1589
1677
|
boot();
|
|
1590
1678
|
})();
|
|
1591
1679
|
`;
|
|
1680
|
+
/** The shell HTML: empty sidebar/detail/topbar containers, the stylesheet, the
|
|
1681
|
+
* client script, and the snapshot embedded as JSON for the client's first paint. */
|
|
1592
1682
|
function renderAppShell(snapshot) {
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1683
|
+
const safe = {
|
|
1684
|
+
...snapshot,
|
|
1685
|
+
report: sanitizeReport(snapshot.report)
|
|
1686
|
+
};
|
|
1687
|
+
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>`;
|
|
1596
1688
|
}
|
|
1689
|
+
/**
|
|
1690
|
+
* Static (non-live) document over a prebuilt JsonReport — kept as the public name the
|
|
1691
|
+
* html reporter has always exported.
|
|
1692
|
+
*/
|
|
1597
1693
|
function buildHtmlDocument(report, meta) {
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1694
|
+
return renderAppShell({
|
|
1695
|
+
report,
|
|
1696
|
+
badges: {},
|
|
1697
|
+
analyzing: false,
|
|
1698
|
+
sequence: 0,
|
|
1699
|
+
live: false,
|
|
1700
|
+
meta
|
|
1701
|
+
});
|
|
1606
1702
|
}
|
|
1703
|
+
/** Render results as the self-contained HTML report (the CLI's `--reporter html`). */
|
|
1607
1704
|
function formatHtmlReport(results, config, meta) {
|
|
1608
|
-
|
|
1705
|
+
return buildHtmlDocument(buildJsonReport(results, config, meta), meta);
|
|
1609
1706
|
}
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
APP_STYLE,
|
|
1613
|
-
BAND_COLOR,
|
|
1614
|
-
CHILD_NODE_KEYS,
|
|
1615
|
-
IDREF_ATTRS,
|
|
1616
|
-
LANDMARK_ROLES,
|
|
1617
|
-
READ_CONCURRENCY,
|
|
1618
|
-
ROBOTS_SOURCE_PATHS,
|
|
1619
|
-
SITEMAP_SOURCE_PATHS,
|
|
1620
|
-
SVELTE_CONFIG_FILES,
|
|
1621
|
-
VITE_CONFIG_FILES,
|
|
1622
|
-
a11yAccessibleName,
|
|
1623
|
-
a11yDeprecatedAria,
|
|
1624
|
-
a11yDeprecatedAttr,
|
|
1625
|
-
a11yDeprecatedElement,
|
|
1626
|
-
a11yDisallowedAriaProps,
|
|
1627
|
-
a11yDisallowedElement,
|
|
1628
|
-
a11yDoctype,
|
|
1629
|
-
a11yDuplicateLandmark,
|
|
1630
|
-
a11yIdDuplication,
|
|
1631
|
-
a11yInteractiveNesting,
|
|
1632
|
-
a11yInvalidAriaValue,
|
|
1633
|
-
a11yInvalidRole,
|
|
1634
|
-
a11yLabelHasControl,
|
|
1635
|
-
a11yNoMissingIdRef,
|
|
1636
|
-
a11yPermittedContents,
|
|
1637
|
-
a11yPlaceholderLabelOption,
|
|
1638
|
-
a11yRequireDatetime,
|
|
1639
|
-
a11yRequiredAriaProps,
|
|
1640
|
-
a11yRequiredElement,
|
|
1641
|
-
a11yTopLevelLandmark,
|
|
1642
|
-
a11yUnknownAriaAttribute,
|
|
1643
|
-
a11yUseList,
|
|
1644
|
-
allRules,
|
|
1645
|
-
applyInlineDirectives,
|
|
1646
|
-
applyOverrides,
|
|
1647
|
-
applyRuleSeverities,
|
|
1648
|
-
architectureComponentSize,
|
|
1649
|
-
architectureDirectoryNaming,
|
|
1650
|
-
architectureDocLinkTarget,
|
|
1651
|
-
architecturePrivateScopeImport,
|
|
1652
|
-
architecturePropCount,
|
|
1653
|
-
architectureReservedDirectoryNames,
|
|
1654
|
-
architectureReservedNamePlacement,
|
|
1655
|
-
architectureRouteComponentImport,
|
|
1656
|
-
architectureUnitEntryFile,
|
|
1657
|
-
attrText,
|
|
1658
|
-
attrTextOf,
|
|
1659
|
-
attrValue,
|
|
1660
|
-
attrValueOf,
|
|
1661
|
-
buildHtmlDocument,
|
|
1662
|
-
buildJsonReport,
|
|
1663
|
-
classify,
|
|
1664
|
-
collectComponentFacts,
|
|
1665
|
-
collectKitModuleFacts,
|
|
1666
|
-
collectSourceFiles,
|
|
1667
|
-
collectSuppressions,
|
|
1668
|
-
compileOverrides,
|
|
1669
|
-
computeHealth,
|
|
1670
|
-
computeScore,
|
|
1671
|
-
correctnessBasePathNavigation,
|
|
1672
|
-
correctnessCheckableBindValue,
|
|
1673
|
-
correctnessEachIndexKey,
|
|
1674
|
-
correctnessEachKey,
|
|
1675
|
-
correctnessEffectAsDerived,
|
|
1676
|
-
correctnessEffectAsOnMount,
|
|
1677
|
-
correctnessInstanceBrowserGlobal,
|
|
1678
|
-
correctnessNonreactiveBuiltinState,
|
|
1679
|
-
correctnessOrphanEffect,
|
|
1680
|
-
correctnessOrphanLifecycle,
|
|
1681
|
-
correctnessPropMutation,
|
|
1682
|
-
correctnessServerBrowserGlobal,
|
|
1683
|
-
correctnessStalePropDerivation,
|
|
1684
|
-
correctnessUnmutatedState,
|
|
1685
|
-
decodeFragmentId,
|
|
1686
|
-
defaultConfig,
|
|
1687
|
-
defaultProject,
|
|
1688
|
-
docsUrlFor,
|
|
1689
|
-
effectiveSeverity,
|
|
1690
|
-
emptyComponentFacts,
|
|
1691
|
-
emptyKitModuleFacts,
|
|
1692
|
-
escapeHtml,
|
|
1693
|
-
explainRule,
|
|
1694
|
-
findAttr,
|
|
1695
|
-
findKitAliasesInSvelteConfig,
|
|
1696
|
-
findKitPathsBaseInSvelteConfig,
|
|
1697
|
-
findKitPathsBaseInViteConfig,
|
|
1698
|
-
findMinifyDisabled,
|
|
1699
|
-
foldOccurrences,
|
|
1700
|
-
formatAgentReport,
|
|
1701
|
-
formatConsoleReport,
|
|
1702
|
-
formatFailedRuleWarning,
|
|
1703
|
-
formatGithubReport,
|
|
1704
|
-
formatHtmlReport,
|
|
1705
|
-
formatJsonReport,
|
|
1706
|
-
formatMarkdownReport,
|
|
1707
|
-
formatSarifReport,
|
|
1708
|
-
hasFailureAtOrAbove,
|
|
1709
|
-
headTagRule,
|
|
1710
|
-
imageRule,
|
|
1711
|
-
intOption,
|
|
1712
|
-
isMentionedAnywhere,
|
|
1713
|
-
isPenalized,
|
|
1714
|
-
isTopFragment,
|
|
1715
|
-
lineOf,
|
|
1716
|
-
linkRule,
|
|
1717
|
-
listOption,
|
|
1718
|
-
mapOption,
|
|
1719
|
-
noColorPalette,
|
|
1720
|
-
overrideMatches,
|
|
1721
|
-
parseComponentFacts,
|
|
1722
|
-
parseKitModuleFacts,
|
|
1723
|
-
parseSvelte,
|
|
1724
|
-
performanceFontPreloadCrossorigin,
|
|
1725
|
-
performanceHeavyImport,
|
|
1726
|
-
performanceImageDimensions,
|
|
1727
|
-
performanceImageLoadingHint,
|
|
1728
|
-
performanceLcpImage,
|
|
1729
|
-
performanceLoadWaterfall,
|
|
1730
|
-
performanceMinifyDisabled,
|
|
1731
|
-
performanceNamespaceImport,
|
|
1732
|
-
performancePreconnect,
|
|
1733
|
-
performancePreloadMissingAs,
|
|
1734
|
-
performanceRenderBlockingScript,
|
|
1735
|
-
performanceResponsiveImage,
|
|
1736
|
-
performanceSequentialAwaits,
|
|
1737
|
-
performanceStateRaw,
|
|
1738
|
-
renderAppShell,
|
|
1739
|
-
resolveKitAliases,
|
|
1740
|
-
resolveKitPathsBase,
|
|
1741
|
-
resolveRepoLocalPath,
|
|
1742
|
-
resolveRuleOptions,
|
|
1743
|
-
resolveRunesModuleSpecifier,
|
|
1744
|
-
runRules,
|
|
1745
|
-
safeHref,
|
|
1746
|
-
scoreBand,
|
|
1747
|
-
scoreColor,
|
|
1748
|
-
scoresByCategory,
|
|
1749
|
-
securityHandlerStateWrite,
|
|
1750
|
-
securityJavascriptUrl,
|
|
1751
|
-
securityRawHtml,
|
|
1752
|
-
securityServerModuleState,
|
|
1753
|
-
securitySharedStateImport,
|
|
1754
|
-
selectRules,
|
|
1755
|
-
seoCanonicalUrl,
|
|
1756
|
-
seoCharset,
|
|
1757
|
-
seoDescriptionLength,
|
|
1758
|
-
seoDescriptionPresence,
|
|
1759
|
-
seoDuplicateDescription,
|
|
1760
|
-
seoDuplicateTitle,
|
|
1761
|
-
seoHeadingLevelSkip,
|
|
1762
|
-
seoHreflang,
|
|
1763
|
-
seoHtmlLang,
|
|
1764
|
-
seoImageAlt,
|
|
1765
|
-
seoIndexability,
|
|
1766
|
-
seoJsonLd,
|
|
1767
|
-
seoJsonLdDateFormat,
|
|
1768
|
-
seoJsonLdDeprecatedType,
|
|
1769
|
-
seoJsonLdPlaceholder,
|
|
1770
|
-
seoJsonLdRelativeUrl,
|
|
1771
|
-
seoJsonLdRequiredProps,
|
|
1772
|
-
seoJsonLdValidity,
|
|
1773
|
-
seoOgDescription,
|
|
1774
|
-
seoOgImage,
|
|
1775
|
-
seoOgTitle,
|
|
1776
|
-
seoOgUrl,
|
|
1777
|
-
seoRobotsTxt,
|
|
1778
|
-
seoSingleH1,
|
|
1779
|
-
seoSitemapInRobots,
|
|
1780
|
-
seoSitemapXml,
|
|
1781
|
-
seoSsrDisabled,
|
|
1782
|
-
seoTitleLength,
|
|
1783
|
-
seoTitlePresence,
|
|
1784
|
-
seoTwitterCard,
|
|
1785
|
-
seoViewport,
|
|
1786
|
-
settingOptions,
|
|
1787
|
-
settingSeverity,
|
|
1788
|
-
shouldSkipRangeCheck,
|
|
1789
|
-
skippedFileWarnings,
|
|
1790
|
-
splitTokens,
|
|
1791
|
-
stripTextDirective,
|
|
1792
|
-
summarize,
|
|
1793
|
-
terminalSafe,
|
|
1794
|
-
textFromNodes,
|
|
1795
|
-
unknownDirectiveIds,
|
|
1796
|
-
validateRuleOptions,
|
|
1797
|
-
validateRuleSetting,
|
|
1798
|
-
valueFromNodes,
|
|
1799
|
-
withFailedRulesOff,
|
|
1800
|
-
withReadLimit
|
|
1801
|
-
};
|
|
1707
|
+
//#endregion
|
|
1708
|
+
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 };
|