@svelte-vitals/core 0.24.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/index.d.ts +206 -36
- package/dist/index.js +1773 -181
- 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,
|
|
@@ -2793,200 +3878,693 @@ function formatMarkdownReport(results, config, meta) {
|
|
|
2793
3878
|
return lines.join("\n");
|
|
2794
3879
|
}
|
|
2795
3880
|
|
|
2796
|
-
// src/reporter/
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
warn: "#E8A317",
|
|
2800
|
-
poor: "#E5484D"
|
|
2801
|
-
};
|
|
2802
|
-
function scoreBand(score) {
|
|
2803
|
-
return score >= 90 ? "good" : score >= 50 ? "warn" : "poor";
|
|
3881
|
+
// src/reporter/app-shell.ts
|
|
3882
|
+
function embedJson(value) {
|
|
3883
|
+
return JSON.stringify(value).replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
2804
3884
|
}
|
|
2805
|
-
function
|
|
2806
|
-
return
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
);
|
|
3885
|
+
function sanitizeDocsUrl(issue) {
|
|
3886
|
+
if (issue.docsUrl === void 0) return issue;
|
|
3887
|
+
if (safeHref(issue.docsUrl) !== null) return issue;
|
|
3888
|
+
return { ...issue, docsUrl: void 0 };
|
|
2810
3889
|
}
|
|
2811
|
-
function
|
|
2812
|
-
|
|
2813
|
-
|
|
3890
|
+
function sanitizeReport(report) {
|
|
3891
|
+
return {
|
|
3892
|
+
...report,
|
|
3893
|
+
routes: report.routes.map((route) => ({ ...route, issues: route.issues.map(sanitizeDocsUrl) })),
|
|
3894
|
+
siteIssues: report.siteIssues.map(sanitizeDocsUrl)
|
|
3895
|
+
};
|
|
2814
3896
|
}
|
|
2815
|
-
var
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
const sev = SEVERITIES.includes(issue.severity) ? issue.severity : "info";
|
|
2820
|
-
const dyn = issue.detection.value === "dynamic" ? ' <span class="dyn" title="set dynamically (verified at runtime)">\u21AF</span>' : "";
|
|
2821
|
-
const line = issue.line !== void 0 ? `:${issue.line}` : "";
|
|
2822
|
-
const fix = issue.fix?.snippet !== void 0 ? `<div class="fix"><div class="label">fix</div><pre><code>${escapeHtml(issue.fix.snippet)}</code></pre></div>` : "";
|
|
2823
|
-
const href = issue.docsUrl ? safeHref(issue.docsUrl) : null;
|
|
2824
|
-
const docs = href ? `<a class="f-link" href="${escapeHtml(href)}">Learn more</a>` : "";
|
|
2825
|
-
return `<article class="finding sev-${sev}" data-severity="${sev}" data-category="${escapeHtml(issue.category)}"><div class="f-head"><span class="ruleid">${escapeHtml(issue.id)}</span><span class="f-title">${escapeHtml(issue.title)}</span><span class="sev-tag ${sev}">${sev}</span></div>` + (issue.location ? `<p class="f-loc">${escapeHtml(issue.location)}${line}${dyn}</p>` : "") + (issue.recommendation ? `<p class="f-rec">${escapeHtml(issue.recommendation)}</p>` : "") + fix + docs + `</article>`;
|
|
2826
|
-
}
|
|
2827
|
-
function renderTopbar(report, meta) {
|
|
2828
|
-
const findings = report.routes.reduce((n, r) => n + r.issues.length, 0) + report.siteIssues.length;
|
|
2829
|
-
const core = meta.coreVersion ? `<span title="@svelte-vitals/core version">core v${escapeHtml(meta.coreVersion)}</span>` : "";
|
|
2830
|
-
return `<header class="topbar"><div class="brand"><span class="bolt">\u21AF</span>svelte-<span class="v">vitals</span></div><div class="meta"><span>v${escapeHtml(meta.version)}</span>` + core + `<span>${report.routes.length} routes</span><span>${findings} findings</span></div></header>`;
|
|
2831
|
-
}
|
|
2832
|
-
function renderHero(report) {
|
|
2833
|
-
const C = 2 * Math.PI * 58;
|
|
2834
|
-
const offset = (C * (1 - report.score / 100)).toFixed(1);
|
|
2835
|
-
const hb = scoreBand(report.score);
|
|
2836
|
-
const s = report.summary;
|
|
2837
|
-
const dynNote = s.dynamic > 0 ? `<span class="tally"><span class="dot dyn-dot">\u21AF</span>Dynamic <span class="n">${s.dynamic}</span></span>` : "";
|
|
2838
|
-
const cats = Object.entries(report.categories).map(([cat, { score }]) => {
|
|
2839
|
-
const b = scoreBand(score);
|
|
2840
|
-
const weight = report.weights[cat];
|
|
2841
|
-
const w = weight !== void 0 ? `<span class="w">weight ${weight}</span>` : "";
|
|
2842
|
-
const name = categoryLabel(cat);
|
|
2843
|
-
return `<div class="cat"><div class="top"><span class="name">${escapeHtml(name)} ${w}</span><span class="sc" style="color:${BAND_COLOR[b]}">${score}</span></div><div class="bar"><i style="width:${score}%;background:${BAND_COLOR[b]}"></i></div></div>`;
|
|
2844
|
-
}).join("");
|
|
2845
|
-
return `<section class="hero"><div class="gauge"><svg width="132" height="132" viewBox="0 0 132 132" aria-hidden="true"><circle cx="66" cy="66" r="58" fill="none" stroke="#e4e7ec" stroke-width="11"></circle><circle id="arc" cx="66" cy="66" r="58" fill="none" stroke="${BAND_COLOR[hb]}" stroke-width="11" stroke-linecap="round" stroke-dasharray="${C.toFixed(1)}" stroke-dashoffset="${offset}"></circle></svg><div class="num"><strong id="hnum">${report.score}</strong><span>Health</span></div></div><div class="readout"><div class="eyebrow">SvelteKit \xB7 SEO & Performance</div><div class="tallies"><span class="tally"><span class="dot crit"></span>Critical <span class="n">${s.critical}</span></span><span class="tally"><span class="dot warn"></span>Warning <span class="n">${s.warning}</span></span><span class="tally"><span class="dot info"></span>Info <span class="n">${s.info}</span></span><span class="tally"><span class="dot pass"></span>Passed <span class="n">${s.passed}</span></span>` + dynNote + `</div><div class="cats">${cats}</div></div></section>`;
|
|
2846
|
-
}
|
|
2847
|
-
var ROUTE_BADGES = ["measured", "static"];
|
|
2848
|
-
function renderRoutes(report, routeBadges) {
|
|
2849
|
-
if (report.routes.length === 0) return "";
|
|
2850
|
-
const rows = report.routes.map((r) => {
|
|
2851
|
-
const b = scoreBand(r.score);
|
|
2852
|
-
const crit = r.issues.filter((i) => i.severity === "critical").length;
|
|
2853
|
-
const warn = r.issues.filter((i) => i.severity === "warning").length;
|
|
2854
|
-
const info = r.issues.filter((i) => i.severity === "info").length;
|
|
2855
|
-
const parts = [];
|
|
2856
|
-
if (crit) parts.push(`${crit} critical`);
|
|
2857
|
-
if (warn) parts.push(`${warn} warning${warn > 1 ? "s" : ""}`);
|
|
2858
|
-
if (info) parts.push(`${info} info`);
|
|
2859
|
-
const sum = parts.length ? parts.join(" \xB7 ") : '<span class="none">no issues</span>';
|
|
2860
|
-
const body = r.issues.length ? r.issues.map(renderFinding).join("") : '<p class="empty">No issues found on this route.</p>';
|
|
2861
|
-
const rawBadge = routeBadges?.[r.route];
|
|
2862
|
-
const badge = rawBadge && ROUTE_BADGES.includes(rawBadge) ? rawBadge : void 0;
|
|
2863
|
-
const badgeHtml = badge ? ` <span class="badge badge-${badge}">${escapeHtml(badge)}</span>` : "";
|
|
2864
|
-
return `<details class="route" id="${slug(r.route)}" data-score="${r.score}"${r.issues.length ? " open" : ""}><summary><span class="route-name"><span class="path">${escapeHtml(r.route)}</span>${badgeHtml}</span><span class="issue-sum">${sum}</span><span class="score-chip"><span class="ring" style="background:${BAND_COLOR[b]}"></span>${r.score}</span><span class="chev">\u203A</span></summary><div class="route-body">${body}</div></details>`;
|
|
2865
|
-
}).join("");
|
|
2866
|
-
return `<section class="section"><h2>Routes</h2><div class="routes">${rows}</div></section>`;
|
|
2867
|
-
}
|
|
2868
|
-
function renderSiteChecks(report) {
|
|
2869
|
-
if (report.siteIssues.length === 0) return "";
|
|
2870
|
-
const cards = report.siteIssues.map(renderFinding).join("");
|
|
2871
|
-
return `<section class="section"><h2>Site checks</h2>${cards}</section>`;
|
|
2872
|
-
}
|
|
2873
|
-
function renderFilters(report) {
|
|
2874
|
-
const chip = (filter, label, pressed = false) => `<button class="chip" type="button" aria-pressed="${pressed}" data-filter="${escapeHtml(filter)}">${escapeHtml(label)}</button>`;
|
|
2875
|
-
const catChips = Object.keys(report.categories).map((cat) => chip(cat, categoryLabel(cat))).join("");
|
|
2876
|
-
return `<div class="filters" role="group" aria-label="Filter findings">` + chip("all", "All", true) + chip("critical", "Critical") + chip("warning", "Warning") + chip("info", "Info") + catChips + `</div>`;
|
|
2877
|
-
}
|
|
2878
|
-
var STYLE = `
|
|
2879
|
-
:root{--ground: #f6f7f9;--panel: #fff;--ink: #0c1322;--muted: #5a6472;--faint: #8c95a3;--line: #e4e7ec;--line-strong: #d3d8e0;--accent: #ff3e00;--good: #2fa968;--warn: #e8a317;--poor: #e5484d;--code-bg: #0e1525;--code-ink: #e7ecf4;--radius: 12px;--mono: ui-monospace,"SF Mono","JetBrains Mono",Menlo,Consolas,monospace;--sans: system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif}
|
|
3897
|
+
var APP_STYLE = `
|
|
3898
|
+
:root{--ground:#f6f7f9;--panel:#fff;--ink:#0c1322;--muted:#5a6472;--faint:#8c95a3;--line:#e4e7ec;--line-strong:#d3d8e0;--accent:#ff3e00;--good:#2fa968;--warn:#e8a317;--poor:#e5484d;--code-bg:#0e1525;--code-ink:#e7ecf4;--active-bg:#0c1322;--active-ink:#fff;--mono:ui-monospace,"SF Mono","JetBrains Mono",Menlo,Consolas,monospace;--sans:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif}
|
|
3899
|
+
:root[data-theme="dark"]{--ground:#0b0e14;--panel:#12161f;--ink:#e7ecf4;--muted:#9aa4b2;--faint:#6b7484;--line:#232838;--line-strong:#2d3345;--code-bg:#05070c;--code-ink:#e7ecf4;--active-bg:#e7ecf4;--active-ink:#0b0e14}
|
|
3900
|
+
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]){--ground:#0b0e14;--panel:#12161f;--ink:#e7ecf4;--muted:#9aa4b2;--faint:#6b7484;--line:#232838;--line-strong:#2d3345;--code-bg:#05070c;--code-ink:#e7ecf4;--active-bg:#e7ecf4;--active-ink:#0b0e14}}
|
|
2880
3901
|
*{box-sizing:border-box}
|
|
2881
|
-
body{margin:0;
|
|
2882
|
-
|
|
2883
|
-
.
|
|
2884
|
-
.
|
|
2885
|
-
.
|
|
2886
|
-
.
|
|
2887
|
-
.
|
|
2888
|
-
.
|
|
2889
|
-
.
|
|
2890
|
-
.
|
|
2891
|
-
.
|
|
2892
|
-
.
|
|
2893
|
-
.
|
|
2894
|
-
.
|
|
2895
|
-
.
|
|
2896
|
-
.
|
|
2897
|
-
.
|
|
2898
|
-
.
|
|
2899
|
-
.
|
|
2900
|
-
.
|
|
2901
|
-
.
|
|
2902
|
-
.
|
|
2903
|
-
.
|
|
2904
|
-
.
|
|
2905
|
-
.
|
|
2906
|
-
.
|
|
2907
|
-
.
|
|
2908
|
-
.
|
|
2909
|
-
.
|
|
2910
|
-
.
|
|
2911
|
-
.
|
|
2912
|
-
.
|
|
2913
|
-
.
|
|
2914
|
-
.
|
|
2915
|
-
.
|
|
2916
|
-
.
|
|
2917
|
-
.
|
|
2918
|
-
.
|
|
2919
|
-
.
|
|
2920
|
-
.
|
|
2921
|
-
.
|
|
2922
|
-
.
|
|
2923
|
-
.
|
|
2924
|
-
.
|
|
2925
|
-
.
|
|
2926
|
-
.
|
|
2927
|
-
.
|
|
2928
|
-
.
|
|
2929
|
-
.
|
|
2930
|
-
.
|
|
2931
|
-
.
|
|
2932
|
-
.
|
|
2933
|
-
.
|
|
2934
|
-
.
|
|
2935
|
-
.
|
|
2936
|
-
.
|
|
2937
|
-
.
|
|
2938
|
-
.
|
|
2939
|
-
.
|
|
2940
|
-
.
|
|
2941
|
-
.
|
|
2942
|
-
.
|
|
2943
|
-
.
|
|
2944
|
-
.
|
|
2945
|
-
|
|
3902
|
+
html,body{margin:0;height:100%}
|
|
3903
|
+
body{background:var(--ground);color:var(--ink);font-family:var(--sans);line-height:1.5;-webkit-font-smoothing:antialiased}
|
|
3904
|
+
.dv-app{display:grid;grid-template-rows:auto 1fr;grid-template-columns:280px 1fr;grid-template-areas:"top top" "side main";height:100vh}
|
|
3905
|
+
.dv-topbar{grid-area:top;border-bottom:1px solid var(--line);background:var(--panel)}
|
|
3906
|
+
.dv-topbar-inner{display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap;padding:12px 20px}
|
|
3907
|
+
.dv-brand{display:flex;align-items:center;background:none;border:none;padding:0;cursor:pointer;border-radius:6px}
|
|
3908
|
+
.dv-brand svg{height:28px;width:auto;display:block}
|
|
3909
|
+
.dv-brand:focus-visible{outline:2px solid var(--accent);outline-offset:4px}
|
|
3910
|
+
.dv-meta{font-family:var(--mono);font-size:12px;color:var(--muted);display:flex;gap:12px;flex-wrap:wrap}
|
|
3911
|
+
.dv-status{display:flex;align-items:center;gap:10px}
|
|
3912
|
+
.dv-analyzing{font-size:12px;color:var(--accent);font-weight:600}
|
|
3913
|
+
.dv-conn{width:8px;height:8px;border-radius:50%;background:var(--faint);display:inline-block}
|
|
3914
|
+
.dv-conn-connected{background:var(--good)}
|
|
3915
|
+
.dv-conn-reconnecting{background:var(--warn)}
|
|
3916
|
+
.dv-menu-toggle{display:none;border:1px solid var(--line-strong);background:var(--panel);color:var(--ink);border-radius:8px;width:28px;height:28px;cursor:pointer}
|
|
3917
|
+
.dv-theme-toggle{border:1px solid var(--line-strong);background:var(--panel);color:var(--ink);border-radius:999px;width:28px;height:28px;cursor:pointer}
|
|
3918
|
+
.dv-theme-toggle:focus-visible,.dv-menu-toggle:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
|
|
3919
|
+
.dv-sidebar{grid-area:side;border-right:1px solid var(--line);background:var(--panel);overflow-y:auto}
|
|
3920
|
+
.dv-sidebar-inner{display:flex;flex-direction:column;gap:10px;padding:14px}
|
|
3921
|
+
.dv-search{font:inherit;font-size:13px;padding:7px 10px;border:1px solid var(--line-strong);border-radius:8px;background:var(--ground);color:var(--ink)}
|
|
3922
|
+
.dv-sort{font:inherit;font-size:12.5px;padding:6px 8px;border:1px solid var(--line-strong);border-radius:8px;background:var(--ground);color:var(--ink)}
|
|
3923
|
+
.dv-nav{display:flex;flex-direction:column;gap:2px}
|
|
3924
|
+
.dv-nav-item{display:flex;flex-direction:column;gap:4px;padding:8px 10px;border-radius:8px;cursor:pointer}
|
|
3925
|
+
.dv-nav-item:hover{background:var(--ground)}
|
|
3926
|
+
.dv-nav-item.active{background:var(--active-bg);color:var(--active-ink)}
|
|
3927
|
+
.dv-nav-item:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}
|
|
3928
|
+
.dv-nav-label{font-family:var(--mono);font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
3929
|
+
.dv-nav-meta{display:flex;align-items:center;gap:8px;font-size:11.5px;color:var(--muted)}
|
|
3930
|
+
.dv-nav-item.active .dv-nav-meta{color:inherit}
|
|
3931
|
+
.dv-nav-score{font-family:var(--mono);font-weight:700}
|
|
3932
|
+
.dv-badge{font-size:9.5px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;padding:1px 6px;border-radius:999px}
|
|
3933
|
+
.dv-badge-measured{background:rgba(47,169,104,.16);color:var(--good)}
|
|
3934
|
+
.dv-badge-static{background:rgba(140,149,163,.2);color:var(--muted)}
|
|
3935
|
+
.dv-detail{grid-area:main;overflow-y:auto;padding:24px 28px 80px}
|
|
3936
|
+
.dv-gauge{position:relative;width:132px;height:132px;margin-bottom:20px}
|
|
3937
|
+
.dv-gauge svg{position:absolute;inset:0;transform:rotate(-90deg)}
|
|
3938
|
+
.dv-gauge-track{stroke:var(--line)}
|
|
3939
|
+
.dv-gauge-num{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center}
|
|
3940
|
+
.dv-gauge-num strong{font-family:var(--mono);font-size:36px;font-weight:600}
|
|
3941
|
+
.dv-gauge-num span{font-size:10px;text-transform:uppercase;letter-spacing:.14em;color:var(--muted)}
|
|
3942
|
+
.dv-cats{display:flex;gap:22px;flex-wrap:wrap;margin-bottom:20px}
|
|
3943
|
+
.dv-cat{min-width:180px;flex:1}
|
|
3944
|
+
.dv-cat-top{display:flex;justify-content:space-between;font-size:13px;margin-bottom:6px}
|
|
3945
|
+
.dv-bar{height:7px;border-radius:999px;background:var(--line);overflow:hidden}
|
|
3946
|
+
.dv-bar>i{display:block;height:100%;border-radius:999px}
|
|
3947
|
+
.dv-filters{display:flex;gap:8px;flex-wrap:wrap;margin:16px 0}
|
|
3948
|
+
.dv-chip{font:inherit;font-size:12.5px;font-weight:600;cursor:pointer;background:var(--panel);border:1px solid var(--line-strong);color:var(--muted);padding:5px 12px;border-radius:999px}
|
|
3949
|
+
.dv-chip[aria-pressed="true"]{background:var(--active-bg);border-color:var(--active-bg);color:var(--active-ink)}
|
|
3950
|
+
.dv-chip:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
|
|
3951
|
+
.dv-section h2{font-size:12px;text-transform:uppercase;letter-spacing:.12em;color:var(--muted);margin:24px 0 12px}
|
|
3952
|
+
.dv-detail-header{display:flex;align-items:center;gap:12px;margin-bottom:14px;flex-wrap:wrap}
|
|
3953
|
+
.dv-route-path{font-family:var(--mono);font-size:16px;font-weight:600}
|
|
3954
|
+
.dv-score-chip{font-family:var(--mono);font-weight:700}
|
|
3955
|
+
.dv-finding{background:var(--panel);border:1px solid var(--line);border-left-width:3px;border-radius:10px;padding:16px 18px;margin:0 0 12px}
|
|
3956
|
+
.dv-finding-critical{border-left-color:var(--poor)}
|
|
3957
|
+
.dv-finding-warning{border-left-color:var(--warn)}
|
|
3958
|
+
.dv-finding-info{border-left-color:var(--faint)}
|
|
3959
|
+
.dv-f-head{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
|
|
3960
|
+
.dv-ruleid{font-family:var(--mono);font-size:12px;font-weight:600;background:var(--ground);padding:2px 8px;border-radius:6px}
|
|
3961
|
+
.dv-f-title{font-weight:650;font-size:15px}
|
|
3962
|
+
.dv-sev-tag{margin-left:auto;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.08em}
|
|
3963
|
+
.dv-sev-critical{color:var(--poor)}
|
|
3964
|
+
.dv-sev-warning{color:var(--warn)}
|
|
3965
|
+
.dv-sev-info{color:var(--faint)}
|
|
3966
|
+
.dv-f-route{display:block;font:inherit;font-family:var(--mono);font-size:12.5px;font-weight:600;color:var(--accent);background:none;border:none;padding:0;margin:8px 0 0;cursor:pointer;text-align:left}
|
|
3967
|
+
.dv-f-route:hover{text-decoration:underline}
|
|
3968
|
+
.dv-f-route:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
|
|
3969
|
+
.dv-f-loc{font-family:var(--mono);font-size:12.5px;color:var(--muted);margin:8px 0 0}
|
|
3970
|
+
.dv-f-rec{font-size:14px;margin:10px 0 0}
|
|
3971
|
+
.dv-fix{margin:12px 0 0;background:var(--code-bg);border-radius:8px;overflow:hidden}
|
|
3972
|
+
.dv-fix-label{font-family:var(--mono);font-size:11px;letter-spacing:.1em;text-transform:uppercase;color:#8da0bd;padding:8px 14px 0}
|
|
3973
|
+
.dv-fix pre{margin:0;padding:8px 14px 14px;overflow-x:auto}
|
|
3974
|
+
.dv-fix code{font-family:var(--mono);font-size:12.5px;color:var(--code-ink);line-height:1.65;white-space:pre}
|
|
3975
|
+
.tok-kw{color:#ff7ab8}
|
|
3976
|
+
.tok-str{color:#9ece6a}
|
|
3977
|
+
.tok-num{color:#ff9e64}
|
|
3978
|
+
.tok-cm{color:#6b7280;font-style:italic}
|
|
3979
|
+
.tok-id{color:var(--code-ink)}
|
|
3980
|
+
.tok-pn{color:#8da0bd}
|
|
3981
|
+
.dv-f-link{display:inline-block;margin-top:12px;font-size:13px;font-weight:600;color:var(--accent);text-decoration:none}
|
|
3982
|
+
.dv-f-link:hover{text-decoration:underline}
|
|
3983
|
+
.dv-ai-prompt{margin-top:12px;border:1px solid var(--line);border-radius:8px;overflow:hidden}
|
|
3984
|
+
.dv-ai-prompt>summary{cursor:pointer;list-style:none;padding:8px 12px;font-size:12px;font-weight:600;color:var(--muted);display:flex;align-items:center;gap:6px;user-select:none}
|
|
3985
|
+
.dv-ai-prompt>summary::-webkit-details-marker{display:none}
|
|
3986
|
+
.dv-ai-prompt>summary::before{content:"\u25B8";display:inline-block;transition:transform .15s ease}
|
|
3987
|
+
.dv-ai-prompt[open]>summary::before{transform:rotate(90deg)}
|
|
3988
|
+
.dv-ai-prompt-body{padding:0 12px 12px;display:flex;flex-direction:column;gap:8px}
|
|
3989
|
+
.dv-ai-prompt-pre{margin:0;padding:10px 12px;background:var(--code-bg);color:var(--code-ink);border-radius:8px;font-family:var(--mono);font-size:12px;line-height:1.6;white-space:pre-wrap;word-break:break-word;max-height:280px;overflow-y:auto}
|
|
3990
|
+
.dv-ai-copy-btn{align-self:flex-start;font:inherit;font-size:12px;font-weight:600;cursor:pointer;background:var(--panel);border:1px solid var(--line-strong);color:var(--ink);padding:5px 12px;border-radius:999px}
|
|
3991
|
+
.dv-ai-copy-btn:hover{border-color:var(--faint)}
|
|
3992
|
+
.dv-ai-copy-btn:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
|
|
3993
|
+
.dv-empty{color:var(--muted);font-size:13px}
|
|
3994
|
+
@media (max-width:640px){.dv-app{grid-template-columns:1fr;grid-template-areas:"top" "main"}.dv-menu-toggle{display:inline-flex}.dv-sidebar{position:fixed;inset:0 20% 0 0;transform:translateX(-100%);transition:transform .2s ease;z-index:10}.dv-sidebar.open{transform:translateX(0)}}
|
|
2946
3995
|
@media (prefers-reduced-motion:reduce){*{transition:none!important}}
|
|
2947
3996
|
`;
|
|
2948
|
-
var
|
|
3997
|
+
var APP_SCRIPT = `
|
|
2949
3998
|
(function(){
|
|
2950
|
-
var
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
3999
|
+
var BAND_COLOR = { good: '#2fa968', warn: '#e8a317', poor: '#e5484d' };
|
|
4000
|
+
function scoreBand(score) { return score >= 90 ? 'good' : score >= 50 ? 'warn' : 'poor'; }
|
|
4001
|
+
|
|
4002
|
+
// Same mark as the docs site's hero wordmark (docs/public/wordmark.svg) \u2014 an inline
|
|
4003
|
+
// copy, not an <img src>, since the dashboard is a single self-contained HTML response
|
|
4004
|
+
// with no other static assets to serve alongside it. Fixed brand colors (not CSS custom
|
|
4005
|
+
// properties), matching the docs usage: the wordmark reads the same in both themes.
|
|
4006
|
+
var WORDMARK_SVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 380 56" role="img" aria-labelledby="dv-wordmark-title"><title id="dv-wordmark-title">svelte-vitals</title><defs><clipPath id="dv-wordmark-clip"><rect x="2" y="2" width="52" height="52" rx="14"/></clipPath></defs><rect x="2" y="2" width="52" height="52" rx="14" fill="#FF3E00"/><polyline clip-path="url(#dv-wordmark-clip)" points="4,28 15,28 17.5,23.5 20,28 23,28 26,7 29,49 32,28 35,28 37,24.5 39.5,28 52,28" fill="none" stroke="#fff" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/><text x="70" y="38" font-family="ui-sans-serif, system-ui, -apple-system, \\'Segoe UI\\', Roboto, sans-serif" font-size="30" font-weight="700" fill="#FF3E00">svelte-vitals</text></svg>';
|
|
4007
|
+
|
|
4008
|
+
function h(tag, attrs, kids) {
|
|
4009
|
+
var n = document.createElement(tag);
|
|
4010
|
+
if (attrs) {
|
|
4011
|
+
for (var k in attrs) {
|
|
4012
|
+
if (!Object.prototype.hasOwnProperty.call(attrs, k)) continue;
|
|
4013
|
+
var v = attrs[k];
|
|
4014
|
+
if (v === undefined || v === null || v === false) continue;
|
|
4015
|
+
if (k === 'class') n.className = v;
|
|
4016
|
+
else if (k === 'text') n.textContent = v;
|
|
4017
|
+
else if (k.indexOf('on') === 0 && typeof v === 'function') n.addEventListener(k.slice(2), v);
|
|
4018
|
+
else n.setAttribute(k, v === true ? '' : String(v));
|
|
4019
|
+
}
|
|
4020
|
+
}
|
|
4021
|
+
(kids || []).forEach(function (c) {
|
|
4022
|
+
if (c === undefined || c === null || c === false) return;
|
|
4023
|
+
n.appendChild(typeof c === 'string' ? document.createTextNode(c) : c);
|
|
4024
|
+
});
|
|
4025
|
+
return n;
|
|
4026
|
+
}
|
|
4027
|
+
function clear(n) { while (n.firstChild) n.removeChild(n.firstChild); }
|
|
4028
|
+
function mount(id, node) { var el = document.getElementById(id); clear(el); el.appendChild(node); }
|
|
4029
|
+
|
|
4030
|
+
var HL_KEYWORDS = ['import','export','from','const','let','var','function','return','if','else','for','while','class','new','await','async','default','type','interface','extends','implements','this','typeof','instanceof','of','in','true','false','null','undefined'];
|
|
4031
|
+
var HL_LANGS = { js: 1, javascript: 1, ts: 1, typescript: 1, svelte: 1, html: 1, css: 1 };
|
|
4032
|
+
|
|
4033
|
+
function highlightTokens(code) {
|
|
4034
|
+
var tokens = [];
|
|
4035
|
+
var i = 0;
|
|
4036
|
+
var n = code.length;
|
|
4037
|
+
var reIdent = /[A-Za-z_$][A-Za-z0-9_$]*/y;
|
|
4038
|
+
var reNum = /\\d+(\\.\\d+)?/y;
|
|
4039
|
+
while (i < n) {
|
|
4040
|
+
var ch = code[i];
|
|
4041
|
+
if (ch === '/' && code[i + 1] === '/') {
|
|
4042
|
+
var end = code.indexOf('\\n', i);
|
|
4043
|
+
if (end === -1) end = n;
|
|
4044
|
+
tokens.push({ text: code.slice(i, end), cls: 'cm' });
|
|
4045
|
+
i = end;
|
|
4046
|
+
continue;
|
|
4047
|
+
}
|
|
4048
|
+
if (ch === '/' && code[i + 1] === '*') {
|
|
4049
|
+
var end2 = code.indexOf('*/', i + 2);
|
|
4050
|
+
end2 = end2 === -1 ? n : end2 + 2;
|
|
4051
|
+
tokens.push({ text: code.slice(i, end2), cls: 'cm' });
|
|
4052
|
+
i = end2;
|
|
4053
|
+
continue;
|
|
4054
|
+
}
|
|
4055
|
+
if (ch === '"' || ch === "'" || ch === '\`') {
|
|
4056
|
+
var quote = ch;
|
|
4057
|
+
var j = i + 1;
|
|
4058
|
+
while (j < n && code[j] !== quote) {
|
|
4059
|
+
if (code[j] === '\\\\') j++;
|
|
4060
|
+
j++;
|
|
4061
|
+
}
|
|
4062
|
+
j = Math.min(j + 1, n);
|
|
4063
|
+
tokens.push({ text: code.slice(i, j), cls: 'str' });
|
|
4064
|
+
i = j;
|
|
4065
|
+
continue;
|
|
4066
|
+
}
|
|
4067
|
+
reIdent.lastIndex = i;
|
|
4068
|
+
var mIdent = reIdent.exec(code);
|
|
4069
|
+
if (mIdent && mIdent.index === i) {
|
|
4070
|
+
var word = mIdent[0];
|
|
4071
|
+
tokens.push({ text: word, cls: HL_KEYWORDS.indexOf(word) !== -1 ? 'kw' : 'id' });
|
|
4072
|
+
i += word.length;
|
|
4073
|
+
continue;
|
|
4074
|
+
}
|
|
4075
|
+
reNum.lastIndex = i;
|
|
4076
|
+
var mNum = reNum.exec(code);
|
|
4077
|
+
if (mNum && mNum.index === i) {
|
|
4078
|
+
tokens.push({ text: mNum[0], cls: 'num' });
|
|
4079
|
+
i += mNum[0].length;
|
|
4080
|
+
continue;
|
|
4081
|
+
}
|
|
4082
|
+
tokens.push({ text: ch, cls: 'pn' });
|
|
4083
|
+
i += 1;
|
|
4084
|
+
}
|
|
4085
|
+
return tokens;
|
|
4086
|
+
}
|
|
4087
|
+
|
|
4088
|
+
function renderFixSnippet(fix) {
|
|
4089
|
+
var pre = h('pre', null, []);
|
|
4090
|
+
var code = h('code', null, []);
|
|
4091
|
+
var lang = (fix.lang || 'svelte').toLowerCase();
|
|
4092
|
+
if (HL_LANGS[lang]) {
|
|
4093
|
+
highlightTokens(fix.snippet).forEach(function (t) {
|
|
4094
|
+
code.appendChild(h('span', { class: 'tok-' + t.cls, text: t.text }, []));
|
|
2963
4095
|
});
|
|
2964
|
-
|
|
4096
|
+
} else {
|
|
4097
|
+
code.textContent = fix.snippet;
|
|
4098
|
+
}
|
|
4099
|
+
pre.appendChild(code);
|
|
4100
|
+
return pre;
|
|
4101
|
+
}
|
|
4102
|
+
|
|
4103
|
+
// Plain-text, copy-pasteable prompt for a single finding \u2014 same ingredients as the
|
|
4104
|
+
// agent reporter's per-finding block (rule id, location, recommendation, fix, docs),
|
|
4105
|
+
// reshaped for a standalone request rather than a whole-project remediation doc.
|
|
4106
|
+
function buildAiPrompt(issue, route) {
|
|
4107
|
+
var lines = ['Fix this svelte-vitals finding:', ''];
|
|
4108
|
+
lines.push('- Rule: ' + issue.id + ' \u2014 ' + issue.title + ' (' + issue.severity + ')');
|
|
4109
|
+
if (route) lines.push('- Route: ' + route);
|
|
4110
|
+
if (issue.location) {
|
|
4111
|
+
lines.push('- Location: ' + issue.location + (issue.line !== undefined ? ':' + issue.line : ''));
|
|
4112
|
+
}
|
|
4113
|
+
if (issue.recommendation) lines.push('- Recommendation: ' + issue.recommendation);
|
|
4114
|
+
if (issue.fix) {
|
|
4115
|
+
lines.push('- Fix: ' + issue.fix.description);
|
|
4116
|
+
if (issue.fix.snippet) {
|
|
4117
|
+
lines.push('', '\`\`\`' + (issue.fix.lang || 'svelte'), issue.fix.snippet, '\`\`\`');
|
|
4118
|
+
}
|
|
4119
|
+
}
|
|
4120
|
+
if (issue.docsUrl) lines.push('- Docs: ' + issue.docsUrl);
|
|
4121
|
+
lines.push(
|
|
4122
|
+
'',
|
|
4123
|
+
'After fixing, re-run \`svelte-vitals --diff\` (or revisit this route) to confirm ' +
|
|
4124
|
+
issue.id +
|
|
4125
|
+
' passes' +
|
|
4126
|
+
(route ? ' for ' + route : '') +
|
|
4127
|
+
'.'
|
|
4128
|
+
);
|
|
4129
|
+
return lines.join('\\n');
|
|
4130
|
+
}
|
|
4131
|
+
|
|
4132
|
+
function copyToClipboard(text, btn) {
|
|
4133
|
+
var original = 'Copy';
|
|
4134
|
+
function reset(label) {
|
|
4135
|
+
btn.textContent = label;
|
|
4136
|
+
setTimeout(function () { btn.textContent = original; }, 1500);
|
|
2965
4137
|
}
|
|
4138
|
+
function done() { reset('Copied!'); }
|
|
4139
|
+
function fail() { reset('Copy failed'); }
|
|
4140
|
+
function fallbackCopy() {
|
|
4141
|
+
var ta = document.createElement('textarea');
|
|
4142
|
+
ta.value = text;
|
|
4143
|
+
ta.setAttribute('readonly', '');
|
|
4144
|
+
ta.style.position = 'fixed';
|
|
4145
|
+
ta.style.opacity = '0';
|
|
4146
|
+
document.body.appendChild(ta);
|
|
4147
|
+
ta.select();
|
|
4148
|
+
var ok = false;
|
|
4149
|
+
try { ok = document.execCommand('copy'); } catch (e) {}
|
|
4150
|
+
document.body.removeChild(ta);
|
|
4151
|
+
return ok;
|
|
4152
|
+
}
|
|
4153
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
4154
|
+
navigator.clipboard.writeText(text).then(done, function () { fallbackCopy() ? done() : fail(); });
|
|
4155
|
+
} else {
|
|
4156
|
+
fallbackCopy() ? done() : fail();
|
|
4157
|
+
}
|
|
4158
|
+
}
|
|
4159
|
+
|
|
4160
|
+
function renderAiPrompt(issue, route) {
|
|
4161
|
+
var text = buildAiPrompt(issue, route);
|
|
4162
|
+
var btn = h('button', { type: 'button', class: 'dv-ai-copy-btn', text: 'Copy' }, []);
|
|
4163
|
+
btn.addEventListener('click', function () { copyToClipboard(text, btn); });
|
|
4164
|
+
return h('details', { class: 'dv-ai-prompt' }, [
|
|
4165
|
+
h('summary', { text: 'AI Prompt' }, []),
|
|
4166
|
+
h('div', { class: 'dv-ai-prompt-body' }, [
|
|
4167
|
+
h('pre', { class: 'dv-ai-prompt-pre', text: text }, []),
|
|
4168
|
+
btn
|
|
4169
|
+
])
|
|
4170
|
+
]);
|
|
2966
4171
|
}
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
4172
|
+
|
|
4173
|
+
var state = {
|
|
4174
|
+
snapshot: null,
|
|
4175
|
+
selected: 'overview',
|
|
4176
|
+
search: '',
|
|
4177
|
+
sort: 'score-asc',
|
|
4178
|
+
filter: 'all',
|
|
4179
|
+
theme: initialTheme(),
|
|
4180
|
+
connection: 'connecting',
|
|
4181
|
+
routeBySlug: {}
|
|
4182
|
+
};
|
|
4183
|
+
|
|
4184
|
+
function initialTheme() {
|
|
4185
|
+
try {
|
|
4186
|
+
var stored = localStorage.getItem('svelte-vitals-theme');
|
|
4187
|
+
if (stored === 'dark' || stored === 'light') return stored;
|
|
4188
|
+
} catch (e) {}
|
|
4189
|
+
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
4190
|
+
}
|
|
4191
|
+
function applyTheme() { document.documentElement.setAttribute('data-theme', state.theme); }
|
|
4192
|
+
function toggleTheme() {
|
|
4193
|
+
state.theme = state.theme === 'dark' ? 'light' : 'dark';
|
|
4194
|
+
try { localStorage.setItem('svelte-vitals-theme', state.theme); } catch (e) {}
|
|
4195
|
+
applyTheme();
|
|
4196
|
+
renderTopbar();
|
|
4197
|
+
}
|
|
4198
|
+
function toggleSidebar() {
|
|
4199
|
+
var sb = document.getElementById('dv-sidebar');
|
|
4200
|
+
if (sb) sb.classList.toggle('open');
|
|
4201
|
+
}
|
|
4202
|
+
|
|
4203
|
+
function brandEl() {
|
|
4204
|
+
var el = h('button', { type: 'button', class: 'dv-brand', 'aria-label': 'Go to Overview', onclick: function () { selectItem('overview'); } }, []);
|
|
4205
|
+
el.innerHTML = WORDMARK_SVG;
|
|
4206
|
+
return el;
|
|
4207
|
+
}
|
|
4208
|
+
|
|
4209
|
+
function renderTopbar() {
|
|
4210
|
+
var s = state.snapshot;
|
|
4211
|
+
var findings = s.report.routes.reduce(function (n, r) { return n + r.issues.length; }, 0) + s.report.siteIssues.length;
|
|
4212
|
+
var kids = [
|
|
4213
|
+
h('button', { type: 'button', class: 'dv-menu-toggle', 'aria-label': 'Toggle route list', onclick: toggleSidebar, text: '\u2261' }, []),
|
|
4214
|
+
brandEl(),
|
|
4215
|
+
h('div', { class: 'dv-meta' }, [
|
|
4216
|
+
h('span', { text: 'v' + s.meta.version }, []),
|
|
4217
|
+
s.meta.coreVersion ? h('span', { title: '@svelte-vitals/core version', text: 'core v' + s.meta.coreVersion }, []) : null,
|
|
4218
|
+
h('span', { text: s.report.routes.length + ' routes' }, []),
|
|
4219
|
+
h('span', { text: findings + ' findings' }, [])
|
|
4220
|
+
].filter(Boolean)),
|
|
4221
|
+
h('div', { class: 'dv-status' }, [
|
|
4222
|
+
s.live && s.analyzing ? h('span', { class: 'dv-analyzing', text: 'Analyzing\u2026' }, []) : null,
|
|
4223
|
+
s.live ? h('span', { class: 'dv-conn dv-conn-' + state.connection, title: state.connection }, []) : null,
|
|
4224
|
+
h('button', { type: 'button', class: 'dv-theme-toggle', 'aria-label': 'Toggle dark mode', onclick: toggleTheme, text: state.theme === 'dark' ? '\u2600' : '\u263E' }, [])
|
|
4225
|
+
].filter(Boolean))
|
|
4226
|
+
];
|
|
4227
|
+
mount('dv-topbar', h('div', { class: 'dv-topbar-inner' }, kids));
|
|
4228
|
+
}
|
|
4229
|
+
|
|
4230
|
+
function slugify(route) {
|
|
4231
|
+
return 'route-' + route.replace(/[^a-zA-Z0-9]+/g, '-').replace(/^-|-$/g, '').toLowerCase();
|
|
4232
|
+
}
|
|
4233
|
+
|
|
4234
|
+
function matchesSearch(route, q) {
|
|
4235
|
+
if (!q) return true;
|
|
4236
|
+
q = q.toLowerCase();
|
|
4237
|
+
if (route.route.toLowerCase().indexOf(q) !== -1) return true;
|
|
4238
|
+
return route.issues.some(function (iss) {
|
|
4239
|
+
return (iss.id + ' ' + iss.title + ' ' + (iss.location || '')).toLowerCase().indexOf(q) !== -1;
|
|
2972
4240
|
});
|
|
2973
4241
|
}
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
4242
|
+
|
|
4243
|
+
function sortedRoutes() {
|
|
4244
|
+
var s = state.snapshot;
|
|
4245
|
+
var q = state.search.trim();
|
|
4246
|
+
var list = s.report.routes.filter(function (r) { return matchesSearch(r, q); }).slice();
|
|
4247
|
+
var sort = state.sort;
|
|
4248
|
+
if (sort === 'score-asc') list.sort(function (a, b) { return a.score - b.score; });
|
|
4249
|
+
else if (sort === 'score-desc') list.sort(function (a, b) { return b.score - a.score; });
|
|
4250
|
+
else if (sort === 'alpha') list.sort(function (a, b) { return a.route.localeCompare(b.route); });
|
|
4251
|
+
else if (sort === 'most-findings') list.sort(function (a, b) { return b.issues.length - a.issues.length; });
|
|
4252
|
+
return list;
|
|
4253
|
+
}
|
|
4254
|
+
|
|
4255
|
+
function renderNavItem(label, key, route, active) {
|
|
4256
|
+
var kids = [h('span', { class: 'dv-nav-label', text: label }, [])];
|
|
4257
|
+
if (route) {
|
|
4258
|
+
var band = scoreBand(route.score);
|
|
4259
|
+
var crit = route.issues.filter(function (i) { return i.severity === 'critical'; }).length;
|
|
4260
|
+
var warn = route.issues.filter(function (i) { return i.severity === 'warning'; }).length;
|
|
4261
|
+
var info = route.issues.filter(function (i) { return i.severity === 'info'; }).length;
|
|
4262
|
+
var summary = [];
|
|
4263
|
+
if (crit) summary.push(crit + ' critical');
|
|
4264
|
+
if (warn) summary.push(warn + ' warning' + (warn > 1 ? 's' : ''));
|
|
4265
|
+
if (info) summary.push(info + ' info');
|
|
4266
|
+
var badge = state.snapshot.badges[route.route];
|
|
4267
|
+
kids.push(h('span', { class: 'dv-nav-meta' }, [
|
|
4268
|
+
badge ? h('span', { class: 'dv-badge dv-badge-' + badge, text: badge }, []) : null,
|
|
4269
|
+
h('span', { class: 'dv-nav-score', style: 'color:' + BAND_COLOR[band], text: String(route.score) }, []),
|
|
4270
|
+
h('span', { class: 'dv-nav-sum', text: summary.length ? summary.join(' \xB7 ') : 'no issues' }, [])
|
|
4271
|
+
].filter(Boolean)));
|
|
4272
|
+
}
|
|
4273
|
+
return h('div', {
|
|
4274
|
+
class: 'dv-nav-item' + (active ? ' active' : ''),
|
|
4275
|
+
role: 'option',
|
|
4276
|
+
'aria-selected': active ? 'true' : 'false',
|
|
4277
|
+
tabindex: '0',
|
|
4278
|
+
onclick: function () { selectItem(key); },
|
|
4279
|
+
onkeydown: function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); selectItem(key); } }
|
|
4280
|
+
}, kids);
|
|
4281
|
+
}
|
|
4282
|
+
|
|
4283
|
+
function selectItem(key) {
|
|
4284
|
+
state.selected = key;
|
|
4285
|
+
location.hash = key === 'overview' ? 'overview' : 'route/' + slugify(key);
|
|
4286
|
+
var sb = document.getElementById('dv-sidebar');
|
|
4287
|
+
if (sb) sb.classList.remove('open');
|
|
4288
|
+
renderSidebar();
|
|
4289
|
+
renderDetail();
|
|
4290
|
+
}
|
|
4291
|
+
|
|
4292
|
+
function renderSidebar() {
|
|
4293
|
+
var s = state.snapshot;
|
|
4294
|
+
state.routeBySlug = {};
|
|
4295
|
+
|
|
4296
|
+
// mount() clears and rebuilds the whole sidebar, including the <input> itself, so
|
|
4297
|
+
// a naive re-render on every keystroke drops focus and cursor position \u2014 capture
|
|
4298
|
+
// them before rebuilding and restore them on the freshly-created input afterward.
|
|
4299
|
+
var prevSearch = document.querySelector('.dv-search');
|
|
4300
|
+
var hadFocus = !!prevSearch && document.activeElement === prevSearch;
|
|
4301
|
+
var selStart = hadFocus ? prevSearch.selectionStart : null;
|
|
4302
|
+
var selEnd = hadFocus ? prevSearch.selectionEnd : null;
|
|
4303
|
+
|
|
4304
|
+
var searchInput = h('input', {
|
|
4305
|
+
type: 'search',
|
|
4306
|
+
class: 'dv-search',
|
|
4307
|
+
placeholder: 'Search routes or rules\u2026',
|
|
4308
|
+
value: state.search,
|
|
4309
|
+
oninput: function (e) { state.search = e.target.value; renderSidebar(); }
|
|
4310
|
+
}, []);
|
|
4311
|
+
|
|
4312
|
+
var sortSelect = h('select', { class: 'dv-sort', 'aria-label': 'Sort routes', onchange: function (e) { state.sort = e.target.value; renderSidebar(); } }, [
|
|
4313
|
+
h('option', { value: 'score-asc', selected: state.sort === 'score-asc' || undefined, text: 'Score (worst first)' }, []),
|
|
4314
|
+
h('option', { value: 'score-desc', selected: state.sort === 'score-desc' || undefined, text: 'Score (best first)' }, []),
|
|
4315
|
+
h('option', { value: 'alpha', selected: state.sort === 'alpha' || undefined, text: 'Alphabetical' }, []),
|
|
4316
|
+
h('option', { value: 'most-findings', selected: state.sort === 'most-findings' || undefined, text: 'Most findings' }, [])
|
|
4317
|
+
]);
|
|
4318
|
+
|
|
4319
|
+
var items = [renderNavItem('Overview', 'overview', null, state.selected === 'overview')];
|
|
4320
|
+
sortedRoutes().forEach(function (r) {
|
|
4321
|
+
var slug = slugify(r.route);
|
|
4322
|
+
state.routeBySlug[slug] = r.route;
|
|
4323
|
+
items.push(renderNavItem(r.route, r.route, r, state.selected === r.route));
|
|
2979
4324
|
});
|
|
2980
|
-
|
|
4325
|
+
|
|
4326
|
+
var nav = h('div', { class: 'dv-nav', role: 'listbox', 'aria-label': 'Routes' }, items);
|
|
4327
|
+
mount('dv-sidebar', h('div', { class: 'dv-sidebar-inner' }, [searchInput, sortSelect, nav]));
|
|
4328
|
+
|
|
4329
|
+
if (hadFocus) {
|
|
4330
|
+
searchInput.focus();
|
|
4331
|
+
if (selStart !== null && searchInput.setSelectionRange) {
|
|
4332
|
+
searchInput.setSelectionRange(selStart, selEnd);
|
|
4333
|
+
}
|
|
4334
|
+
}
|
|
4335
|
+
}
|
|
4336
|
+
|
|
4337
|
+
function renderFilterChips(categories) {
|
|
4338
|
+
var chip = function (filter, label) {
|
|
4339
|
+
return h('button', {
|
|
4340
|
+
type: 'button', class: 'dv-chip', 'aria-pressed': state.filter === filter ? 'true' : 'false',
|
|
4341
|
+
onclick: function () { state.filter = filter; renderDetail(); },
|
|
4342
|
+
text: label
|
|
4343
|
+
}, []);
|
|
4344
|
+
};
|
|
4345
|
+
var catChips = Object.keys(categories).map(function (cat) {
|
|
4346
|
+
var name = cat === 'seo' ? 'SEO' : cat.charAt(0).toUpperCase() + cat.slice(1);
|
|
4347
|
+
return chip(cat, name);
|
|
4348
|
+
});
|
|
4349
|
+
return h('div', { class: 'dv-filters', role: 'group', 'aria-label': 'Filter findings' },
|
|
4350
|
+
[chip('all', 'All'), chip('critical', 'Critical'), chip('warning', 'Warning'), chip('info', 'Info')].concat(catChips));
|
|
4351
|
+
}
|
|
4352
|
+
|
|
4353
|
+
function passesFilter(issue) {
|
|
4354
|
+
var f = state.filter;
|
|
4355
|
+
return f === 'all' || issue.severity === f || issue.category === f;
|
|
4356
|
+
}
|
|
4357
|
+
|
|
4358
|
+
var SEVERITY_ORDER = { critical: 0, warning: 1, info: 2 };
|
|
4359
|
+
|
|
4360
|
+
function renderFinding(issue, route, promptRoute) {
|
|
4361
|
+
var kids = [
|
|
4362
|
+
h('div', { class: 'dv-f-head' }, [
|
|
4363
|
+
h('span', { class: 'dv-ruleid', text: issue.id }, []),
|
|
4364
|
+
h('span', { class: 'dv-f-title', text: issue.title }, []),
|
|
4365
|
+
h('span', { class: 'dv-sev-tag dv-sev-' + issue.severity, text: issue.severity }, [])
|
|
4366
|
+
])
|
|
4367
|
+
];
|
|
4368
|
+
if (route) {
|
|
4369
|
+
kids.push(h('button', { type: 'button', class: 'dv-f-route', onclick: function () { selectItem(route); }, text: route }, []));
|
|
4370
|
+
}
|
|
4371
|
+
if (issue.location) {
|
|
4372
|
+
kids.push(h('p', { class: 'dv-f-loc', text: issue.location + (issue.line !== undefined ? ':' + issue.line : '') }, []));
|
|
4373
|
+
}
|
|
4374
|
+
if (issue.recommendation) {
|
|
4375
|
+
kids.push(h('p', { class: 'dv-f-rec', text: issue.recommendation }, []));
|
|
4376
|
+
}
|
|
4377
|
+
if (issue.fix && issue.fix.snippet) {
|
|
4378
|
+
kids.push(h('div', { class: 'dv-fix' }, [h('div', { class: 'dv-fix-label', text: 'fix' }, []), renderFixSnippet(issue.fix)]));
|
|
4379
|
+
}
|
|
4380
|
+
if (issue.docsUrl) {
|
|
4381
|
+
kids.push(h('a', { class: 'dv-f-link', href: issue.docsUrl, text: 'Learn more' }, []));
|
|
4382
|
+
}
|
|
4383
|
+
kids.push(renderAiPrompt(issue, promptRoute !== undefined ? promptRoute : route));
|
|
4384
|
+
return h('article', { class: 'dv-finding dv-finding-' + issue.severity }, kids);
|
|
4385
|
+
}
|
|
4386
|
+
|
|
4387
|
+
function renderGauge(score) {
|
|
4388
|
+
var band = scoreBand(score);
|
|
4389
|
+
var svgNs = 'http://www.w3.org/2000/svg';
|
|
4390
|
+
var C = 2 * Math.PI * 58;
|
|
4391
|
+
var offset = (C * (1 - score / 100)).toFixed(1);
|
|
4392
|
+
var svg = document.createElementNS(svgNs, 'svg');
|
|
4393
|
+
svg.setAttribute('width', '132');
|
|
4394
|
+
svg.setAttribute('height', '132');
|
|
4395
|
+
svg.setAttribute('viewBox', '0 0 132 132');
|
|
4396
|
+
var bg = document.createElementNS(svgNs, 'circle');
|
|
4397
|
+
bg.setAttribute('cx', '66'); bg.setAttribute('cy', '66'); bg.setAttribute('r', '58');
|
|
4398
|
+
bg.setAttribute('fill', 'none'); bg.setAttribute('class', 'dv-gauge-track'); bg.setAttribute('stroke-width', '11');
|
|
4399
|
+
var arc = document.createElementNS(svgNs, 'circle');
|
|
4400
|
+
arc.setAttribute('cx', '66'); arc.setAttribute('cy', '66'); arc.setAttribute('r', '58');
|
|
4401
|
+
arc.setAttribute('fill', 'none'); arc.setAttribute('stroke', BAND_COLOR[band]); arc.setAttribute('stroke-width', '11');
|
|
4402
|
+
arc.setAttribute('stroke-linecap', 'round');
|
|
4403
|
+
arc.setAttribute('stroke-dasharray', C.toFixed(1));
|
|
4404
|
+
arc.setAttribute('stroke-dashoffset', offset);
|
|
4405
|
+
svg.appendChild(bg);
|
|
4406
|
+
svg.appendChild(arc);
|
|
4407
|
+
var wrap = h('div', { class: 'dv-gauge' }, [h('div', { class: 'dv-gauge-num' }, [h('strong', { text: String(score) }, []), h('span', { text: 'Health' }, [])])]);
|
|
4408
|
+
wrap.insertBefore(svg, wrap.firstChild);
|
|
4409
|
+
return wrap;
|
|
4410
|
+
}
|
|
4411
|
+
|
|
4412
|
+
function renderOverview(s) {
|
|
4413
|
+
var gauge = renderGauge(s.report.score);
|
|
4414
|
+
var cats = Object.keys(s.report.categories).map(function (cat) {
|
|
4415
|
+
var c = s.report.categories[cat];
|
|
4416
|
+
var band = scoreBand(c.score);
|
|
4417
|
+
var weight = s.report.weights[cat];
|
|
4418
|
+
var name = cat === 'seo' ? 'SEO' : cat.charAt(0).toUpperCase() + cat.slice(1);
|
|
4419
|
+
return h('div', { class: 'dv-cat' }, [
|
|
4420
|
+
h('div', { class: 'dv-cat-top' }, [
|
|
4421
|
+
h('span', { text: name + (weight !== undefined ? ' (weight ' + weight + ')' : '') }, []),
|
|
4422
|
+
h('span', { style: 'color:' + BAND_COLOR[band], text: String(c.score) }, [])
|
|
4423
|
+
]),
|
|
4424
|
+
h('div', { class: 'dv-bar' }, [h('i', { style: 'width:' + c.score + '%;background:' + BAND_COLOR[band] }, [])])
|
|
4425
|
+
]);
|
|
4426
|
+
});
|
|
4427
|
+
var chips = renderFilterChips(s.report.categories);
|
|
4428
|
+
var totalCount = s.report.routes.reduce(function (n, r) { return n + r.issues.length; }, 0) + s.report.siteIssues.length;
|
|
4429
|
+
var entries = [];
|
|
4430
|
+
s.report.routes.forEach(function (r) {
|
|
4431
|
+
r.issues.forEach(function (issue) { entries.push({ issue: issue, route: r.route }); });
|
|
4432
|
+
});
|
|
4433
|
+
s.report.siteIssues.forEach(function (issue) { entries.push({ issue: issue, route: null }); });
|
|
4434
|
+
entries = entries.filter(function (e) { return passesFilter(e.issue); });
|
|
4435
|
+
entries.sort(function (a, b) {
|
|
4436
|
+
var sd = SEVERITY_ORDER[a.issue.severity] - SEVERITY_ORDER[b.issue.severity];
|
|
4437
|
+
if (sd !== 0) return sd;
|
|
4438
|
+
return (a.route || '').localeCompare(b.route || '');
|
|
4439
|
+
});
|
|
4440
|
+
var body = entries.length
|
|
4441
|
+
? entries.map(function (e) { return renderFinding(e.issue, e.route); })
|
|
4442
|
+
: [h('p', { class: 'dv-empty', text: totalCount ? 'No issues match the current filter.' : 'No issues found \u2014 nice work!' }, [])];
|
|
4443
|
+
var findings = h('section', { class: 'dv-section' }, [h('h2', { text: 'Findings' }, [])].concat(body));
|
|
4444
|
+
return h('div', { class: 'dv-overview' }, [gauge, h('div', { class: 'dv-cats' }, cats), chips, findings].filter(Boolean));
|
|
4445
|
+
}
|
|
4446
|
+
|
|
4447
|
+
function renderRouteDetail(route) {
|
|
4448
|
+
var badge = state.snapshot.badges[route.route];
|
|
4449
|
+
var band = scoreBand(route.score);
|
|
4450
|
+
var header = h('div', { class: 'dv-detail-header' }, [
|
|
4451
|
+
h('span', { class: 'dv-route-path', text: route.route }, []),
|
|
4452
|
+
badge ? h('span', { class: 'dv-badge dv-badge-' + badge, text: badge }, []) : null,
|
|
4453
|
+
h('span', { class: 'dv-score-chip', style: 'color:' + BAND_COLOR[band], text: String(route.score) }, [])
|
|
4454
|
+
].filter(Boolean));
|
|
4455
|
+
var chips = renderFilterChips(state.snapshot.report.categories);
|
|
4456
|
+
var findings = route.issues.filter(passesFilter);
|
|
4457
|
+
var body = findings.length
|
|
4458
|
+
? findings.map(function (issue) { return renderFinding(issue, undefined, route.route); })
|
|
4459
|
+
: [h('p', { class: 'dv-empty', text: 'No issues match the current filter.' }, [])];
|
|
4460
|
+
return h('div', { class: 'dv-route-detail' }, [header, chips].concat(body));
|
|
4461
|
+
}
|
|
4462
|
+
|
|
4463
|
+
function renderDetail() {
|
|
4464
|
+
var s = state.snapshot;
|
|
4465
|
+
if (state.selected === 'overview') {
|
|
4466
|
+
mount('dv-detail', renderOverview(s));
|
|
4467
|
+
return;
|
|
4468
|
+
}
|
|
4469
|
+
var route = s.report.routes.filter(function (r) { return r.route === state.selected; })[0];
|
|
4470
|
+
if (!route) {
|
|
4471
|
+
state.selected = 'overview';
|
|
4472
|
+
mount('dv-detail', renderOverview(s));
|
|
4473
|
+
return;
|
|
4474
|
+
}
|
|
4475
|
+
mount('dv-detail', renderRouteDetail(route));
|
|
4476
|
+
}
|
|
4477
|
+
|
|
4478
|
+
function renderAll() {
|
|
4479
|
+
renderTopbar();
|
|
4480
|
+
renderSidebar();
|
|
4481
|
+
renderDetail();
|
|
4482
|
+
}
|
|
4483
|
+
|
|
4484
|
+
function restoreSelectionFromHash() {
|
|
4485
|
+
var raw = location.hash.replace(/^#/, '');
|
|
4486
|
+
if (!raw || raw === 'overview') { state.selected = 'overview'; return; }
|
|
4487
|
+
var m = /^route\\/(.+)$/.exec(raw);
|
|
4488
|
+
if (m && state.routeBySlug[m[1]]) state.selected = state.routeBySlug[m[1]];
|
|
4489
|
+
}
|
|
4490
|
+
|
|
4491
|
+
function fetchSnapshot() {
|
|
4492
|
+
fetch('/__svelte-vitals/data.json').then(function (r) { return r.json(); }).then(function (data) {
|
|
4493
|
+
if (state.snapshot && data.sequence <= state.snapshot.sequence) return;
|
|
4494
|
+
state.snapshot = data;
|
|
4495
|
+
renderAll();
|
|
4496
|
+
}).catch(function () {});
|
|
4497
|
+
}
|
|
4498
|
+
|
|
4499
|
+
function boot() {
|
|
4500
|
+
var raw = document.getElementById('svelte-vitals-data');
|
|
4501
|
+
state.snapshot = JSON.parse(raw.textContent);
|
|
4502
|
+
applyTheme();
|
|
4503
|
+
renderSidebar(); // populates routeBySlug before the hash can be trusted
|
|
4504
|
+
restoreSelectionFromHash();
|
|
4505
|
+
renderAll();
|
|
4506
|
+
|
|
4507
|
+
window.addEventListener('hashchange', function () {
|
|
4508
|
+
restoreSelectionFromHash();
|
|
4509
|
+
renderSidebar();
|
|
4510
|
+
renderDetail();
|
|
4511
|
+
});
|
|
4512
|
+
|
|
4513
|
+
// Static export (the CLI's --reporter html): no dev server behind the page, so no
|
|
4514
|
+
// SSE connection, no /data.json refetch, no connection indicator.
|
|
4515
|
+
if (state.snapshot.live && typeof EventSource !== 'undefined') {
|
|
4516
|
+
var es = new EventSource('/__svelte-vitals/events');
|
|
4517
|
+
es.addEventListener('open', function () { state.connection = 'connected'; renderTopbar(); fetchSnapshot(); });
|
|
4518
|
+
es.addEventListener('update', fetchSnapshot);
|
|
4519
|
+
es.addEventListener('error', function () { state.connection = 'reconnecting'; renderTopbar(); });
|
|
4520
|
+
}
|
|
4521
|
+
}
|
|
4522
|
+
|
|
4523
|
+
boot();
|
|
2981
4524
|
})();
|
|
2982
4525
|
`;
|
|
4526
|
+
function renderAppShell(snapshot) {
|
|
4527
|
+
const safe = { ...snapshot, report: sanitizeReport(snapshot.report) };
|
|
4528
|
+
const title = snapshot.live ? "svelte-vitals dashboard" : "svelte-vitals report";
|
|
4529
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>${title}</title><style>${APP_STYLE}</style></head><body><div class="dv-app" id="dv-app"><header class="dv-topbar" id="dv-topbar"></header><nav class="dv-sidebar" id="dv-sidebar"></nav><main class="dv-detail" id="dv-detail"></main></div><script type="application/json" id="svelte-vitals-data">${embedJson(safe)}</script><script>${APP_SCRIPT}</script></body></html>`;
|
|
4530
|
+
}
|
|
2983
4531
|
function buildHtmlDocument(report, meta, opts) {
|
|
2984
|
-
|
|
4532
|
+
const badges = Object.fromEntries(
|
|
4533
|
+
Object.entries(opts?.routeBadges ?? {}).filter(([, b]) => b === "measured" || b === "static")
|
|
4534
|
+
);
|
|
4535
|
+
return renderAppShell({
|
|
4536
|
+
report,
|
|
4537
|
+
badges,
|
|
4538
|
+
analyzing: false,
|
|
4539
|
+
sequence: 0,
|
|
4540
|
+
live: false,
|
|
4541
|
+
meta
|
|
4542
|
+
});
|
|
2985
4543
|
}
|
|
2986
4544
|
function formatHtmlReport(results, config, meta) {
|
|
2987
4545
|
return buildHtmlDocument(buildJsonReport(results, config, meta), meta);
|
|
2988
4546
|
}
|
|
2989
4547
|
|
|
4548
|
+
// src/reporter/html.ts
|
|
4549
|
+
var BAND_COLOR = {
|
|
4550
|
+
good: "#2FA968",
|
|
4551
|
+
warn: "#E8A317",
|
|
4552
|
+
poor: "#E5484D"
|
|
4553
|
+
};
|
|
4554
|
+
function scoreBand(score) {
|
|
4555
|
+
return score >= 90 ? "good" : score >= 50 ? "warn" : "poor";
|
|
4556
|
+
}
|
|
4557
|
+
function escapeHtml(s) {
|
|
4558
|
+
return s.replace(
|
|
4559
|
+
/[&<>"']/g,
|
|
4560
|
+
(c) => c === "&" ? "&" : c === "<" ? "<" : c === ">" ? ">" : c === '"' ? """ : "'"
|
|
4561
|
+
);
|
|
4562
|
+
}
|
|
4563
|
+
function safeHref(url) {
|
|
4564
|
+
const normalized = url.replace(/\s/g, "").toLowerCase();
|
|
4565
|
+
return /^https?:\/\//.test(normalized) ? url : null;
|
|
4566
|
+
}
|
|
4567
|
+
|
|
2990
4568
|
// src/config-apply.ts
|
|
2991
4569
|
function selectRules(rules, config) {
|
|
2992
4570
|
return rules.filter((rule) => config.rules[rule.id] !== "off");
|
|
@@ -2998,6 +4576,8 @@ function applyRuleSeverities(results, config) {
|
|
|
2998
4576
|
});
|
|
2999
4577
|
}
|
|
3000
4578
|
export {
|
|
4579
|
+
APP_SCRIPT,
|
|
4580
|
+
APP_STYLE,
|
|
3001
4581
|
BAND_COLOR,
|
|
3002
4582
|
CHILD_NODE_KEYS,
|
|
3003
4583
|
ROBOTS_SOURCE_PATHS,
|
|
@@ -3014,6 +4594,7 @@ export {
|
|
|
3014
4594
|
buildJsonReport,
|
|
3015
4595
|
classify,
|
|
3016
4596
|
collectComponentFacts,
|
|
4597
|
+
collectKitModuleFacts,
|
|
3017
4598
|
computeHealth,
|
|
3018
4599
|
computeScore,
|
|
3019
4600
|
correct001EachKey,
|
|
@@ -3021,12 +4602,17 @@ export {
|
|
|
3021
4602
|
correct003EffectAsOnMount,
|
|
3022
4603
|
correct004UnmutatedState,
|
|
3023
4604
|
correct005PropMutation,
|
|
4605
|
+
correct006OrphanEffect,
|
|
4606
|
+
correct007OrphanLifecycle,
|
|
4607
|
+
correct008BrowserGlobals,
|
|
4608
|
+
correct009InstanceBrowserGlobals,
|
|
3024
4609
|
defaultConfig,
|
|
3025
4610
|
defaultProject,
|
|
3026
4611
|
defineConfig,
|
|
3027
4612
|
docsUrlFor,
|
|
3028
4613
|
effectiveSeverity,
|
|
3029
4614
|
emptyComponentFacts,
|
|
4615
|
+
emptyKitModuleFacts,
|
|
3030
4616
|
escapeHtml,
|
|
3031
4617
|
explainRule,
|
|
3032
4618
|
findAttr,
|
|
@@ -3045,6 +4631,7 @@ export {
|
|
|
3045
4631
|
linkRule,
|
|
3046
4632
|
noColorPalette,
|
|
3047
4633
|
parseComponentFacts,
|
|
4634
|
+
parseKitModuleFacts,
|
|
3048
4635
|
perf001ImageDimensions,
|
|
3049
4636
|
perf002ImageLoading,
|
|
3050
4637
|
perf003PreloadAs,
|
|
@@ -3055,6 +4642,8 @@ export {
|
|
|
3055
4642
|
perf008Preconnect,
|
|
3056
4643
|
perf009HeavyImport,
|
|
3057
4644
|
perf010NamespaceImport,
|
|
4645
|
+
renderAppShell,
|
|
4646
|
+
resolveRunesModuleSpecifier,
|
|
3058
4647
|
runRules,
|
|
3059
4648
|
safeHref,
|
|
3060
4649
|
scoreBand,
|
|
@@ -3062,6 +4651,9 @@ export {
|
|
|
3062
4651
|
scoresByCategory,
|
|
3063
4652
|
sec001Html,
|
|
3064
4653
|
sec002JavascriptUrl,
|
|
4654
|
+
sec003LoadStateWrite,
|
|
4655
|
+
sec004ServerModuleState,
|
|
4656
|
+
sec005SharedStateImport,
|
|
3065
4657
|
selectRules,
|
|
3066
4658
|
seo001Title,
|
|
3067
4659
|
seo002Description,
|