@svelte-vitals/core 0.25.0 → 0.27.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.
Files changed (3) hide show
  1. package/dist/index.d.ts +187 -26
  2. package/dist/index.js +1141 -8
  3. package/package.json +1 -1
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,
@@ -2789,6 +3874,10 @@ function formatMarkdownReport(results, config, meta) {
2789
3874
  lines.push("");
2790
3875
  lines.push(`\u2026and ${findings.length - MAX_FINDINGS} more (run \`npx svelte-vitals\` locally for the full report)`);
2791
3876
  }
3877
+ lines.push("");
3878
+ lines.push(
3879
+ "_Expected findings (e.g. routes behind auth)? See [Excluding routes or rules](https://oekazuma.github.io/svelte-vitals/guides/ci/#excluding-routes-or-rules)._"
3880
+ );
2792
3881
  }
2793
3882
  return lines.join("\n");
2794
3883
  }
@@ -3490,6 +4579,38 @@ function applyRuleSeverities(results, config) {
3490
4579
  return setting && setting !== "off" ? { ...result, severity: setting } : result;
3491
4580
  });
3492
4581
  }
4582
+ function routeGlobToRegExp(pattern) {
4583
+ const body = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "\0").replace(/\*/g, "[^/]*").split("\0").join(".*");
4584
+ const source = body.endsWith("/.*") ? `${body.slice(0, -3)}(/.*)?` : body;
4585
+ return new RegExp(`^${source}$`);
4586
+ }
4587
+ function toPatterns(globs) {
4588
+ if (globs === void 0) return [];
4589
+ return (Array.isArray(globs) ? globs : [globs]).map(routeGlobToRegExp);
4590
+ }
4591
+ function applyOverrides(results, config) {
4592
+ const overrides = config.overrides;
4593
+ if (!overrides || overrides.length === 0) return results;
4594
+ const compiled = overrides.map((o) => ({
4595
+ routes: toPatterns(o.route),
4596
+ files: toPatterns(o.files),
4597
+ rules: o.rules
4598
+ }));
4599
+ const out = [];
4600
+ for (const result of results) {
4601
+ const { route, location } = result;
4602
+ let setting;
4603
+ for (const o of compiled) {
4604
+ const matched = route !== void 0 && o.routes.some((p) => p.test(route)) || location !== void 0 && o.files.some((p) => p.test(location));
4605
+ if (!matched) continue;
4606
+ const s = o.rules[result.id] ?? o.rules[result.category ?? "seo"];
4607
+ if (s !== void 0) setting = s;
4608
+ }
4609
+ if (setting === void 0) out.push(result);
4610
+ else if (setting !== "off") out.push({ ...result, severity: setting });
4611
+ }
4612
+ return out;
4613
+ }
3493
4614
  export {
3494
4615
  APP_SCRIPT,
3495
4616
  APP_STYLE,
@@ -3498,6 +4619,7 @@ export {
3498
4619
  ROBOTS_SOURCE_PATHS,
3499
4620
  SITEMAP_SOURCE_PATHS,
3500
4621
  allRules,
4622
+ applyOverrides,
3501
4623
  applyRuleSeverities,
3502
4624
  arch001ComponentSize,
3503
4625
  arch002PropCount,
@@ -3509,6 +4631,7 @@ export {
3509
4631
  buildJsonReport,
3510
4632
  classify,
3511
4633
  collectComponentFacts,
4634
+ collectKitModuleFacts,
3512
4635
  computeHealth,
3513
4636
  computeScore,
3514
4637
  correct001EachKey,
@@ -3516,12 +4639,17 @@ export {
3516
4639
  correct003EffectAsOnMount,
3517
4640
  correct004UnmutatedState,
3518
4641
  correct005PropMutation,
4642
+ correct006OrphanEffect,
4643
+ correct007OrphanLifecycle,
4644
+ correct008BrowserGlobals,
4645
+ correct009InstanceBrowserGlobals,
3519
4646
  defaultConfig,
3520
4647
  defaultProject,
3521
4648
  defineConfig,
3522
4649
  docsUrlFor,
3523
4650
  effectiveSeverity,
3524
4651
  emptyComponentFacts,
4652
+ emptyKitModuleFacts,
3525
4653
  escapeHtml,
3526
4654
  explainRule,
3527
4655
  findAttr,
@@ -3540,6 +4668,7 @@ export {
3540
4668
  linkRule,
3541
4669
  noColorPalette,
3542
4670
  parseComponentFacts,
4671
+ parseKitModuleFacts,
3543
4672
  perf001ImageDimensions,
3544
4673
  perf002ImageLoading,
3545
4674
  perf003PreloadAs,
@@ -3551,6 +4680,7 @@ export {
3551
4680
  perf009HeavyImport,
3552
4681
  perf010NamespaceImport,
3553
4682
  renderAppShell,
4683
+ resolveRunesModuleSpecifier,
3554
4684
  runRules,
3555
4685
  safeHref,
3556
4686
  scoreBand,
@@ -3558,6 +4688,9 @@ export {
3558
4688
  scoresByCategory,
3559
4689
  sec001Html,
3560
4690
  sec002JavascriptUrl,
4691
+ sec003LoadStateWrite,
4692
+ sec004ServerModuleState,
4693
+ sec005SharedStateImport,
3561
4694
  selectRules,
3562
4695
  seo001Title,
3563
4696
  seo002Description,