@svelte-vitals/core 0.25.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.d.ts +155 -26
- package/dist/index.js +1104 -8
- 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
|
|
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
|
|
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 (!
|
|
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 (
|
|
467
|
-
collectImportSources(
|
|
468
|
-
collectNamespaceImports(
|
|
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,
|
|
@@ -3509,6 +4594,7 @@ export {
|
|
|
3509
4594
|
buildJsonReport,
|
|
3510
4595
|
classify,
|
|
3511
4596
|
collectComponentFacts,
|
|
4597
|
+
collectKitModuleFacts,
|
|
3512
4598
|
computeHealth,
|
|
3513
4599
|
computeScore,
|
|
3514
4600
|
correct001EachKey,
|
|
@@ -3516,12 +4602,17 @@ export {
|
|
|
3516
4602
|
correct003EffectAsOnMount,
|
|
3517
4603
|
correct004UnmutatedState,
|
|
3518
4604
|
correct005PropMutation,
|
|
4605
|
+
correct006OrphanEffect,
|
|
4606
|
+
correct007OrphanLifecycle,
|
|
4607
|
+
correct008BrowserGlobals,
|
|
4608
|
+
correct009InstanceBrowserGlobals,
|
|
3519
4609
|
defaultConfig,
|
|
3520
4610
|
defaultProject,
|
|
3521
4611
|
defineConfig,
|
|
3522
4612
|
docsUrlFor,
|
|
3523
4613
|
effectiveSeverity,
|
|
3524
4614
|
emptyComponentFacts,
|
|
4615
|
+
emptyKitModuleFacts,
|
|
3525
4616
|
escapeHtml,
|
|
3526
4617
|
explainRule,
|
|
3527
4618
|
findAttr,
|
|
@@ -3540,6 +4631,7 @@ export {
|
|
|
3540
4631
|
linkRule,
|
|
3541
4632
|
noColorPalette,
|
|
3542
4633
|
parseComponentFacts,
|
|
4634
|
+
parseKitModuleFacts,
|
|
3543
4635
|
perf001ImageDimensions,
|
|
3544
4636
|
perf002ImageLoading,
|
|
3545
4637
|
perf003PreloadAs,
|
|
@@ -3551,6 +4643,7 @@ export {
|
|
|
3551
4643
|
perf009HeavyImport,
|
|
3552
4644
|
perf010NamespaceImport,
|
|
3553
4645
|
renderAppShell,
|
|
4646
|
+
resolveRunesModuleSpecifier,
|
|
3554
4647
|
runRules,
|
|
3555
4648
|
safeHref,
|
|
3556
4649
|
scoreBand,
|
|
@@ -3558,6 +4651,9 @@ export {
|
|
|
3558
4651
|
scoresByCategory,
|
|
3559
4652
|
sec001Html,
|
|
3560
4653
|
sec002JavascriptUrl,
|
|
4654
|
+
sec003LoadStateWrite,
|
|
4655
|
+
sec004ServerModuleState,
|
|
4656
|
+
sec005SharedStateImport,
|
|
3561
4657
|
selectRules,
|
|
3562
4658
|
seo001Title,
|
|
3563
4659
|
seo002Description,
|