@svelte-vitals/core 0.43.1 → 0.45.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1732 @@
1
+ import {
2
+ CHILD_NODE_KEYS,
3
+ IDREF_ATTRS,
4
+ LANDMARK_ROLES,
5
+ SEVERITY_RANK,
6
+ a11yAccessibleName,
7
+ a11yDoctype,
8
+ a11yDuplicateLandmark,
9
+ a11yIdDuplication,
10
+ a11yInteractiveNesting,
11
+ a11yInvalidAriaValue,
12
+ a11yInvalidRole,
13
+ a11yLabelHasControl,
14
+ a11yNoMissingIdRef,
15
+ a11yPlaceholderLabelOption,
16
+ a11yRequireDatetime,
17
+ a11yRequiredAriaProps,
18
+ a11yTopLevelLandmark,
19
+ a11yUnknownAriaAttribute,
20
+ a11yUseList,
21
+ allRules,
22
+ applyOverrides,
23
+ applyRuleSeverities,
24
+ architectureComponentSize,
25
+ architectureDirectoryNaming,
26
+ architectureDocLinkTarget,
27
+ architecturePrivateScopeImport,
28
+ architecturePropCount,
29
+ architectureReservedDirectoryNames,
30
+ architectureReservedNamePlacement,
31
+ architectureRouteComponentImport,
32
+ architectureUnitEntryFile,
33
+ attrText,
34
+ attrTextOf,
35
+ attrValue,
36
+ attrValueOf,
37
+ buildJsonReport,
38
+ classify,
39
+ collectNamedImportAliases,
40
+ collectTopLevelBindings,
41
+ compileOverrides,
42
+ computeHealth,
43
+ computeScore,
44
+ correctnessBasePathNavigation,
45
+ correctnessCheckableBindValue,
46
+ correctnessEachIndexKey,
47
+ correctnessEachKey,
48
+ correctnessEffectAsDerived,
49
+ correctnessEffectAsOnMount,
50
+ correctnessInstanceBrowserGlobal,
51
+ correctnessNonreactiveBuiltinState,
52
+ correctnessOrphanEffect,
53
+ correctnessOrphanLifecycle,
54
+ correctnessPropMutation,
55
+ correctnessServerBrowserGlobal,
56
+ correctnessStalePropDerivation,
57
+ correctnessUnmutatedState,
58
+ decodeFragmentId,
59
+ defaultConfig,
60
+ defaultProject,
61
+ docsUrlFor,
62
+ effectiveSeverity,
63
+ explainRule,
64
+ findAttr,
65
+ foldOccurrences,
66
+ formatFailedRuleWarning,
67
+ formatGithubReport,
68
+ formatJsonReport,
69
+ formatMarkdownReport,
70
+ hasFailureAtOrAbove,
71
+ headTagRule,
72
+ imageRule,
73
+ intOption,
74
+ isMentionedAnywhere,
75
+ isPenalized,
76
+ isTopFragment,
77
+ lineOf,
78
+ linkRule,
79
+ listOption,
80
+ mapOption,
81
+ mdEscape,
82
+ messageText,
83
+ overrideMatches,
84
+ parseComponentFacts,
85
+ parseInMemoryExports,
86
+ parseKitModuleFacts,
87
+ parseModuleProgram,
88
+ parseSvelte,
89
+ performanceFontPreloadCrossorigin,
90
+ performanceHeavyImport,
91
+ performanceImageDimensions,
92
+ performanceImageLoadingHint,
93
+ performanceLcpImage,
94
+ performanceLoadWaterfall,
95
+ performanceMinifyDisabled,
96
+ performanceNamespaceImport,
97
+ performancePreconnect,
98
+ performancePreloadMissingAs,
99
+ performanceRenderBlockingScript,
100
+ performanceResponsiveImage,
101
+ performanceSequentialAwaits,
102
+ performanceStateRaw,
103
+ resolveRepoLocalPath,
104
+ resolveRuleOptions,
105
+ resolveRunesModuleSpecifier,
106
+ ruleMetaById,
107
+ scoresByCategory,
108
+ securityHandlerStateWrite,
109
+ securityJavascriptUrl,
110
+ securityRawHtml,
111
+ securityServerModuleState,
112
+ securitySharedStateImport,
113
+ selectRules,
114
+ seoCanonicalUrl,
115
+ seoCharset,
116
+ seoDescriptionLength,
117
+ seoDescriptionPresence,
118
+ seoDuplicateDescription,
119
+ seoDuplicateTitle,
120
+ seoHeadingLevelSkip,
121
+ seoHreflang,
122
+ seoHtmlLang,
123
+ seoImageAlt,
124
+ seoIndexability,
125
+ seoJsonLd,
126
+ seoJsonLdDateFormat,
127
+ seoJsonLdDeprecatedType,
128
+ seoJsonLdPlaceholder,
129
+ seoJsonLdRelativeUrl,
130
+ seoJsonLdRequiredProps,
131
+ seoJsonLdValidity,
132
+ seoOgDescription,
133
+ seoOgImage,
134
+ seoOgTitle,
135
+ seoOgUrl,
136
+ seoRobotsTxt,
137
+ seoSingleH1,
138
+ seoSitemapInRobots,
139
+ seoSitemapXml,
140
+ seoSsrDisabled,
141
+ seoTitleLength,
142
+ seoTitlePresence,
143
+ seoTwitterCard,
144
+ seoViewport,
145
+ settingOptions,
146
+ settingSeverity,
147
+ severityToSarifLevel,
148
+ shouldSkipRangeCheck,
149
+ splitTokens,
150
+ stripTextDirective,
151
+ summarize,
152
+ terminalSafe,
153
+ textFromNodes,
154
+ unwrapTs,
155
+ validateRuleOptions,
156
+ validateRuleSetting,
157
+ valueFromNodes,
158
+ withFailedRulesOff
159
+ } from "./chunk-BCJZO532.js";
160
+
161
+ // src/component.ts
162
+ function skippedFileWarnings(facts) {
163
+ const list = (files) => {
164
+ const shown = files.slice(0, 10);
165
+ return files.length > shown.length ? `${shown.join(", ")}, \u2026 and ${files.length - shown.length} more` : shown.join(", ");
166
+ };
167
+ const names = (pick) => [...new Set(facts.filter(pick).map((f) => f.file))].sort();
168
+ const unread = names((f) => f.readFailed === true);
169
+ const unparsed = names((f) => f.parseFailed === true && f.readFailed !== true);
170
+ const out = [];
171
+ if (unread.length > 0) {
172
+ out.push(
173
+ `skipped ${unread.length} file(s) that could not be read: ${list(unread)}`,
174
+ "this is an environment problem, not a code one \u2014 check file permissions and the open-file limit (`ulimit -n`)."
175
+ );
176
+ }
177
+ if (unparsed.length > 0) {
178
+ out.push(
179
+ `skipped ${unparsed.length} file(s) that could not be parsed: ${list(unparsed)}`,
180
+ "findings for these files are unavailable until they parse."
181
+ );
182
+ }
183
+ return out;
184
+ }
185
+
186
+ // src/component-collect.ts
187
+ function emptyComponentFacts(file) {
188
+ return {
189
+ file,
190
+ eachBlocks: [],
191
+ effects: [],
192
+ htmlTags: [],
193
+ javascriptUrls: [],
194
+ loc: 0,
195
+ propCount: 0,
196
+ imports: [],
197
+ importSpans: [],
198
+ namespaceImports: [],
199
+ constableStates: [],
200
+ mutatedProps: [],
201
+ stalePropDerivations: [],
202
+ rawableStates: [],
203
+ nonreactiveBuiltinStates: [],
204
+ checkableBindValues: [],
205
+ basePathLinks: [],
206
+ orphanEffects: [],
207
+ orphanLifecycleCalls: [],
208
+ browserGlobalRefs: [],
209
+ moduleStateDecls: [],
210
+ suppressions: [],
211
+ commentLinks: []
212
+ };
213
+ }
214
+ async function collectComponentFacts(rt, cwd) {
215
+ const files = await rt.glob("src/**/*.svelte{,.ts,.js}", cwd);
216
+ return Promise.all(
217
+ files.sort().map(async (rel) => {
218
+ let source;
219
+ try {
220
+ source = await rt.readFile(rt.join(cwd, rel));
221
+ } catch {
222
+ return { ...emptyComponentFacts(rel), parseFailed: true, readFailed: true };
223
+ }
224
+ try {
225
+ return { file: rel, ...parseComponentFacts(source, rel) };
226
+ } catch {
227
+ return { ...emptyComponentFacts(rel), parseFailed: true };
228
+ }
229
+ })
230
+ );
231
+ }
232
+
233
+ // src/source-files.ts
234
+ async function collectSourceFiles(rt, cwd) {
235
+ const files = await rt.glob("src/**/*", cwd);
236
+ return files.slice().sort();
237
+ }
238
+
239
+ // src/kit-module-collect.ts
240
+ function emptyKitModuleFacts(file, kind) {
241
+ return {
242
+ file,
243
+ kind,
244
+ moduleStateReassignments: [],
245
+ importedStateWrites: [],
246
+ importedStateWritesOutsideHandlers: [],
247
+ pendingServerStoreWrites: [],
248
+ runesModuleImports: [],
249
+ lifecycleCalls: [],
250
+ browserGlobalRefs: [],
251
+ basePathLinks: [],
252
+ suppressions: []
253
+ };
254
+ }
255
+ function kindOf(file) {
256
+ const base = file.split("/").pop() ?? file;
257
+ return base.includes(".server.") || base.startsWith("+server.") ? "server" : "universal";
258
+ }
259
+ async function collectKitModuleFacts(rt, cwd, aliases) {
260
+ const patterns = [
261
+ "src/routes/**/+{page,layout}.server.{ts,js}",
262
+ "src/routes/**/+{page,layout}.{ts,js}",
263
+ "src/routes/**/+server.{ts,js}",
264
+ "src/hooks.server.{ts,js}"
265
+ ];
266
+ const lists = await Promise.all(patterns.map((p) => rt.glob(p, cwd)));
267
+ const files = [...new Set(lists.flat())];
268
+ const facts = await Promise.all(
269
+ files.sort().map(async (rel) => {
270
+ const kind = kindOf(rel);
271
+ let source;
272
+ try {
273
+ source = await rt.readFile(rt.join(cwd, rel));
274
+ } catch {
275
+ return { ...emptyKitModuleFacts(rel, kind), parseFailed: true, readFailed: true };
276
+ }
277
+ try {
278
+ return { file: rel, kind, ...parseKitModuleFacts(source, rel, aliases) };
279
+ } catch {
280
+ return { ...emptyKitModuleFacts(rel, kind), parseFailed: true };
281
+ }
282
+ })
283
+ );
284
+ return arbitrateServerStoreWrites(rt, cwd, facts);
285
+ }
286
+ function moduleCandidates(repoPath) {
287
+ if (repoPath.endsWith(".js")) return [repoPath, `${repoPath.slice(0, -3)}.ts`];
288
+ if (repoPath.endsWith(".ts")) return [repoPath];
289
+ return [`${repoPath}.ts`, `${repoPath}.js`, `${repoPath}/index.ts`, `${repoPath}/index.js`];
290
+ }
291
+ async function inMemoryExportsOf(rt, cwd, repoPath) {
292
+ for (const rel of moduleCandidates(repoPath)) {
293
+ try {
294
+ if (!await rt.exists(rt.join(cwd, rel))) continue;
295
+ return parseInMemoryExports(await rt.readFile(rt.join(cwd, rel)), rel);
296
+ } catch {
297
+ return /* @__PURE__ */ new Set();
298
+ }
299
+ }
300
+ return /* @__PURE__ */ new Set();
301
+ }
302
+ async function arbitrateServerStoreWrites(rt, cwd, facts) {
303
+ const targets = [...new Set(facts.flatMap((f) => f.pendingServerStoreWrites.map((w) => w.resolved)))];
304
+ if (targets.length === 0) return facts;
305
+ const byPath = new Map(
306
+ await Promise.all(targets.map(async (t) => [t, await inMemoryExportsOf(rt, cwd, t)]))
307
+ );
308
+ return facts.map((f) => {
309
+ const promoted = f.pendingServerStoreWrites.filter((w) => byPath.get(w.resolved)?.has(w.imported)).map((w) => ({ name: w.name, line: w.line, via: "set-call" }));
310
+ if (promoted.length === 0) return f;
311
+ return {
312
+ ...f,
313
+ importedStateWrites: [...f.importedStateWrites, ...promoted].sort((a, b) => a.line - b.line)
314
+ };
315
+ });
316
+ }
317
+
318
+ // src/config-object.ts
319
+ function propOf(obj, name) {
320
+ let found;
321
+ for (const p of obj.properties) {
322
+ if (p.type === "SpreadElement") {
323
+ if (found) found = void 0;
324
+ continue;
325
+ }
326
+ if (p.type !== "Property" || p.computed) continue;
327
+ if (p.key.type === "Identifier" && p.key.name === name) found = p;
328
+ else if (p.key.type === "Literal" && p.key.value === name) found = p;
329
+ }
330
+ return found;
331
+ }
332
+ function unwrapToObjectExpression(expr, bindings) {
333
+ let current = expr;
334
+ for (let i = 0; i < 4 && current; i++) {
335
+ const e = unwrapTs(current);
336
+ if (e.type === "ObjectExpression") return e;
337
+ if (e.type === "Identifier") {
338
+ current = bindings.get(e.name);
339
+ continue;
340
+ }
341
+ if (e.type === "CallExpression") {
342
+ current = e.arguments[0];
343
+ continue;
344
+ }
345
+ return void 0;
346
+ }
347
+ const final = current ? unwrapTs(current) : void 0;
348
+ return final?.type === "ObjectExpression" ? final : void 0;
349
+ }
350
+ function findExportedExpression(program) {
351
+ let exported;
352
+ for (const stmt of program.body) {
353
+ if (stmt.type === "ExportDefaultDeclaration") exported = stmt.declaration;
354
+ }
355
+ if (exported) return exported;
356
+ let cjsExported;
357
+ for (const stmt of program.body) {
358
+ if (stmt.type !== "ExpressionStatement") continue;
359
+ const expr = stmt.expression;
360
+ if (expr.type !== "AssignmentExpression" || expr.operator !== "=") continue;
361
+ const left = expr.left;
362
+ if (left.type === "MemberExpression" && !left.computed && left.object.type === "Identifier" && left.object.name === "module" && left.property.type === "Identifier" && left.property.name === "exports") {
363
+ cjsExported = expr.right;
364
+ }
365
+ }
366
+ return cjsExported;
367
+ }
368
+ function resolveConfigObject(program) {
369
+ const exported = findExportedExpression(program);
370
+ if (!exported) return void 0;
371
+ return unwrapToObjectExpression(exported, collectTopLevelBindings(program));
372
+ }
373
+
374
+ // src/vite-config-parse.ts
375
+ function findMinifyDisabled(source) {
376
+ let program;
377
+ let wrapped;
378
+ try {
379
+ ({ program, wrapped } = parseModuleProgram(source, "vite.config.ts"));
380
+ } catch {
381
+ return void 0;
382
+ }
383
+ if (!program) return void 0;
384
+ const config = resolveConfigObject(program);
385
+ if (!config) return void 0;
386
+ const build = propOf(config, "build");
387
+ const buildValue = build ? unwrapTs(build.value) : void 0;
388
+ if (buildValue?.type !== "ObjectExpression") return void 0;
389
+ const minify = propOf(buildValue, "minify");
390
+ const minifyValue = minify ? unwrapTs(minify.value) : void 0;
391
+ if (!minify || minifyValue?.type !== "Literal" || minifyValue.value !== false) return void 0;
392
+ return { line: Math.max(0, lineOf(wrapped, minify.start) - 1) };
393
+ }
394
+
395
+ // src/svelte-config-parse.ts
396
+ function basePathOf(kitConfig, bindings) {
397
+ const paths = propOf(kitConfig, "paths");
398
+ const pathsObj = paths ? unwrapToObjectExpression(paths.value, bindings) : void 0;
399
+ if (!pathsObj) return void 0;
400
+ const base = propOf(pathsObj, "base");
401
+ if (!base) return void 0;
402
+ const value = unwrapTs(base.value);
403
+ if (value.type === "Literal") {
404
+ return typeof value.value === "string" && value.value !== "" ? { value: value.value } : void 0;
405
+ }
406
+ return {};
407
+ }
408
+ function keyNameOf(p) {
409
+ if (p.computed) return void 0;
410
+ if (p.key.type === "Identifier") return p.key.name;
411
+ if (p.key.type === "Literal" && typeof p.key.value === "string") return p.key.value;
412
+ return void 0;
413
+ }
414
+ function stringValueOf(p) {
415
+ const v = unwrapTs(p.value);
416
+ return v.type === "Literal" && typeof v.value === "string" ? v.value : void 0;
417
+ }
418
+ function aliasEntriesOf(kitConfig, bindings) {
419
+ const alias = propOf(kitConfig, "alias");
420
+ if (!alias) return [];
421
+ const obj = unwrapToObjectExpression(alias.value, bindings);
422
+ if (!obj) return void 0;
423
+ const out = [];
424
+ const at = /* @__PURE__ */ new Map();
425
+ for (const p of obj.properties) {
426
+ if (p.type !== "Property") return void 0;
427
+ const key = keyNameOf(p);
428
+ if (key === void 0) return void 0;
429
+ const entry = { key, value: stringValueOf(p) ?? null };
430
+ const seen = at.get(key);
431
+ if (seen === void 0) {
432
+ at.set(key, out.length);
433
+ out.push(entry);
434
+ } else out[seen] = entry;
435
+ }
436
+ return out;
437
+ }
438
+ function filesLibOf(kitConfig, bindings) {
439
+ const files = propOf(kitConfig, "files");
440
+ const obj = files ? unwrapToObjectExpression(files.value, bindings) : void 0;
441
+ const lib = obj ? propOf(obj, "lib") : void 0;
442
+ if (!lib) return void 0;
443
+ return stringValueOf(lib) ?? null;
444
+ }
445
+ function findKitAliasesInSvelteConfig(source) {
446
+ const program = programOf(source, "svelte.config.js");
447
+ const config = program ? resolveConfigObject(program) : void 0;
448
+ if (!program || !config) return { entries: [] };
449
+ const bindings = collectTopLevelBindings(program);
450
+ const kit = propOf(config, "kit");
451
+ const kitObj = kit ? unwrapToObjectExpression(kit.value, bindings) : void 0;
452
+ if (!kitObj) return { entries: [] };
453
+ const filesLib = filesLibOf(kitObj, bindings);
454
+ return { entries: aliasEntriesOf(kitObj, bindings), ...filesLib !== void 0 ? { filesLib } : {} };
455
+ }
456
+ function normalizeAliasValue(value) {
457
+ const posix = value.replace(/\\/g, "/");
458
+ const noStar = posix.endsWith("/*") ? posix.slice(0, -2) : posix;
459
+ return noStar.replace(/\/+$/, "");
460
+ }
461
+ function compileKitAliases(raw) {
462
+ const filesLib = raw.filesLib === null ? null : normalizeAliasValue(raw.filesLib ?? "src/lib");
463
+ const out = [{ find: "$lib", replacement: filesLib, match: "prefix" }];
464
+ const entries = raw.entries ?? [];
465
+ const declared = new Set(entries.map((e) => e.key));
466
+ for (const { key, value } of entries) {
467
+ const star = key.endsWith("/*");
468
+ out.push({
469
+ find: star ? key.slice(0, -2) : key,
470
+ replacement: value === null ? null : normalizeAliasValue(value),
471
+ match: star ? "contents" : declared.has(`${key}/*`) ? "exact" : "prefix"
472
+ });
473
+ }
474
+ return out;
475
+ }
476
+ function resolveKitAliases(viteConfig, svelteConfig) {
477
+ if (viteConfig && findKitPathsBaseInViteConfig(viteConfig.source).kind !== "no-plugin-config") return void 0;
478
+ if (!svelteConfig) return void 0;
479
+ return compileKitAliases(findKitAliasesInSvelteConfig(svelteConfig.source));
480
+ }
481
+ function programOf(source, filename) {
482
+ try {
483
+ return parseModuleProgram(source, filename).program ?? void 0;
484
+ } catch {
485
+ return void 0;
486
+ }
487
+ }
488
+ function findKitPathsBaseInSvelteConfig(source) {
489
+ const program = programOf(source, "svelte.config.js");
490
+ if (!program) return void 0;
491
+ const config = resolveConfigObject(program);
492
+ if (!config) return void 0;
493
+ const bindings = collectTopLevelBindings(program);
494
+ const kit = propOf(config, "kit");
495
+ const kitObj = kit ? unwrapToObjectExpression(kit.value, bindings) : void 0;
496
+ return kitObj ? basePathOf(kitObj, bindings) : void 0;
497
+ }
498
+ function sveltekitLocalNames(program) {
499
+ const out = collectNamedImportAliases(program, "@sveltejs/kit/vite", /* @__PURE__ */ new Set(["sveltekit"]));
500
+ if (out.size === 0) out.add("sveltekit");
501
+ return out;
502
+ }
503
+ function findKitPathsBaseInViteConfig(source) {
504
+ const none = { kind: "no-plugin-config" };
505
+ const program = programOf(source, "vite.config.ts");
506
+ if (!program) return none;
507
+ const config = resolveConfigObject(program);
508
+ if (!config) return none;
509
+ const bindings = collectTopLevelBindings(program);
510
+ const plugins = propOf(config, "plugins");
511
+ const pluginsValue = plugins ? unwrapTs(plugins.value) : void 0;
512
+ if (pluginsValue?.type !== "ArrayExpression") return none;
513
+ const locals = sveltekitLocalNames(program);
514
+ for (const el of pluginsValue.elements) {
515
+ if (!el || el.type === "SpreadElement") continue;
516
+ const call = unwrapTs(el);
517
+ if (call.type !== "CallExpression") continue;
518
+ if (call.callee.type !== "Identifier" || !locals.has(call.callee.name)) continue;
519
+ const arg = call.arguments[0];
520
+ if (arg === void 0) return none;
521
+ const kitConfig = unwrapToObjectExpression(arg, bindings);
522
+ if (!kitConfig) return { kind: "unresolvable" };
523
+ const base = basePathOf(kitConfig, bindings);
524
+ return base ? { kind: "resolved", base } : { kind: "resolved" };
525
+ }
526
+ return none;
527
+ }
528
+ function resolveKitPathsBase(viteConfig, svelteConfig) {
529
+ if (viteConfig) {
530
+ const result = findKitPathsBaseInViteConfig(viteConfig.source);
531
+ if (result.kind === "unresolvable") return void 0;
532
+ if (result.kind === "resolved") {
533
+ return result.base ? { ...result.base, file: viteConfig.file } : void 0;
534
+ }
535
+ }
536
+ if (!svelteConfig) return void 0;
537
+ const base = findKitPathsBaseInSvelteConfig(svelteConfig.source);
538
+ return base ? { ...base, file: svelteConfig.file } : void 0;
539
+ }
540
+
541
+ // src/project-paths.ts
542
+ var ROBOTS_SOURCE_PATHS = [
543
+ "static/robots.txt",
544
+ "src/routes/robots.txt/+server.ts",
545
+ "src/routes/robots.txt/+server.js"
546
+ ];
547
+ var SITEMAP_SOURCE_PATHS = [
548
+ "static/sitemap.xml",
549
+ "src/routes/sitemap.xml/+server.ts",
550
+ "src/routes/sitemap.xml/+server.js"
551
+ ];
552
+ var VITE_CONFIG_FILES = [
553
+ "vite.config.js",
554
+ "vite.config.mjs",
555
+ "vite.config.ts",
556
+ "vite.config.cjs",
557
+ "vite.config.mts",
558
+ "vite.config.cts"
559
+ ];
560
+ var SVELTE_CONFIG_FILES = ["svelte.config.js", "svelte.config.ts"];
561
+
562
+ // src/runtime.ts
563
+ var READ_CONCURRENCY = 64;
564
+ function withReadLimit(readFile, limit = READ_CONCURRENCY) {
565
+ let active = 0;
566
+ const waiting = [];
567
+ const release = () => {
568
+ active--;
569
+ waiting.shift()?.();
570
+ };
571
+ return async (path) => {
572
+ if (active >= limit) await new Promise((resolve) => waiting.push(resolve));
573
+ active++;
574
+ try {
575
+ return await readFile(path);
576
+ } finally {
577
+ release();
578
+ }
579
+ };
580
+ }
581
+
582
+ // src/engine.ts
583
+ async function runRules(rules, ctx) {
584
+ const examined = {};
585
+ const perRule = await Promise.all(
586
+ rules.map(async (rule) => {
587
+ try {
588
+ return await rule.check({ ...ctx, recordExamined: (counts) => void (examined[rule.id] = counts) });
589
+ } catch (err) {
590
+ return { id: rule.id, message: err instanceof Error ? err.message : String(err) };
591
+ }
592
+ })
593
+ );
594
+ const results = [];
595
+ const failedRules = [];
596
+ for (const outcome of perRule) {
597
+ if (Array.isArray(outcome)) results.push(...outcome);
598
+ else failedRules.push(outcome);
599
+ }
600
+ return { results, examined, failedRules };
601
+ }
602
+
603
+ // src/reporter/palette.ts
604
+ var noColorPalette = {
605
+ bold: (s) => s,
606
+ dim: (s) => s,
607
+ red: (s) => s,
608
+ yellow: (s) => s,
609
+ green: (s) => s,
610
+ cyan: (s) => s
611
+ };
612
+ function scoreColor(p, score) {
613
+ if (score >= 90) return p.green;
614
+ if (score >= 70) return p.yellow;
615
+ return p.red;
616
+ }
617
+
618
+ // src/reporter/console.ts
619
+ 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";
620
+ var SEVERITY_TITLE = {
621
+ critical: "Critical",
622
+ warning: "Warnings",
623
+ info: "Info"
624
+ };
625
+ var CATEGORY_LABEL = {
626
+ seo: "SEO",
627
+ performance: "Performance",
628
+ correctness: "Correctness",
629
+ security: "Security",
630
+ architecture: "Architecture",
631
+ a11y: "Accessibility"
632
+ };
633
+ var CATEGORY_ORDER = Object.keys(CATEGORY_LABEL);
634
+ var categoryLabel = (c) => CATEGORY_LABEL[c];
635
+ var MAX_RULE_GROUPS_PER_BUCKET = 5;
636
+ function groupByRule(results) {
637
+ const groups = /* @__PURE__ */ new Map();
638
+ for (const r of results) {
639
+ const bucket = groups.get(r.id);
640
+ if (bucket) bucket.push(r);
641
+ else groups.set(r.id, [r]);
642
+ }
643
+ return [...groups.entries()].map(([id, rs]) => ({ id, results: rs })).sort((a, b) => b.results.length - a.results.length || a.id.localeCompare(b.id));
644
+ }
645
+ function scoreLine(p, label, { score, scoreModel }) {
646
+ const parts = [`route avg ${scoreModel.routeAverage}`];
647
+ if (scoreModel.sitePenalty > 0) parts.push(`site \u2212${scoreModel.sitePenalty}`);
648
+ if (scoreModel.criticalCap !== null) parts.push(`capped at ${scoreModel.criticalCap}: critical present`);
649
+ return `${label} Score: ${scoreColor(p, score)(`${score}/100`)} ${p.dim(`(${parts.join(" \xB7 ")})`)}`;
650
+ }
651
+ var MAX_ROUTES_BY_ROUTE = 10;
652
+ function byRouteTree(p, results, config, verbose) {
653
+ const routes = /* @__PURE__ */ new Map();
654
+ for (const r of results) {
655
+ if (r.route === void 0) continue;
656
+ if (!routes.has(r.route)) routes.set(r.route, []);
657
+ routes.get(r.route).push(r);
658
+ }
659
+ const scored = [...routes.entries()].map(([route, rs]) => ({
660
+ route,
661
+ rs,
662
+ score: computeScore(rs, config, { applyCriticalCap: false }).score
663
+ }));
664
+ scored.sort((a, b) => a.score - b.score || a.route.localeCompare(b.route));
665
+ const shown = verbose ? scored : scored.slice(0, MAX_ROUTES_BY_ROUTE);
666
+ const lines = [p.bold("By route"), p.dim(RULE)];
667
+ for (const { route, rs, score } of shown) {
668
+ lines.push(`${terminalSafe(route).padEnd(28)} ${scoreColor(p, score)(`${score}`)}`);
669
+ for (const r of rs.filter((x) => classify(x, config) === "fail")) {
670
+ lines.push(` ${p.red("\u2717")} ${r.id} ${terminalSafe(r.message)}`);
671
+ }
672
+ }
673
+ if (!verbose && scored.length > MAX_ROUTES_BY_ROUTE) {
674
+ const remaining = scored.slice(MAX_ROUTES_BY_ROUTE);
675
+ const avgScore = Math.floor(remaining.reduce((sum, r) => sum + r.score, 0) / remaining.length);
676
+ lines.push(
677
+ p.dim(
678
+ `\u2026and ${remaining.length} more route${remaining.length > 1 ? "s" : ""} (avg score ${avgScore}) \u2014 run with --verbose to see all`
679
+ )
680
+ );
681
+ }
682
+ lines.push("");
683
+ return lines;
684
+ }
685
+ function formatConsoleReport(results, config, options = {}) {
686
+ const p = options.palette ?? noColorPalette;
687
+ const summary = summarize(results, config);
688
+ const { health, categories: byCat } = computeHealth(results, config);
689
+ const present = CATEGORY_ORDER.filter((c) => byCat[c] !== void 0);
690
+ const lines = [];
691
+ if (!options.omitHeader) {
692
+ lines.push(
693
+ p.bold(`Svelte Vitals \xB7 ${options.mode ?? "static mode"}`),
694
+ "",
695
+ `${p.bold("Health:")} ${scoreColor(p, health)(`${health}/100`)}`
696
+ );
697
+ }
698
+ for (const c of present) {
699
+ lines.push(scoreLine(p, categoryLabel(c), byCat[c]));
700
+ }
701
+ lines.push("");
702
+ const SEVERITY_COLOR = {
703
+ critical: (s) => p.red(p.bold(s)),
704
+ warning: (s) => p.yellow(p.bold(s)),
705
+ info: (s) => p.dim(s)
706
+ };
707
+ const failures = results.filter((r) => classify(r, config) === "fail");
708
+ for (const severity of ["critical", "warning", "info"]) {
709
+ const bucket = failures.filter((r) => effectiveSeverity(r, config) === severity);
710
+ if (bucket.length === 0) continue;
711
+ lines.push(SEVERITY_COLOR[severity](`${SEVERITY_TITLE[severity]} (${bucket.length})`), p.dim(RULE));
712
+ if (options.verbose) {
713
+ for (const r of bucket) {
714
+ lines.push(`${p.red("\u2717")} ${r.id} ${terminalSafe(r.message)}`);
715
+ if (r.route) lines.push(p.dim(` ${terminalSafe(r.route)}`));
716
+ if (r.location) lines.push(p.dim(` ${terminalSafe(r.location)}${r.line ? `:${r.line}` : ""}`));
717
+ }
718
+ } else {
719
+ const groups = groupByRule(bucket);
720
+ const shownGroups = groups.slice(0, MAX_RULE_GROUPS_PER_BUCKET);
721
+ for (const group of shownGroups) {
722
+ const r = group.results[0];
723
+ lines.push(`${p.red("\u2717")} ${r.id} ${terminalSafe(r.message)}`);
724
+ if (r.route) lines.push(p.dim(` ${terminalSafe(r.route)}`));
725
+ if (r.location) lines.push(p.dim(` ${terminalSafe(r.location)}${r.line ? `:${r.line}` : ""}`));
726
+ if (group.results.length > 1) {
727
+ lines.push(p.dim(` \u2026and ${group.results.length - 1} more`));
728
+ }
729
+ }
730
+ if (groups.length > MAX_RULE_GROUPS_PER_BUCKET) {
731
+ const remaining = groups.length - MAX_RULE_GROUPS_PER_BUCKET;
732
+ lines.push(
733
+ p.dim(`\u2026and ${remaining} more rule${remaining > 1 ? "s" : ""} affected \u2014 run with --verbose to see all`)
734
+ );
735
+ }
736
+ }
737
+ lines.push("");
738
+ }
739
+ const passed = results.filter((r) => classify(r, config) !== "fail");
740
+ if (passed.length > 0) {
741
+ lines.push(p.bold(`Passed (${passed.length})`), p.dim(RULE));
742
+ if (options.verbose) {
743
+ for (const r of passed) {
744
+ const marker = classify(r, config) === "dynamic" ? p.cyan(" \u21AF dynamic") : "";
745
+ const where = r.location ?? r.route;
746
+ const suffix = where ? ` ${terminalSafe(where)}` : "";
747
+ lines.push(`${p.green("\u2713")} ${r.id} ${terminalSafe(r.message)}${marker}${suffix}`);
748
+ }
749
+ }
750
+ lines.push("");
751
+ }
752
+ if (options.byRoute) lines.push(...byRouteTree(p, results, config, options.verbose ?? false));
753
+ if (options.verbose && summary.dynamic > 0) lines.push(p.dim("\u21AF = set dynamically (verified at runtime)."));
754
+ return lines.join("\n").replace(/\n+$/, "\n");
755
+ }
756
+
757
+ // src/reporter/agent.ts
758
+ function formatAgentReport(results, config) {
759
+ const failing = results.filter((r) => classify(r, config) === "fail");
760
+ const { health } = computeHealth(results, config);
761
+ const lines = ["# svelte-vitals \u2014 fixes", "", `Health: ${health}/100`, ""];
762
+ if (failing.length === 0) {
763
+ lines.push("No issues to fix.", "");
764
+ return lines.join("\n").replace(/\n+$/, "\n");
765
+ }
766
+ lines.push(
767
+ `${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).`,
768
+ ""
769
+ );
770
+ const groups = /* @__PURE__ */ new Map();
771
+ for (const r of failing) {
772
+ const key = r.location ?? r.route ?? "(project)";
773
+ if (!groups.has(key)) groups.set(key, []);
774
+ groups.get(key).push(r);
775
+ }
776
+ const groupSeverity = (rs) => Math.min(...rs.map((r) => SEVERITY_RANK[effectiveSeverity(r, config)]));
777
+ const orderedGroups = [...groups.entries()].sort(
778
+ (a, b) => groupSeverity(a[1]) - groupSeverity(b[1]) || a[0].localeCompare(b[0])
779
+ );
780
+ for (const [loc, rs] of orderedGroups) {
781
+ rs.sort(
782
+ (x, y) => SEVERITY_RANK[effectiveSeverity(x, config)] - SEVERITY_RANK[effectiveSeverity(y, config)] || x.id.localeCompare(y.id)
783
+ );
784
+ lines.push(`## ${mdEscape(loc)}`, "");
785
+ for (const r of rs) {
786
+ lines.push(`### ${r.id} \xB7 ${mdEscape(r.message)} (${effectiveSeverity(r, config)})`);
787
+ if (r.fix) {
788
+ lines.push(`- Fix: ${mdEscape(r.fix.description)}`);
789
+ if (r.fix.snippet) lines.push("", "```" + (r.fix.lang ?? "svelte"), r.fix.snippet, "```");
790
+ } else if (r.recommendation) {
791
+ lines.push(`- Fix: ${mdEscape(r.recommendation)}`);
792
+ }
793
+ if (r.docsUrl) lines.push(`- Docs: ${r.docsUrl}`);
794
+ lines.push(`- Accept: re-run svelte-vitals; ${r.id} passes${r.route ? ` for ${mdEscape(r.route)}` : ""}.`, "");
795
+ }
796
+ }
797
+ return lines.join("\n").replace(/\n+$/, "\n");
798
+ }
799
+
800
+ // src/reporter/sarif.ts
801
+ function formatSarifReport(results, config, meta) {
802
+ const penalized = results.filter((r) => isPenalized(r.detection, config.treatDynamicAs));
803
+ const rules = [];
804
+ const ruleIndex = /* @__PURE__ */ new Map();
805
+ const sarifResults = penalized.map((r) => {
806
+ if (!ruleIndex.has(r.id)) {
807
+ const m = ruleMetaById(r.id);
808
+ const name = m?.title ?? r.id;
809
+ ruleIndex.set(r.id, rules.length);
810
+ rules.push({
811
+ id: r.id,
812
+ name,
813
+ shortDescription: { text: name },
814
+ helpUri: r.docsUrl ?? m?.docsUrl ?? docsUrlFor(r.id),
815
+ defaultConfiguration: { level: severityToSarifLevel(m?.severity ?? r.severity) }
816
+ });
817
+ }
818
+ const result = {
819
+ ruleId: r.id,
820
+ ruleIndex: ruleIndex.get(r.id),
821
+ level: severityToSarifLevel(effectiveSeverity(r, config)),
822
+ message: { text: messageText(r) },
823
+ partialFingerprints: {
824
+ "svelteVitals/v1": `${r.id}:${r.route ?? "project"}${r.line !== void 0 ? `:${r.location ?? ""}:${r.line}` : ""}`
825
+ }
826
+ };
827
+ if (r.location) {
828
+ result.locations = [
829
+ {
830
+ physicalLocation: {
831
+ artifactLocation: { uri: r.location },
832
+ ...r.line !== void 0 ? { region: { startLine: r.line } } : {}
833
+ }
834
+ }
835
+ ];
836
+ }
837
+ return result;
838
+ });
839
+ const log = {
840
+ $schema: "https://json.schemastore.org/sarif-2.1.0.json",
841
+ version: "2.1.0",
842
+ runs: [
843
+ {
844
+ tool: {
845
+ driver: {
846
+ name: "svelte-vitals",
847
+ informationUri: "https://oekazuma.github.io/svelte-vitals",
848
+ version: meta.version,
849
+ rules
850
+ }
851
+ },
852
+ results: sarifResults
853
+ }
854
+ ]
855
+ };
856
+ return JSON.stringify(log, null, 2);
857
+ }
858
+
859
+ // src/reporter/app-shell.ts
860
+ var BAND_COLOR = {
861
+ good: "#2FA968",
862
+ warn: "#E8A317",
863
+ poor: "#E5484D"
864
+ };
865
+ function scoreBand(score) {
866
+ return score >= 90 ? "good" : score >= 50 ? "warn" : "poor";
867
+ }
868
+ function escapeHtml(s) {
869
+ return s.replace(
870
+ /[&<>"']/g,
871
+ (c) => c === "&" ? "&amp;" : c === "<" ? "&lt;" : c === ">" ? "&gt;" : c === '"' ? "&quot;" : "&#39;"
872
+ );
873
+ }
874
+ function safeHref(url) {
875
+ const normalized = url.replace(/\s/g, "").toLowerCase();
876
+ return /^https?:\/\//.test(normalized) ? url : null;
877
+ }
878
+ function embedJson(value) {
879
+ return JSON.stringify(value).replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
880
+ }
881
+ function sanitizeDocsUrl(issue) {
882
+ if (issue.docsUrl === void 0) return issue;
883
+ if (safeHref(issue.docsUrl) !== null) return issue;
884
+ return { ...issue, docsUrl: void 0 };
885
+ }
886
+ function sanitizeReport(report) {
887
+ return {
888
+ ...report,
889
+ routes: report.routes.map((route) => ({ ...route, issues: route.issues.map(sanitizeDocsUrl) })),
890
+ siteIssues: report.siteIssues.map(sanitizeDocsUrl)
891
+ };
892
+ }
893
+ var APP_STYLE = `
894
+ :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}
895
+ :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}
896
+ @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}}
897
+ *{box-sizing:border-box}
898
+ html,body{margin:0;height:100%}
899
+ body{background:var(--ground);color:var(--ink);font-family:var(--sans);line-height:1.5;-webkit-font-smoothing:antialiased}
900
+ .dv-app{display:grid;grid-template-rows:auto 1fr;grid-template-columns:280px 1fr;grid-template-areas:"top top" "side main";height:100vh}
901
+ .dv-topbar{grid-area:top;border-bottom:1px solid var(--line);background:var(--panel)}
902
+ .dv-topbar-inner{display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap;padding:12px 20px}
903
+ .dv-brand{display:flex;align-items:center;background:none;border:none;padding:0;cursor:pointer;border-radius:6px}
904
+ .dv-brand svg{height:28px;width:auto;display:block}
905
+ .dv-brand:focus-visible{outline:2px solid var(--accent);outline-offset:4px}
906
+ .dv-meta{font-family:var(--mono);font-size:12px;color:var(--muted);display:flex;gap:12px;flex-wrap:wrap}
907
+ .dv-status{display:flex;align-items:center;gap:10px}
908
+ .dv-analyzing{font-size:12px;color:var(--accent);font-weight:600}
909
+ .dv-conn{width:8px;height:8px;border-radius:50%;background:var(--faint);display:inline-block}
910
+ .dv-conn-connected{background:var(--good)}
911
+ .dv-conn-reconnecting{background:var(--warn)}
912
+ .dv-menu-toggle{display:none;border:1px solid var(--line-strong);background:var(--panel);color:var(--ink);border-radius:8px;width:28px;height:28px;cursor:pointer}
913
+ .dv-theme-toggle{border:1px solid var(--line-strong);background:var(--panel);color:var(--ink);border-radius:999px;width:28px;height:28px;cursor:pointer}
914
+ .dv-theme-toggle:focus-visible,.dv-menu-toggle:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
915
+ .dv-sidebar{grid-area:side;border-right:1px solid var(--line);background:var(--panel);overflow-y:auto}
916
+ .dv-sidebar-inner{display:flex;flex-direction:column;gap:10px;padding:14px}
917
+ .dv-search{font:inherit;font-size:13px;padding:7px 10px;border:1px solid var(--line-strong);border-radius:8px;background:var(--ground);color:var(--ink)}
918
+ .dv-sort{font:inherit;font-size:12.5px;padding:6px 8px;border:1px solid var(--line-strong);border-radius:8px;background:var(--ground);color:var(--ink)}
919
+ .dv-nav{display:flex;flex-direction:column;gap:2px}
920
+ .dv-nav-item{display:flex;flex-direction:column;gap:4px;padding:8px 10px;border-radius:8px;cursor:pointer}
921
+ .dv-nav-item:hover{background:var(--ground)}
922
+ .dv-nav-item.active{background:var(--active-bg);color:var(--active-ink)}
923
+ .dv-nav-item:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}
924
+ .dv-nav-label{font-family:var(--mono);font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
925
+ .dv-nav-meta{display:flex;align-items:center;gap:8px;font-size:11.5px;color:var(--muted)}
926
+ .dv-nav-item.active .dv-nav-meta{color:inherit}
927
+ .dv-nav-score{font-family:var(--mono);font-weight:700}
928
+ .dv-badge{font-size:9.5px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;padding:1px 6px;border-radius:999px}
929
+ .dv-badge-measured{background:rgba(47,169,104,.16);color:var(--good)}
930
+ .dv-badge-static{background:rgba(140,149,163,.2);color:var(--muted)}
931
+ .dv-detail{grid-area:main;overflow-y:auto;padding:24px 28px 80px}
932
+ .dv-gauge{position:relative;width:132px;height:132px;margin-bottom:20px}
933
+ .dv-gauge svg{position:absolute;inset:0;transform:rotate(-90deg)}
934
+ .dv-gauge-track{stroke:var(--line)}
935
+ .dv-gauge-num{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center}
936
+ .dv-gauge-num strong{font-family:var(--mono);font-size:36px;font-weight:600}
937
+ .dv-gauge-num span{font-size:10px;text-transform:uppercase;letter-spacing:.14em;color:var(--muted)}
938
+ .dv-cats{display:flex;gap:22px;flex-wrap:wrap;margin-bottom:20px}
939
+ .dv-cat{min-width:180px;flex:1}
940
+ .dv-cat-top{display:flex;justify-content:space-between;font-size:13px;margin-bottom:6px}
941
+ .dv-bar{height:7px;border-radius:999px;background:var(--line);overflow:hidden}
942
+ .dv-bar>i{display:block;height:100%;border-radius:999px}
943
+ .dv-cat-reach{font-size:11.5px;color:var(--muted);margin-top:5px}
944
+ .dv-filters{display:flex;gap:8px;flex-wrap:wrap;margin:16px 0}
945
+ .dv-chip{font:inherit;font-size:12.5px;font-weight:600;cursor:pointer;background:var(--panel);border:1px solid var(--line-strong);color:var(--muted);padding:5px 12px;border-radius:999px}
946
+ .dv-chip[aria-pressed="true"]{background:var(--active-bg);border-color:var(--active-bg);color:var(--active-ink)}
947
+ .dv-chip:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
948
+ .dv-section h2{font-size:12px;text-transform:uppercase;letter-spacing:.12em;color:var(--muted);margin:24px 0 12px}
949
+ .dv-detail-header{display:flex;align-items:center;gap:12px;margin-bottom:14px;flex-wrap:wrap}
950
+ .dv-route-path{font-family:var(--mono);font-size:16px;font-weight:600}
951
+ .dv-score-chip{font-family:var(--mono);font-weight:700}
952
+ .dv-finding{background:var(--panel);border:1px solid var(--line);border-left-width:3px;border-radius:10px;padding:16px 18px;margin:0 0 12px}
953
+ .dv-finding-critical{border-left-color:var(--poor)}
954
+ .dv-finding-warning{border-left-color:var(--warn)}
955
+ .dv-finding-info{border-left-color:var(--faint)}
956
+ .dv-f-head{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
957
+ .dv-ruleid{font-family:var(--mono);font-size:12px;font-weight:600;background:var(--ground);padding:2px 8px;border-radius:6px}
958
+ .dv-f-title{font-weight:650;font-size:15px}
959
+ .dv-sev-tag{margin-left:auto;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.08em}
960
+ .dv-sev-critical{color:var(--poor)}
961
+ .dv-sev-warning{color:var(--warn)}
962
+ .dv-sev-info{color:var(--faint)}
963
+ .dv-f-route{display:block;font:inherit;font-family:var(--mono);font-size:12.5px;font-weight:600;color:var(--accent);background:none;border:none;padding:0;margin:8px 0 0;cursor:pointer;text-align:left}
964
+ .dv-f-route:hover{text-decoration:underline}
965
+ .dv-f-route:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
966
+ .dv-f-loc{font-family:var(--mono);font-size:12.5px;color:var(--muted);margin:8px 0 0}
967
+ .dv-f-rec{font-size:14px;margin:10px 0 0}
968
+ .dv-fix{margin:12px 0 0;background:var(--code-bg);border-radius:8px;overflow:hidden}
969
+ .dv-fix-label{font-family:var(--mono);font-size:11px;letter-spacing:.1em;text-transform:uppercase;color:#8da0bd;padding:8px 14px 0}
970
+ .dv-fix pre{margin:0;padding:8px 14px 14px;overflow-x:auto}
971
+ .dv-fix code{font-family:var(--mono);font-size:12.5px;color:var(--code-ink);line-height:1.65;white-space:pre}
972
+ .tok-kw{color:#ff7ab8}
973
+ .tok-str{color:#9ece6a}
974
+ .tok-num{color:#ff9e64}
975
+ .tok-cm{color:#6b7280;font-style:italic}
976
+ .tok-id{color:var(--code-ink)}
977
+ .tok-pn{color:#8da0bd}
978
+ .dv-f-link{display:inline-block;margin-top:12px;font-size:13px;font-weight:600;color:var(--accent);text-decoration:none}
979
+ .dv-f-link:hover{text-decoration:underline}
980
+ .dv-ai-prompt{margin-top:12px;border:1px solid var(--line);border-radius:8px;overflow:hidden}
981
+ .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}
982
+ .dv-ai-prompt>summary::-webkit-details-marker{display:none}
983
+ .dv-ai-prompt>summary::before{content:"\u25B8";display:inline-block;transition:transform .15s ease}
984
+ .dv-ai-prompt[open]>summary::before{transform:rotate(90deg)}
985
+ .dv-ai-prompt-body{padding:0 12px 12px;display:flex;flex-direction:column;gap:8px}
986
+ .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}
987
+ .dv-ai-copy-btn{align-self:flex-start;font:inherit;font-size:12px;font-weight:600;cursor:pointer;background:var(--panel);border:1px solid var(--line-strong);color:var(--ink);padding:5px 12px;border-radius:999px}
988
+ .dv-ai-copy-btn:hover{border-color:var(--faint)}
989
+ .dv-ai-copy-btn:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
990
+ .dv-empty{color:var(--muted);font-size:13px}
991
+ @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)}}
992
+ @media (prefers-reduced-motion:reduce){*{transition:none!important}}
993
+ `;
994
+ var APP_SCRIPT = `
995
+ (function(){
996
+ var BAND_COLOR = { good: '#2fa968', warn: '#e8a317', poor: '#e5484d' };
997
+ var CATEGORY_NAMES = ${JSON.stringify(CATEGORY_LABEL)};
998
+ function scoreBand(score) { return score >= 90 ? 'good' : score >= 50 ? 'warn' : 'poor'; }
999
+
1000
+ // Same mark as the docs site's hero wordmark (docs/public/wordmark.svg) \u2014 an inline
1001
+ // copy, not an <img src>, since the dashboard is a single self-contained HTML response
1002
+ // with no other static assets to serve alongside it. Fixed brand colors (not CSS custom
1003
+ // properties), matching the docs usage: the wordmark reads the same in both themes.
1004
+ var WORDMARK_SVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 380 56" role="img" aria-labelledby="dv-wordmark-title"><title id="dv-wordmark-title">svelte-vitals</title><defs><clipPath id="dv-wordmark-clip"><rect x="2" y="2" width="52" height="52" rx="14"/></clipPath></defs><rect x="2" y="2" width="52" height="52" rx="14" fill="#FF3E00"/><polyline clip-path="url(#dv-wordmark-clip)" points="4,28 15,28 17.5,23.5 20,28 23,28 26,7 29,49 32,28 35,28 37,24.5 39.5,28 52,28" fill="none" stroke="#fff" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/><text x="70" y="38" font-family="ui-sans-serif, system-ui, -apple-system, \\'Segoe UI\\', Roboto, sans-serif" font-size="30" font-weight="700" fill="#FF3E00">svelte-vitals</text></svg>';
1005
+
1006
+ function h(tag, attrs, kids) {
1007
+ var n = document.createElement(tag);
1008
+ if (attrs) {
1009
+ for (var k in attrs) {
1010
+ if (!Object.prototype.hasOwnProperty.call(attrs, k)) continue;
1011
+ var v = attrs[k];
1012
+ if (v === undefined || v === null || v === false) continue;
1013
+ if (k === 'class') n.className = v;
1014
+ else if (k === 'text') n.textContent = v;
1015
+ else if (k.indexOf('on') === 0 && typeof v === 'function') n.addEventListener(k.slice(2), v);
1016
+ else n.setAttribute(k, v === true ? '' : String(v));
1017
+ }
1018
+ }
1019
+ (kids || []).forEach(function (c) {
1020
+ if (c === undefined || c === null || c === false) return;
1021
+ n.appendChild(typeof c === 'string' ? document.createTextNode(c) : c);
1022
+ });
1023
+ return n;
1024
+ }
1025
+ function clear(n) { while (n.firstChild) n.removeChild(n.firstChild); }
1026
+ function mount(id, node) { var el = document.getElementById(id); clear(el); el.appendChild(node); }
1027
+
1028
+ var HL_KEYWORDS = ['import','export','from','const','let','var','function','return','if','else','for','while','class','new','await','async','default','type','interface','extends','implements','this','typeof','instanceof','of','in','true','false','null','undefined'];
1029
+ var HL_LANGS = { js: 1, javascript: 1, ts: 1, typescript: 1, svelte: 1, html: 1, css: 1 };
1030
+
1031
+ function highlightTokens(code) {
1032
+ var tokens = [];
1033
+ var i = 0;
1034
+ var n = code.length;
1035
+ var reIdent = /[A-Za-z_$][A-Za-z0-9_$]*/y;
1036
+ var reNum = /\\d+(\\.\\d+)?/y;
1037
+ while (i < n) {
1038
+ var ch = code[i];
1039
+ if (ch === '/' && code[i + 1] === '/') {
1040
+ var end = code.indexOf('\\n', i);
1041
+ if (end === -1) end = n;
1042
+ tokens.push({ text: code.slice(i, end), cls: 'cm' });
1043
+ i = end;
1044
+ continue;
1045
+ }
1046
+ if (ch === '/' && code[i + 1] === '*') {
1047
+ var end2 = code.indexOf('*/', i + 2);
1048
+ end2 = end2 === -1 ? n : end2 + 2;
1049
+ tokens.push({ text: code.slice(i, end2), cls: 'cm' });
1050
+ i = end2;
1051
+ continue;
1052
+ }
1053
+ if (ch === '"' || ch === "'" || ch === '\`') {
1054
+ var quote = ch;
1055
+ var j = i + 1;
1056
+ while (j < n && code[j] !== quote) {
1057
+ if (code[j] === '\\\\') j++;
1058
+ j++;
1059
+ }
1060
+ j = Math.min(j + 1, n);
1061
+ tokens.push({ text: code.slice(i, j), cls: 'str' });
1062
+ i = j;
1063
+ continue;
1064
+ }
1065
+ reIdent.lastIndex = i;
1066
+ var mIdent = reIdent.exec(code);
1067
+ if (mIdent && mIdent.index === i) {
1068
+ var word = mIdent[0];
1069
+ tokens.push({ text: word, cls: HL_KEYWORDS.indexOf(word) !== -1 ? 'kw' : 'id' });
1070
+ i += word.length;
1071
+ continue;
1072
+ }
1073
+ reNum.lastIndex = i;
1074
+ var mNum = reNum.exec(code);
1075
+ if (mNum && mNum.index === i) {
1076
+ tokens.push({ text: mNum[0], cls: 'num' });
1077
+ i += mNum[0].length;
1078
+ continue;
1079
+ }
1080
+ tokens.push({ text: ch, cls: 'pn' });
1081
+ i += 1;
1082
+ }
1083
+ return tokens;
1084
+ }
1085
+
1086
+ function renderFixSnippet(fix) {
1087
+ var pre = h('pre', null, []);
1088
+ var code = h('code', null, []);
1089
+ var lang = (fix.lang || 'svelte').toLowerCase();
1090
+ if (HL_LANGS[lang]) {
1091
+ highlightTokens(fix.snippet).forEach(function (t) {
1092
+ code.appendChild(h('span', { class: 'tok-' + t.cls, text: t.text }, []));
1093
+ });
1094
+ } else {
1095
+ code.textContent = fix.snippet;
1096
+ }
1097
+ pre.appendChild(code);
1098
+ return pre;
1099
+ }
1100
+
1101
+ // Plain-text, copy-pasteable prompt for a single finding \u2014 same ingredients as the
1102
+ // agent reporter's per-finding block (rule id, location, recommendation, fix, docs),
1103
+ // reshaped for a standalone request rather than a whole-project remediation doc.
1104
+ function buildAiPrompt(issue, route) {
1105
+ var lines = ['Fix this svelte-vitals finding:', ''];
1106
+ lines.push('- Rule: ' + issue.id + ' \u2014 ' + issue.title + ' (' + issue.severity + ')');
1107
+ if (route) lines.push('- Route: ' + route);
1108
+ if (issue.location) {
1109
+ lines.push('- Location: ' + issue.location + (issue.line !== undefined ? ':' + issue.line : ''));
1110
+ }
1111
+ if (issue.recommendation) lines.push('- Recommendation: ' + issue.recommendation);
1112
+ if (issue.fix) {
1113
+ lines.push('- Fix: ' + issue.fix.description);
1114
+ if (issue.fix.snippet) {
1115
+ lines.push('', '\`\`\`' + (issue.fix.lang || 'svelte'), issue.fix.snippet, '\`\`\`');
1116
+ }
1117
+ }
1118
+ if (issue.docsUrl) lines.push('- Docs: ' + issue.docsUrl);
1119
+ lines.push(
1120
+ '',
1121
+ 'After fixing, re-run \`svelte-vitals --diff\` (or revisit this route) to confirm ' +
1122
+ issue.id +
1123
+ ' passes' +
1124
+ (route ? ' for ' + route : '') +
1125
+ '.'
1126
+ );
1127
+ return lines.join('\\n');
1128
+ }
1129
+
1130
+ function copyToClipboard(text, btn) {
1131
+ var original = 'Copy';
1132
+ function reset(label) {
1133
+ btn.textContent = label;
1134
+ setTimeout(function () { btn.textContent = original; }, 1500);
1135
+ }
1136
+ function done() { reset('Copied!'); }
1137
+ function fail() { reset('Copy failed'); }
1138
+ function fallbackCopy() {
1139
+ var ta = document.createElement('textarea');
1140
+ ta.value = text;
1141
+ ta.setAttribute('readonly', '');
1142
+ ta.style.position = 'fixed';
1143
+ ta.style.opacity = '0';
1144
+ document.body.appendChild(ta);
1145
+ ta.select();
1146
+ var ok = false;
1147
+ try { ok = document.execCommand('copy'); } catch (e) {}
1148
+ document.body.removeChild(ta);
1149
+ return ok;
1150
+ }
1151
+ if (navigator.clipboard && navigator.clipboard.writeText) {
1152
+ navigator.clipboard.writeText(text).then(done, function () { fallbackCopy() ? done() : fail(); });
1153
+ } else {
1154
+ fallbackCopy() ? done() : fail();
1155
+ }
1156
+ }
1157
+
1158
+ function renderAiPrompt(issue, route) {
1159
+ var text = buildAiPrompt(issue, route);
1160
+ var btn = h('button', { type: 'button', class: 'dv-ai-copy-btn', text: 'Copy' }, []);
1161
+ btn.addEventListener('click', function () { copyToClipboard(text, btn); });
1162
+ return h('details', { class: 'dv-ai-prompt' }, [
1163
+ h('summary', { text: 'AI Prompt' }, []),
1164
+ h('div', { class: 'dv-ai-prompt-body' }, [
1165
+ h('pre', { class: 'dv-ai-prompt-pre', text: text }, []),
1166
+ btn
1167
+ ])
1168
+ ]);
1169
+ }
1170
+
1171
+ var state = {
1172
+ snapshot: null,
1173
+ selected: 'overview',
1174
+ search: '',
1175
+ sort: 'score-asc',
1176
+ filter: 'all',
1177
+ theme: initialTheme(),
1178
+ connection: 'connecting',
1179
+ routeBySlug: {}
1180
+ };
1181
+
1182
+ function initialTheme() {
1183
+ try {
1184
+ var stored = localStorage.getItem('svelte-vitals-theme');
1185
+ if (stored === 'dark' || stored === 'light') return stored;
1186
+ } catch (e) {}
1187
+ return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
1188
+ }
1189
+ function applyTheme() { document.documentElement.setAttribute('data-theme', state.theme); }
1190
+ function toggleTheme() {
1191
+ state.theme = state.theme === 'dark' ? 'light' : 'dark';
1192
+ try { localStorage.setItem('svelte-vitals-theme', state.theme); } catch (e) {}
1193
+ applyTheme();
1194
+ renderTopbar();
1195
+ }
1196
+ function toggleSidebar() {
1197
+ var sb = document.getElementById('dv-sidebar');
1198
+ if (sb) sb.classList.toggle('open');
1199
+ }
1200
+
1201
+ function brandEl() {
1202
+ var el = h('button', { type: 'button', class: 'dv-brand', 'aria-label': 'Go to Overview', onclick: function () { selectItem('overview'); } }, []);
1203
+ el.innerHTML = WORDMARK_SVG;
1204
+ return el;
1205
+ }
1206
+
1207
+ function renderTopbar() {
1208
+ var s = state.snapshot;
1209
+ var findings = s.report.routes.reduce(function (n, r) { return n + r.issues.length; }, 0) + s.report.siteIssues.length;
1210
+ var kids = [
1211
+ h('button', { type: 'button', class: 'dv-menu-toggle', 'aria-label': 'Toggle route list', onclick: toggleSidebar, text: '\u2261' }, []),
1212
+ brandEl(),
1213
+ h('div', { class: 'dv-meta' }, [
1214
+ h('span', { text: 'v' + s.meta.version }, []),
1215
+ s.meta.coreVersion ? h('span', { title: '@svelte-vitals/core version', text: 'core v' + s.meta.coreVersion }, []) : null,
1216
+ h('span', { text: s.report.routes.length + ' routes' }, []),
1217
+ h('span', { text: findings + ' findings' }, [])
1218
+ ].filter(Boolean)),
1219
+ h('div', { class: 'dv-status' }, [
1220
+ s.live && s.analyzing ? h('span', { class: 'dv-analyzing', text: 'Analyzing\u2026' }, []) : null,
1221
+ s.live ? h('span', { class: 'dv-conn dv-conn-' + state.connection, title: state.connection }, []) : null,
1222
+ h('button', { type: 'button', class: 'dv-theme-toggle', 'aria-label': 'Toggle dark mode', onclick: toggleTheme, text: state.theme === 'dark' ? '\u2600' : '\u263E' }, [])
1223
+ ].filter(Boolean))
1224
+ ];
1225
+ mount('dv-topbar', h('div', { class: 'dv-topbar-inner' }, kids));
1226
+ }
1227
+
1228
+ function slugify(route) {
1229
+ return 'route-' + route.replace(/[^a-zA-Z0-9]+/g, '-').replace(/^-|-$/g, '').toLowerCase();
1230
+ }
1231
+
1232
+ function matchesSearch(route, q) {
1233
+ if (!q) return true;
1234
+ q = q.toLowerCase();
1235
+ if (route.route.toLowerCase().indexOf(q) !== -1) return true;
1236
+ return route.issues.some(function (iss) {
1237
+ return (iss.id + ' ' + iss.title + ' ' + (iss.location || '')).toLowerCase().indexOf(q) !== -1;
1238
+ });
1239
+ }
1240
+
1241
+ function sortedRoutes() {
1242
+ var s = state.snapshot;
1243
+ var q = state.search.trim();
1244
+ var list = s.report.routes.filter(function (r) { return matchesSearch(r, q); }).slice();
1245
+ var sort = state.sort;
1246
+ if (sort === 'score-asc') list.sort(function (a, b) { return a.score - b.score; });
1247
+ else if (sort === 'score-desc') list.sort(function (a, b) { return b.score - a.score; });
1248
+ else if (sort === 'alpha') list.sort(function (a, b) { return a.route.localeCompare(b.route); });
1249
+ else if (sort === 'most-findings') list.sort(function (a, b) { return b.issues.length - a.issues.length; });
1250
+ return list;
1251
+ }
1252
+
1253
+ function renderNavItem(label, key, route, active) {
1254
+ var kids = [h('span', { class: 'dv-nav-label', text: label }, [])];
1255
+ if (route) {
1256
+ var band = scoreBand(route.score);
1257
+ var crit = route.issues.filter(function (i) { return i.severity === 'critical'; }).length;
1258
+ var warn = route.issues.filter(function (i) { return i.severity === 'warning'; }).length;
1259
+ var info = route.issues.filter(function (i) { return i.severity === 'info'; }).length;
1260
+ var summary = [];
1261
+ if (crit) summary.push(crit + ' critical');
1262
+ if (warn) summary.push(warn + ' warning' + (warn > 1 ? 's' : ''));
1263
+ if (info) summary.push(info + ' info');
1264
+ var badge = state.snapshot.badges[route.route];
1265
+ kids.push(h('span', { class: 'dv-nav-meta' }, [
1266
+ badge ? h('span', { class: 'dv-badge dv-badge-' + badge, text: badge }, []) : null,
1267
+ h('span', { class: 'dv-nav-score', style: 'color:' + BAND_COLOR[band], text: String(route.score) }, []),
1268
+ h('span', { class: 'dv-nav-sum', text: summary.length ? summary.join(' \xB7 ') : 'no issues' }, [])
1269
+ ].filter(Boolean)));
1270
+ }
1271
+ return h('div', {
1272
+ class: 'dv-nav-item' + (active ? ' active' : ''),
1273
+ role: 'option',
1274
+ 'aria-selected': active ? 'true' : 'false',
1275
+ tabindex: '0',
1276
+ onclick: function () { selectItem(key); },
1277
+ onkeydown: function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); selectItem(key); } }
1278
+ }, kids);
1279
+ }
1280
+
1281
+ function selectItem(key) {
1282
+ state.selected = key;
1283
+ location.hash = key === 'overview' ? 'overview' : 'route/' + slugify(key);
1284
+ var sb = document.getElementById('dv-sidebar');
1285
+ if (sb) sb.classList.remove('open');
1286
+ renderSidebar();
1287
+ renderDetail();
1288
+ }
1289
+
1290
+ function renderSidebar() {
1291
+ var s = state.snapshot;
1292
+ state.routeBySlug = {};
1293
+
1294
+ // mount() clears and rebuilds the whole sidebar, including the <input> itself, so
1295
+ // a naive re-render on every keystroke drops focus and cursor position \u2014 capture
1296
+ // them before rebuilding and restore them on the freshly-created input afterward.
1297
+ var prevSearch = document.querySelector('.dv-search');
1298
+ var hadFocus = !!prevSearch && document.activeElement === prevSearch;
1299
+ var selStart = hadFocus ? prevSearch.selectionStart : null;
1300
+ var selEnd = hadFocus ? prevSearch.selectionEnd : null;
1301
+
1302
+ var searchInput = h('input', {
1303
+ type: 'search',
1304
+ class: 'dv-search',
1305
+ placeholder: 'Search routes or rules\u2026',
1306
+ value: state.search,
1307
+ oninput: function (e) { state.search = e.target.value; renderSidebar(); }
1308
+ }, []);
1309
+
1310
+ var sortSelect = h('select', { class: 'dv-sort', 'aria-label': 'Sort routes', onchange: function (e) { state.sort = e.target.value; renderSidebar(); } }, [
1311
+ h('option', { value: 'score-asc', selected: state.sort === 'score-asc' || undefined, text: 'Score (worst first)' }, []),
1312
+ h('option', { value: 'score-desc', selected: state.sort === 'score-desc' || undefined, text: 'Score (best first)' }, []),
1313
+ h('option', { value: 'alpha', selected: state.sort === 'alpha' || undefined, text: 'Alphabetical' }, []),
1314
+ h('option', { value: 'most-findings', selected: state.sort === 'most-findings' || undefined, text: 'Most findings' }, [])
1315
+ ]);
1316
+
1317
+ var items = [renderNavItem('Overview', 'overview', null, state.selected === 'overview')];
1318
+ sortedRoutes().forEach(function (r) {
1319
+ var slug = slugify(r.route);
1320
+ state.routeBySlug[slug] = r.route;
1321
+ items.push(renderNavItem(r.route, r.route, r, state.selected === r.route));
1322
+ });
1323
+
1324
+ var nav = h('div', { class: 'dv-nav', role: 'listbox', 'aria-label': 'Routes' }, items);
1325
+ mount('dv-sidebar', h('div', { class: 'dv-sidebar-inner' }, [searchInput, sortSelect, nav]));
1326
+
1327
+ if (hadFocus) {
1328
+ searchInput.focus();
1329
+ if (selStart !== null && searchInput.setSelectionRange) {
1330
+ searchInput.setSelectionRange(selStart, selEnd);
1331
+ }
1332
+ }
1333
+ }
1334
+
1335
+ function renderFilterChips(categories) {
1336
+ var chip = function (filter, label) {
1337
+ return h('button', {
1338
+ type: 'button', class: 'dv-chip', 'aria-pressed': state.filter === filter ? 'true' : 'false',
1339
+ onclick: function () { state.filter = filter; renderDetail(); },
1340
+ text: label
1341
+ }, []);
1342
+ };
1343
+ var catChips = Object.keys(categories).map(function (cat) {
1344
+ var name = CATEGORY_NAMES[cat] || cat;
1345
+ return chip(cat, name);
1346
+ });
1347
+ return h('div', { class: 'dv-filters', role: 'group', 'aria-label': 'Filter findings' },
1348
+ [chip('all', 'All'), chip('critical', 'Critical'), chip('warning', 'Warning'), chip('info', 'Info')].concat(catChips));
1349
+ }
1350
+
1351
+ function passesFilter(issue) {
1352
+ var f = state.filter;
1353
+ return f === 'all' || issue.severity === f || issue.category === f;
1354
+ }
1355
+
1356
+ var SEVERITY_ORDER = { critical: 0, warning: 1, info: 2 };
1357
+
1358
+ function renderFinding(issue, route, promptRoute) {
1359
+ var kids = [
1360
+ h('div', { class: 'dv-f-head' }, [
1361
+ h('span', { class: 'dv-ruleid', text: issue.id }, []),
1362
+ h('span', { class: 'dv-f-title', text: issue.title }, []),
1363
+ h('span', { class: 'dv-sev-tag dv-sev-' + issue.severity, text: issue.severity }, [])
1364
+ ])
1365
+ ];
1366
+ if (route) {
1367
+ kids.push(h('button', { type: 'button', class: 'dv-f-route', onclick: function () { selectItem(route); }, text: route }, []));
1368
+ }
1369
+ if (issue.location) {
1370
+ kids.push(h('p', { class: 'dv-f-loc', text: issue.location + (issue.line !== undefined ? ':' + issue.line : '') }, []));
1371
+ }
1372
+ if (issue.recommendation) {
1373
+ kids.push(h('p', { class: 'dv-f-rec', text: issue.recommendation }, []));
1374
+ }
1375
+ if (issue.fix && issue.fix.snippet) {
1376
+ kids.push(h('div', { class: 'dv-fix' }, [h('div', { class: 'dv-fix-label', text: 'fix' }, []), renderFixSnippet(issue.fix)]));
1377
+ }
1378
+ if (issue.docsUrl) {
1379
+ kids.push(h('a', { class: 'dv-f-link', href: issue.docsUrl, text: 'Learn more' }, []));
1380
+ }
1381
+ kids.push(renderAiPrompt(issue, promptRoute !== undefined ? promptRoute : route));
1382
+ return h('article', { class: 'dv-finding dv-finding-' + issue.severity }, kids);
1383
+ }
1384
+
1385
+ function renderGauge(score) {
1386
+ var band = scoreBand(score);
1387
+ var svgNs = 'http://www.w3.org/2000/svg';
1388
+ var C = 2 * Math.PI * 58;
1389
+ var offset = (C * (1 - score / 100)).toFixed(1);
1390
+ var svg = document.createElementNS(svgNs, 'svg');
1391
+ svg.setAttribute('width', '132');
1392
+ svg.setAttribute('height', '132');
1393
+ svg.setAttribute('viewBox', '0 0 132 132');
1394
+ var bg = document.createElementNS(svgNs, 'circle');
1395
+ bg.setAttribute('cx', '66'); bg.setAttribute('cy', '66'); bg.setAttribute('r', '58');
1396
+ bg.setAttribute('fill', 'none'); bg.setAttribute('class', 'dv-gauge-track'); bg.setAttribute('stroke-width', '11');
1397
+ var arc = document.createElementNS(svgNs, 'circle');
1398
+ arc.setAttribute('cx', '66'); arc.setAttribute('cy', '66'); arc.setAttribute('r', '58');
1399
+ arc.setAttribute('fill', 'none'); arc.setAttribute('stroke', BAND_COLOR[band]); arc.setAttribute('stroke-width', '11');
1400
+ arc.setAttribute('stroke-linecap', 'round');
1401
+ arc.setAttribute('stroke-dasharray', C.toFixed(1));
1402
+ arc.setAttribute('stroke-dashoffset', offset);
1403
+ svg.appendChild(bg);
1404
+ svg.appendChild(arc);
1405
+ var wrap = h('div', { class: 'dv-gauge' }, [h('div', { class: 'dv-gauge-num' }, [h('strong', { text: String(score) }, []), h('span', { text: 'Health' }, [])])]);
1406
+ wrap.insertBefore(svg, wrap.firstChild);
1407
+ return wrap;
1408
+ }
1409
+
1410
+ function renderOverview(s) {
1411
+ var gauge = renderGauge(s.report.score);
1412
+ var cats = Object.keys(s.report.categories).map(function (cat) {
1413
+ var c = s.report.categories[cat];
1414
+ var band = scoreBand(c.score);
1415
+ var weight = s.report.weights[cat];
1416
+ var name = CATEGORY_NAMES[cat] || cat;
1417
+ // keys/affectedKeys are absent on hand-built snapshots (older fixtures, tests) \u2014
1418
+ // render nothing rather than "undefined of undefined". 0 affected of N keys is still
1419
+ // rendered: on a real project that's the signal a thin score can't give, that the
1420
+ // category is clean project-wide and not just on the one key it happened to look at
1421
+ // (design: 2026-08-05-score-floor-and-reach-design.md).
1422
+ var reach = typeof c.keys === 'number' && c.keys > 0
1423
+ ? h('div', { class: 'dv-cat-reach', text: c.affectedKeys + ' of ' + c.keys + ' keys affected' }, [])
1424
+ : null;
1425
+ return h('div', { class: 'dv-cat' }, [
1426
+ h('div', { class: 'dv-cat-top' }, [
1427
+ h('span', { text: name + (weight !== undefined ? ' (weight ' + weight + ')' : '') }, []),
1428
+ h('span', { style: 'color:' + BAND_COLOR[band], text: String(c.score) }, [])
1429
+ ]),
1430
+ h('div', { class: 'dv-bar' }, [h('i', { style: 'width:' + c.score + '%;background:' + BAND_COLOR[band] }, [])]),
1431
+ reach
1432
+ ]);
1433
+ });
1434
+ var chips = renderFilterChips(s.report.categories);
1435
+ var totalCount = s.report.routes.reduce(function (n, r) { return n + r.issues.length; }, 0) + s.report.siteIssues.length;
1436
+ var entries = [];
1437
+ s.report.routes.forEach(function (r) {
1438
+ r.issues.forEach(function (issue) { entries.push({ issue: issue, route: r.route }); });
1439
+ });
1440
+ s.report.siteIssues.forEach(function (issue) { entries.push({ issue: issue, route: null }); });
1441
+ entries = entries.filter(function (e) { return passesFilter(e.issue); });
1442
+ entries.sort(function (a, b) {
1443
+ var sd = SEVERITY_ORDER[a.issue.severity] - SEVERITY_ORDER[b.issue.severity];
1444
+ if (sd !== 0) return sd;
1445
+ return (a.route || '').localeCompare(b.route || '');
1446
+ });
1447
+ var body = entries.length
1448
+ ? entries.map(function (e) { return renderFinding(e.issue, e.route); })
1449
+ : [h('p', { class: 'dv-empty', text: totalCount ? 'No issues match the current filter.' : 'No issues found \u2014 nice work!' }, [])];
1450
+ var findings = h('section', { class: 'dv-section' }, [h('h2', { text: 'Findings' }, [])].concat(body));
1451
+ return h('div', { class: 'dv-overview' }, [gauge, h('div', { class: 'dv-cats' }, cats), chips, findings].filter(Boolean));
1452
+ }
1453
+
1454
+ function renderRouteDetail(route) {
1455
+ var badge = state.snapshot.badges[route.route];
1456
+ var band = scoreBand(route.score);
1457
+ var header = h('div', { class: 'dv-detail-header' }, [
1458
+ h('span', { class: 'dv-route-path', text: route.route }, []),
1459
+ badge ? h('span', { class: 'dv-badge dv-badge-' + badge, text: badge }, []) : null,
1460
+ h('span', { class: 'dv-score-chip', style: 'color:' + BAND_COLOR[band], text: String(route.score) }, [])
1461
+ ].filter(Boolean));
1462
+ var chips = renderFilterChips(state.snapshot.report.categories);
1463
+ var findings = route.issues.filter(passesFilter);
1464
+ var body = findings.length
1465
+ ? findings.map(function (issue) { return renderFinding(issue, undefined, route.route); })
1466
+ : [h('p', { class: 'dv-empty', text: 'No issues match the current filter.' }, [])];
1467
+ return h('div', { class: 'dv-route-detail' }, [header, chips].concat(body));
1468
+ }
1469
+
1470
+ function renderDetail() {
1471
+ var s = state.snapshot;
1472
+ if (state.selected === 'overview') {
1473
+ mount('dv-detail', renderOverview(s));
1474
+ return;
1475
+ }
1476
+ var route = s.report.routes.filter(function (r) { return r.route === state.selected; })[0];
1477
+ if (!route) {
1478
+ state.selected = 'overview';
1479
+ mount('dv-detail', renderOverview(s));
1480
+ return;
1481
+ }
1482
+ mount('dv-detail', renderRouteDetail(route));
1483
+ }
1484
+
1485
+ function renderAll() {
1486
+ renderTopbar();
1487
+ renderSidebar();
1488
+ renderDetail();
1489
+ }
1490
+
1491
+ function restoreSelectionFromHash() {
1492
+ var raw = location.hash.replace(/^#/, '');
1493
+ if (!raw || raw === 'overview') { state.selected = 'overview'; return; }
1494
+ var m = /^route\\/(.+)$/.exec(raw);
1495
+ if (m && state.routeBySlug[m[1]]) state.selected = state.routeBySlug[m[1]];
1496
+ }
1497
+
1498
+ function fetchSnapshot() {
1499
+ fetch('/__svelte-vitals/data.json').then(function (r) { return r.json(); }).then(function (data) {
1500
+ if (state.snapshot && data.sequence <= state.snapshot.sequence) return;
1501
+ state.snapshot = data;
1502
+ renderAll();
1503
+ }).catch(function () {});
1504
+ }
1505
+
1506
+ function boot() {
1507
+ var raw = document.getElementById('svelte-vitals-data');
1508
+ state.snapshot = JSON.parse(raw.textContent);
1509
+ applyTheme();
1510
+ renderSidebar(); // populates routeBySlug before the hash can be trusted
1511
+ restoreSelectionFromHash();
1512
+ renderAll();
1513
+
1514
+ window.addEventListener('hashchange', function () {
1515
+ restoreSelectionFromHash();
1516
+ renderSidebar();
1517
+ renderDetail();
1518
+ });
1519
+
1520
+ // Static export (the CLI's --reporter html): no dev server behind the page, so no
1521
+ // SSE connection, no /data.json refetch, no connection indicator.
1522
+ if (state.snapshot.live && typeof EventSource !== 'undefined') {
1523
+ var es = new EventSource('/__svelte-vitals/events');
1524
+ es.addEventListener('open', function () { state.connection = 'connected'; renderTopbar(); fetchSnapshot(); });
1525
+ es.addEventListener('update', fetchSnapshot);
1526
+ es.addEventListener('error', function () { state.connection = 'reconnecting'; renderTopbar(); });
1527
+ }
1528
+ }
1529
+
1530
+ boot();
1531
+ })();
1532
+ `;
1533
+ function renderAppShell(snapshot) {
1534
+ const safe = { ...snapshot, report: sanitizeReport(snapshot.report) };
1535
+ const title = snapshot.live ? "svelte-vitals dashboard" : "svelte-vitals report";
1536
+ 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>`;
1537
+ }
1538
+ function buildHtmlDocument(report, meta) {
1539
+ return renderAppShell({
1540
+ report,
1541
+ badges: {},
1542
+ analyzing: false,
1543
+ sequence: 0,
1544
+ live: false,
1545
+ meta
1546
+ });
1547
+ }
1548
+ function formatHtmlReport(results, config, meta) {
1549
+ return buildHtmlDocument(buildJsonReport(results, config, meta), meta);
1550
+ }
1551
+ export {
1552
+ APP_SCRIPT,
1553
+ APP_STYLE,
1554
+ BAND_COLOR,
1555
+ CHILD_NODE_KEYS,
1556
+ IDREF_ATTRS,
1557
+ LANDMARK_ROLES,
1558
+ READ_CONCURRENCY,
1559
+ ROBOTS_SOURCE_PATHS,
1560
+ SITEMAP_SOURCE_PATHS,
1561
+ SVELTE_CONFIG_FILES,
1562
+ VITE_CONFIG_FILES,
1563
+ a11yAccessibleName,
1564
+ a11yDoctype,
1565
+ a11yDuplicateLandmark,
1566
+ a11yIdDuplication,
1567
+ a11yInteractiveNesting,
1568
+ a11yInvalidAriaValue,
1569
+ a11yInvalidRole,
1570
+ a11yLabelHasControl,
1571
+ a11yNoMissingIdRef,
1572
+ a11yPlaceholderLabelOption,
1573
+ a11yRequireDatetime,
1574
+ a11yRequiredAriaProps,
1575
+ a11yTopLevelLandmark,
1576
+ a11yUnknownAriaAttribute,
1577
+ a11yUseList,
1578
+ allRules,
1579
+ applyOverrides,
1580
+ applyRuleSeverities,
1581
+ architectureComponentSize,
1582
+ architectureDirectoryNaming,
1583
+ architectureDocLinkTarget,
1584
+ architecturePrivateScopeImport,
1585
+ architecturePropCount,
1586
+ architectureReservedDirectoryNames,
1587
+ architectureReservedNamePlacement,
1588
+ architectureRouteComponentImport,
1589
+ architectureUnitEntryFile,
1590
+ attrText,
1591
+ attrTextOf,
1592
+ attrValue,
1593
+ attrValueOf,
1594
+ buildHtmlDocument,
1595
+ buildJsonReport,
1596
+ classify,
1597
+ collectComponentFacts,
1598
+ collectKitModuleFacts,
1599
+ collectSourceFiles,
1600
+ compileOverrides,
1601
+ computeHealth,
1602
+ computeScore,
1603
+ correctnessBasePathNavigation,
1604
+ correctnessCheckableBindValue,
1605
+ correctnessEachIndexKey,
1606
+ correctnessEachKey,
1607
+ correctnessEffectAsDerived,
1608
+ correctnessEffectAsOnMount,
1609
+ correctnessInstanceBrowserGlobal,
1610
+ correctnessNonreactiveBuiltinState,
1611
+ correctnessOrphanEffect,
1612
+ correctnessOrphanLifecycle,
1613
+ correctnessPropMutation,
1614
+ correctnessServerBrowserGlobal,
1615
+ correctnessStalePropDerivation,
1616
+ correctnessUnmutatedState,
1617
+ decodeFragmentId,
1618
+ defaultConfig,
1619
+ defaultProject,
1620
+ docsUrlFor,
1621
+ effectiveSeverity,
1622
+ emptyComponentFacts,
1623
+ emptyKitModuleFacts,
1624
+ escapeHtml,
1625
+ explainRule,
1626
+ findAttr,
1627
+ findKitAliasesInSvelteConfig,
1628
+ findKitPathsBaseInSvelteConfig,
1629
+ findKitPathsBaseInViteConfig,
1630
+ findMinifyDisabled,
1631
+ foldOccurrences,
1632
+ formatAgentReport,
1633
+ formatConsoleReport,
1634
+ formatFailedRuleWarning,
1635
+ formatGithubReport,
1636
+ formatHtmlReport,
1637
+ formatJsonReport,
1638
+ formatMarkdownReport,
1639
+ formatSarifReport,
1640
+ hasFailureAtOrAbove,
1641
+ headTagRule,
1642
+ imageRule,
1643
+ intOption,
1644
+ isMentionedAnywhere,
1645
+ isPenalized,
1646
+ isTopFragment,
1647
+ lineOf,
1648
+ linkRule,
1649
+ listOption,
1650
+ mapOption,
1651
+ noColorPalette,
1652
+ overrideMatches,
1653
+ parseComponentFacts,
1654
+ parseKitModuleFacts,
1655
+ parseSvelte,
1656
+ performanceFontPreloadCrossorigin,
1657
+ performanceHeavyImport,
1658
+ performanceImageDimensions,
1659
+ performanceImageLoadingHint,
1660
+ performanceLcpImage,
1661
+ performanceLoadWaterfall,
1662
+ performanceMinifyDisabled,
1663
+ performanceNamespaceImport,
1664
+ performancePreconnect,
1665
+ performancePreloadMissingAs,
1666
+ performanceRenderBlockingScript,
1667
+ performanceResponsiveImage,
1668
+ performanceSequentialAwaits,
1669
+ performanceStateRaw,
1670
+ renderAppShell,
1671
+ resolveKitAliases,
1672
+ resolveKitPathsBase,
1673
+ resolveRepoLocalPath,
1674
+ resolveRuleOptions,
1675
+ resolveRunesModuleSpecifier,
1676
+ runRules,
1677
+ safeHref,
1678
+ scoreBand,
1679
+ scoreColor,
1680
+ scoresByCategory,
1681
+ securityHandlerStateWrite,
1682
+ securityJavascriptUrl,
1683
+ securityRawHtml,
1684
+ securityServerModuleState,
1685
+ securitySharedStateImport,
1686
+ selectRules,
1687
+ seoCanonicalUrl,
1688
+ seoCharset,
1689
+ seoDescriptionLength,
1690
+ seoDescriptionPresence,
1691
+ seoDuplicateDescription,
1692
+ seoDuplicateTitle,
1693
+ seoHeadingLevelSkip,
1694
+ seoHreflang,
1695
+ seoHtmlLang,
1696
+ seoImageAlt,
1697
+ seoIndexability,
1698
+ seoJsonLd,
1699
+ seoJsonLdDateFormat,
1700
+ seoJsonLdDeprecatedType,
1701
+ seoJsonLdPlaceholder,
1702
+ seoJsonLdRelativeUrl,
1703
+ seoJsonLdRequiredProps,
1704
+ seoJsonLdValidity,
1705
+ seoOgDescription,
1706
+ seoOgImage,
1707
+ seoOgTitle,
1708
+ seoOgUrl,
1709
+ seoRobotsTxt,
1710
+ seoSingleH1,
1711
+ seoSitemapInRobots,
1712
+ seoSitemapXml,
1713
+ seoSsrDisabled,
1714
+ seoTitleLength,
1715
+ seoTitlePresence,
1716
+ seoTwitterCard,
1717
+ seoViewport,
1718
+ settingOptions,
1719
+ settingSeverity,
1720
+ shouldSkipRangeCheck,
1721
+ skippedFileWarnings,
1722
+ splitTokens,
1723
+ stripTextDirective,
1724
+ summarize,
1725
+ terminalSafe,
1726
+ textFromNodes,
1727
+ validateRuleOptions,
1728
+ validateRuleSetting,
1729
+ valueFromNodes,
1730
+ withFailedRulesOff,
1731
+ withReadLimit
1732
+ };