@svelte-vitals/core 0.47.0 → 0.47.2

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