@svelte-vitals/core 0.24.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -103,6 +103,7 @@ function collectEachBlocks(node, source, acc) {
103
103
  if (key in node) collectEachBlocks(node[key], source, acc);
104
104
  }
105
105
  }
106
+ var WALK_IGNORED_KEYS = /* @__PURE__ */ new Set(["type", "start", "end", "loc", "range"]);
106
107
  function walkEstree(node, visit) {
107
108
  if (Array.isArray(node)) {
108
109
  for (const child of node) walkEstree(child, visit);
@@ -111,7 +112,7 @@ function walkEstree(node, visit) {
111
112
  if (!node || typeof node !== "object" || typeof node.type !== "string") return;
112
113
  visit(node);
113
114
  for (const key of Object.keys(node)) {
114
- if (key === "type" || key === "start" || key === "end" || key === "loc" || key === "range") continue;
115
+ if (WALK_IGNORED_KEYS.has(key)) continue;
115
116
  walkEstree(node[key], visit);
116
117
  }
117
118
  }
@@ -123,6 +124,10 @@ function isEffectCall(node) {
123
124
  }
124
125
  return false;
125
126
  }
127
+ function isEffectRootCall(node) {
128
+ const c = node?.callee;
129
+ return c?.type === "MemberExpression" && c.object?.type === "Identifier" && c.object.name === "$effect" && c.property?.type === "Identifier" && c.property.name === "root";
130
+ }
126
131
  function isStateDeclaration(node) {
127
132
  const c = node?.callee;
128
133
  if (c?.type === "Identifier") return c.name === "$state";
@@ -207,7 +212,7 @@ function walkScoped(node, visit, shadowed = /* @__PURE__ */ new Set()) {
207
212
  const scope = introduced.size > 0 ? /* @__PURE__ */ new Set([...shadowed, ...introduced]) : shadowed;
208
213
  visit(node, scope);
209
214
  for (const key of Object.keys(node)) {
210
- if (key === "type" || key === "start" || key === "end" || key === "loc" || key === "range") continue;
215
+ if (WALK_IGNORED_KEYS.has(key)) continue;
211
216
  walkScoped(node[key], visit, scope);
212
217
  }
213
218
  }
@@ -270,7 +275,6 @@ function collectTemplateEscapes(node, stateNames, acc) {
270
275
  var RUNE_NAMES = /* @__PURE__ */ new Set(["$state", "$derived", "$effect", "$props", "$bindable", "$inspect", "$host"]);
271
276
  function bodyReadsReactive(fn, reactiveNames) {
272
277
  let reads = false;
273
- const IGNORED_KEYS = /* @__PURE__ */ new Set(["type", "start", "end", "loc", "range"]);
274
278
  const visit = (n) => {
275
279
  if (reads || !n) return;
276
280
  if (Array.isArray(n)) {
@@ -297,7 +301,7 @@ function bodyReadsReactive(fn, reactiveNames) {
297
301
  return;
298
302
  }
299
303
  for (const key of Object.keys(n)) {
300
- if (!IGNORED_KEYS.has(key)) visit(n[key]);
304
+ if (!WALK_IGNORED_KEYS.has(key)) visit(n[key]);
301
305
  }
302
306
  };
303
307
  visit(fn.body);
@@ -452,7 +456,366 @@ function collectSuppressions(source) {
452
456
  });
453
457
  return out;
454
458
  }
459
+ var EVAL_SCOPE_BOUNDARIES = /* @__PURE__ */ new Set([
460
+ "FunctionDeclaration",
461
+ "FunctionExpression",
462
+ "ArrowFunctionExpression",
463
+ "ClassDeclaration",
464
+ "ClassExpression"
465
+ ]);
466
+ function walkEvalScope(node, visit, shadowed = /* @__PURE__ */ new Set()) {
467
+ if (Array.isArray(node)) {
468
+ for (const child of node) walkEvalScope(child, visit, shadowed);
469
+ return;
470
+ }
471
+ if (!node || typeof node !== "object" || typeof node.type !== "string") return;
472
+ const introduced = scopeIntroducedNames(node);
473
+ const scope = introduced.size > 0 ? /* @__PURE__ */ new Set([...shadowed, ...introduced]) : shadowed;
474
+ if (visit(node, scope)) return;
475
+ if (EVAL_SCOPE_BOUNDARIES.has(node.type)) return;
476
+ for (const key of Object.keys(node)) {
477
+ if (WALK_IGNORED_KEYS.has(key)) continue;
478
+ walkEvalScope(node[key], visit, scope);
479
+ }
480
+ }
481
+ function collectEvalScopeCalls(root, source, matcher, skipSubtree, initialShadowed) {
482
+ const out = [];
483
+ walkEvalScope(
484
+ root,
485
+ (n, shadowed) => {
486
+ if (n.type !== "CallExpression") return void 0;
487
+ if (skipSubtree?.(n)) return true;
488
+ const name = matcher(n, shadowed);
489
+ if (name) out.push({ name, line: lineOf(source, n.start) });
490
+ return void 0;
491
+ },
492
+ initialShadowed
493
+ );
494
+ return out;
495
+ }
496
+ function unwrapExport(stmt) {
497
+ if (stmt.type === "ExportNamedDeclaration") return stmt.declaration ?? stmt;
498
+ if (stmt.type === "ExportDefaultDeclaration") return stmt.declaration;
499
+ return stmt;
500
+ }
501
+ function collectOrphanCalls(program, source, matcher, skipSubtree) {
502
+ const out = collectEvalScopeCalls(program, source, matcher, skipSubtree).map((c) => ({ ...c, kind: "top-level" }));
503
+ const body = program.body ?? [];
504
+ const matchingClasses = /* @__PURE__ */ new Map();
505
+ for (const stmt of body) {
506
+ const decl = unwrapExport(stmt);
507
+ if (decl?.type !== "ClassDeclaration" || decl.id?.type !== "Identifier") continue;
508
+ const ctor = (decl.body?.body ?? []).find(
509
+ (m) => m?.type === "MethodDefinition" && m.kind === "constructor" && m.value?.body
510
+ );
511
+ if (!ctor) continue;
512
+ const ctorShadow = /* @__PURE__ */ new Set();
513
+ for (const p of ctor.value.params ?? []) addBoundNames(p, ctorShadow);
514
+ const calls = collectEvalScopeCalls(ctor.value.body, source, matcher, skipSubtree, ctorShadow);
515
+ if (calls.length > 0) matchingClasses.set(decl.id.name, calls[0].name);
516
+ }
517
+ if (matchingClasses.size > 0) {
518
+ for (const stmt of body) {
519
+ const decl = unwrapExport(stmt);
520
+ const isCandidate = decl?.type === "VariableDeclaration" || decl?.type === "ExpressionStatement" || stmt.type === "ExportDefaultDeclaration" && decl?.type !== "FunctionDeclaration" && decl?.type !== "ClassDeclaration";
521
+ if (!isCandidate) continue;
522
+ walkEvalScope(decl, (n) => {
523
+ if (n.type === "NewExpression" && n.callee?.type === "Identifier" && matchingClasses.has(n.callee.name)) {
524
+ out.push({
525
+ name: matchingClasses.get(n.callee.name),
526
+ line: lineOf(source, n.start),
527
+ kind: "constructor-instantiated",
528
+ className: n.callee.name
529
+ });
530
+ }
531
+ return void 0;
532
+ });
533
+ }
534
+ }
535
+ return out.sort((a, b) => a.line - b.line);
536
+ }
537
+ function collectOrphanEffects(program, source) {
538
+ return collectOrphanCalls(program, source, (n) => isEffectCall(n) ? "$effect" : void 0, isEffectRootCall).map(
539
+ ({ line, kind, className }) => ({ line, kind, ...className !== void 0 ? { className } : {} })
540
+ );
541
+ }
542
+ var LIFECYCLE_NAMES = /* @__PURE__ */ new Set([
543
+ "onMount",
544
+ "onDestroy",
545
+ "beforeUpdate",
546
+ "afterUpdate",
547
+ "createEventDispatcher",
548
+ "getContext",
549
+ "setContext",
550
+ "hasContext",
551
+ "getAllContexts"
552
+ ]);
553
+ function collectSvelteLifecycleImports(program) {
554
+ const locals = /* @__PURE__ */ new Map();
555
+ const namespaces = /* @__PURE__ */ new Set();
556
+ for (const stmt of program.body ?? []) {
557
+ if (stmt?.type !== "ImportDeclaration" || stmt.importKind === "type" || stmt.source?.value !== "svelte") continue;
558
+ for (const s of stmt.specifiers ?? []) {
559
+ if (s?.importKind === "type" || s?.local?.type !== "Identifier") continue;
560
+ if (s.type === "ImportSpecifier" && s.imported?.type === "Identifier" && LIFECYCLE_NAMES.has(s.imported.name)) {
561
+ locals.set(s.local.name, s.imported.name);
562
+ } else if (s.type === "ImportNamespaceSpecifier") {
563
+ namespaces.add(s.local.name);
564
+ }
565
+ }
566
+ }
567
+ return { locals, namespaces };
568
+ }
569
+ function matchLifecycleCall(n, imports) {
570
+ const c = n?.callee;
571
+ if (c?.type === "Identifier") {
572
+ const canonical = imports.locals.get(c.name);
573
+ return canonical ? { canonical, local: c.name } : void 0;
574
+ }
575
+ if (c?.type === "MemberExpression" && !c.computed && c.object?.type === "Identifier" && imports.namespaces.has(c.object.name) && c.property?.type === "Identifier" && LIFECYCLE_NAMES.has(c.property.name)) {
576
+ return { canonical: c.property.name, local: c.object.name };
577
+ }
578
+ return void 0;
579
+ }
580
+ function collectOrphanLifecycleCalls(program, source) {
581
+ const imports = collectSvelteLifecycleImports(program);
582
+ if (imports.locals.size === 0 && imports.namespaces.size === 0) return [];
583
+ return collectOrphanCalls(program, source, (n, shadowed) => {
584
+ const m = matchLifecycleCall(n, imports);
585
+ return m && !shadowed.has(m.local) ? m.canonical : void 0;
586
+ });
587
+ }
588
+ var BROWSER_GLOBALS = /* @__PURE__ */ new Set([
589
+ "window",
590
+ "document",
591
+ "localStorage",
592
+ "sessionStorage",
593
+ "navigator",
594
+ "location",
595
+ "history",
596
+ "screen",
597
+ "matchMedia",
598
+ "requestAnimationFrame",
599
+ "cancelAnimationFrame",
600
+ "IntersectionObserver",
601
+ "ResizeObserver",
602
+ "MutationObserver",
603
+ "alert",
604
+ "confirm",
605
+ "prompt"
606
+ ]);
607
+ function collectBrowserGuardImports(program) {
608
+ const out = /* @__PURE__ */ new Set();
609
+ for (const stmt of program.body ?? []) {
610
+ if (stmt?.type !== "ImportDeclaration" || stmt.importKind === "type" || stmt.source?.value !== "$app/environment")
611
+ continue;
612
+ for (const s of stmt.specifiers ?? []) {
613
+ if (s?.importKind === "type" || s?.local?.type !== "Identifier") continue;
614
+ if (s.type === "ImportSpecifier" && s.imported?.type === "Identifier" && s.imported.name === "browser") {
615
+ out.add(s.local.name);
616
+ }
617
+ }
618
+ }
619
+ return out;
620
+ }
621
+ function collectProgramBindings(program) {
622
+ const bound = /* @__PURE__ */ new Set();
623
+ for (const stmt of program.body ?? []) {
624
+ if (stmt?.type === "ImportDeclaration") {
625
+ for (const s of stmt.specifiers ?? []) if (s?.local?.type === "Identifier") bound.add(s.local.name);
626
+ continue;
627
+ }
628
+ const decl = unwrapExport(stmt);
629
+ if (decl?.type === "VariableDeclaration") {
630
+ for (const d of decl.declarations ?? []) addBoundNames(d?.id, bound);
631
+ } else if ((decl?.type === "FunctionDeclaration" || decl?.type === "ClassDeclaration") && decl.id?.type === "Identifier") {
632
+ bound.add(decl.id.name);
633
+ }
634
+ }
635
+ return bound;
636
+ }
637
+ function guardTerminates(consequent) {
638
+ if (!consequent) return false;
639
+ if (consequent.type === "ReturnStatement" || consequent.type === "ThrowStatement") return true;
640
+ if (consequent.type === "BlockStatement") {
641
+ const last = (consequent.body ?? [])[consequent.body.length - 1];
642
+ return last?.type === "ReturnStatement" || last?.type === "ThrowStatement";
643
+ }
644
+ return false;
645
+ }
646
+ function isBrowserGuardTest(test, guardBindings) {
647
+ let guarded = false;
648
+ walkEstree(test, (n) => {
649
+ if (n.type === "Identifier" && guardBindings.has(n.name)) guarded = true;
650
+ if (n.type === "BinaryExpression" && ["===", "!==", "==", "!="].includes(n.operator)) {
651
+ const sides = [n.left, n.right];
652
+ const hasTypeofGlobal = sides.some(
653
+ (s) => s?.type === "UnaryExpression" && s.operator === "typeof" && s.argument?.type === "Identifier" && BROWSER_GLOBALS.has(s.argument.name)
654
+ );
655
+ const hasUndefinedString = sides.some((s) => s?.type === "Literal" && s.value === "undefined");
656
+ if (hasTypeofGlobal && hasUndefinedString) guarded = true;
657
+ }
658
+ });
659
+ return guarded;
660
+ }
661
+ function collectDerivedGuardBindings(program, guards) {
662
+ const derived = /* @__PURE__ */ new Set();
663
+ for (const stmt of program.body ?? []) {
664
+ const decl = unwrapExport(stmt);
665
+ if (decl?.type !== "VariableDeclaration" || decl.kind !== "const" && decl.kind !== "let") continue;
666
+ for (const d of decl.declarations ?? []) {
667
+ if (d?.id?.type === "Identifier" && d.init && isBrowserGuardTest(d.init, guards)) {
668
+ derived.add(d.id.name);
669
+ }
670
+ }
671
+ }
672
+ return derived;
673
+ }
674
+ function collectBrowserGlobalRefs(program, source, extra) {
675
+ const out = [];
676
+ const bound = /* @__PURE__ */ new Set([...collectProgramBindings(program), ...extra?.bound ?? []]);
677
+ const guards = /* @__PURE__ */ new Set([...collectBrowserGuardImports(program), ...extra?.guards ?? []]);
678
+ for (const name of collectDerivedGuardBindings(program, guards)) guards.add(name);
679
+ const visit = (n, shadowed) => {
680
+ if (!n) return;
681
+ if (Array.isArray(n)) {
682
+ for (const c of n) visit(c, shadowed);
683
+ return;
684
+ }
685
+ if (typeof n !== "object" || typeof n.type !== "string") return;
686
+ if (EVAL_SCOPE_BOUNDARIES.has(n.type)) return;
687
+ if ((n.type === "IfStatement" || n.type === "ConditionalExpression") && isBrowserGuardTest(n.test, guards)) return;
688
+ if (n.type === "LogicalExpression" && isBrowserGuardTest(n.left, guards)) return;
689
+ const introduced = scopeIntroducedNames(n);
690
+ const scope = introduced.size > 0 ? /* @__PURE__ */ new Set([...shadowed, ...introduced]) : shadowed;
691
+ switch (n.type) {
692
+ case "Identifier":
693
+ if (BROWSER_GLOBALS.has(n.name) && !bound.has(n.name) && !scope.has(n.name)) {
694
+ out.push({ name: n.name, line: lineOf(source, n.start) });
695
+ }
696
+ return;
697
+ case "UnaryExpression":
698
+ if (n.operator === "typeof" && n.argument?.type === "Identifier") return;
699
+ break;
700
+ case "MemberExpression":
701
+ visit(n.object, scope);
702
+ if (n.computed) visit(n.property, scope);
703
+ return;
704
+ case "Property":
705
+ if (n.computed) visit(n.key, scope);
706
+ visit(n.value, scope);
707
+ return;
708
+ case "VariableDeclarator":
709
+ visit(n.init, scope);
710
+ return;
711
+ case "LabeledStatement":
712
+ visit(n.body, scope);
713
+ return;
714
+ case "BreakStatement":
715
+ case "ContinueStatement":
716
+ case "ImportDeclaration":
717
+ case "ExportAllDeclaration":
718
+ return;
719
+ case "ExportNamedDeclaration":
720
+ if (!n.declaration) return;
721
+ break;
722
+ case "BlockStatement":
723
+ case "Program":
724
+ for (const stmt of n.body ?? []) {
725
+ visit(stmt, scope);
726
+ if (stmt?.type === "IfStatement" && isBrowserGuardTest(stmt.test, guards) && guardTerminates(stmt.consequent)) {
727
+ break;
728
+ }
729
+ }
730
+ return;
731
+ default:
732
+ if (n.type.startsWith("TS")) {
733
+ if (n.type === "TSAsExpression" || n.type === "TSSatisfiesExpression" || n.type === "TSNonNullExpression" || n.type === "TSInstantiationExpression") {
734
+ visit(n.expression, scope);
735
+ }
736
+ return;
737
+ }
738
+ }
739
+ for (const key of Object.keys(n)) {
740
+ if (WALK_IGNORED_KEYS.has(key)) continue;
741
+ visit(n[key], scope);
742
+ }
743
+ };
744
+ visit(program, /* @__PURE__ */ new Set());
745
+ return out;
746
+ }
747
+ var MODULE_FILE_RE = /\.svelte\.(ts|js)$/;
748
+ function parseModuleProgram(source, filename) {
749
+ const neutralized = source.replace(/<\/script/gi, "<_script");
750
+ const wrapped = `<script lang="ts">
751
+ ${neutralized}
752
+ </script>`;
753
+ const ast = parse(wrapped, { modern: true, filename });
754
+ return { program: ast.instance?.content, wrapped };
755
+ }
756
+ function collectModuleStateDecls(program, source) {
757
+ const out = [];
758
+ const body = program.body ?? [];
759
+ const statefulClasses = /* @__PURE__ */ new Set();
760
+ for (const stmt of body) {
761
+ const decl = unwrapExport(stmt);
762
+ if (decl?.type === "VariableDeclaration") {
763
+ for (const d of decl.declarations ?? []) {
764
+ if (d?.id?.type === "Identifier" && d.init && isStateDeclaration(d.init)) {
765
+ out.push({ name: d.id.name, line: lineOf(source, d.start) });
766
+ }
767
+ }
768
+ } else if (decl?.type === "ClassDeclaration" && decl.id?.type === "Identifier") {
769
+ const hasStateField = (decl.body?.body ?? []).some(
770
+ (m) => m?.type === "PropertyDefinition" && m.value && isStateDeclaration(m.value)
771
+ );
772
+ if (hasStateField) statefulClasses.add(decl.id.name);
773
+ }
774
+ }
775
+ if (statefulClasses.size > 0) {
776
+ for (const stmt of body) {
777
+ const decl = unwrapExport(stmt);
778
+ if (decl?.type !== "VariableDeclaration") continue;
779
+ for (const d of decl.declarations ?? []) {
780
+ if (d?.init?.type === "NewExpression" && d.init.callee?.type === "Identifier" && statefulClasses.has(d.init.callee.name)) {
781
+ out.push({
782
+ name: d.id?.type === "Identifier" ? d.id.name : d.init.callee.name,
783
+ line: lineOf(source, d.start)
784
+ });
785
+ }
786
+ }
787
+ }
788
+ }
789
+ return out.sort((a, b) => a.line - b.line);
790
+ }
791
+ function parseModuleFacts(source, filename) {
792
+ const { program, wrapped } = parseModuleProgram(source, filename);
793
+ const shift = (line) => Math.max(0, line - 1);
794
+ const orphanEffects = program ? collectOrphanEffects(program, wrapped).map((f) => ({ ...f, line: shift(f.line) })) : [];
795
+ const orphanLifecycleCalls = program ? collectOrphanLifecycleCalls(program, wrapped).map((f) => ({ ...f, line: shift(f.line) })) : [];
796
+ const browserGlobalRefs = program ? collectBrowserGlobalRefs(program, wrapped).map((r) => ({ ...r, line: shift(r.line), context: "module" })) : [];
797
+ const moduleStateDecls = program ? collectModuleStateDecls(program, wrapped).map((d) => ({ ...d, line: shift(d.line) })) : [];
798
+ return {
799
+ eachBlocks: [],
800
+ effects: [],
801
+ htmlTags: [],
802
+ javascriptUrls: [],
803
+ loc: 0,
804
+ propCount: 0,
805
+ imports: [],
806
+ importSpans: [],
807
+ namespaceImports: [],
808
+ constableStates: [],
809
+ mutatedProps: [],
810
+ suppressions: collectSuppressions(source),
811
+ orphanEffects,
812
+ orphanLifecycleCalls,
813
+ browserGlobalRefs,
814
+ moduleStateDecls
815
+ };
816
+ }
455
817
  function parseComponentFacts(source, filename) {
818
+ if (MODULE_FILE_RE.test(filename)) return parseModuleFacts(source, filename);
456
819
  const ast = parse(source, { modern: true, filename });
457
820
  const eachBlocks = [];
458
821
  collectEachBlocks(ast.fragment ?? ast, source, eachBlocks);
@@ -461,11 +824,20 @@ function parseComponentFacts(source, filename) {
461
824
  collectSecurityFacts(ast.fragment ?? ast, source, htmlTags, javascriptUrls);
462
825
  const loc = countLines(source);
463
826
  const suppressions = collectSuppressions(source);
827
+ const moduleProgram = ast.module?.content;
464
828
  const importSpans = [];
465
829
  const namespaceImports = [];
466
- if (ast.module?.content) {
467
- collectImportSources(ast.module.content, source, importSpans);
468
- collectNamespaceImports(ast.module.content, source, namespaceImports);
830
+ if (moduleProgram) {
831
+ collectImportSources(moduleProgram, source, importSpans);
832
+ collectNamespaceImports(moduleProgram, source, namespaceImports);
833
+ }
834
+ const orphanEffects = moduleProgram ? collectOrphanEffects(moduleProgram, source) : [];
835
+ const orphanLifecycleCalls = moduleProgram ? collectOrphanLifecycleCalls(moduleProgram, source) : [];
836
+ const browserGlobalRefs = [];
837
+ if (moduleProgram) {
838
+ for (const r of collectBrowserGlobalRefs(moduleProgram, source)) {
839
+ browserGlobalRefs.push({ ...r, context: "module" });
840
+ }
469
841
  }
470
842
  const effects = [];
471
843
  const constableStates = [];
@@ -510,6 +882,17 @@ function parseComponentFacts(source, filename) {
510
882
  for (const d of stateDecls) {
511
883
  if (!writtenOrEscaped.has(d.name)) constableStates.push(d);
512
884
  }
885
+ let moduleExtra;
886
+ if (moduleProgram) {
887
+ const moduleBrowserImports = collectBrowserGuardImports(moduleProgram);
888
+ moduleExtra = {
889
+ guards: /* @__PURE__ */ new Set([...moduleBrowserImports, ...collectDerivedGuardBindings(moduleProgram, moduleBrowserImports)]),
890
+ bound: collectProgramBindings(moduleProgram)
891
+ };
892
+ }
893
+ for (const r of collectBrowserGlobalRefs(program, source, moduleExtra)) {
894
+ browserGlobalRefs.push({ ...r, context: "instance" });
895
+ }
513
896
  }
514
897
  const imports = importSpans.map((s) => s.source);
515
898
  return {
@@ -524,6 +907,10 @@ function parseComponentFacts(source, filename) {
524
907
  namespaceImports,
525
908
  constableStates,
526
909
  mutatedProps,
910
+ orphanEffects,
911
+ orphanLifecycleCalls,
912
+ browserGlobalRefs,
913
+ moduleStateDecls: [],
527
914
  suppressions
528
915
  };
529
916
  }
@@ -543,11 +930,15 @@ function emptyComponentFacts(file) {
543
930
  namespaceImports: [],
544
931
  constableStates: [],
545
932
  mutatedProps: [],
933
+ orphanEffects: [],
934
+ orphanLifecycleCalls: [],
935
+ browserGlobalRefs: [],
936
+ moduleStateDecls: [],
546
937
  suppressions: []
547
938
  };
548
939
  }
549
940
  async function collectComponentFacts(rt, cwd) {
550
- const files = await rt.glob("src/**/*.svelte", cwd);
941
+ const files = await rt.glob("src/**/*.svelte{,.ts,.js}", cwd);
551
942
  return Promise.all(
552
943
  files.sort().map(async (rel) => {
553
944
  try {
@@ -560,6 +951,386 @@ async function collectComponentFacts(rt, cwd) {
560
951
  );
561
952
  }
562
953
 
954
+ // src/kit-module-parse.ts
955
+ var HANDLER_NAMES = /* @__PURE__ */ new Set([
956
+ "load",
957
+ "handle",
958
+ "handleFetch",
959
+ "handleError",
960
+ "GET",
961
+ "POST",
962
+ "PUT",
963
+ "PATCH",
964
+ "DELETE",
965
+ "HEAD",
966
+ "OPTIONS",
967
+ "fallback"
968
+ ]);
969
+ function unwrapTs(expr) {
970
+ let cur = expr;
971
+ while (cur?.type === "TSSatisfiesExpression" || cur?.type === "TSAsExpression") cur = cur.expression;
972
+ return cur;
973
+ }
974
+ function isFunctionNode(n) {
975
+ return n?.type === "FunctionDeclaration" || n?.type === "FunctionExpression" || n?.type === "ArrowFunctionExpression";
976
+ }
977
+ function collectTopLevelBindings(program) {
978
+ const bindings = /* @__PURE__ */ new Map();
979
+ for (const stmt of program.body ?? []) {
980
+ const decl = unwrapExport(stmt);
981
+ if (decl?.type === "FunctionDeclaration" && decl.id?.type === "Identifier") {
982
+ bindings.set(decl.id.name, decl);
983
+ } else if (decl?.type === "VariableDeclaration") {
984
+ for (const d of decl.declarations ?? []) {
985
+ if (d?.id?.type === "Identifier" && d.init) bindings.set(d.id.name, unwrapTs(d.init));
986
+ }
987
+ }
988
+ }
989
+ return bindings;
990
+ }
991
+ function addActionsMembers(obj, handlers) {
992
+ for (const p of obj.properties ?? []) {
993
+ if (p?.type !== "Property") continue;
994
+ const v = unwrapTs(p.value);
995
+ if (isFunctionNode(v)) handlers.add(v);
996
+ }
997
+ }
998
+ function resolveAliasHandlerExports(program, bindings, handlers) {
999
+ for (const stmt of program.body ?? []) {
1000
+ if (stmt?.type !== "ExportNamedDeclaration" || !stmt.specifiers || stmt.source || stmt.exportKind === "type")
1001
+ continue;
1002
+ for (const s of stmt.specifiers) {
1003
+ if (s?.exportKind === "type" || s?.exported?.type !== "Identifier" || s?.local?.type !== "Identifier") continue;
1004
+ const exportedName = s.exported.name;
1005
+ const resolved = bindings.get(s.local.name);
1006
+ if (HANDLER_NAMES.has(exportedName) && isFunctionNode(resolved)) {
1007
+ handlers.add(resolved);
1008
+ } else if (exportedName === "actions" && resolved?.type === "ObjectExpression") {
1009
+ addActionsMembers(resolved, handlers);
1010
+ }
1011
+ }
1012
+ }
1013
+ }
1014
+ function resolveAliasStartupExports(program, bindings, startup) {
1015
+ for (const stmt of program.body ?? []) {
1016
+ if (stmt?.type !== "ExportNamedDeclaration" || !stmt.specifiers || stmt.source || stmt.exportKind === "type")
1017
+ continue;
1018
+ for (const s of stmt.specifiers) {
1019
+ if (s?.exportKind === "type" || s?.exported?.type !== "Identifier" || s?.local?.type !== "Identifier") continue;
1020
+ if (s.exported.name !== "init") continue;
1021
+ const resolved = bindings.get(s.local.name);
1022
+ if (isFunctionNode(resolved)) startup.add(resolved);
1023
+ }
1024
+ }
1025
+ }
1026
+ function collectHandlerFunctions(program) {
1027
+ const handlers = /* @__PURE__ */ new Set();
1028
+ for (const stmt of program.body ?? []) {
1029
+ if (stmt?.type !== "ExportNamedDeclaration" || !stmt.declaration) continue;
1030
+ const decl = stmt.declaration;
1031
+ if (decl.type === "FunctionDeclaration" && decl.id?.type === "Identifier" && HANDLER_NAMES.has(decl.id.name)) {
1032
+ handlers.add(decl);
1033
+ continue;
1034
+ }
1035
+ if (decl.type !== "VariableDeclaration") continue;
1036
+ for (const d of decl.declarations ?? []) {
1037
+ if (d?.id?.type !== "Identifier" || !d.init) continue;
1038
+ const init = unwrapTs(d.init);
1039
+ if (HANDLER_NAMES.has(d.id.name) && isFunctionNode(init)) {
1040
+ handlers.add(init);
1041
+ } else if (d.id.name === "actions" && init?.type === "ObjectExpression") {
1042
+ addActionsMembers(init, handlers);
1043
+ }
1044
+ }
1045
+ }
1046
+ resolveAliasHandlerExports(program, collectTopLevelBindings(program), handlers);
1047
+ return handlers;
1048
+ }
1049
+ function collectStartupFunctions(program) {
1050
+ const startup = /* @__PURE__ */ new Set();
1051
+ for (const stmt of program.body ?? []) {
1052
+ if (stmt?.type !== "ExportNamedDeclaration" || !stmt.declaration) continue;
1053
+ const decl = stmt.declaration;
1054
+ if (decl.type === "FunctionDeclaration" && decl.id?.type === "Identifier" && decl.id.name === "init") {
1055
+ startup.add(decl);
1056
+ continue;
1057
+ }
1058
+ if (decl.type !== "VariableDeclaration") continue;
1059
+ for (const d of decl.declarations ?? []) {
1060
+ if (d?.id?.type !== "Identifier" || !d.init) continue;
1061
+ const init = unwrapTs(d.init);
1062
+ if (d.id.name === "init" && isFunctionNode(init)) startup.add(init);
1063
+ }
1064
+ }
1065
+ resolveAliasStartupExports(program, collectTopLevelBindings(program), startup);
1066
+ return startup;
1067
+ }
1068
+ function hasSsrFalseOptOut(program) {
1069
+ const isFalse = (init) => {
1070
+ const v = unwrapTs(init);
1071
+ return v?.type === "Literal" && v.value === false;
1072
+ };
1073
+ for (const stmt of program.body ?? []) {
1074
+ const decl = unwrapExport(stmt);
1075
+ if (decl?.type !== "VariableDeclaration") continue;
1076
+ for (const d of decl.declarations ?? []) {
1077
+ if (d?.id?.type === "Identifier" && d.id.name === "ssr" && d.init && isFalse(d.init)) {
1078
+ if (stmt.type === "ExportNamedDeclaration") return true;
1079
+ }
1080
+ }
1081
+ }
1082
+ const bindings = collectTopLevelBindings(program);
1083
+ for (const stmt of program.body ?? []) {
1084
+ if (stmt?.type !== "ExportNamedDeclaration" || !stmt.specifiers || stmt.source || stmt.exportKind === "type")
1085
+ continue;
1086
+ for (const s of stmt.specifiers) {
1087
+ if (s?.exportKind === "type" || s?.exported?.type !== "Identifier" || s?.local?.type !== "Identifier") continue;
1088
+ if (s.exported.name !== "ssr") continue;
1089
+ const resolved = bindings.get(s.local.name);
1090
+ if (resolved?.type === "Literal" && resolved.value === false) return true;
1091
+ }
1092
+ }
1093
+ return false;
1094
+ }
1095
+ function walkKit(node, handlerFns, startupFns, visit, shadowed = /* @__PURE__ */ new Set(), inFunction = false, inHandler = false, inStartup = false) {
1096
+ if (Array.isArray(node)) {
1097
+ for (const child of node) walkKit(child, handlerFns, startupFns, visit, shadowed, inFunction, inHandler, inStartup);
1098
+ return;
1099
+ }
1100
+ if (!node || typeof node !== "object" || typeof node.type !== "string") return;
1101
+ const introduced = scopeIntroducedNames(node);
1102
+ const scope = introduced.size > 0 ? /* @__PURE__ */ new Set([...shadowed, ...introduced]) : shadowed;
1103
+ const isBoundary = isFunctionNode(node) || node.type === "ClassDeclaration" || node.type === "ClassExpression";
1104
+ const nextInFunction = inFunction || isBoundary;
1105
+ const nextInHandler = inHandler || handlerFns.has(node);
1106
+ const nextInStartup = inStartup || startupFns.has(node);
1107
+ visit(node, scope, inFunction, inHandler, inStartup);
1108
+ for (const key of Object.keys(node)) {
1109
+ if (WALK_IGNORED_KEYS.has(key)) continue;
1110
+ walkKit(node[key], handlerFns, startupFns, visit, scope, nextInFunction, nextInHandler, nextInStartup);
1111
+ }
1112
+ }
1113
+ function normalizePosix(path) {
1114
+ const out = [];
1115
+ for (const seg of path.split("/")) {
1116
+ if (seg === "" || seg === ".") continue;
1117
+ if (seg === "..") {
1118
+ if (out.length === 0) return void 0;
1119
+ out.pop();
1120
+ } else out.push(seg);
1121
+ }
1122
+ return out.join("/");
1123
+ }
1124
+ function resolveRepoLocalPath(spec, importerFile) {
1125
+ let path;
1126
+ if (spec.startsWith("$lib/")) path = `src/lib/${spec.slice("$lib/".length)}`;
1127
+ else if (spec.startsWith("./") || spec.startsWith("../")) {
1128
+ const dir = importerFile.split("/").slice(0, -1).join("/");
1129
+ path = `${dir}/${spec}`;
1130
+ } else return void 0;
1131
+ return normalizePosix(path);
1132
+ }
1133
+ function resolveRunesModuleSpecifier(spec, importerFile) {
1134
+ const path = resolveRepoLocalPath(spec, importerFile);
1135
+ if (path === void 0) return void 0;
1136
+ if (/\.svelte\.(ts|js)$/.test(path)) return path;
1137
+ if (path.endsWith(".svelte")) return `${path}.ts`;
1138
+ return void 0;
1139
+ }
1140
+ function isLocalStateSpecifier(spec, importerFile) {
1141
+ const path = resolveRepoLocalPath(spec, importerFile);
1142
+ if (path === void 0) return false;
1143
+ return path !== "src/lib/server" && !path.startsWith("src/lib/server/");
1144
+ }
1145
+ function parseKitModuleFacts(source, filename) {
1146
+ const suppressions = collectSuppressions(source);
1147
+ const { program, wrapped } = parseModuleProgram(source, filename);
1148
+ const moduleStateReassignments = [];
1149
+ const importedStateWrites = [];
1150
+ const importedStateWritesOutsideHandlers = [];
1151
+ const runesModuleImports = [];
1152
+ const lifecycleCalls = [];
1153
+ const browserGlobalRefs = [];
1154
+ if (!program) {
1155
+ return {
1156
+ moduleStateReassignments,
1157
+ importedStateWrites,
1158
+ importedStateWritesOutsideHandlers,
1159
+ runesModuleImports,
1160
+ lifecycleCalls,
1161
+ browserGlobalRefs,
1162
+ suppressions
1163
+ };
1164
+ }
1165
+ const line = (start) => Math.max(0, lineOf(wrapped, start) - 1);
1166
+ const importedSpecifiers = /* @__PURE__ */ new Map();
1167
+ for (const stmt of program.body ?? []) {
1168
+ if (stmt?.type !== "ImportDeclaration" || stmt.importKind === "type") continue;
1169
+ const spec = typeof stmt.source?.value === "string" ? stmt.source.value : "";
1170
+ const names = [];
1171
+ for (const s of stmt.specifiers ?? []) {
1172
+ if (s?.importKind === "type" || s?.local?.type !== "Identifier") continue;
1173
+ names.push(s.local.name);
1174
+ importedSpecifiers.set(s.local.name, spec);
1175
+ }
1176
+ if (names.length === 0) continue;
1177
+ const resolved = resolveRunesModuleSpecifier(spec, filename);
1178
+ if (resolved) runesModuleImports.push({ source: spec, resolved, names, line: line(stmt.start) });
1179
+ }
1180
+ const moduleLets = /* @__PURE__ */ new Set();
1181
+ for (const stmt of program.body ?? []) {
1182
+ const decl = unwrapExport(stmt);
1183
+ if (decl?.type === "VariableDeclaration" && (decl.kind === "let" || decl.kind === "var")) {
1184
+ for (const d of decl.declarations ?? []) addBoundNames(d?.id, moduleLets);
1185
+ }
1186
+ }
1187
+ const handlerFns = collectHandlerFunctions(program);
1188
+ const startupFns = collectStartupFunctions(program);
1189
+ const svelteImports = collectSvelteLifecycleImports(program);
1190
+ if (!hasSsrFalseOptOut(program)) {
1191
+ const shiftLine = (l) => Math.max(0, l - 1);
1192
+ const browserImports = collectBrowserGuardImports(program);
1193
+ const guards = /* @__PURE__ */ new Set([...browserImports, ...collectDerivedGuardBindings(program, browserImports)]);
1194
+ const bound = collectProgramBindings(program);
1195
+ for (const r of collectBrowserGlobalRefs(program, wrapped, { guards, bound })) {
1196
+ browserGlobalRefs.push({ name: r.name, line: shiftLine(r.line), inHandler: false });
1197
+ }
1198
+ const scanFn = (fn, inHandler) => {
1199
+ if (!fn?.body) return;
1200
+ const params = /* @__PURE__ */ new Set();
1201
+ for (const p of fn.params ?? []) addBoundNames(p, params);
1202
+ for (const r of collectBrowserGlobalRefs(fn.body, wrapped, { guards, bound: /* @__PURE__ */ new Set([...bound, ...params]) })) {
1203
+ browserGlobalRefs.push({ name: r.name, line: shiftLine(r.line), inHandler });
1204
+ }
1205
+ };
1206
+ for (const fn of handlerFns) scanFn(fn, true);
1207
+ for (const fn of startupFns) {
1208
+ if (handlerFns.has(fn)) continue;
1209
+ scanFn(fn, false);
1210
+ }
1211
+ }
1212
+ walkKit(program, handlerFns, startupFns, (n, shadowed, inFunction, inHandler, inStartup) => {
1213
+ if (inFunction && !inStartup) {
1214
+ const flagLet = (name) => {
1215
+ if (name && !shadowed.has(name) && moduleLets.has(name)) {
1216
+ moduleStateReassignments.push({ name, line: line(n.start), inHandler });
1217
+ }
1218
+ };
1219
+ if (n.type === "AssignmentExpression") {
1220
+ if (n.left?.type === "Identifier") flagLet(n.left.name);
1221
+ else if (n.left?.type === "ObjectPattern" || n.left?.type === "ArrayPattern") {
1222
+ const bound = /* @__PURE__ */ new Set();
1223
+ addBoundNames(n.left, bound);
1224
+ for (const b of bound) flagLet(b);
1225
+ }
1226
+ } else if (n.type === "UpdateExpression" && n.argument?.type === "Identifier") {
1227
+ flagLet(n.argument.name);
1228
+ }
1229
+ }
1230
+ let write;
1231
+ const importedRoot = (expr) => {
1232
+ const r = rootObjectName(expr);
1233
+ return r && !shadowed.has(r) && importedSpecifiers.has(r) ? r : void 0;
1234
+ };
1235
+ if (n.type === "AssignmentExpression" && n.left?.type === "MemberExpression") {
1236
+ const r = importedRoot(n.left);
1237
+ if (r) write = { name: r, via: "assignment" };
1238
+ } else if (n.type === "UpdateExpression" && n.argument?.type === "MemberExpression") {
1239
+ const r = importedRoot(n.argument);
1240
+ if (r) write = { name: r, via: "assignment" };
1241
+ } else if (n.type === "UnaryExpression" && n.operator === "delete") {
1242
+ const r = importedRoot(n.argument);
1243
+ if (r) write = { name: r, via: "assignment" };
1244
+ } else if (n.type === "CallExpression" && n.callee?.type === "MemberExpression") {
1245
+ const method = n.callee.property?.type === "Identifier" ? n.callee.property.name : void 0;
1246
+ if (method === "set" || method === "update") {
1247
+ const r = importedRoot(n.callee.object);
1248
+ if (r && isLocalStateSpecifier(importedSpecifiers.get(r), filename)) write = { name: r, via: "set-call" };
1249
+ }
1250
+ } else if (n.type === "AssignmentExpression" && (n.left?.type === "ObjectPattern" || n.left?.type === "ArrayPattern")) {
1251
+ const scanPatternTargets = (pat) => {
1252
+ if (!pat || write) return;
1253
+ if (pat.type === "MemberExpression") {
1254
+ const r = importedRoot(pat);
1255
+ if (r) write = { name: r, via: "assignment" };
1256
+ } else if (pat.type === "ObjectPattern") {
1257
+ for (const p of pat.properties ?? []) {
1258
+ if (p?.type === "Property") scanPatternTargets(p.value);
1259
+ else if (p?.type === "RestElement") scanPatternTargets(p.argument);
1260
+ }
1261
+ } else if (pat.type === "ArrayPattern") {
1262
+ for (const el of pat.elements ?? []) scanPatternTargets(el);
1263
+ } else if (pat.type === "AssignmentPattern") {
1264
+ scanPatternTargets(pat.left);
1265
+ } else if (pat.type === "RestElement") {
1266
+ scanPatternTargets(pat.argument);
1267
+ }
1268
+ };
1269
+ scanPatternTargets(n.left);
1270
+ }
1271
+ if (write) {
1272
+ if (inHandler) importedStateWrites.push({ ...write, line: line(n.start) });
1273
+ else importedStateWritesOutsideHandlers.push({ name: write.name, line: line(n.start) });
1274
+ }
1275
+ if (n.type === "CallExpression" && (!inFunction || inHandler || inStartup)) {
1276
+ const m = matchLifecycleCall(n, svelteImports);
1277
+ if (m && !shadowed.has(m.local)) {
1278
+ lifecycleCalls.push({ name: m.canonical, line: line(n.start), inHandler });
1279
+ }
1280
+ }
1281
+ });
1282
+ const byLine = (arr) => arr.sort((a, b) => a.line - b.line);
1283
+ return {
1284
+ moduleStateReassignments: byLine(moduleStateReassignments),
1285
+ importedStateWrites: byLine(importedStateWrites),
1286
+ importedStateWritesOutsideHandlers: byLine(importedStateWritesOutsideHandlers),
1287
+ runesModuleImports: byLine(runesModuleImports),
1288
+ lifecycleCalls: byLine(lifecycleCalls),
1289
+ browserGlobalRefs: byLine(browserGlobalRefs),
1290
+ suppressions
1291
+ };
1292
+ }
1293
+
1294
+ // src/kit-module-collect.ts
1295
+ function emptyKitModuleFacts(file, kind) {
1296
+ return {
1297
+ file,
1298
+ kind,
1299
+ moduleStateReassignments: [],
1300
+ importedStateWrites: [],
1301
+ importedStateWritesOutsideHandlers: [],
1302
+ runesModuleImports: [],
1303
+ lifecycleCalls: [],
1304
+ browserGlobalRefs: [],
1305
+ suppressions: []
1306
+ };
1307
+ }
1308
+ function kindOf(file) {
1309
+ const base = file.split("/").pop() ?? file;
1310
+ return base.includes(".server.") || base.startsWith("+server.") ? "server" : "universal";
1311
+ }
1312
+ async function collectKitModuleFacts(rt, cwd) {
1313
+ const patterns = [
1314
+ "src/routes/**/+{page,layout}.server.{ts,js}",
1315
+ "src/routes/**/+{page,layout}.{ts,js}",
1316
+ "src/routes/**/+server.{ts,js}",
1317
+ "src/hooks.server.{ts,js}"
1318
+ ];
1319
+ const lists = await Promise.all(patterns.map((p) => rt.glob(p, cwd)));
1320
+ const files = [...new Set(lists.flat())];
1321
+ return Promise.all(
1322
+ files.sort().map(async (rel) => {
1323
+ const kind = kindOf(rel);
1324
+ try {
1325
+ const source = await rt.readFile(rt.join(cwd, rel));
1326
+ return { file: rel, kind, ...parseKitModuleFacts(source, rel) };
1327
+ } catch {
1328
+ return emptyKitModuleFacts(rel, kind);
1329
+ }
1330
+ })
1331
+ );
1332
+ }
1333
+
563
1334
  // src/project-paths.ts
564
1335
  var ROBOTS_SOURCE_PATHS = [
565
1336
  "static/robots.txt",
@@ -2098,6 +2869,197 @@ var correct005PropMutation = componentRule({
2098
2869
  }))
2099
2870
  });
2100
2871
 
2872
+ // src/rules/correctness/correct006-orphan-effect.ts
2873
+ var correct006OrphanEffect = componentRule({
2874
+ id: "CORRECT006",
2875
+ title: "Orphan $effect",
2876
+ category: "correctness",
2877
+ severity: "critical",
2878
+ label: "$effect context",
2879
+ recommendation: "Wrap the effect in $effect.root (and own the returned cleanup), or restructure so the effect is created during component initialisation (e.g. call a setup method from a component).",
2880
+ rationale: "An $effect created outside component initialisation throws effect_orphan at runtime \u2014 the compiler does not catch it, and it typically surfaces as a production 500.",
2881
+ // `orphanEffects` is typed required, but a facts object built by an older/external
2882
+ // constructor may omit it — default to empty rather than let `applies` throw and
2883
+ // take the whole `runRules` Promise.all down with it.
2884
+ applies: (c) => (c.orphanEffects ?? []).length > 0,
2885
+ bad: (c) => (c.orphanEffects ?? []).map((o) => ({
2886
+ line: o.line,
2887
+ message: o.kind === "top-level" ? "$effect at module scope runs outside component initialisation \u2014 it throws effect_orphan at runtime" : `class "${o.className}" runs $effect in its constructor and is instantiated at module scope \u2014 it throws effect_orphan at runtime`
2888
+ }))
2889
+ });
2890
+
2891
+ // src/rules/correctness/correct007-orphan-lifecycle.ts
2892
+ var PENALIZED3 = { presence: "none", value: "absent" };
2893
+ var PASS3 = { presence: "own", value: "static" };
2894
+ var ID = "CORRECT007";
2895
+ var DOCS_URL = docsUrlFor(ID);
2896
+ var LABEL = "Lifecycle-call context";
2897
+ var RECOMMENDATION = "Call lifecycle/context functions during component initialisation (the top level of a component's <script>). In load, return the data and call setContext in a layout/page component; in shared modules, expose a setup function that components call during init.";
2898
+ var topLevelMessage = (name) => `${name}() runs at module evaluation, outside component initialisation \u2014 it throws lifecycle_outside_component at runtime`;
2899
+ function isSuppressed2(suppressions, line) {
2900
+ return (suppressions ?? []).some((s) => s.line === line && (!s.ruleIds || s.ruleIds.includes(ID)));
2901
+ }
2902
+ function emitFile(out, file, issues, suppressions) {
2903
+ const bad = issues.filter((b) => !(b.line > 0 && isSuppressed2(suppressions, b.line)));
2904
+ if (bad.length === 0) {
2905
+ out.push({
2906
+ id: ID,
2907
+ category: "correctness",
2908
+ severity: "critical",
2909
+ detection: PASS3,
2910
+ route: file,
2911
+ message: LABEL,
2912
+ recommendation: RECOMMENDATION,
2913
+ docsUrl: DOCS_URL
2914
+ });
2915
+ return;
2916
+ }
2917
+ for (const b of bad) {
2918
+ out.push({
2919
+ id: ID,
2920
+ category: "correctness",
2921
+ severity: "critical",
2922
+ detection: PENALIZED3,
2923
+ route: file,
2924
+ location: file,
2925
+ ...b.line > 0 ? { line: b.line } : {},
2926
+ message: b.message,
2927
+ recommendation: RECOMMENDATION,
2928
+ docsUrl: DOCS_URL
2929
+ });
2930
+ }
2931
+ }
2932
+ var correct007OrphanLifecycle = {
2933
+ id: ID,
2934
+ title: "Lifecycle call outside component initialisation",
2935
+ category: "correctness",
2936
+ severity: "critical",
2937
+ scope: "component",
2938
+ rationale: "Svelte lifecycle and context functions require an active component context; called at module scope, in a shared-state class constructor, or in a load/handler they throw lifecycle_outside_component at runtime \u2014 the compiler does not catch it, and it surfaces as a production crash.",
2939
+ async check(ctx) {
2940
+ const out = [];
2941
+ for (const c of ctx.components ?? []) {
2942
+ const calls = c.orphanLifecycleCalls ?? [];
2943
+ if (calls.length === 0) continue;
2944
+ emitFile(
2945
+ out,
2946
+ c.file,
2947
+ calls.map((o) => ({
2948
+ line: o.line,
2949
+ message: o.kind === "top-level" ? topLevelMessage(o.name) : `class "${o.className}" calls ${o.name}() in its constructor and is instantiated at module scope \u2014 it throws lifecycle_outside_component at runtime`
2950
+ })),
2951
+ c.suppressions
2952
+ );
2953
+ }
2954
+ for (const m of ctx.kitModules ?? []) {
2955
+ const calls = m.lifecycleCalls ?? [];
2956
+ if (calls.length === 0) continue;
2957
+ emitFile(
2958
+ out,
2959
+ m.file,
2960
+ calls.map((l) => ({
2961
+ line: l.line,
2962
+ message: l.inHandler ? `${l.name}() is called in a load/handler \u2014 it runs on every request, outside component initialisation, and throws lifecycle_outside_component at runtime` : `${l.name}() runs outside component initialisation (module evaluation or the init hook) \u2014 it throws lifecycle_outside_component at runtime`
2963
+ })),
2964
+ m.suppressions
2965
+ );
2966
+ }
2967
+ return out;
2968
+ }
2969
+ };
2970
+
2971
+ // src/rules/correctness/correct008-browser-globals.ts
2972
+ var PENALIZED4 = { presence: "none", value: "absent" };
2973
+ var PASS4 = { presence: "own", value: "static" };
2974
+ var ID2 = "CORRECT008";
2975
+ var DOCS_URL2 = docsUrlFor(ID2);
2976
+ var LABEL2 = "Server-safe module code";
2977
+ var RECOMMENDATION2 = "Move browser-only code into onMount or $effect (they never run on the server), or guard it with browser from $app/environment (or a typeof check).";
2978
+ var moduleMessage = (name) => `${name} is accessed at module scope \u2014 it does not exist on the server, so importing this file crashes SSR with "${name} is not defined"`;
2979
+ function isSuppressed3(suppressions, line) {
2980
+ return (suppressions ?? []).some((s) => s.line === line && (!s.ruleIds || s.ruleIds.includes(ID2)));
2981
+ }
2982
+ function emitFile2(out, file, issues, suppressions) {
2983
+ const bad = issues.filter((b) => !(b.line > 0 && isSuppressed3(suppressions, b.line)));
2984
+ if (bad.length === 0) {
2985
+ out.push({
2986
+ id: ID2,
2987
+ category: "correctness",
2988
+ severity: "critical",
2989
+ detection: PASS4,
2990
+ route: file,
2991
+ message: LABEL2,
2992
+ recommendation: RECOMMENDATION2,
2993
+ docsUrl: DOCS_URL2
2994
+ });
2995
+ return;
2996
+ }
2997
+ for (const b of bad) {
2998
+ out.push({
2999
+ id: ID2,
3000
+ category: "correctness",
3001
+ severity: "critical",
3002
+ detection: PENALIZED4,
3003
+ route: file,
3004
+ location: file,
3005
+ ...b.line > 0 ? { line: b.line } : {},
3006
+ message: b.message,
3007
+ recommendation: RECOMMENDATION2,
3008
+ docsUrl: DOCS_URL2
3009
+ });
3010
+ }
3011
+ }
3012
+ var correct008BrowserGlobals = {
3013
+ id: ID2,
3014
+ title: "Browser global in server module code",
3015
+ category: "correctness",
3016
+ severity: "critical",
3017
+ scope: "component",
3018
+ rationale: "window, document, localStorage and friends do not exist on the server; a read in module scope or a load/handler crashes SSR with a ReferenceError \u2014 the compiler does not catch it, and it surfaces as a production 500.",
3019
+ async check(ctx) {
3020
+ const out = [];
3021
+ for (const c of ctx.components ?? []) {
3022
+ const refs = (c.browserGlobalRefs ?? []).filter((r) => r.context === "module");
3023
+ if (refs.length === 0) continue;
3024
+ emitFile2(
3025
+ out,
3026
+ c.file,
3027
+ refs.map((r) => ({ line: r.line, message: moduleMessage(r.name) })),
3028
+ c.suppressions
3029
+ );
3030
+ }
3031
+ for (const m of ctx.kitModules ?? []) {
3032
+ const refs = m.browserGlobalRefs ?? [];
3033
+ if (refs.length === 0) continue;
3034
+ emitFile2(
3035
+ out,
3036
+ m.file,
3037
+ refs.map((r) => ({
3038
+ line: r.line,
3039
+ message: r.inHandler ? `${r.name} is accessed in a load/handler \u2014 it runs on the server during SSR, where ${r.name} is not defined` : moduleMessage(r.name)
3040
+ })),
3041
+ m.suppressions
3042
+ );
3043
+ }
3044
+ return out;
3045
+ }
3046
+ };
3047
+
3048
+ // src/rules/correctness/correct009-instance-browser-globals.ts
3049
+ var correct009InstanceBrowserGlobals = componentRule({
3050
+ id: "CORRECT009",
3051
+ title: "Browser global during component initialisation",
3052
+ category: "correctness",
3053
+ label: "Server-safe component init",
3054
+ recommendation: "Move browser-only code into onMount or $effect (they never run on the server), or guard it with browser from $app/environment (or a typeof check).",
3055
+ rationale: "A component instance script runs on the server on every SSR render, where window/document/localStorage do not exist. Warning, not critical: a component rendered only behind a parent {#if browser} (or a client-only dynamic import) is a legitimate pattern that static analysis cannot prove cross-file.",
3056
+ applies: (c) => (c.browserGlobalRefs ?? []).some((r) => r.context === "instance"),
3057
+ bad: (c) => (c.browserGlobalRefs ?? []).filter((r) => r.context === "instance").map((r) => ({
3058
+ line: r.line,
3059
+ message: `${r.name} is accessed during component initialisation \u2014 during SSR this runs on the server, where ${r.name} is not defined`
3060
+ }))
3061
+ });
3062
+
2101
3063
  // src/rules/security/sec001-002.ts
2102
3064
  var sec001Html = componentRule({
2103
3065
  id: "SEC001",
@@ -2120,6 +3082,122 @@ var sec002JavascriptUrl = componentRule({
2120
3082
  bad: (c) => c.javascriptUrls.map((u) => ({ line: u.line, message: "javascript: URL in an attribute" }))
2121
3083
  });
2122
3084
 
3085
+ // src/rules/kit-module-rule.ts
3086
+ var PENALIZED5 = { presence: "none", value: "absent" };
3087
+ var PASS5 = { presence: "own", value: "static" };
3088
+ function isSuppressed4(m, ruleId, line) {
3089
+ return (m.suppressions ?? []).some((s) => s.line === line && (!s.ruleIds || s.ruleIds.includes(ruleId)));
3090
+ }
3091
+ function kitModuleRule(opts) {
3092
+ const docsUrl7 = docsUrlFor(opts.id);
3093
+ const severity = opts.severity ?? "warning";
3094
+ return {
3095
+ id: opts.id,
3096
+ title: opts.title,
3097
+ category: opts.category,
3098
+ severity,
3099
+ scope: "component",
3100
+ rationale: opts.rationale,
3101
+ async check(ctx) {
3102
+ const out = [];
3103
+ for (const m of ctx.kitModules ?? []) {
3104
+ if (!opts.applies(m, ctx)) continue;
3105
+ const bad = opts.bad(m, ctx).filter((b) => !(b.line > 0 && isSuppressed4(m, opts.id, b.line)));
3106
+ if (bad.length === 0) {
3107
+ out.push({
3108
+ id: opts.id,
3109
+ category: opts.category,
3110
+ severity,
3111
+ detection: PASS5,
3112
+ route: m.file,
3113
+ message: opts.label,
3114
+ recommendation: opts.recommendation,
3115
+ docsUrl: docsUrl7
3116
+ });
3117
+ continue;
3118
+ }
3119
+ for (const b of bad) {
3120
+ out.push({
3121
+ id: opts.id,
3122
+ category: opts.category,
3123
+ severity,
3124
+ detection: PENALIZED5,
3125
+ route: m.file,
3126
+ location: m.file,
3127
+ ...b.line > 0 ? { line: b.line } : {},
3128
+ message: b.message,
3129
+ recommendation: opts.recommendation,
3130
+ docsUrl: docsUrl7
3131
+ });
3132
+ }
3133
+ }
3134
+ return out;
3135
+ }
3136
+ };
3137
+ }
3138
+
3139
+ // src/rules/security/sec003-load-state-write.ts
3140
+ var sec003LoadStateWrite = kitModuleRule({
3141
+ id: "SEC003",
3142
+ title: "Handler writes imported state",
3143
+ category: "security",
3144
+ severity: "critical",
3145
+ label: "Load/handler purity",
3146
+ recommendation: "Return the data from load (or the action) and pass it via page data instead of writing it to module state; per-user data belongs in cookies/locals plus a database.",
3147
+ rationale: "SvelteKit's docs mark this NEVER-DO-THIS: the server is one long-lived process shared by every user, so module state written during a request is visible to ALL later requests \u2014 one user's data can be served to another.",
3148
+ applies: (m) => m.importedStateWrites.length > 0,
3149
+ bad: (m) => m.importedStateWrites.map((w) => ({
3150
+ line: w.line,
3151
+ message: `a server-executed handler writes imported module state "${w.name}" \u2014 shared across all requests on the server, one user's data can leak to another`
3152
+ }))
3153
+ });
3154
+
3155
+ // src/rules/security/sec004-server-module-state.ts
3156
+ var sec004ServerModuleState = kitModuleRule({
3157
+ id: "SEC004",
3158
+ title: "Server module-scope state",
3159
+ category: "security",
3160
+ label: "Server module state",
3161
+ recommendation: "Do not keep request data in module scope on the server \u2014 authenticate with cookies/locals and persist per-user data in a database. For a deliberate process-wide cache, prefer a const container (e.g. a Map) or add an inline suppression.",
3162
+ rationale: `Module scope on the server is one shared, long-lived instance (SvelteKit docs: "Avoid shared state on the server"): a value reassigned during one user's request is served to every other user, and it silently resets on every deploy or restart.`,
3163
+ applies: (m) => m.moduleStateReassignments.length > 0,
3164
+ bad: (m) => m.moduleStateReassignments.map((r) => ({
3165
+ line: r.line,
3166
+ message: r.inHandler ? `module-scope variable "${r.name}" is reassigned from a request handler \u2014 its value is shared across all requests on the server` : `module-scope variable "${r.name}" is reassigned from a function \u2014 if it runs during a request, the value is shared across all requests on the server`
3167
+ }))
3168
+ });
3169
+
3170
+ // src/rules/security/sec005-shared-state-import.ts
3171
+ function extSibling(path) {
3172
+ return path.endsWith(".svelte.ts") ? path.replace(/\.svelte\.ts$/, ".svelte.js") : path.replace(/\.svelte\.js$/, ".svelte.ts");
3173
+ }
3174
+ var sec005SharedStateImport = kitModuleRule({
3175
+ id: "SEC005",
3176
+ title: "Shared runes-state import on the server",
3177
+ category: "security",
3178
+ label: "Server state imports",
3179
+ recommendation: "Keep module-scope $state out of server-executed code: return data from load and share it via page data or the context API. If the module is genuinely client-only, restructure so server files do not import it, or add an inline suppression.",
3180
+ rationale: "A .svelte.ts module with module-scope $state is one shared instance on the server: mutated, it leaks data between users; read-only, every request sees the same boot-time value instead of per-user data.",
3181
+ applies: (m) => m.runesModuleImports.length > 0,
3182
+ bad: (m, ctx) => {
3183
+ const stateFiles = new Set((ctx.components ?? []).filter((c) => c.moduleStateDecls.length > 0).map((c) => c.file));
3184
+ const writtenOutside = new Set(m.importedStateWritesOutsideHandlers.map((w) => w.name));
3185
+ const writtenInHandler = new Set(m.importedStateWrites.map((w) => w.name));
3186
+ const out = [];
3187
+ for (const imp of m.runesModuleImports) {
3188
+ if (!stateFiles.has(imp.resolved) && !stateFiles.has(extSibling(imp.resolved))) continue;
3189
+ const names = imp.names.filter((n) => !writtenInHandler.has(n));
3190
+ if (names.length === 0) continue;
3191
+ const mutates = names.some((n) => writtenOutside.has(n));
3192
+ out.push({
3193
+ line: imp.line,
3194
+ message: mutates ? `server-executed code mutates shared module state from "${imp.source}" \u2014 on the server it is one instance shared by every request` : `"${imp.source}" holds module-scope $state \u2014 on the server it is shared by every request and keeps its boot-time value (a leak if it ever holds per-user data)`
3195
+ });
3196
+ }
3197
+ return out;
3198
+ }
3199
+ });
3200
+
2123
3201
  // src/rules/architecture/arch001-002.ts
2124
3202
  var MAX_LOC = 400;
2125
3203
  var MAX_PROPS = 10;
@@ -2246,8 +3324,15 @@ var allRules = [
2246
3324
  correct003EffectAsOnMount,
2247
3325
  correct004UnmutatedState,
2248
3326
  correct005PropMutation,
3327
+ correct006OrphanEffect,
3328
+ correct007OrphanLifecycle,
3329
+ correct008BrowserGlobals,
3330
+ correct009InstanceBrowserGlobals,
2249
3331
  sec001Html,
2250
3332
  sec002JavascriptUrl,
3333
+ sec003LoadStateWrite,
3334
+ sec004ServerModuleState,
3335
+ sec005SharedStateImport,
2251
3336
  arch001ComponentSize,
2252
3337
  arch002PropCount,
2253
3338
  perf009HeavyImport,
@@ -2793,200 +3878,693 @@ function formatMarkdownReport(results, config, meta) {
2793
3878
  return lines.join("\n");
2794
3879
  }
2795
3880
 
2796
- // src/reporter/html.ts
2797
- var BAND_COLOR = {
2798
- good: "#2FA968",
2799
- warn: "#E8A317",
2800
- poor: "#E5484D"
2801
- };
2802
- function scoreBand(score) {
2803
- return score >= 90 ? "good" : score >= 50 ? "warn" : "poor";
3881
+ // src/reporter/app-shell.ts
3882
+ function embedJson(value) {
3883
+ return JSON.stringify(value).replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
2804
3884
  }
2805
- function escapeHtml(s) {
2806
- return s.replace(
2807
- /[&<>"']/g,
2808
- (c) => c === "&" ? "&amp;" : c === "<" ? "&lt;" : c === ">" ? "&gt;" : c === '"' ? "&quot;" : "&#39;"
2809
- );
3885
+ function sanitizeDocsUrl(issue) {
3886
+ if (issue.docsUrl === void 0) return issue;
3887
+ if (safeHref(issue.docsUrl) !== null) return issue;
3888
+ return { ...issue, docsUrl: void 0 };
2810
3889
  }
2811
- function safeHref(url) {
2812
- const normalized = url.replace(/\s/g, "").toLowerCase();
2813
- return /^https?:\/\//.test(normalized) ? url : null;
3890
+ function sanitizeReport(report) {
3891
+ return {
3892
+ ...report,
3893
+ routes: report.routes.map((route) => ({ ...route, issues: route.issues.map(sanitizeDocsUrl) })),
3894
+ siteIssues: report.siteIssues.map(sanitizeDocsUrl)
3895
+ };
2814
3896
  }
2815
- var slug = (route) => "route-" + route.replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-|-$/g, "").toLowerCase();
2816
- var categoryLabel = (cat) => cat === "seo" ? "SEO" : cat.charAt(0).toUpperCase() + cat.slice(1);
2817
- var SEVERITIES = ["critical", "warning", "info"];
2818
- function renderFinding(issue) {
2819
- const sev = SEVERITIES.includes(issue.severity) ? issue.severity : "info";
2820
- const dyn = issue.detection.value === "dynamic" ? ' <span class="dyn" title="set dynamically (verified at runtime)">\u21AF</span>' : "";
2821
- const line = issue.line !== void 0 ? `:${issue.line}` : "";
2822
- const fix = issue.fix?.snippet !== void 0 ? `<div class="fix"><div class="label">fix</div><pre><code>${escapeHtml(issue.fix.snippet)}</code></pre></div>` : "";
2823
- const href = issue.docsUrl ? safeHref(issue.docsUrl) : null;
2824
- const docs = href ? `<a class="f-link" href="${escapeHtml(href)}">Learn more</a>` : "";
2825
- return `<article class="finding sev-${sev}" data-severity="${sev}" data-category="${escapeHtml(issue.category)}"><div class="f-head"><span class="ruleid">${escapeHtml(issue.id)}</span><span class="f-title">${escapeHtml(issue.title)}</span><span class="sev-tag ${sev}">${sev}</span></div>` + (issue.location ? `<p class="f-loc">${escapeHtml(issue.location)}${line}${dyn}</p>` : "") + (issue.recommendation ? `<p class="f-rec">${escapeHtml(issue.recommendation)}</p>` : "") + fix + docs + `</article>`;
2826
- }
2827
- function renderTopbar(report, meta) {
2828
- const findings = report.routes.reduce((n, r) => n + r.issues.length, 0) + report.siteIssues.length;
2829
- const core = meta.coreVersion ? `<span title="@svelte-vitals/core version">core v${escapeHtml(meta.coreVersion)}</span>` : "";
2830
- return `<header class="topbar"><div class="brand"><span class="bolt">\u21AF</span>svelte-<span class="v">vitals</span></div><div class="meta"><span>v${escapeHtml(meta.version)}</span>` + core + `<span>${report.routes.length} routes</span><span>${findings} findings</span></div></header>`;
2831
- }
2832
- function renderHero(report) {
2833
- const C = 2 * Math.PI * 58;
2834
- const offset = (C * (1 - report.score / 100)).toFixed(1);
2835
- const hb = scoreBand(report.score);
2836
- const s = report.summary;
2837
- const dynNote = s.dynamic > 0 ? `<span class="tally"><span class="dot dyn-dot">\u21AF</span>Dynamic <span class="n">${s.dynamic}</span></span>` : "";
2838
- const cats = Object.entries(report.categories).map(([cat, { score }]) => {
2839
- const b = scoreBand(score);
2840
- const weight = report.weights[cat];
2841
- const w = weight !== void 0 ? `<span class="w">weight ${weight}</span>` : "";
2842
- const name = categoryLabel(cat);
2843
- return `<div class="cat"><div class="top"><span class="name">${escapeHtml(name)} ${w}</span><span class="sc" style="color:${BAND_COLOR[b]}">${score}</span></div><div class="bar"><i style="width:${score}%;background:${BAND_COLOR[b]}"></i></div></div>`;
2844
- }).join("");
2845
- return `<section class="hero"><div class="gauge"><svg width="132" height="132" viewBox="0 0 132 132" aria-hidden="true"><circle cx="66" cy="66" r="58" fill="none" stroke="#e4e7ec" stroke-width="11"></circle><circle id="arc" cx="66" cy="66" r="58" fill="none" stroke="${BAND_COLOR[hb]}" stroke-width="11" stroke-linecap="round" stroke-dasharray="${C.toFixed(1)}" stroke-dashoffset="${offset}"></circle></svg><div class="num"><strong id="hnum">${report.score}</strong><span>Health</span></div></div><div class="readout"><div class="eyebrow">SvelteKit \xB7 SEO &amp; Performance</div><div class="tallies"><span class="tally"><span class="dot crit"></span>Critical <span class="n">${s.critical}</span></span><span class="tally"><span class="dot warn"></span>Warning <span class="n">${s.warning}</span></span><span class="tally"><span class="dot info"></span>Info <span class="n">${s.info}</span></span><span class="tally"><span class="dot pass"></span>Passed <span class="n">${s.passed}</span></span>` + dynNote + `</div><div class="cats">${cats}</div></div></section>`;
2846
- }
2847
- var ROUTE_BADGES = ["measured", "static"];
2848
- function renderRoutes(report, routeBadges) {
2849
- if (report.routes.length === 0) return "";
2850
- const rows = report.routes.map((r) => {
2851
- const b = scoreBand(r.score);
2852
- const crit = r.issues.filter((i) => i.severity === "critical").length;
2853
- const warn = r.issues.filter((i) => i.severity === "warning").length;
2854
- const info = r.issues.filter((i) => i.severity === "info").length;
2855
- const parts = [];
2856
- if (crit) parts.push(`${crit} critical`);
2857
- if (warn) parts.push(`${warn} warning${warn > 1 ? "s" : ""}`);
2858
- if (info) parts.push(`${info} info`);
2859
- const sum = parts.length ? parts.join(" \xB7 ") : '<span class="none">no issues</span>';
2860
- const body = r.issues.length ? r.issues.map(renderFinding).join("") : '<p class="empty">No issues found on this route.</p>';
2861
- const rawBadge = routeBadges?.[r.route];
2862
- const badge = rawBadge && ROUTE_BADGES.includes(rawBadge) ? rawBadge : void 0;
2863
- const badgeHtml = badge ? ` <span class="badge badge-${badge}">${escapeHtml(badge)}</span>` : "";
2864
- return `<details class="route" id="${slug(r.route)}" data-score="${r.score}"${r.issues.length ? " open" : ""}><summary><span class="route-name"><span class="path">${escapeHtml(r.route)}</span>${badgeHtml}</span><span class="issue-sum">${sum}</span><span class="score-chip"><span class="ring" style="background:${BAND_COLOR[b]}"></span>${r.score}</span><span class="chev">\u203A</span></summary><div class="route-body">${body}</div></details>`;
2865
- }).join("");
2866
- return `<section class="section"><h2>Routes</h2><div class="routes">${rows}</div></section>`;
2867
- }
2868
- function renderSiteChecks(report) {
2869
- if (report.siteIssues.length === 0) return "";
2870
- const cards = report.siteIssues.map(renderFinding).join("");
2871
- return `<section class="section"><h2>Site checks</h2>${cards}</section>`;
2872
- }
2873
- function renderFilters(report) {
2874
- const chip = (filter, label, pressed = false) => `<button class="chip" type="button" aria-pressed="${pressed}" data-filter="${escapeHtml(filter)}">${escapeHtml(label)}</button>`;
2875
- const catChips = Object.keys(report.categories).map((cat) => chip(cat, categoryLabel(cat))).join("");
2876
- return `<div class="filters" role="group" aria-label="Filter findings">` + chip("all", "All", true) + chip("critical", "Critical") + chip("warning", "Warning") + chip("info", "Info") + catChips + `</div>`;
2877
- }
2878
- var STYLE = `
2879
- :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;--radius: 12px;--mono: ui-monospace,"SF Mono","JetBrains Mono",Menlo,Consolas,monospace;--sans: system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif}
3897
+ var APP_STYLE = `
3898
+ :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}
3899
+ :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}
3900
+ @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}}
2880
3901
  *{box-sizing:border-box}
2881
- body{margin:0;background:var(--ground);color:var(--ink);font-family:var(--sans);line-height:1.5;-webkit-font-smoothing:antialiased}
2882
- .wrap{max-width:960px;margin:0 auto;padding:0 20px 96px}
2883
- .topbar{display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap;padding:18px 0 16px;border-bottom:1px solid var(--line)}
2884
- .brand{display:flex;align-items:baseline;gap:8px;font-weight:700;font-size:18px;letter-spacing:-.02em}
2885
- .brand .bolt{color:var(--accent);font-size:20px}.brand .v{color:var(--accent)}
2886
- .meta{font-family:var(--mono);font-size:12.5px;color:var(--muted);display:flex;gap:14px;flex-wrap:wrap}
2887
- .hero{display:grid;grid-template-columns:auto 1fr;gap:28px;align-items:center;padding:30px 0 24px}
2888
- .gauge{position:relative;width:132px;height:132px}.gauge svg{transform:rotate(-90deg);display:block}
2889
- .gauge .num{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center}
2890
- .gauge .num strong{font-family:var(--mono);font-size:40px;font-weight:600;letter-spacing:-.03em;line-height:1;font-variant-numeric:tabular-nums}
2891
- .gauge .num span{font-size:11px;text-transform:uppercase;letter-spacing:.14em;color:var(--muted);margin-top:6px}
2892
- .readout{min-width:0}
2893
- .eyebrow{font-size:11px;text-transform:uppercase;letter-spacing:.16em;color:var(--accent);font-weight:700}
2894
- .eyebrow::before{content:"\u21AF "}
2895
- .tallies{display:flex;gap:8px;flex-wrap:wrap;margin:16px 0 18px}
2896
- .tally{display:inline-flex;align-items:center;gap:7px;font-size:13px;font-weight:600;background:var(--panel);border:1px solid var(--line);padding:5px 11px;border-radius:999px}
2897
- .tally .dot{width:9px;height:9px;border-radius:50%}.tally .n{font-family:var(--mono);font-variant-numeric:tabular-nums}
2898
- .dot.crit{background:var(--poor)}.dot.warn{background:var(--warn)}.dot.info{background:var(--faint)}.dot.pass{background:var(--good)}
2899
- .dot.dyn-dot{background:transparent;color:var(--accent);font-weight:700;width:auto;height:auto}
2900
- .cats{display:flex;gap:22px;flex-wrap:wrap}
2901
- .cat{min-width:190px;flex:1}.cat .top{display:flex;align-items:baseline;justify-content:space-between;margin-bottom:6px}
2902
- .cat .name{font-size:13px;font-weight:600}.cat .name .w{color:var(--faint);font-family:var(--mono);font-size:11px;font-weight:500;margin-left:6px}
2903
- .cat .sc{font-family:var(--mono);font-weight:600;font-variant-numeric:tabular-nums}
2904
- .bar{height:7px;border-radius:999px;background:var(--line);overflow:hidden}.bar>i{display:block;height:100%;border-radius:999px}
2905
- .section{margin-top:40px}
2906
- .section>h2{font-size:13px;text-transform:uppercase;letter-spacing:.12em;color:var(--muted);font-weight:700;margin:0 0 14px}
2907
- .filters{display:flex;gap:8px;flex-wrap:wrap;margin-top:28px}
2908
- .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}
2909
- .chip:hover{border-color:var(--faint);color:var(--ink)}
2910
- .chip[aria-pressed="true"]{background:var(--ink);border-color:var(--ink);color:#fff}
2911
- .chip:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
2912
- .routes{border:1px solid var(--line);border-radius:var(--radius);overflow:hidden;background:var(--panel)}
2913
- .route{border-top:1px solid var(--line)}.route:first-child{border-top:0}
2914
- .route>summary{display:grid;grid-template-columns:1fr auto auto auto;gap:16px;align-items:center;padding:13px 16px;cursor:pointer;list-style:none}
2915
- .route>summary::-webkit-details-marker{display:none}
2916
- .route>summary:hover{background:#fbfcfd}
2917
- .route-name{font-family:var(--mono);font-size:13.5px;min-width:0;display:flex;align-items:center;gap:8px}
2918
- .route-name .path{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
2919
- .badge{font-family:var(--sans);font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;padding:2px 7px;border-radius:999px;flex-shrink:0}
2920
- .badge-measured{background:rgba(47,169,104,.14);color:var(--good)}
2921
- .badge-static{background:rgba(140,149,163,.18);color:var(--muted)}
2922
- .issue-sum{font-size:12.5px;color:var(--muted);font-family:var(--mono);white-space:nowrap}.issue-sum .none{color:var(--good)}
2923
- .score-chip{display:inline-flex;align-items:center;gap:7px;font-family:var(--mono);font-weight:600;font-variant-numeric:tabular-nums;font-size:14px}
2924
- .score-chip .ring{width:10px;height:10px;border-radius:50%}
2925
- .chev{color:var(--faint);font-family:var(--mono);transition:transform .15s ease}
2926
- .route[open]>summary .chev{transform:rotate(90deg)}
2927
- .route-body{padding:4px 16px 16px}.empty{color:var(--muted);font-size:13px;margin:6px 0}
2928
- .finding{background:var(--panel);border:1px solid var(--line);border-left-width:3px;border-radius:10px;padding:16px 18px;margin:0 0 12px}
2929
- .finding.sev-critical{border-left-color:var(--poor)}.finding.sev-warning{border-left-color:var(--warn)}.finding.sev-info{border-left-color:var(--faint)}
2930
- .f-head{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
2931
- .ruleid{font-family:var(--mono);font-size:12px;font-weight:600;background:#eef1f5;padding:2px 8px;border-radius:6px}
2932
- .f-title{font-weight:650;font-size:15px}
2933
- .sev-tag{margin-left:auto;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.08em}
2934
- .sev-tag.critical{color:var(--poor)}.sev-tag.warning{color:var(--warn)}.sev-tag.info{color:var(--faint)}
2935
- .f-loc{font-family:var(--mono);font-size:12.5px;color:var(--muted);margin:8px 0 0}.dyn{color:var(--accent);font-weight:700}
2936
- .f-rec{font-size:14px;color:#2b3340;margin:10px 0 0}
2937
- .fix{margin:12px 0 0;background:var(--code-bg);border-radius:8px;overflow:hidden}
2938
- .fix .label{font-family:var(--mono);font-size:11px;letter-spacing:.1em;text-transform:uppercase;color:#8da0bd;padding:8px 14px 0}
2939
- .fix pre{margin:0;padding:8px 14px 14px;overflow-x:auto}
2940
- .fix code{font-family:var(--mono);font-size:12.5px;color:var(--code-ink);line-height:1.65;white-space:pre}
2941
- .f-link{display:inline-block;margin-top:12px;font-size:13px;font-weight:600;color:var(--accent);text-decoration:none}
2942
- .f-link:hover{text-decoration:underline}.f-link::after{content:" \u2192"}
2943
- .foot{margin-top:48px;padding-top:18px;border-top:1px solid var(--line);font-family:var(--mono);font-size:12px;color:var(--faint);display:flex;justify-content:space-between;flex-wrap:wrap;gap:8px}
2944
- .foot a{color:var(--accent);text-decoration:none}
2945
- @media (max-width:640px){.hero{grid-template-columns:1fr;justify-items:start}.route>summary{grid-template-columns:1fr auto}.chev{display:none}}
3902
+ html,body{margin:0;height:100%}
3903
+ body{background:var(--ground);color:var(--ink);font-family:var(--sans);line-height:1.5;-webkit-font-smoothing:antialiased}
3904
+ .dv-app{display:grid;grid-template-rows:auto 1fr;grid-template-columns:280px 1fr;grid-template-areas:"top top" "side main";height:100vh}
3905
+ .dv-topbar{grid-area:top;border-bottom:1px solid var(--line);background:var(--panel)}
3906
+ .dv-topbar-inner{display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap;padding:12px 20px}
3907
+ .dv-brand{display:flex;align-items:center;background:none;border:none;padding:0;cursor:pointer;border-radius:6px}
3908
+ .dv-brand svg{height:28px;width:auto;display:block}
3909
+ .dv-brand:focus-visible{outline:2px solid var(--accent);outline-offset:4px}
3910
+ .dv-meta{font-family:var(--mono);font-size:12px;color:var(--muted);display:flex;gap:12px;flex-wrap:wrap}
3911
+ .dv-status{display:flex;align-items:center;gap:10px}
3912
+ .dv-analyzing{font-size:12px;color:var(--accent);font-weight:600}
3913
+ .dv-conn{width:8px;height:8px;border-radius:50%;background:var(--faint);display:inline-block}
3914
+ .dv-conn-connected{background:var(--good)}
3915
+ .dv-conn-reconnecting{background:var(--warn)}
3916
+ .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}
3917
+ .dv-theme-toggle{border:1px solid var(--line-strong);background:var(--panel);color:var(--ink);border-radius:999px;width:28px;height:28px;cursor:pointer}
3918
+ .dv-theme-toggle:focus-visible,.dv-menu-toggle:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
3919
+ .dv-sidebar{grid-area:side;border-right:1px solid var(--line);background:var(--panel);overflow-y:auto}
3920
+ .dv-sidebar-inner{display:flex;flex-direction:column;gap:10px;padding:14px}
3921
+ .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)}
3922
+ .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)}
3923
+ .dv-nav{display:flex;flex-direction:column;gap:2px}
3924
+ .dv-nav-item{display:flex;flex-direction:column;gap:4px;padding:8px 10px;border-radius:8px;cursor:pointer}
3925
+ .dv-nav-item:hover{background:var(--ground)}
3926
+ .dv-nav-item.active{background:var(--active-bg);color:var(--active-ink)}
3927
+ .dv-nav-item:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}
3928
+ .dv-nav-label{font-family:var(--mono);font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
3929
+ .dv-nav-meta{display:flex;align-items:center;gap:8px;font-size:11.5px;color:var(--muted)}
3930
+ .dv-nav-item.active .dv-nav-meta{color:inherit}
3931
+ .dv-nav-score{font-family:var(--mono);font-weight:700}
3932
+ .dv-badge{font-size:9.5px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;padding:1px 6px;border-radius:999px}
3933
+ .dv-badge-measured{background:rgba(47,169,104,.16);color:var(--good)}
3934
+ .dv-badge-static{background:rgba(140,149,163,.2);color:var(--muted)}
3935
+ .dv-detail{grid-area:main;overflow-y:auto;padding:24px 28px 80px}
3936
+ .dv-gauge{position:relative;width:132px;height:132px;margin-bottom:20px}
3937
+ .dv-gauge svg{position:absolute;inset:0;transform:rotate(-90deg)}
3938
+ .dv-gauge-track{stroke:var(--line)}
3939
+ .dv-gauge-num{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center}
3940
+ .dv-gauge-num strong{font-family:var(--mono);font-size:36px;font-weight:600}
3941
+ .dv-gauge-num span{font-size:10px;text-transform:uppercase;letter-spacing:.14em;color:var(--muted)}
3942
+ .dv-cats{display:flex;gap:22px;flex-wrap:wrap;margin-bottom:20px}
3943
+ .dv-cat{min-width:180px;flex:1}
3944
+ .dv-cat-top{display:flex;justify-content:space-between;font-size:13px;margin-bottom:6px}
3945
+ .dv-bar{height:7px;border-radius:999px;background:var(--line);overflow:hidden}
3946
+ .dv-bar>i{display:block;height:100%;border-radius:999px}
3947
+ .dv-filters{display:flex;gap:8px;flex-wrap:wrap;margin:16px 0}
3948
+ .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}
3949
+ .dv-chip[aria-pressed="true"]{background:var(--active-bg);border-color:var(--active-bg);color:var(--active-ink)}
3950
+ .dv-chip:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
3951
+ .dv-section h2{font-size:12px;text-transform:uppercase;letter-spacing:.12em;color:var(--muted);margin:24px 0 12px}
3952
+ .dv-detail-header{display:flex;align-items:center;gap:12px;margin-bottom:14px;flex-wrap:wrap}
3953
+ .dv-route-path{font-family:var(--mono);font-size:16px;font-weight:600}
3954
+ .dv-score-chip{font-family:var(--mono);font-weight:700}
3955
+ .dv-finding{background:var(--panel);border:1px solid var(--line);border-left-width:3px;border-radius:10px;padding:16px 18px;margin:0 0 12px}
3956
+ .dv-finding-critical{border-left-color:var(--poor)}
3957
+ .dv-finding-warning{border-left-color:var(--warn)}
3958
+ .dv-finding-info{border-left-color:var(--faint)}
3959
+ .dv-f-head{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
3960
+ .dv-ruleid{font-family:var(--mono);font-size:12px;font-weight:600;background:var(--ground);padding:2px 8px;border-radius:6px}
3961
+ .dv-f-title{font-weight:650;font-size:15px}
3962
+ .dv-sev-tag{margin-left:auto;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.08em}
3963
+ .dv-sev-critical{color:var(--poor)}
3964
+ .dv-sev-warning{color:var(--warn)}
3965
+ .dv-sev-info{color:var(--faint)}
3966
+ .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}
3967
+ .dv-f-route:hover{text-decoration:underline}
3968
+ .dv-f-route:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
3969
+ .dv-f-loc{font-family:var(--mono);font-size:12.5px;color:var(--muted);margin:8px 0 0}
3970
+ .dv-f-rec{font-size:14px;margin:10px 0 0}
3971
+ .dv-fix{margin:12px 0 0;background:var(--code-bg);border-radius:8px;overflow:hidden}
3972
+ .dv-fix-label{font-family:var(--mono);font-size:11px;letter-spacing:.1em;text-transform:uppercase;color:#8da0bd;padding:8px 14px 0}
3973
+ .dv-fix pre{margin:0;padding:8px 14px 14px;overflow-x:auto}
3974
+ .dv-fix code{font-family:var(--mono);font-size:12.5px;color:var(--code-ink);line-height:1.65;white-space:pre}
3975
+ .tok-kw{color:#ff7ab8}
3976
+ .tok-str{color:#9ece6a}
3977
+ .tok-num{color:#ff9e64}
3978
+ .tok-cm{color:#6b7280;font-style:italic}
3979
+ .tok-id{color:var(--code-ink)}
3980
+ .tok-pn{color:#8da0bd}
3981
+ .dv-f-link{display:inline-block;margin-top:12px;font-size:13px;font-weight:600;color:var(--accent);text-decoration:none}
3982
+ .dv-f-link:hover{text-decoration:underline}
3983
+ .dv-ai-prompt{margin-top:12px;border:1px solid var(--line);border-radius:8px;overflow:hidden}
3984
+ .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}
3985
+ .dv-ai-prompt>summary::-webkit-details-marker{display:none}
3986
+ .dv-ai-prompt>summary::before{content:"\u25B8";display:inline-block;transition:transform .15s ease}
3987
+ .dv-ai-prompt[open]>summary::before{transform:rotate(90deg)}
3988
+ .dv-ai-prompt-body{padding:0 12px 12px;display:flex;flex-direction:column;gap:8px}
3989
+ .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}
3990
+ .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}
3991
+ .dv-ai-copy-btn:hover{border-color:var(--faint)}
3992
+ .dv-ai-copy-btn:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
3993
+ .dv-empty{color:var(--muted);font-size:13px}
3994
+ @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)}}
2946
3995
  @media (prefers-reduced-motion:reduce){*{transition:none!important}}
2947
3996
  `;
2948
- var SCRIPT = `
3997
+ var APP_SCRIPT = `
2949
3998
  (function(){
2950
- var arc=document.getElementById('arc'),num=document.getElementById('hnum');
2951
- if(arc&&num){
2952
- var C=2*Math.PI*58,score=parseInt(num.textContent,10)||0;
2953
- var reduce=window.matchMedia('(prefers-reduced-motion: reduce)').matches;
2954
- if(!reduce){
2955
- arc.style.transition='stroke-dashoffset 1.1s cubic-bezier(.22,.61,.36,1)';
2956
- arc.style.strokeDashoffset=C.toFixed(1);
2957
- var start=null;
2958
- requestAnimationFrame(function step(t){
2959
- if(start===null)start=t;
2960
- var p=Math.min((t-start)/1100,1);
2961
- num.textContent=Math.round(score*(p<1?1-Math.pow(1-p,3):1));
2962
- if(p<1)requestAnimationFrame(step);
3999
+ var BAND_COLOR = { good: '#2fa968', warn: '#e8a317', poor: '#e5484d' };
4000
+ function scoreBand(score) { return score >= 90 ? 'good' : score >= 50 ? 'warn' : 'poor'; }
4001
+
4002
+ // Same mark as the docs site's hero wordmark (docs/public/wordmark.svg) \u2014 an inline
4003
+ // copy, not an <img src>, since the dashboard is a single self-contained HTML response
4004
+ // with no other static assets to serve alongside it. Fixed brand colors (not CSS custom
4005
+ // properties), matching the docs usage: the wordmark reads the same in both themes.
4006
+ 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>';
4007
+
4008
+ function h(tag, attrs, kids) {
4009
+ var n = document.createElement(tag);
4010
+ if (attrs) {
4011
+ for (var k in attrs) {
4012
+ if (!Object.prototype.hasOwnProperty.call(attrs, k)) continue;
4013
+ var v = attrs[k];
4014
+ if (v === undefined || v === null || v === false) continue;
4015
+ if (k === 'class') n.className = v;
4016
+ else if (k === 'text') n.textContent = v;
4017
+ else if (k.indexOf('on') === 0 && typeof v === 'function') n.addEventListener(k.slice(2), v);
4018
+ else n.setAttribute(k, v === true ? '' : String(v));
4019
+ }
4020
+ }
4021
+ (kids || []).forEach(function (c) {
4022
+ if (c === undefined || c === null || c === false) return;
4023
+ n.appendChild(typeof c === 'string' ? document.createTextNode(c) : c);
4024
+ });
4025
+ return n;
4026
+ }
4027
+ function clear(n) { while (n.firstChild) n.removeChild(n.firstChild); }
4028
+ function mount(id, node) { var el = document.getElementById(id); clear(el); el.appendChild(node); }
4029
+
4030
+ 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'];
4031
+ var HL_LANGS = { js: 1, javascript: 1, ts: 1, typescript: 1, svelte: 1, html: 1, css: 1 };
4032
+
4033
+ function highlightTokens(code) {
4034
+ var tokens = [];
4035
+ var i = 0;
4036
+ var n = code.length;
4037
+ var reIdent = /[A-Za-z_$][A-Za-z0-9_$]*/y;
4038
+ var reNum = /\\d+(\\.\\d+)?/y;
4039
+ while (i < n) {
4040
+ var ch = code[i];
4041
+ if (ch === '/' && code[i + 1] === '/') {
4042
+ var end = code.indexOf('\\n', i);
4043
+ if (end === -1) end = n;
4044
+ tokens.push({ text: code.slice(i, end), cls: 'cm' });
4045
+ i = end;
4046
+ continue;
4047
+ }
4048
+ if (ch === '/' && code[i + 1] === '*') {
4049
+ var end2 = code.indexOf('*/', i + 2);
4050
+ end2 = end2 === -1 ? n : end2 + 2;
4051
+ tokens.push({ text: code.slice(i, end2), cls: 'cm' });
4052
+ i = end2;
4053
+ continue;
4054
+ }
4055
+ if (ch === '"' || ch === "'" || ch === '\`') {
4056
+ var quote = ch;
4057
+ var j = i + 1;
4058
+ while (j < n && code[j] !== quote) {
4059
+ if (code[j] === '\\\\') j++;
4060
+ j++;
4061
+ }
4062
+ j = Math.min(j + 1, n);
4063
+ tokens.push({ text: code.slice(i, j), cls: 'str' });
4064
+ i = j;
4065
+ continue;
4066
+ }
4067
+ reIdent.lastIndex = i;
4068
+ var mIdent = reIdent.exec(code);
4069
+ if (mIdent && mIdent.index === i) {
4070
+ var word = mIdent[0];
4071
+ tokens.push({ text: word, cls: HL_KEYWORDS.indexOf(word) !== -1 ? 'kw' : 'id' });
4072
+ i += word.length;
4073
+ continue;
4074
+ }
4075
+ reNum.lastIndex = i;
4076
+ var mNum = reNum.exec(code);
4077
+ if (mNum && mNum.index === i) {
4078
+ tokens.push({ text: mNum[0], cls: 'num' });
4079
+ i += mNum[0].length;
4080
+ continue;
4081
+ }
4082
+ tokens.push({ text: ch, cls: 'pn' });
4083
+ i += 1;
4084
+ }
4085
+ return tokens;
4086
+ }
4087
+
4088
+ function renderFixSnippet(fix) {
4089
+ var pre = h('pre', null, []);
4090
+ var code = h('code', null, []);
4091
+ var lang = (fix.lang || 'svelte').toLowerCase();
4092
+ if (HL_LANGS[lang]) {
4093
+ highlightTokens(fix.snippet).forEach(function (t) {
4094
+ code.appendChild(h('span', { class: 'tok-' + t.cls, text: t.text }, []));
2963
4095
  });
2964
- requestAnimationFrame(function(){arc.style.strokeDashoffset=(C*(1-score/100)).toFixed(1);});
4096
+ } else {
4097
+ code.textContent = fix.snippet;
4098
+ }
4099
+ pre.appendChild(code);
4100
+ return pre;
4101
+ }
4102
+
4103
+ // Plain-text, copy-pasteable prompt for a single finding \u2014 same ingredients as the
4104
+ // agent reporter's per-finding block (rule id, location, recommendation, fix, docs),
4105
+ // reshaped for a standalone request rather than a whole-project remediation doc.
4106
+ function buildAiPrompt(issue, route) {
4107
+ var lines = ['Fix this svelte-vitals finding:', ''];
4108
+ lines.push('- Rule: ' + issue.id + ' \u2014 ' + issue.title + ' (' + issue.severity + ')');
4109
+ if (route) lines.push('- Route: ' + route);
4110
+ if (issue.location) {
4111
+ lines.push('- Location: ' + issue.location + (issue.line !== undefined ? ':' + issue.line : ''));
4112
+ }
4113
+ if (issue.recommendation) lines.push('- Recommendation: ' + issue.recommendation);
4114
+ if (issue.fix) {
4115
+ lines.push('- Fix: ' + issue.fix.description);
4116
+ if (issue.fix.snippet) {
4117
+ lines.push('', '\`\`\`' + (issue.fix.lang || 'svelte'), issue.fix.snippet, '\`\`\`');
4118
+ }
4119
+ }
4120
+ if (issue.docsUrl) lines.push('- Docs: ' + issue.docsUrl);
4121
+ lines.push(
4122
+ '',
4123
+ 'After fixing, re-run \`svelte-vitals --diff\` (or revisit this route) to confirm ' +
4124
+ issue.id +
4125
+ ' passes' +
4126
+ (route ? ' for ' + route : '') +
4127
+ '.'
4128
+ );
4129
+ return lines.join('\\n');
4130
+ }
4131
+
4132
+ function copyToClipboard(text, btn) {
4133
+ var original = 'Copy';
4134
+ function reset(label) {
4135
+ btn.textContent = label;
4136
+ setTimeout(function () { btn.textContent = original; }, 1500);
2965
4137
  }
4138
+ function done() { reset('Copied!'); }
4139
+ function fail() { reset('Copy failed'); }
4140
+ function fallbackCopy() {
4141
+ var ta = document.createElement('textarea');
4142
+ ta.value = text;
4143
+ ta.setAttribute('readonly', '');
4144
+ ta.style.position = 'fixed';
4145
+ ta.style.opacity = '0';
4146
+ document.body.appendChild(ta);
4147
+ ta.select();
4148
+ var ok = false;
4149
+ try { ok = document.execCommand('copy'); } catch (e) {}
4150
+ document.body.removeChild(ta);
4151
+ return ok;
4152
+ }
4153
+ if (navigator.clipboard && navigator.clipboard.writeText) {
4154
+ navigator.clipboard.writeText(text).then(done, function () { fallbackCopy() ? done() : fail(); });
4155
+ } else {
4156
+ fallbackCopy() ? done() : fail();
4157
+ }
4158
+ }
4159
+
4160
+ function renderAiPrompt(issue, route) {
4161
+ var text = buildAiPrompt(issue, route);
4162
+ var btn = h('button', { type: 'button', class: 'dv-ai-copy-btn', text: 'Copy' }, []);
4163
+ btn.addEventListener('click', function () { copyToClipboard(text, btn); });
4164
+ return h('details', { class: 'dv-ai-prompt' }, [
4165
+ h('summary', { text: 'AI Prompt' }, []),
4166
+ h('div', { class: 'dv-ai-prompt-body' }, [
4167
+ h('pre', { class: 'dv-ai-prompt-pre', text: text }, []),
4168
+ btn
4169
+ ])
4170
+ ]);
2966
4171
  }
2967
- var chips=document.querySelectorAll('.chip'),findings=document.querySelectorAll('.finding');
2968
- function apply(f){
2969
- findings.forEach(function(el){
2970
- var ok=f==='all'||el.getAttribute('data-severity')===f||el.getAttribute('data-category')===f;
2971
- el.style.display=ok?'':'none';
4172
+
4173
+ var state = {
4174
+ snapshot: null,
4175
+ selected: 'overview',
4176
+ search: '',
4177
+ sort: 'score-asc',
4178
+ filter: 'all',
4179
+ theme: initialTheme(),
4180
+ connection: 'connecting',
4181
+ routeBySlug: {}
4182
+ };
4183
+
4184
+ function initialTheme() {
4185
+ try {
4186
+ var stored = localStorage.getItem('svelte-vitals-theme');
4187
+ if (stored === 'dark' || stored === 'light') return stored;
4188
+ } catch (e) {}
4189
+ return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
4190
+ }
4191
+ function applyTheme() { document.documentElement.setAttribute('data-theme', state.theme); }
4192
+ function toggleTheme() {
4193
+ state.theme = state.theme === 'dark' ? 'light' : 'dark';
4194
+ try { localStorage.setItem('svelte-vitals-theme', state.theme); } catch (e) {}
4195
+ applyTheme();
4196
+ renderTopbar();
4197
+ }
4198
+ function toggleSidebar() {
4199
+ var sb = document.getElementById('dv-sidebar');
4200
+ if (sb) sb.classList.toggle('open');
4201
+ }
4202
+
4203
+ function brandEl() {
4204
+ var el = h('button', { type: 'button', class: 'dv-brand', 'aria-label': 'Go to Overview', onclick: function () { selectItem('overview'); } }, []);
4205
+ el.innerHTML = WORDMARK_SVG;
4206
+ return el;
4207
+ }
4208
+
4209
+ function renderTopbar() {
4210
+ var s = state.snapshot;
4211
+ var findings = s.report.routes.reduce(function (n, r) { return n + r.issues.length; }, 0) + s.report.siteIssues.length;
4212
+ var kids = [
4213
+ h('button', { type: 'button', class: 'dv-menu-toggle', 'aria-label': 'Toggle route list', onclick: toggleSidebar, text: '\u2261' }, []),
4214
+ brandEl(),
4215
+ h('div', { class: 'dv-meta' }, [
4216
+ h('span', { text: 'v' + s.meta.version }, []),
4217
+ s.meta.coreVersion ? h('span', { title: '@svelte-vitals/core version', text: 'core v' + s.meta.coreVersion }, []) : null,
4218
+ h('span', { text: s.report.routes.length + ' routes' }, []),
4219
+ h('span', { text: findings + ' findings' }, [])
4220
+ ].filter(Boolean)),
4221
+ h('div', { class: 'dv-status' }, [
4222
+ s.live && s.analyzing ? h('span', { class: 'dv-analyzing', text: 'Analyzing\u2026' }, []) : null,
4223
+ s.live ? h('span', { class: 'dv-conn dv-conn-' + state.connection, title: state.connection }, []) : null,
4224
+ h('button', { type: 'button', class: 'dv-theme-toggle', 'aria-label': 'Toggle dark mode', onclick: toggleTheme, text: state.theme === 'dark' ? '\u2600' : '\u263E' }, [])
4225
+ ].filter(Boolean))
4226
+ ];
4227
+ mount('dv-topbar', h('div', { class: 'dv-topbar-inner' }, kids));
4228
+ }
4229
+
4230
+ function slugify(route) {
4231
+ return 'route-' + route.replace(/[^a-zA-Z0-9]+/g, '-').replace(/^-|-$/g, '').toLowerCase();
4232
+ }
4233
+
4234
+ function matchesSearch(route, q) {
4235
+ if (!q) return true;
4236
+ q = q.toLowerCase();
4237
+ if (route.route.toLowerCase().indexOf(q) !== -1) return true;
4238
+ return route.issues.some(function (iss) {
4239
+ return (iss.id + ' ' + iss.title + ' ' + (iss.location || '')).toLowerCase().indexOf(q) !== -1;
2972
4240
  });
2973
4241
  }
2974
- chips.forEach(function(c){
2975
- c.addEventListener('click',function(){
2976
- chips.forEach(function(o){o.setAttribute('aria-pressed','false');});
2977
- c.setAttribute('aria-pressed','true');
2978
- apply(c.getAttribute('data-filter')||'all');
4242
+
4243
+ function sortedRoutes() {
4244
+ var s = state.snapshot;
4245
+ var q = state.search.trim();
4246
+ var list = s.report.routes.filter(function (r) { return matchesSearch(r, q); }).slice();
4247
+ var sort = state.sort;
4248
+ if (sort === 'score-asc') list.sort(function (a, b) { return a.score - b.score; });
4249
+ else if (sort === 'score-desc') list.sort(function (a, b) { return b.score - a.score; });
4250
+ else if (sort === 'alpha') list.sort(function (a, b) { return a.route.localeCompare(b.route); });
4251
+ else if (sort === 'most-findings') list.sort(function (a, b) { return b.issues.length - a.issues.length; });
4252
+ return list;
4253
+ }
4254
+
4255
+ function renderNavItem(label, key, route, active) {
4256
+ var kids = [h('span', { class: 'dv-nav-label', text: label }, [])];
4257
+ if (route) {
4258
+ var band = scoreBand(route.score);
4259
+ var crit = route.issues.filter(function (i) { return i.severity === 'critical'; }).length;
4260
+ var warn = route.issues.filter(function (i) { return i.severity === 'warning'; }).length;
4261
+ var info = route.issues.filter(function (i) { return i.severity === 'info'; }).length;
4262
+ var summary = [];
4263
+ if (crit) summary.push(crit + ' critical');
4264
+ if (warn) summary.push(warn + ' warning' + (warn > 1 ? 's' : ''));
4265
+ if (info) summary.push(info + ' info');
4266
+ var badge = state.snapshot.badges[route.route];
4267
+ kids.push(h('span', { class: 'dv-nav-meta' }, [
4268
+ badge ? h('span', { class: 'dv-badge dv-badge-' + badge, text: badge }, []) : null,
4269
+ h('span', { class: 'dv-nav-score', style: 'color:' + BAND_COLOR[band], text: String(route.score) }, []),
4270
+ h('span', { class: 'dv-nav-sum', text: summary.length ? summary.join(' \xB7 ') : 'no issues' }, [])
4271
+ ].filter(Boolean)));
4272
+ }
4273
+ return h('div', {
4274
+ class: 'dv-nav-item' + (active ? ' active' : ''),
4275
+ role: 'option',
4276
+ 'aria-selected': active ? 'true' : 'false',
4277
+ tabindex: '0',
4278
+ onclick: function () { selectItem(key); },
4279
+ onkeydown: function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); selectItem(key); } }
4280
+ }, kids);
4281
+ }
4282
+
4283
+ function selectItem(key) {
4284
+ state.selected = key;
4285
+ location.hash = key === 'overview' ? 'overview' : 'route/' + slugify(key);
4286
+ var sb = document.getElementById('dv-sidebar');
4287
+ if (sb) sb.classList.remove('open');
4288
+ renderSidebar();
4289
+ renderDetail();
4290
+ }
4291
+
4292
+ function renderSidebar() {
4293
+ var s = state.snapshot;
4294
+ state.routeBySlug = {};
4295
+
4296
+ // mount() clears and rebuilds the whole sidebar, including the <input> itself, so
4297
+ // a naive re-render on every keystroke drops focus and cursor position \u2014 capture
4298
+ // them before rebuilding and restore them on the freshly-created input afterward.
4299
+ var prevSearch = document.querySelector('.dv-search');
4300
+ var hadFocus = !!prevSearch && document.activeElement === prevSearch;
4301
+ var selStart = hadFocus ? prevSearch.selectionStart : null;
4302
+ var selEnd = hadFocus ? prevSearch.selectionEnd : null;
4303
+
4304
+ var searchInput = h('input', {
4305
+ type: 'search',
4306
+ class: 'dv-search',
4307
+ placeholder: 'Search routes or rules\u2026',
4308
+ value: state.search,
4309
+ oninput: function (e) { state.search = e.target.value; renderSidebar(); }
4310
+ }, []);
4311
+
4312
+ var sortSelect = h('select', { class: 'dv-sort', 'aria-label': 'Sort routes', onchange: function (e) { state.sort = e.target.value; renderSidebar(); } }, [
4313
+ h('option', { value: 'score-asc', selected: state.sort === 'score-asc' || undefined, text: 'Score (worst first)' }, []),
4314
+ h('option', { value: 'score-desc', selected: state.sort === 'score-desc' || undefined, text: 'Score (best first)' }, []),
4315
+ h('option', { value: 'alpha', selected: state.sort === 'alpha' || undefined, text: 'Alphabetical' }, []),
4316
+ h('option', { value: 'most-findings', selected: state.sort === 'most-findings' || undefined, text: 'Most findings' }, [])
4317
+ ]);
4318
+
4319
+ var items = [renderNavItem('Overview', 'overview', null, state.selected === 'overview')];
4320
+ sortedRoutes().forEach(function (r) {
4321
+ var slug = slugify(r.route);
4322
+ state.routeBySlug[slug] = r.route;
4323
+ items.push(renderNavItem(r.route, r.route, r, state.selected === r.route));
2979
4324
  });
2980
- });
4325
+
4326
+ var nav = h('div', { class: 'dv-nav', role: 'listbox', 'aria-label': 'Routes' }, items);
4327
+ mount('dv-sidebar', h('div', { class: 'dv-sidebar-inner' }, [searchInput, sortSelect, nav]));
4328
+
4329
+ if (hadFocus) {
4330
+ searchInput.focus();
4331
+ if (selStart !== null && searchInput.setSelectionRange) {
4332
+ searchInput.setSelectionRange(selStart, selEnd);
4333
+ }
4334
+ }
4335
+ }
4336
+
4337
+ function renderFilterChips(categories) {
4338
+ var chip = function (filter, label) {
4339
+ return h('button', {
4340
+ type: 'button', class: 'dv-chip', 'aria-pressed': state.filter === filter ? 'true' : 'false',
4341
+ onclick: function () { state.filter = filter; renderDetail(); },
4342
+ text: label
4343
+ }, []);
4344
+ };
4345
+ var catChips = Object.keys(categories).map(function (cat) {
4346
+ var name = cat === 'seo' ? 'SEO' : cat.charAt(0).toUpperCase() + cat.slice(1);
4347
+ return chip(cat, name);
4348
+ });
4349
+ return h('div', { class: 'dv-filters', role: 'group', 'aria-label': 'Filter findings' },
4350
+ [chip('all', 'All'), chip('critical', 'Critical'), chip('warning', 'Warning'), chip('info', 'Info')].concat(catChips));
4351
+ }
4352
+
4353
+ function passesFilter(issue) {
4354
+ var f = state.filter;
4355
+ return f === 'all' || issue.severity === f || issue.category === f;
4356
+ }
4357
+
4358
+ var SEVERITY_ORDER = { critical: 0, warning: 1, info: 2 };
4359
+
4360
+ function renderFinding(issue, route, promptRoute) {
4361
+ var kids = [
4362
+ h('div', { class: 'dv-f-head' }, [
4363
+ h('span', { class: 'dv-ruleid', text: issue.id }, []),
4364
+ h('span', { class: 'dv-f-title', text: issue.title }, []),
4365
+ h('span', { class: 'dv-sev-tag dv-sev-' + issue.severity, text: issue.severity }, [])
4366
+ ])
4367
+ ];
4368
+ if (route) {
4369
+ kids.push(h('button', { type: 'button', class: 'dv-f-route', onclick: function () { selectItem(route); }, text: route }, []));
4370
+ }
4371
+ if (issue.location) {
4372
+ kids.push(h('p', { class: 'dv-f-loc', text: issue.location + (issue.line !== undefined ? ':' + issue.line : '') }, []));
4373
+ }
4374
+ if (issue.recommendation) {
4375
+ kids.push(h('p', { class: 'dv-f-rec', text: issue.recommendation }, []));
4376
+ }
4377
+ if (issue.fix && issue.fix.snippet) {
4378
+ kids.push(h('div', { class: 'dv-fix' }, [h('div', { class: 'dv-fix-label', text: 'fix' }, []), renderFixSnippet(issue.fix)]));
4379
+ }
4380
+ if (issue.docsUrl) {
4381
+ kids.push(h('a', { class: 'dv-f-link', href: issue.docsUrl, text: 'Learn more' }, []));
4382
+ }
4383
+ kids.push(renderAiPrompt(issue, promptRoute !== undefined ? promptRoute : route));
4384
+ return h('article', { class: 'dv-finding dv-finding-' + issue.severity }, kids);
4385
+ }
4386
+
4387
+ function renderGauge(score) {
4388
+ var band = scoreBand(score);
4389
+ var svgNs = 'http://www.w3.org/2000/svg';
4390
+ var C = 2 * Math.PI * 58;
4391
+ var offset = (C * (1 - score / 100)).toFixed(1);
4392
+ var svg = document.createElementNS(svgNs, 'svg');
4393
+ svg.setAttribute('width', '132');
4394
+ svg.setAttribute('height', '132');
4395
+ svg.setAttribute('viewBox', '0 0 132 132');
4396
+ var bg = document.createElementNS(svgNs, 'circle');
4397
+ bg.setAttribute('cx', '66'); bg.setAttribute('cy', '66'); bg.setAttribute('r', '58');
4398
+ bg.setAttribute('fill', 'none'); bg.setAttribute('class', 'dv-gauge-track'); bg.setAttribute('stroke-width', '11');
4399
+ var arc = document.createElementNS(svgNs, 'circle');
4400
+ arc.setAttribute('cx', '66'); arc.setAttribute('cy', '66'); arc.setAttribute('r', '58');
4401
+ arc.setAttribute('fill', 'none'); arc.setAttribute('stroke', BAND_COLOR[band]); arc.setAttribute('stroke-width', '11');
4402
+ arc.setAttribute('stroke-linecap', 'round');
4403
+ arc.setAttribute('stroke-dasharray', C.toFixed(1));
4404
+ arc.setAttribute('stroke-dashoffset', offset);
4405
+ svg.appendChild(bg);
4406
+ svg.appendChild(arc);
4407
+ var wrap = h('div', { class: 'dv-gauge' }, [h('div', { class: 'dv-gauge-num' }, [h('strong', { text: String(score) }, []), h('span', { text: 'Health' }, [])])]);
4408
+ wrap.insertBefore(svg, wrap.firstChild);
4409
+ return wrap;
4410
+ }
4411
+
4412
+ function renderOverview(s) {
4413
+ var gauge = renderGauge(s.report.score);
4414
+ var cats = Object.keys(s.report.categories).map(function (cat) {
4415
+ var c = s.report.categories[cat];
4416
+ var band = scoreBand(c.score);
4417
+ var weight = s.report.weights[cat];
4418
+ var name = cat === 'seo' ? 'SEO' : cat.charAt(0).toUpperCase() + cat.slice(1);
4419
+ return h('div', { class: 'dv-cat' }, [
4420
+ h('div', { class: 'dv-cat-top' }, [
4421
+ h('span', { text: name + (weight !== undefined ? ' (weight ' + weight + ')' : '') }, []),
4422
+ h('span', { style: 'color:' + BAND_COLOR[band], text: String(c.score) }, [])
4423
+ ]),
4424
+ h('div', { class: 'dv-bar' }, [h('i', { style: 'width:' + c.score + '%;background:' + BAND_COLOR[band] }, [])])
4425
+ ]);
4426
+ });
4427
+ var chips = renderFilterChips(s.report.categories);
4428
+ var totalCount = s.report.routes.reduce(function (n, r) { return n + r.issues.length; }, 0) + s.report.siteIssues.length;
4429
+ var entries = [];
4430
+ s.report.routes.forEach(function (r) {
4431
+ r.issues.forEach(function (issue) { entries.push({ issue: issue, route: r.route }); });
4432
+ });
4433
+ s.report.siteIssues.forEach(function (issue) { entries.push({ issue: issue, route: null }); });
4434
+ entries = entries.filter(function (e) { return passesFilter(e.issue); });
4435
+ entries.sort(function (a, b) {
4436
+ var sd = SEVERITY_ORDER[a.issue.severity] - SEVERITY_ORDER[b.issue.severity];
4437
+ if (sd !== 0) return sd;
4438
+ return (a.route || '').localeCompare(b.route || '');
4439
+ });
4440
+ var body = entries.length
4441
+ ? entries.map(function (e) { return renderFinding(e.issue, e.route); })
4442
+ : [h('p', { class: 'dv-empty', text: totalCount ? 'No issues match the current filter.' : 'No issues found \u2014 nice work!' }, [])];
4443
+ var findings = h('section', { class: 'dv-section' }, [h('h2', { text: 'Findings' }, [])].concat(body));
4444
+ return h('div', { class: 'dv-overview' }, [gauge, h('div', { class: 'dv-cats' }, cats), chips, findings].filter(Boolean));
4445
+ }
4446
+
4447
+ function renderRouteDetail(route) {
4448
+ var badge = state.snapshot.badges[route.route];
4449
+ var band = scoreBand(route.score);
4450
+ var header = h('div', { class: 'dv-detail-header' }, [
4451
+ h('span', { class: 'dv-route-path', text: route.route }, []),
4452
+ badge ? h('span', { class: 'dv-badge dv-badge-' + badge, text: badge }, []) : null,
4453
+ h('span', { class: 'dv-score-chip', style: 'color:' + BAND_COLOR[band], text: String(route.score) }, [])
4454
+ ].filter(Boolean));
4455
+ var chips = renderFilterChips(state.snapshot.report.categories);
4456
+ var findings = route.issues.filter(passesFilter);
4457
+ var body = findings.length
4458
+ ? findings.map(function (issue) { return renderFinding(issue, undefined, route.route); })
4459
+ : [h('p', { class: 'dv-empty', text: 'No issues match the current filter.' }, [])];
4460
+ return h('div', { class: 'dv-route-detail' }, [header, chips].concat(body));
4461
+ }
4462
+
4463
+ function renderDetail() {
4464
+ var s = state.snapshot;
4465
+ if (state.selected === 'overview') {
4466
+ mount('dv-detail', renderOverview(s));
4467
+ return;
4468
+ }
4469
+ var route = s.report.routes.filter(function (r) { return r.route === state.selected; })[0];
4470
+ if (!route) {
4471
+ state.selected = 'overview';
4472
+ mount('dv-detail', renderOverview(s));
4473
+ return;
4474
+ }
4475
+ mount('dv-detail', renderRouteDetail(route));
4476
+ }
4477
+
4478
+ function renderAll() {
4479
+ renderTopbar();
4480
+ renderSidebar();
4481
+ renderDetail();
4482
+ }
4483
+
4484
+ function restoreSelectionFromHash() {
4485
+ var raw = location.hash.replace(/^#/, '');
4486
+ if (!raw || raw === 'overview') { state.selected = 'overview'; return; }
4487
+ var m = /^route\\/(.+)$/.exec(raw);
4488
+ if (m && state.routeBySlug[m[1]]) state.selected = state.routeBySlug[m[1]];
4489
+ }
4490
+
4491
+ function fetchSnapshot() {
4492
+ fetch('/__svelte-vitals/data.json').then(function (r) { return r.json(); }).then(function (data) {
4493
+ if (state.snapshot && data.sequence <= state.snapshot.sequence) return;
4494
+ state.snapshot = data;
4495
+ renderAll();
4496
+ }).catch(function () {});
4497
+ }
4498
+
4499
+ function boot() {
4500
+ var raw = document.getElementById('svelte-vitals-data');
4501
+ state.snapshot = JSON.parse(raw.textContent);
4502
+ applyTheme();
4503
+ renderSidebar(); // populates routeBySlug before the hash can be trusted
4504
+ restoreSelectionFromHash();
4505
+ renderAll();
4506
+
4507
+ window.addEventListener('hashchange', function () {
4508
+ restoreSelectionFromHash();
4509
+ renderSidebar();
4510
+ renderDetail();
4511
+ });
4512
+
4513
+ // Static export (the CLI's --reporter html): no dev server behind the page, so no
4514
+ // SSE connection, no /data.json refetch, no connection indicator.
4515
+ if (state.snapshot.live && typeof EventSource !== 'undefined') {
4516
+ var es = new EventSource('/__svelte-vitals/events');
4517
+ es.addEventListener('open', function () { state.connection = 'connected'; renderTopbar(); fetchSnapshot(); });
4518
+ es.addEventListener('update', fetchSnapshot);
4519
+ es.addEventListener('error', function () { state.connection = 'reconnecting'; renderTopbar(); });
4520
+ }
4521
+ }
4522
+
4523
+ boot();
2981
4524
  })();
2982
4525
  `;
4526
+ function renderAppShell(snapshot) {
4527
+ const safe = { ...snapshot, report: sanitizeReport(snapshot.report) };
4528
+ const title = snapshot.live ? "svelte-vitals dashboard" : "svelte-vitals report";
4529
+ 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>`;
4530
+ }
2983
4531
  function buildHtmlDocument(report, meta, opts) {
2984
- return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>svelte-vitals report</title><style>${STYLE}</style></head><body><div class="wrap">` + renderTopbar(report, meta) + renderHero(report) + renderFilters(report) + renderRoutes(report, opts?.routeBadges) + renderSiteChecks(report) + `<footer class="foot"><span>Generated by svelte-vitals \xB7 static analysis, no browser</span><span>oekazuma.github.io/svelte-vitals</span></footer></div><script>${SCRIPT}</script></body></html>`;
4532
+ const badges = Object.fromEntries(
4533
+ Object.entries(opts?.routeBadges ?? {}).filter(([, b]) => b === "measured" || b === "static")
4534
+ );
4535
+ return renderAppShell({
4536
+ report,
4537
+ badges,
4538
+ analyzing: false,
4539
+ sequence: 0,
4540
+ live: false,
4541
+ meta
4542
+ });
2985
4543
  }
2986
4544
  function formatHtmlReport(results, config, meta) {
2987
4545
  return buildHtmlDocument(buildJsonReport(results, config, meta), meta);
2988
4546
  }
2989
4547
 
4548
+ // src/reporter/html.ts
4549
+ var BAND_COLOR = {
4550
+ good: "#2FA968",
4551
+ warn: "#E8A317",
4552
+ poor: "#E5484D"
4553
+ };
4554
+ function scoreBand(score) {
4555
+ return score >= 90 ? "good" : score >= 50 ? "warn" : "poor";
4556
+ }
4557
+ function escapeHtml(s) {
4558
+ return s.replace(
4559
+ /[&<>"']/g,
4560
+ (c) => c === "&" ? "&amp;" : c === "<" ? "&lt;" : c === ">" ? "&gt;" : c === '"' ? "&quot;" : "&#39;"
4561
+ );
4562
+ }
4563
+ function safeHref(url) {
4564
+ const normalized = url.replace(/\s/g, "").toLowerCase();
4565
+ return /^https?:\/\//.test(normalized) ? url : null;
4566
+ }
4567
+
2990
4568
  // src/config-apply.ts
2991
4569
  function selectRules(rules, config) {
2992
4570
  return rules.filter((rule) => config.rules[rule.id] !== "off");
@@ -2998,6 +4576,8 @@ function applyRuleSeverities(results, config) {
2998
4576
  });
2999
4577
  }
3000
4578
  export {
4579
+ APP_SCRIPT,
4580
+ APP_STYLE,
3001
4581
  BAND_COLOR,
3002
4582
  CHILD_NODE_KEYS,
3003
4583
  ROBOTS_SOURCE_PATHS,
@@ -3014,6 +4594,7 @@ export {
3014
4594
  buildJsonReport,
3015
4595
  classify,
3016
4596
  collectComponentFacts,
4597
+ collectKitModuleFacts,
3017
4598
  computeHealth,
3018
4599
  computeScore,
3019
4600
  correct001EachKey,
@@ -3021,12 +4602,17 @@ export {
3021
4602
  correct003EffectAsOnMount,
3022
4603
  correct004UnmutatedState,
3023
4604
  correct005PropMutation,
4605
+ correct006OrphanEffect,
4606
+ correct007OrphanLifecycle,
4607
+ correct008BrowserGlobals,
4608
+ correct009InstanceBrowserGlobals,
3024
4609
  defaultConfig,
3025
4610
  defaultProject,
3026
4611
  defineConfig,
3027
4612
  docsUrlFor,
3028
4613
  effectiveSeverity,
3029
4614
  emptyComponentFacts,
4615
+ emptyKitModuleFacts,
3030
4616
  escapeHtml,
3031
4617
  explainRule,
3032
4618
  findAttr,
@@ -3045,6 +4631,7 @@ export {
3045
4631
  linkRule,
3046
4632
  noColorPalette,
3047
4633
  parseComponentFacts,
4634
+ parseKitModuleFacts,
3048
4635
  perf001ImageDimensions,
3049
4636
  perf002ImageLoading,
3050
4637
  perf003PreloadAs,
@@ -3055,6 +4642,8 @@ export {
3055
4642
  perf008Preconnect,
3056
4643
  perf009HeavyImport,
3057
4644
  perf010NamespaceImport,
4645
+ renderAppShell,
4646
+ resolveRunesModuleSpecifier,
3058
4647
  runRules,
3059
4648
  safeHref,
3060
4649
  scoreBand,
@@ -3062,6 +4651,9 @@ export {
3062
4651
  scoresByCategory,
3063
4652
  sec001Html,
3064
4653
  sec002JavascriptUrl,
4654
+ sec003LoadStateWrite,
4655
+ sec004ServerModuleState,
4656
+ sec005SharedStateImport,
3065
4657
  selectRules,
3066
4658
  seo001Title,
3067
4659
  seo002Description,