@svelte-vitals/core 0.28.0 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.d.ts +337 -23
  2. package/dist/index.js +910 -135
  3. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ var defaultProject = {
4
4
  hasSitemap: false,
5
5
  htmlLang: { presence: "none", value: "absent" }
6
6
  };
7
+ var CATEGORIES = ["seo", "performance", "correctness", "security", "architecture"];
7
8
  var defaultConfig = {
8
9
  treatDynamicAs: "pass",
9
10
  metaComponents: [],
@@ -17,6 +18,11 @@ function defineConfig(config = {}) {
17
18
  // src/component-parse.ts
18
19
  import { parse } from "svelte/compiler";
19
20
 
21
+ // src/base-path.ts
22
+ function isRootRelativePath(value) {
23
+ return value.startsWith("/") && !value.startsWith("//");
24
+ }
25
+
20
26
  // src/svelte-ast.ts
21
27
  var CHILD_NODE_KEYS = [
22
28
  "fragment",
@@ -245,8 +251,10 @@ function scopeIntroducedNames(node) {
245
251
  addBoundNames(node.param, introduced);
246
252
  } else if (node.type === "BlockStatement") {
247
253
  for (const stmt of node.body ?? []) {
248
- if (stmt?.type === "VariableDeclaration" && stmt.kind !== "var") {
254
+ if (stmt?.type === "VariableDeclaration") {
249
255
  for (const d of stmt.declarations ?? []) addBoundNames(d.id, introduced);
256
+ } else if ((stmt?.type === "FunctionDeclaration" || stmt?.type === "ClassDeclaration") && typeof stmt.id?.name === "string") {
257
+ introduced.add(stmt.id.name);
250
258
  }
251
259
  }
252
260
  } else if (node.type === "ForStatement" || node.type === "ForOfStatement" || node.type === "ForInStatement") {
@@ -262,6 +270,12 @@ function scopeIntroducedNames(node) {
262
270
  } else if (node.type === "AwaitBlock") {
263
271
  if (node.value) addBoundNames(node.value, introduced);
264
272
  if (node.error) addBoundNames(node.error, introduced);
273
+ } else if (node.type === "Fragment") {
274
+ for (const child of node.nodes ?? []) {
275
+ if (child?.type === "ConstTag" || child?.type === "DeclarationTag") {
276
+ for (const d of child.declaration?.declarations ?? []) addBoundNames(d.id, introduced);
277
+ }
278
+ }
265
279
  }
266
280
  return introduced;
267
281
  }
@@ -330,6 +344,81 @@ function isDeferredBody(n) {
330
344
  function isPlainStateCall(node) {
331
345
  return node?.type === "CallExpression" && node.callee?.type === "Identifier" && node.callee.name === "$state";
332
346
  }
347
+ var BUILTIN_STATE_TYPES = /* @__PURE__ */ new Set(["Map", "Set", "Date", "URL", "URLSearchParams"]);
348
+ var BUILTIN_MUTATIONS = {
349
+ Map: /* @__PURE__ */ new Set(["set", "delete", "clear"]),
350
+ Set: /* @__PURE__ */ new Set(["add", "delete", "clear"]),
351
+ Date: /* @__PURE__ */ new Set([
352
+ "setTime",
353
+ "setFullYear",
354
+ "setMonth",
355
+ "setDate",
356
+ "setHours",
357
+ "setMinutes",
358
+ "setSeconds",
359
+ "setMilliseconds",
360
+ "setYear",
361
+ "setUTCFullYear",
362
+ "setUTCMonth",
363
+ "setUTCDate",
364
+ "setUTCHours",
365
+ "setUTCMinutes",
366
+ "setUTCSeconds",
367
+ "setUTCMilliseconds"
368
+ ]),
369
+ URL: /* @__PURE__ */ new Set(),
370
+ URLSearchParams: /* @__PURE__ */ new Set(["append", "set", "delete", "sort"])
371
+ };
372
+ function collectBuiltinStateSignals(node, candidates, mutated, reassigned, shadowed = /* @__PURE__ */ new Set(), inFunction = false) {
373
+ if (Array.isArray(node)) {
374
+ for (const child of node) collectBuiltinStateSignals(child, candidates, mutated, reassigned, shadowed, inFunction);
375
+ return;
376
+ }
377
+ if (!node || typeof node !== "object" || typeof node.type !== "string") return;
378
+ const introduced = scopeIntroducedNames(node);
379
+ const scope = introduced.size > 0 ? /* @__PURE__ */ new Set([...shadowed, ...introduced]) : shadowed;
380
+ const boundary = isDeferredBody(node) || node.type === "ClassDeclaration" || node.type === "ClassExpression";
381
+ const nextInFunction = inFunction || boundary;
382
+ const hit = (name) => typeof name === "string" && candidates.has(name) && !scope.has(name) ? name : void 0;
383
+ if (node.type === "AssignmentExpression") {
384
+ if (node.left?.type === "Identifier") {
385
+ const n = hit(node.left.name);
386
+ const isBareSelfAssign = node.right?.type === "Identifier" && node.right.name === n;
387
+ if (n && !isBareSelfAssign) reassigned.add(n);
388
+ } else if (node.left?.type === "ObjectPattern" || node.left?.type === "ArrayPattern") {
389
+ const bound = /* @__PURE__ */ new Set();
390
+ addBoundNames(node.left, bound);
391
+ for (const name of bound) {
392
+ const n = hit(name);
393
+ if (n) reassigned.add(n);
394
+ }
395
+ } else if (node.left?.type === "MemberExpression" && inFunction) {
396
+ const n = hit(rootObjectName(node.left));
397
+ if (n && candidates.get(n) === "URL") mutated.add(n);
398
+ }
399
+ } else if (node.type === "UpdateExpression" && node.argument?.type === "MemberExpression" && inFunction) {
400
+ const n = hit(rootObjectName(node.argument));
401
+ if (n && candidates.get(n) === "URL") mutated.add(n);
402
+ } else if (node.type === "UnaryExpression" && node.operator === "delete" && inFunction) {
403
+ const n = hit(rootObjectName(node.argument));
404
+ if (n && candidates.get(n) === "URL") mutated.add(n);
405
+ } else if (node.type === "CallExpression" && node.callee?.type === "MemberExpression" && !node.callee.computed && inFunction) {
406
+ const method = node.callee.property?.name;
407
+ if (typeof method === "string") {
408
+ if (node.callee.object?.type === "Identifier") {
409
+ const n = hit(node.callee.object.name);
410
+ if (n && BUILTIN_MUTATIONS[candidates.get(n)]?.has(method)) mutated.add(n);
411
+ } else if (node.callee.object?.type === "MemberExpression") {
412
+ const n = hit(rootObjectName(node.callee));
413
+ if (n && candidates.get(n) === "URL" && BUILTIN_MUTATIONS.URLSearchParams.has(method)) mutated.add(n);
414
+ }
415
+ }
416
+ }
417
+ for (const key of Object.keys(node)) {
418
+ if (WALK_IGNORED_KEYS.has(key)) continue;
419
+ collectBuiltinStateSignals(node[key], candidates, mutated, reassigned, scope, nextInFunction);
420
+ }
421
+ }
333
422
  function collectPatternAliasRefs(node, names, acc, scope, ownRhs) {
334
423
  if (!node || typeof node !== "object" || typeof node.type !== "string") return;
335
424
  if (node.type === "Identifier") return;
@@ -594,6 +683,7 @@ function bodyIsEmpty(fn) {
594
683
  return false;
595
684
  }
596
685
  var URL_ATTRS = ["href", "src", "action", "formaction"];
686
+ var CHECKABLE_INPUT_TYPES = /* @__PURE__ */ new Set(["checkbox", "radio"]);
597
687
  function collectSecurityFacts(node, source, htmlTags, jsUrls) {
598
688
  if (Array.isArray(node)) {
599
689
  for (const child of node) collectSecurityFacts(child, source, htmlTags, jsUrls);
@@ -615,6 +705,59 @@ function collectSecurityFacts(node, source, htmlTags, jsUrls) {
615
705
  if (key in node) collectSecurityFacts(node[key], source, htmlTags, jsUrls);
616
706
  }
617
707
  }
708
+ function collectCheckableBindValues(node, source, acc) {
709
+ if (Array.isArray(node)) {
710
+ for (const child of node) collectCheckableBindValues(child, source, acc);
711
+ return;
712
+ }
713
+ if (!node || typeof node !== "object") return;
714
+ if (node.type === "RegularElement" && node.name === "input" && Array.isArray(node.attributes)) {
715
+ const typeAttr = findAttr(node.attributes, "type");
716
+ const typeValue = typeAttr ? attrTextOf(typeAttr) : void 0;
717
+ if (typeValue && CHECKABLE_INPUT_TYPES.has(typeValue)) {
718
+ const bindValue = node.attributes.find((a) => a?.type === "BindDirective" && a.name === "value");
719
+ if (bindValue) {
720
+ acc.push({
721
+ kind: typeValue,
722
+ line: lineOf(source, bindValue.start ?? node.start)
723
+ });
724
+ }
725
+ }
726
+ }
727
+ for (const key of CHILD_NODE_KEYS) {
728
+ if (key in node) collectCheckableBindValues(node[key], source, acc);
729
+ }
730
+ }
731
+ function collectHrefLinks(node, source, acc) {
732
+ if (Array.isArray(node)) {
733
+ for (const child of node) collectHrefLinks(child, source, acc);
734
+ return;
735
+ }
736
+ if (!node || typeof node !== "object") return;
737
+ if (node.type === "RegularElement" && node.name === "a" && Array.isArray(node.attributes)) {
738
+ const attr = findAttr(node.attributes, "href");
739
+ const value = attr ? attrTextOf(attr) : void 0;
740
+ if (value !== void 0 && isRootRelativePath(value)) {
741
+ acc.push({ kind: "href", path: value, line: lineOf(source, attr?.start ?? node.start) });
742
+ }
743
+ }
744
+ for (const key of CHILD_NODE_KEYS) {
745
+ if (key in node) collectHrefLinks(node[key], source, acc);
746
+ }
747
+ }
748
+ var GOTO_NAMES = /* @__PURE__ */ new Set(["goto"]);
749
+ function collectGotoLinks(locals, roots, source, acc) {
750
+ if (locals.size === 0) return;
751
+ for (const root of roots) {
752
+ if (!root) continue;
753
+ walkEstree(root, (n) => {
754
+ if (n.type !== "CallExpression" || n.callee?.type !== "Identifier" || !locals.has(n.callee.name)) return;
755
+ const arg = n.arguments?.[0];
756
+ if (arg?.type !== "Literal" || typeof arg.value !== "string" || !isRootRelativePath(arg.value)) return;
757
+ acc.push({ kind: "goto", path: arg.value, line: lineOf(source, n.start) });
758
+ });
759
+ }
760
+ }
618
761
  function isPropsCall(node) {
619
762
  return node?.type === "CallExpression" && node.callee?.type === "Identifier" && node.callee.name === "$props";
620
763
  }
@@ -651,6 +794,16 @@ function collectPropNames(program, includeBindable) {
651
794
  });
652
795
  return ambiguous || seen > 1 ? /* @__PURE__ */ new Set() : names;
653
796
  }
797
+ function collectLegacyPropNames(program) {
798
+ const names = /* @__PURE__ */ new Set();
799
+ for (const stmt of program.body ?? []) {
800
+ if (stmt?.type !== "ExportNamedDeclaration" || stmt.declaration?.type !== "VariableDeclaration") continue;
801
+ for (const d of stmt.declaration.declarations ?? []) {
802
+ if (d?.id?.type === "Identifier") names.add(d.id.name);
803
+ }
804
+ }
805
+ return names;
806
+ }
654
807
  var MUTATING_METHODS = /* @__PURE__ */ new Set([
655
808
  "push",
656
809
  "pop",
@@ -890,20 +1043,25 @@ var BROWSER_GLOBALS = /* @__PURE__ */ new Set([
890
1043
  "confirm",
891
1044
  "prompt"
892
1045
  ]);
893
- function collectBrowserGuardImports(program) {
1046
+ function collectNamedImportAliases(program, moduleSource, names) {
894
1047
  const out = /* @__PURE__ */ new Set();
895
1048
  for (const stmt of program.body ?? []) {
896
- if (stmt?.type !== "ImportDeclaration" || stmt.importKind === "type" || stmt.source?.value !== "$app/environment")
1049
+ if (stmt?.type !== "ImportDeclaration" || stmt.importKind === "type" || stmt.source?.value !== moduleSource) {
897
1050
  continue;
1051
+ }
898
1052
  for (const s of stmt.specifiers ?? []) {
899
1053
  if (s?.importKind === "type" || s?.local?.type !== "Identifier") continue;
900
- if (s.type === "ImportSpecifier" && s.imported?.type === "Identifier" && s.imported.name === "browser") {
1054
+ if (s.type === "ImportSpecifier" && s.imported?.type === "Identifier" && names.has(s.imported.name)) {
901
1055
  out.add(s.local.name);
902
1056
  }
903
1057
  }
904
1058
  }
905
1059
  return out;
906
1060
  }
1061
+ var BROWSER_GUARD_NAMES = /* @__PURE__ */ new Set(["browser"]);
1062
+ function collectBrowserGuardImports(program) {
1063
+ return collectNamedImportAliases(program, "$app/environment", BROWSER_GUARD_NAMES);
1064
+ }
907
1065
  function collectProgramBindings(program) {
908
1066
  const bound = /* @__PURE__ */ new Set();
909
1067
  for (const stmt of program.body ?? []) {
@@ -1081,6 +1239,14 @@ function parseModuleFacts(source, filename) {
1081
1239
  const orphanLifecycleCalls = program ? collectOrphanLifecycleCalls(program, wrapped).map((f) => ({ ...f, line: shift(f.line) })) : [];
1082
1240
  const browserGlobalRefs = program ? collectBrowserGlobalRefs(program, wrapped).map((r) => ({ ...r, line: shift(r.line), context: "module" })) : [];
1083
1241
  const moduleStateDecls = program ? collectModuleStateDecls(program, wrapped).map((d) => ({ ...d, line: shift(d.line) })) : [];
1242
+ const basePathLinks = [];
1243
+ if (program) {
1244
+ const locals = collectNamedImportAliases(program, "$app/navigation", GOTO_NAMES);
1245
+ const raw = [];
1246
+ collectGotoLinks(locals, [program], wrapped, raw);
1247
+ for (const l of raw) basePathLinks.push({ ...l, line: shift(l.line) });
1248
+ basePathLinks.sort((a, b) => a.line - b.line);
1249
+ }
1084
1250
  return {
1085
1251
  eachBlocks: [],
1086
1252
  effects: [],
@@ -1095,6 +1261,9 @@ function parseModuleFacts(source, filename) {
1095
1261
  mutatedProps: [],
1096
1262
  stalePropDerivations: [],
1097
1263
  rawableStates: [],
1264
+ nonreactiveBuiltinStates: [],
1265
+ checkableBindValues: [],
1266
+ basePathLinks,
1098
1267
  suppressions: collectSuppressions(source),
1099
1268
  orphanEffects,
1100
1269
  orphanLifecycleCalls,
@@ -1110,6 +1279,18 @@ function parseComponentFacts(source, filename) {
1110
1279
  const htmlTags = [];
1111
1280
  const javascriptUrls = [];
1112
1281
  collectSecurityFacts(ast.fragment ?? ast, source, htmlTags, javascriptUrls);
1282
+ const checkableBindValues = [];
1283
+ collectCheckableBindValues(ast.fragment ?? ast, source, checkableBindValues);
1284
+ const basePathLinks = [];
1285
+ collectHrefLinks(ast.fragment ?? ast, source, basePathLinks);
1286
+ const gotoPrograms = [ast.module?.content, ast.instance?.content].filter(Boolean);
1287
+ const gotoLocals = /* @__PURE__ */ new Set();
1288
+ for (const p of gotoPrograms)
1289
+ for (const n of collectNamedImportAliases(p, "$app/navigation", GOTO_NAMES)) {
1290
+ gotoLocals.add(n);
1291
+ }
1292
+ collectGotoLinks(gotoLocals, [...gotoPrograms, ast.fragment], source, basePathLinks);
1293
+ basePathLinks.sort((a, b) => a.line - b.line);
1113
1294
  const loc = countLines(source);
1114
1295
  const suppressions = collectSuppressions(source);
1115
1296
  const moduleProgram = ast.module?.content;
@@ -1132,16 +1313,20 @@ function parseComponentFacts(source, filename) {
1132
1313
  const mutatedProps = [];
1133
1314
  const stalePropDerivations = [];
1134
1315
  const rawableStates = [];
1316
+ const nonreactiveBuiltinStates = [];
1135
1317
  let propCount = 0;
1136
1318
  const program = ast.instance?.content;
1137
1319
  if (program) {
1138
1320
  collectImportSources(program, source, importSpans);
1139
1321
  collectNamespaceImports(program, source, namespaceImports);
1140
1322
  propCount = countProps(program);
1141
- const nonBindableProps = collectPropNames(program, false);
1142
- collectPropMutations(program, nonBindableProps, source, mutatedProps);
1143
- if (ast.fragment) collectPropMutations(ast.fragment, nonBindableProps, source, mutatedProps);
1144
- const allPropNames = collectPropNames(program, true);
1323
+ const legacyPropNames = collectLegacyPropNames(program);
1324
+ const nonBindableProps = /* @__PURE__ */ new Set([...collectPropNames(program, false), ...legacyPropNames]);
1325
+ const rawMutations = [];
1326
+ collectPropMutations(program, nonBindableProps, source, rawMutations);
1327
+ if (ast.fragment) collectPropMutations(ast.fragment, nonBindableProps, source, rawMutations);
1328
+ for (const m of rawMutations) mutatedProps.push(legacyPropNames.has(m.name) ? { ...m, legacy: true } : m);
1329
+ const allPropNames = /* @__PURE__ */ new Set([...collectPropNames(program, true), ...legacyPropNames]);
1145
1330
  if (allPropNames.size > 0) {
1146
1331
  const candidates = collectStalePropCandidates(program, allPropNames, source);
1147
1332
  if (candidates.length > 0) {
@@ -1154,8 +1339,11 @@ function parseComponentFacts(source, filename) {
1154
1339
  }
1155
1340
  const referenced = /* @__PURE__ */ new Set();
1156
1341
  if (ast.fragment) collectFragmentRefs(ast.fragment, candidateNames, referenced);
1342
+ const isLegacy = legacyPropNames.size > 0;
1157
1343
  for (const c of candidates) {
1158
- if (!disqualified.has(c.name) && referenced.has(c.name)) stalePropDerivations.push(c);
1344
+ if (!disqualified.has(c.name) && referenced.has(c.name)) {
1345
+ stalePropDerivations.push(isLegacy ? { ...c, legacy: true } : c);
1346
+ }
1159
1347
  }
1160
1348
  }
1161
1349
  }
@@ -1186,6 +1374,7 @@ function parseComponentFacts(source, filename) {
1186
1374
  if (ast.fragment) {
1187
1375
  collectStateWrites(ast.fragment, stateNames, writtenOrEscaped);
1188
1376
  collectTemplateEscapes(ast.fragment, stateNames, writtenOrEscaped);
1377
+ collectDirectiveEscapes(ast.fragment, stateNames, writtenOrEscaped);
1189
1378
  }
1190
1379
  for (const d of stateDecls) {
1191
1380
  if (!writtenOrEscaped.has(d.name)) constableStates.push(d);
@@ -1225,6 +1414,29 @@ function parseComponentFacts(source, filename) {
1225
1414
  if (reassigned && !dirty) rawableStates.push(c);
1226
1415
  }
1227
1416
  }
1417
+ const builtinCandidates = /* @__PURE__ */ new Map();
1418
+ for (const stmt of program.body ?? []) {
1419
+ if (stmt?.type !== "VariableDeclaration") continue;
1420
+ for (const d of stmt.declarations ?? []) {
1421
+ if (d?.id?.type !== "Identifier" || !d.init || !isPlainStateCall(d.init)) continue;
1422
+ const arg = unwrapTs(d.init.arguments?.[0]);
1423
+ if (arg?.type === "NewExpression" && arg.callee?.type === "Identifier" && BUILTIN_STATE_TYPES.has(arg.callee.name)) {
1424
+ builtinCandidates.set(d.id.name, { type: arg.callee.name, line: lineOf(source, d.start) });
1425
+ }
1426
+ }
1427
+ }
1428
+ if (builtinCandidates.size > 0) {
1429
+ const types = new Map([...builtinCandidates].map(([n, meta]) => [n, meta.type]));
1430
+ const mutatedBuiltins = /* @__PURE__ */ new Set();
1431
+ const reassignedBuiltins = /* @__PURE__ */ new Set();
1432
+ collectBuiltinStateSignals(program, types, mutatedBuiltins, reassignedBuiltins);
1433
+ if (ast.fragment) collectBuiltinStateSignals(ast.fragment, types, mutatedBuiltins, reassignedBuiltins);
1434
+ for (const [name, meta] of builtinCandidates) {
1435
+ if (mutatedBuiltins.has(name) && !reassignedBuiltins.has(name)) {
1436
+ nonreactiveBuiltinStates.push({ name, type: meta.type, line: meta.line });
1437
+ }
1438
+ }
1439
+ }
1228
1440
  let moduleExtra;
1229
1441
  if (moduleProgram) {
1230
1442
  const moduleBrowserImports = collectBrowserGuardImports(moduleProgram);
@@ -1252,6 +1464,9 @@ function parseComponentFacts(source, filename) {
1252
1464
  mutatedProps,
1253
1465
  stalePropDerivations,
1254
1466
  rawableStates,
1467
+ nonreactiveBuiltinStates,
1468
+ checkableBindValues,
1469
+ basePathLinks,
1255
1470
  orphanEffects,
1256
1471
  orphanLifecycleCalls,
1257
1472
  browserGlobalRefs,
@@ -1277,6 +1492,9 @@ function emptyComponentFacts(file) {
1277
1492
  mutatedProps: [],
1278
1493
  stalePropDerivations: [],
1279
1494
  rawableStates: [],
1495
+ nonreactiveBuiltinStates: [],
1496
+ checkableBindValues: [],
1497
+ basePathLinks: [],
1280
1498
  orphanEffects: [],
1281
1499
  orphanLifecycleCalls: [],
1282
1500
  browserGlobalRefs: [],
@@ -1413,6 +1631,22 @@ function collectAwaits(node, out = []) {
1413
1631
  }
1414
1632
  return out;
1415
1633
  }
1634
+ var REDIRECT_NAMES = /* @__PURE__ */ new Set(["redirect"]);
1635
+ function collectRedirectCalls(node, locals, out = []) {
1636
+ if (Array.isArray(node)) {
1637
+ for (const child of node) collectRedirectCalls(child, locals, out);
1638
+ return out;
1639
+ }
1640
+ if (!node || typeof node !== "object" || typeof node.type !== "string") return out;
1641
+ if (node.type === "CallExpression" && node.callee?.type === "Identifier" && locals.has(node.callee.name)) {
1642
+ out.push(node);
1643
+ }
1644
+ for (const key of Object.keys(node)) {
1645
+ if (WALK_IGNORED_KEYS.has(key)) continue;
1646
+ collectRedirectCalls(node[key], locals, out);
1647
+ }
1648
+ return out;
1649
+ }
1416
1650
  function isParentCall(arg) {
1417
1651
  const e = unwrapTs(arg);
1418
1652
  if (e?.type !== "CallExpression") return false;
@@ -1600,6 +1834,7 @@ function parseKitModuleFacts(source, filename) {
1600
1834
  runesModuleImports,
1601
1835
  lifecycleCalls,
1602
1836
  browserGlobalRefs,
1837
+ basePathLinks: [],
1603
1838
  suppressions
1604
1839
  };
1605
1840
  }
@@ -1724,6 +1959,15 @@ function parseKitModuleFacts(source, filename) {
1724
1959
  }
1725
1960
  });
1726
1961
  const byLine = (arr) => arr.sort((a, b) => a.line - b.line);
1962
+ const basePathLinks = [];
1963
+ const redirectLocals = collectNamedImportAliases(program, "@sveltejs/kit", REDIRECT_NAMES);
1964
+ if (redirectLocals.size > 0) {
1965
+ for (const call of collectRedirectCalls(program, redirectLocals)) {
1966
+ const arg = call.arguments?.[1];
1967
+ if (arg?.type !== "Literal" || typeof arg.value !== "string" || !isRootRelativePath(arg.value)) continue;
1968
+ basePathLinks.push({ kind: "redirect", path: arg.value, line: line(call.start) });
1969
+ }
1970
+ }
1727
1971
  return {
1728
1972
  moduleStateReassignments: byLine(moduleStateReassignments),
1729
1973
  importedStateWrites: byLine(importedStateWrites),
@@ -1731,6 +1975,7 @@ function parseKitModuleFacts(source, filename) {
1731
1975
  runesModuleImports: byLine(runesModuleImports),
1732
1976
  lifecycleCalls: byLine(lifecycleCalls),
1733
1977
  browserGlobalRefs: byLine(browserGlobalRefs),
1978
+ basePathLinks: byLine(basePathLinks),
1734
1979
  ...ssrOptOut ? { ssrDisabled: { line: Math.max(0, ssrOptOut.line - 1) } } : {},
1735
1980
  ...csrOptOut ? { csrDisabled: { line: Math.max(0, csrOptOut.line - 1) } } : {},
1736
1981
  ...waterfalls.dependentLines.length > 0 || waterfalls.independentLines.length > 0 ? { loadWaterfalls: waterfalls } : {},
@@ -1749,6 +1994,7 @@ function emptyKitModuleFacts(file, kind) {
1749
1994
  runesModuleImports: [],
1750
1995
  lifecycleCalls: [],
1751
1996
  browserGlobalRefs: [],
1997
+ basePathLinks: [],
1752
1998
  suppressions: []
1753
1999
  };
1754
2000
  }
@@ -1778,7 +2024,7 @@ async function collectKitModuleFacts(rt, cwd) {
1778
2024
  );
1779
2025
  }
1780
2026
 
1781
- // src/vite-config-parse.ts
2027
+ // src/config-object.ts
1782
2028
  function propOf(obj, name) {
1783
2029
  let found;
1784
2030
  for (const p of obj.properties) {
@@ -1833,6 +2079,8 @@ function resolveConfigObject(program) {
1833
2079
  if (!exported) return void 0;
1834
2080
  return unwrapToObjectExpression(exported, collectTopLevelBindings(program));
1835
2081
  }
2082
+
2083
+ // src/vite-config-parse.ts
1836
2084
  function findMinifyDisabled(source) {
1837
2085
  let program;
1838
2086
  let wrapped;
@@ -1853,6 +2101,79 @@ function findMinifyDisabled(source) {
1853
2101
  return { line: Math.max(0, lineOf(wrapped, minify.start) - 1) };
1854
2102
  }
1855
2103
 
2104
+ // src/svelte-config-parse.ts
2105
+ function basePathOf(kitConfig, bindings) {
2106
+ const paths = propOf(kitConfig, "paths");
2107
+ const pathsObj = paths ? unwrapToObjectExpression(paths.value, bindings) : void 0;
2108
+ if (!pathsObj) return void 0;
2109
+ const base = propOf(pathsObj, "base");
2110
+ if (!base) return void 0;
2111
+ const value = unwrapTs(base.value);
2112
+ if (value.type === "Literal") {
2113
+ return typeof value.value === "string" && value.value !== "" ? { value: value.value } : void 0;
2114
+ }
2115
+ return {};
2116
+ }
2117
+ function programOf(source, filename) {
2118
+ try {
2119
+ return parseModuleProgram(source, filename).program ?? void 0;
2120
+ } catch {
2121
+ return void 0;
2122
+ }
2123
+ }
2124
+ function findKitPathsBaseInSvelteConfig(source) {
2125
+ const program = programOf(source, "svelte.config.js");
2126
+ if (!program) return void 0;
2127
+ const config = resolveConfigObject(program);
2128
+ if (!config) return void 0;
2129
+ const bindings = collectTopLevelBindings(program);
2130
+ const kit = propOf(config, "kit");
2131
+ const kitObj = kit ? unwrapToObjectExpression(kit.value, bindings) : void 0;
2132
+ return kitObj ? basePathOf(kitObj, bindings) : void 0;
2133
+ }
2134
+ function sveltekitLocalNames(program) {
2135
+ const out = collectNamedImportAliases(program, "@sveltejs/kit/vite", /* @__PURE__ */ new Set(["sveltekit"]));
2136
+ if (out.size === 0) out.add("sveltekit");
2137
+ return out;
2138
+ }
2139
+ function findKitPathsBaseInViteConfig(source) {
2140
+ const none = { kind: "no-plugin-config" };
2141
+ const program = programOf(source, "vite.config.ts");
2142
+ if (!program) return none;
2143
+ const config = resolveConfigObject(program);
2144
+ if (!config) return none;
2145
+ const bindings = collectTopLevelBindings(program);
2146
+ const plugins = propOf(config, "plugins");
2147
+ const pluginsValue = plugins ? unwrapTs(plugins.value) : void 0;
2148
+ if (pluginsValue?.type !== "ArrayExpression") return none;
2149
+ const locals = sveltekitLocalNames(program);
2150
+ for (const el of pluginsValue.elements) {
2151
+ if (!el || el.type === "SpreadElement") continue;
2152
+ const call = unwrapTs(el);
2153
+ if (call.type !== "CallExpression") continue;
2154
+ if (call.callee.type !== "Identifier" || !locals.has(call.callee.name)) continue;
2155
+ const arg = call.arguments[0];
2156
+ if (arg === void 0) return none;
2157
+ const kitConfig = unwrapToObjectExpression(arg, bindings);
2158
+ if (!kitConfig) return { kind: "unresolvable" };
2159
+ const base = basePathOf(kitConfig, bindings);
2160
+ return base ? { kind: "resolved", base } : { kind: "resolved" };
2161
+ }
2162
+ return none;
2163
+ }
2164
+ function resolveKitPathsBase(viteConfig, svelteConfig) {
2165
+ if (viteConfig) {
2166
+ const result = findKitPathsBaseInViteConfig(viteConfig.source);
2167
+ if (result.kind === "unresolvable") return void 0;
2168
+ if (result.kind === "resolved") {
2169
+ return result.base ? { ...result.base, file: viteConfig.file } : void 0;
2170
+ }
2171
+ }
2172
+ if (!svelteConfig) return void 0;
2173
+ const base = findKitPathsBaseInSvelteConfig(svelteConfig.source);
2174
+ return base ? { ...base, file: svelteConfig.file } : void 0;
2175
+ }
2176
+
1856
2177
  // src/project-paths.ts
1857
2178
  var ROBOTS_SOURCE_PATHS = [
1858
2179
  "static/robots.txt",
@@ -1864,6 +2185,15 @@ var SITEMAP_SOURCE_PATHS = [
1864
2185
  "src/routes/sitemap.xml/+server.ts",
1865
2186
  "src/routes/sitemap.xml/+server.js"
1866
2187
  ];
2188
+ var VITE_CONFIG_FILES = [
2189
+ "vite.config.js",
2190
+ "vite.config.mjs",
2191
+ "vite.config.ts",
2192
+ "vite.config.cjs",
2193
+ "vite.config.mts",
2194
+ "vite.config.cts"
2195
+ ];
2196
+ var SVELTE_CONFIG_FILES = ["svelte.config.js", "svelte.config.ts"];
1867
2197
 
1868
2198
  // src/rule.ts
1869
2199
  function docsUrlFor(id) {
@@ -1933,7 +2263,7 @@ function detect(head, match) {
1933
2263
  return tag ? { presence: tag.presence, value: tag.value } : { presence: "none", value: "absent" };
1934
2264
  }
1935
2265
  function headTagRule(opts) {
1936
- const docsUrl7 = docsUrlFor(opts.id);
2266
+ const docsUrl8 = docsUrlFor(opts.id);
1937
2267
  return {
1938
2268
  id: opts.id,
1939
2269
  title: opts.title,
@@ -1956,7 +2286,7 @@ function headTagRule(opts) {
1956
2286
  location: head.file,
1957
2287
  message,
1958
2288
  recommendation: opts.recommendation,
1959
- docsUrl: docsUrl7,
2289
+ docsUrl: docsUrl8,
1960
2290
  // Copy per finding: opts.fix is a rule-level template shared across all
1961
2291
  // results this rule emits; a fresh object keeps findings independent.
1962
2292
  ...opts.fix ? { fix: { ...opts.fix } } : {}
@@ -2149,7 +2479,7 @@ var seoHtmlLang = {
2149
2479
 
2150
2480
  // src/rules/perf/image-rule.ts
2151
2481
  function imageRule(opts) {
2152
- const docsUrl7 = docsUrlFor(opts.id);
2482
+ const docsUrl8 = docsUrlFor(opts.id);
2153
2483
  const category = opts.category ?? "performance";
2154
2484
  return {
2155
2485
  id: opts.id,
@@ -2173,7 +2503,7 @@ function imageRule(opts) {
2173
2503
  route: route.route,
2174
2504
  message: opts.label,
2175
2505
  recommendation: opts.recommendation,
2176
- docsUrl: docsUrl7
2506
+ docsUrl: docsUrl8
2177
2507
  });
2178
2508
  continue;
2179
2509
  }
@@ -2188,7 +2518,7 @@ function imageRule(opts) {
2188
2518
  ...img.line > 0 ? { line: img.line } : {},
2189
2519
  message: `Missing ${opts.label}`,
2190
2520
  recommendation: opts.recommendation,
2191
- docsUrl: docsUrl7,
2521
+ docsUrl: docsUrl8,
2192
2522
  ...opts.fix ? { fix: { ...opts.fix } } : {}
2193
2523
  });
2194
2524
  }
@@ -2248,7 +2578,7 @@ var performanceResponsiveImage = imageRule({
2248
2578
 
2249
2579
  // src/rules/perf/link-rule.ts
2250
2580
  function linkRule(opts) {
2251
- const docsUrl7 = docsUrlFor(opts.id);
2581
+ const docsUrl8 = docsUrlFor(opts.id);
2252
2582
  return {
2253
2583
  id: opts.id,
2254
2584
  title: opts.title,
@@ -2272,7 +2602,7 @@ function linkRule(opts) {
2272
2602
  route: head.route,
2273
2603
  message: opts.label,
2274
2604
  recommendation: opts.recommendation,
2275
- docsUrl: docsUrl7
2605
+ docsUrl: docsUrl8
2276
2606
  });
2277
2607
  continue;
2278
2608
  }
@@ -2289,7 +2619,7 @@ function linkRule(opts) {
2289
2619
  location: tag.file ?? head.file,
2290
2620
  message: `Missing ${opts.label}`,
2291
2621
  recommendation: opts.recommendation,
2292
- docsUrl: docsUrl7,
2622
+ docsUrl: docsUrl8,
2293
2623
  ...opts.fix ? { fix: { ...opts.fix } } : {}
2294
2624
  });
2295
2625
  }
@@ -2436,10 +2766,197 @@ var performanceRenderBlockingScript = {
2436
2766
  }
2437
2767
  };
2438
2768
 
2769
+ // src/config-apply.ts
2770
+ function settingSeverity(setting) {
2771
+ if (setting === void 0) return void 0;
2772
+ if (typeof setting === "string") return setting;
2773
+ return setting.severity;
2774
+ }
2775
+ function settingOptions(setting) {
2776
+ return setting !== void 0 && typeof setting !== "string" ? setting.options : void 0;
2777
+ }
2778
+ function selectRules(rules, config) {
2779
+ return rules.filter((rule) => settingSeverity(config.rules[rule.id]) !== "off");
2780
+ }
2781
+ function applyRuleSeverities(results, config) {
2782
+ return results.map((result) => {
2783
+ const severity = settingSeverity(config.rules[result.id]);
2784
+ return severity !== void 0 && severity !== "off" ? { ...result, severity } : result;
2785
+ });
2786
+ }
2787
+ function routeGlobToRegExp(pattern) {
2788
+ const body = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "\0").replace(/\*/g, "[^/]*").split("\0").join(".*");
2789
+ const source = body.endsWith("/.*") ? `${body.slice(0, -3)}(/.*)?` : body;
2790
+ return new RegExp(`^${source}$`);
2791
+ }
2792
+ function toPatterns(globs) {
2793
+ if (globs === void 0) return [];
2794
+ return (Array.isArray(globs) ? globs : [globs]).map(routeGlobToRegExp);
2795
+ }
2796
+ function compileOverrides(config) {
2797
+ return (config.overrides ?? []).map((o) => ({
2798
+ routes: toPatterns(o.route),
2799
+ files: toPatterns(o.files),
2800
+ rules: o.rules
2801
+ }));
2802
+ }
2803
+ function overrideMatches(o, target) {
2804
+ const { route, file } = target;
2805
+ return route !== void 0 && o.routes.some((p) => p.test(route)) || file !== void 0 && o.files.some((p) => p.test(file));
2806
+ }
2807
+ function applyOverrides(results, config) {
2808
+ const compiled = compileOverrides(config);
2809
+ if (compiled.length === 0) return results;
2810
+ const out = [];
2811
+ for (const result of results) {
2812
+ let severity;
2813
+ for (const o of compiled) {
2814
+ if (!overrideMatches(o, { route: result.route, file: result.location })) continue;
2815
+ const sev = settingSeverity(o.rules[result.id]) ?? settingSeverity(o.rules[result.category ?? "seo"]);
2816
+ if (sev !== void 0) severity = sev;
2817
+ }
2818
+ if (severity === void 0) out.push(result);
2819
+ else if (severity !== "off") out.push({ ...result, severity });
2820
+ }
2821
+ return out;
2822
+ }
2823
+
2824
+ // src/rule-options.ts
2825
+ var RULE_SETTING_VALUES = ["off", "critical", "warning", "info"];
2826
+ function defaultsOf(spec) {
2827
+ const out = {};
2828
+ for (const [key, s] of Object.entries(spec)) {
2829
+ out[key] = s.kind === "integer" ? s.default : s.kind === "string-list" ? [...s.default] : { ...s.default };
2830
+ }
2831
+ return out;
2832
+ }
2833
+ function intOption(options, key, fallback = 0) {
2834
+ const v = options[key];
2835
+ return typeof v === "number" ? v : fallback;
2836
+ }
2837
+ function listOption(options, key) {
2838
+ const v = options[key];
2839
+ return Array.isArray(v) ? v : [];
2840
+ }
2841
+ function mapOption(options, key) {
2842
+ const v = options[key];
2843
+ return typeof v === "object" && v !== null && !Array.isArray(v) ? v : {};
2844
+ }
2845
+ function resolveRuleOptions(ruleId, spec, config, target, compiled) {
2846
+ if (!spec) return {};
2847
+ const out = defaultsOf(spec);
2848
+ const layers = [settingOptions(config.rules[ruleId])];
2849
+ if (target) {
2850
+ for (const o of compiled ?? compileOverrides(config)) {
2851
+ if (overrideMatches(o, target)) layers.push(settingOptions(o.rules[ruleId]));
2852
+ }
2853
+ }
2854
+ for (const layer of layers) {
2855
+ if (!layer) continue;
2856
+ for (const [key, value] of Object.entries(layer)) {
2857
+ const s = spec[key];
2858
+ if (!s) continue;
2859
+ if (s.kind === "integer") out[key] = value;
2860
+ else if (s.kind === "string-list") out[key] = [...out[key], ...value];
2861
+ else out[key] = { ...out[key], ...value };
2862
+ }
2863
+ }
2864
+ return out;
2865
+ }
2866
+ function validateRuleOptions(ruleId, spec, options, baseline, skipRangeCheck) {
2867
+ if (!spec) return Object.keys(options).length === 0 ? [] : [`${ruleId} takes no options.`];
2868
+ const errors = [];
2869
+ const badKeys = /* @__PURE__ */ new Set();
2870
+ const isNonEmptyString = (v) => typeof v === "string" && v.length > 0;
2871
+ for (const [key, value] of Object.entries(options)) {
2872
+ const s = spec[key];
2873
+ if (!s) {
2874
+ errors.push(`${ruleId}: unknown option '${key}'. Known options: ${Object.keys(spec).join(", ")}.`);
2875
+ continue;
2876
+ }
2877
+ if (s.kind === "integer") {
2878
+ if (typeof value !== "number" || !Number.isInteger(value)) {
2879
+ errors.push(`${ruleId}.${key} must be an integer.`);
2880
+ badKeys.add(key);
2881
+ } else if (s.min !== void 0 && value < s.min) {
2882
+ errors.push(`${ruleId}.${key} must be >= ${s.min}.`);
2883
+ badKeys.add(key);
2884
+ } else if (s.max !== void 0 && value > s.max) {
2885
+ errors.push(`${ruleId}.${key} must be <= ${s.max}.`);
2886
+ badKeys.add(key);
2887
+ }
2888
+ } else if (s.kind === "string-list") {
2889
+ if (!Array.isArray(value) || !value.every(isNonEmptyString)) {
2890
+ errors.push(`${ruleId}.${key} must be an array of non-empty strings.`);
2891
+ }
2892
+ } else if (typeof value !== "object" || value === null || Array.isArray(value) || !Object.values(value).every(isNonEmptyString)) {
2893
+ errors.push(`${ruleId}.${key} must be an object of string \u2192 non-empty string.`);
2894
+ }
2895
+ }
2896
+ const minSpec = spec.min;
2897
+ const maxSpec = spec.max;
2898
+ if (minSpec?.kind === "integer" && maxSpec?.kind === "integer" && !badKeys.has("min") && !badKeys.has("max") && !skipRangeCheck) {
2899
+ const base = baseline ?? defaultsOf(spec);
2900
+ const minVal = "min" in options ? options.min : base.min;
2901
+ const maxVal = "max" in options ? options.max : base.max;
2902
+ if (typeof minVal === "number" && typeof maxVal === "number" && minVal > maxVal) {
2903
+ errors.push(`${ruleId}: min (${minVal}) must be <= max (${maxVal}).`);
2904
+ }
2905
+ }
2906
+ return errors;
2907
+ }
2908
+ function isPlainObject(value) {
2909
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2910
+ }
2911
+ function otherOverrideNarrowsOppositeSide(overrides, selfIndex, key, side) {
2912
+ return overrides.some((entry, i) => {
2913
+ if (i === selfIndex || !isPlainObject(entry) || !isPlainObject(entry.rules)) return false;
2914
+ const setting = entry.rules[key];
2915
+ return isPlainObject(setting) && isPlainObject(setting.options) && side in setting.options;
2916
+ });
2917
+ }
2918
+ function shouldSkipRangeCheck(overrides, selfIndex, key, setting) {
2919
+ if (!isPlainObject(setting) || !isPlainObject(setting.options)) return false;
2920
+ const setsMin = "min" in setting.options;
2921
+ const setsMax = "max" in setting.options;
2922
+ if (setsMin === setsMax) return false;
2923
+ return otherOverrideNarrowsOppositeSide(overrides, selfIndex, key, setsMin ? "max" : "min");
2924
+ }
2925
+ function validateRuleSetting(label, ruleId, setting, spec, opts) {
2926
+ const expected = RULE_SETTING_VALUES.join("|");
2927
+ if (typeof setting === "string") {
2928
+ return RULE_SETTING_VALUES.includes(setting) ? [] : [`${label}: invalid setting '${setting}'; expected ${expected}.`];
2929
+ }
2930
+ if (!isPlainObject(setting)) {
2931
+ return [`${label}: must be ${expected} or an object with 'severity' and/or 'options'.`];
2932
+ }
2933
+ const errors = [];
2934
+ const unknownKeys = Object.keys(setting).filter((k) => k !== "severity" && k !== "options");
2935
+ if (unknownKeys.length > 0) {
2936
+ errors.push(`${label}: unknown key(s) ${unknownKeys.join(", ")}; expected severity, options.`);
2937
+ }
2938
+ if (setting.severity !== void 0 && !RULE_SETTING_VALUES.includes(setting.severity)) {
2939
+ errors.push(`${label}.severity: invalid setting '${String(setting.severity)}'; expected ${expected}.`);
2940
+ }
2941
+ if (setting.options === void 0) return errors;
2942
+ if (!opts.allowOptions) {
2943
+ errors.push(`${label}: options are not allowed on a category key.`);
2944
+ return errors;
2945
+ }
2946
+ if (!isPlainObject(setting.options)) {
2947
+ errors.push(`${label}.options: must be an object.`);
2948
+ return errors;
2949
+ }
2950
+ const optionErrors = validateRuleOptions(ruleId, spec, setting.options, opts.baseline, opts.skipRangeCheck);
2951
+ if (optionErrors.length > 0) errors.push(`${label}: ${optionErrors.join(" ")}`);
2952
+ return errors;
2953
+ }
2954
+
2439
2955
  // src/rules/perf/preconnect.ts
2440
2956
  var docsUrl3 = docsUrlFor("performance/preconnect");
2441
2957
  var recommendation3 = 'Add <link rel="preconnect"> (or dns-prefetch) for the third-party origin so the connection is set up early.';
2442
2958
  var THIRD_PARTY_ORIGINS = /* @__PURE__ */ new Set(["fonts.googleapis.com", "fonts.gstatic.com"]);
2959
+ var OPTIONS = { origins: { kind: "string-list", default: [...THIRD_PARTY_ORIGINS] } };
2443
2960
  function hostOf(href) {
2444
2961
  const m = /^(?:https?:)?\/\/([^/?#]+)/i.exec(href);
2445
2962
  return m ? m[1].toLowerCase() : void 0;
@@ -2456,17 +2973,30 @@ var performancePreconnect = {
2456
2973
  snippet: '<link rel="preconnect" href="https://fonts.googleapis.com" />',
2457
2974
  lang: "html"
2458
2975
  },
2976
+ options: OPTIONS,
2459
2977
  async check(ctx) {
2460
2978
  const out = [];
2979
+ const compiled = compileOverrides(ctx.config);
2461
2980
  for (const head of ctx.heads) {
2462
2981
  const referenced = /* @__PURE__ */ new Map();
2463
2982
  const covered = /* @__PURE__ */ new Set();
2464
2983
  for (const tag of head.tags) {
2465
2984
  if (tag.kind !== "link" && tag.kind !== "script" || typeof tag.href !== "string") continue;
2466
2985
  const host = hostOf(tag.href);
2467
- if (!host || !THIRD_PARTY_ORIGINS.has(host)) continue;
2468
- if (tag.kind === "link" && (tag.rel === "preconnect" || tag.rel === "dns-prefetch")) covered.add(host);
2469
- else if (!referenced.has(host)) referenced.set(host, tag.file);
2986
+ if (!host) continue;
2987
+ if (tag.kind === "link" && (tag.rel === "preconnect" || tag.rel === "dns-prefetch")) {
2988
+ covered.add(host);
2989
+ continue;
2990
+ }
2991
+ const o = resolveRuleOptions(
2992
+ "performance/preconnect",
2993
+ OPTIONS,
2994
+ ctx.config,
2995
+ { route: head.route, file: tag.file ?? head.file },
2996
+ compiled
2997
+ );
2998
+ if (!listOption(o, "origins").includes(host)) continue;
2999
+ if (!referenced.has(host)) referenced.set(host, tag.file);
2470
3000
  }
2471
3001
  if (referenced.size === 0) continue;
2472
3002
  const missing = [...referenced].filter(([host]) => !covered.has(host));
@@ -2517,7 +3047,7 @@ var seoIndexability = {
2517
3047
  rationale: "A noindex directive removes the page from search results; an accidental noindex on a public route silently deindexes it.",
2518
3048
  fix: FIX5,
2519
3049
  async check(ctx) {
2520
- const docsUrl7 = docsUrlFor("seo/indexability");
3050
+ const docsUrl8 = docsUrlFor("seo/indexability");
2521
3051
  const out = [];
2522
3052
  for (const head of ctx.heads) {
2523
3053
  const noindexed = head.tags.some((t) => t.kind === "meta" && t.name === "robots" && t.noindex === true);
@@ -2532,7 +3062,7 @@ var seoIndexability = {
2532
3062
  location: head.file,
2533
3063
  message: "Route is noindex \u2014 verify this is intentional",
2534
3064
  recommendation: 'If this route should be indexed, remove noindex from its <meta name="robots">.',
2535
- docsUrl: docsUrl7,
3065
+ docsUrl: docsUrl8,
2536
3066
  fix: { ...FIX5 }
2537
3067
  });
2538
3068
  }
@@ -2783,7 +3313,7 @@ function jsonldTags(head) {
2783
3313
  return head.tags.filter((t) => t.kind === "jsonld" && typeof t.jsonld === "string");
2784
3314
  }
2785
3315
  function jsonldRule(opts) {
2786
- const docsUrl7 = docsUrlFor(opts.id);
3316
+ const docsUrl8 = docsUrlFor(opts.id);
2787
3317
  return {
2788
3318
  id: opts.id,
2789
3319
  title: opts.title,
@@ -2811,7 +3341,7 @@ function jsonldRule(opts) {
2811
3341
  location: head.file,
2812
3342
  message: problem,
2813
3343
  recommendation: opts.recommendation,
2814
- docsUrl: docsUrl7,
3344
+ docsUrl: docsUrl8,
2815
3345
  ...opts.fix ? { fix: { ...opts.fix } } : {}
2816
3346
  } : {
2817
3347
  id: opts.id,
@@ -2821,7 +3351,7 @@ function jsonldRule(opts) {
2821
3351
  route: head.route,
2822
3352
  message: opts.label,
2823
3353
  recommendation: opts.recommendation,
2824
- docsUrl: docsUrl7
3354
+ docsUrl: docsUrl8
2825
3355
  }
2826
3356
  );
2827
3357
  }
@@ -2845,7 +3375,7 @@ var seoJsonLdValidity = {
2845
3375
  lang: "svelte"
2846
3376
  },
2847
3377
  async check(ctx) {
2848
- const docsUrl7 = docsUrlFor("seo/json-ld-validity");
3378
+ const docsUrl8 = docsUrlFor("seo/json-ld-validity");
2849
3379
  const out = [];
2850
3380
  for (const head of ctx.heads) {
2851
3381
  for (const tag of jsonldTags(head)) {
@@ -2864,7 +3394,7 @@ var seoJsonLdValidity = {
2864
3394
  location: head.file,
2865
3395
  message: problem,
2866
3396
  recommendation: "Make the JSON-LD valid JSON with both @context and @type.",
2867
- docsUrl: docsUrl7,
3397
+ docsUrl: docsUrl8,
2868
3398
  fix: { ...seoJsonLdValidity.fix }
2869
3399
  } : {
2870
3400
  id: "seo/json-ld-validity",
@@ -2874,7 +3404,7 @@ var seoJsonLdValidity = {
2874
3404
  route: head.route,
2875
3405
  message: "JSON-LD validity",
2876
3406
  recommendation: "Make the JSON-LD valid JSON with both @context and @type.",
2877
- docsUrl: docsUrl7
3407
+ docsUrl: docsUrl8
2878
3408
  }
2879
3409
  );
2880
3410
  }
@@ -2985,7 +3515,11 @@ function visibleLength(s) {
2985
3515
 
2986
3516
  // src/rules/seo/length-rule.ts
2987
3517
  function lengthRule(opts) {
2988
- const docsUrl7 = docsUrlFor(opts.id);
3518
+ const docsUrl8 = docsUrlFor(opts.id);
3519
+ const spec = {
3520
+ min: { kind: "integer", default: opts.min, min: 0 },
3521
+ max: { kind: "integer", default: opts.max, min: 1 }
3522
+ };
2989
3523
  return {
2990
3524
  id: opts.id,
2991
3525
  title: opts.title,
@@ -2993,15 +3527,22 @@ function lengthRule(opts) {
2993
3527
  severity: "info",
2994
3528
  scope: "route",
2995
3529
  rationale: opts.rationale,
3530
+ options: spec,
2996
3531
  async check(ctx) {
2997
3532
  const out = [];
3533
+ const compiled = compileOverrides(ctx.config);
2998
3534
  for (const head of ctx.heads) {
2999
3535
  const tag = head.tags.find(opts.match);
3000
3536
  if (!tag || typeof tag.text !== "string") continue;
3537
+ const location = tag.file ?? head.file;
3538
+ const o = resolveRuleOptions(opts.id, spec, ctx.config, { route: head.route, file: location }, compiled);
3539
+ const min = intOption(o, "min", opts.min);
3540
+ const max = intOption(o, "max", opts.max);
3541
+ const recommendation8 = typeof opts.recommendation === "function" ? opts.recommendation(o) : opts.recommendation;
3001
3542
  const len = visibleLength(tag.text);
3002
3543
  let problem;
3003
- if (len < opts.min) problem = `${opts.noun} is too short (${len} chars; aim for ${opts.min}\u2013${opts.max})`;
3004
- else if (len > opts.max) problem = `${opts.noun} is too long (${len} chars; aim for ${opts.min}\u2013${opts.max})`;
3544
+ if (len < min) problem = `${opts.noun} is too short (${len} chars; aim for ${min}\u2013${max})`;
3545
+ else if (len > max) problem = `${opts.noun} is too long (${len} chars; aim for ${min}\u2013${max})`;
3005
3546
  out.push(
3006
3547
  problem ? {
3007
3548
  id: opts.id,
@@ -3009,10 +3550,10 @@ function lengthRule(opts) {
3009
3550
  severity: "info",
3010
3551
  detection: PENALIZED,
3011
3552
  route: head.route,
3012
- location: tag.file ?? head.file,
3553
+ location,
3013
3554
  message: problem,
3014
- recommendation: opts.recommendation,
3015
- docsUrl: docsUrl7
3555
+ recommendation: recommendation8,
3556
+ docsUrl: docsUrl8
3016
3557
  } : {
3017
3558
  id: opts.id,
3018
3559
  category: "seo",
@@ -3020,8 +3561,8 @@ function lengthRule(opts) {
3020
3561
  detection: PASS,
3021
3562
  route: head.route,
3022
3563
  message: opts.label,
3023
- recommendation: opts.recommendation,
3024
- docsUrl: docsUrl7
3564
+ recommendation: recommendation8,
3565
+ docsUrl: docsUrl8
3025
3566
  }
3026
3567
  );
3027
3568
  }
@@ -3031,28 +3572,32 @@ function lengthRule(opts) {
3031
3572
  }
3032
3573
 
3033
3574
  // src/rules/seo/title-length.ts
3575
+ var MIN = 30;
3576
+ var MAX = 60;
3034
3577
  var seoTitleLength = lengthRule({
3035
3578
  id: "seo/title-length",
3036
3579
  title: "Title length",
3037
3580
  label: "Title length",
3038
3581
  noun: "Title",
3039
3582
  match: (t) => t.kind === "title",
3040
- min: 30,
3041
- max: 60,
3042
- recommendation: "Aim for a title of 30\u201360 characters so it is not truncated in search results.",
3583
+ min: MIN,
3584
+ max: MAX,
3585
+ recommendation: (o) => `Aim for a title of ${intOption(o, "min", MIN)}\u2013${intOption(o, "max", MAX)} characters so it is not truncated in search results.`,
3043
3586
  rationale: "A title that is too short wastes the strongest on-page signal; one that is too long is truncated in the SERP."
3044
3587
  });
3045
3588
 
3046
3589
  // src/rules/seo/description-length.ts
3590
+ var MIN2 = 70;
3591
+ var MAX2 = 160;
3047
3592
  var seoDescriptionLength = lengthRule({
3048
3593
  id: "seo/description-length",
3049
3594
  title: "Description length",
3050
3595
  label: "Description length",
3051
3596
  noun: "Description",
3052
3597
  match: (t) => t.kind === "meta" && t.name === "description",
3053
- min: 70,
3054
- max: 160,
3055
- recommendation: "Aim for a meta description of 70\u2013160 characters so it is not truncated in search results.",
3598
+ min: MIN2,
3599
+ max: MAX2,
3600
+ recommendation: (o) => `Aim for a meta description of ${intOption(o, "min", MIN2)}\u2013${intOption(o, "max", MAX2)} characters so it is not truncated in search results.`,
3056
3601
  rationale: "A description that is too short under-uses the SERP snippet; one that is too long is truncated by search engines."
3057
3602
  });
3058
3603
 
@@ -3202,7 +3747,7 @@ var seoSingleH1 = {
3202
3747
 
3203
3748
  // src/rules/seo/uniqueness-rule.ts
3204
3749
  function uniquenessRule(opts) {
3205
- const docsUrl7 = docsUrlFor(opts.id);
3750
+ const docsUrl8 = docsUrlFor(opts.id);
3206
3751
  return {
3207
3752
  id: opts.id,
3208
3753
  title: opts.title,
@@ -3232,7 +3777,7 @@ function uniquenessRule(opts) {
3232
3777
  location: e.file,
3233
3778
  message: `${opts.noun} is duplicated across ${n} routes`,
3234
3779
  recommendation: opts.recommendation,
3235
- docsUrl: docsUrl7
3780
+ docsUrl: docsUrl8
3236
3781
  } : {
3237
3782
  id: opts.id,
3238
3783
  category: "seo",
@@ -3241,7 +3786,7 @@ function uniquenessRule(opts) {
3241
3786
  route: e.route,
3242
3787
  message: opts.label,
3243
3788
  recommendation: opts.recommendation,
3244
- docsUrl: docsUrl7
3789
+ docsUrl: docsUrl8
3245
3790
  };
3246
3791
  });
3247
3792
  }
@@ -3329,7 +3874,7 @@ function isSuppressed(m, ruleId, line) {
3329
3874
  return (m.suppressions ?? []).some((s) => s.line === line && (!s.ruleIds || s.ruleIds.includes(ruleId)));
3330
3875
  }
3331
3876
  function kitModuleRule(opts) {
3332
- const docsUrl7 = docsUrlFor(opts.id);
3877
+ const docsUrl8 = docsUrlFor(opts.id);
3333
3878
  const severity = opts.severity ?? "warning";
3334
3879
  return {
3335
3880
  id: opts.id,
@@ -3353,7 +3898,7 @@ function kitModuleRule(opts) {
3353
3898
  route: m.file,
3354
3899
  message: opts.label,
3355
3900
  recommendation: opts.recommendation,
3356
- docsUrl: docsUrl7
3901
+ docsUrl: docsUrl8
3357
3902
  });
3358
3903
  continue;
3359
3904
  }
@@ -3368,7 +3913,7 @@ function kitModuleRule(opts) {
3368
3913
  ...b.line > 0 ? { line: b.line } : {},
3369
3914
  message: b.message,
3370
3915
  recommendation: opts.recommendation,
3371
- docsUrl: docsUrl7,
3916
+ docsUrl: docsUrl8,
3372
3917
  ...opts.fix ? { fix: { ...opts.fix } } : {}
3373
3918
  });
3374
3919
  }
@@ -3404,7 +3949,7 @@ function isSuppressed2(c, ruleId, line) {
3404
3949
  return (c.suppressions ?? []).some((s) => s.line === line && (!s.ruleIds || s.ruleIds.includes(ruleId)));
3405
3950
  }
3406
3951
  function componentRule(opts) {
3407
- const docsUrl7 = docsUrlFor(opts.id);
3952
+ const docsUrl8 = docsUrlFor(opts.id);
3408
3953
  const severity = opts.severity ?? "warning";
3409
3954
  return {
3410
3955
  id: opts.id,
@@ -3414,11 +3959,15 @@ function componentRule(opts) {
3414
3959
  scope: "component",
3415
3960
  rationale: opts.rationale,
3416
3961
  ...opts.fix ? { fix: opts.fix } : {},
3962
+ ...opts.options ? { options: opts.options } : {},
3417
3963
  async check(ctx) {
3418
3964
  const out = [];
3965
+ const compiled = compileOverrides(ctx.config);
3419
3966
  for (const c of ctx.components ?? []) {
3420
- if (!opts.applies(c)) continue;
3421
- const bad = opts.bad(c).filter((b) => !(b.line > 0 && isSuppressed2(c, opts.id, b.line)));
3967
+ const o = resolveRuleOptions(opts.id, opts.options, ctx.config, { route: c.file, file: c.file }, compiled);
3968
+ const recommendation8 = typeof opts.recommendation === "function" ? opts.recommendation(o) : opts.recommendation;
3969
+ if (!opts.applies(c, o)) continue;
3970
+ const bad = opts.bad(c, o).filter((b) => !(b.line > 0 && isSuppressed2(c, opts.id, b.line)));
3422
3971
  if (bad.length === 0) {
3423
3972
  out.push({
3424
3973
  id: opts.id,
@@ -3427,8 +3976,8 @@ function componentRule(opts) {
3427
3976
  detection: PASS3,
3428
3977
  route: c.file,
3429
3978
  message: opts.label,
3430
- recommendation: opts.recommendation,
3431
- docsUrl: docsUrl7
3979
+ recommendation: recommendation8,
3980
+ docsUrl: docsUrl8
3432
3981
  });
3433
3982
  continue;
3434
3983
  }
@@ -3442,8 +3991,8 @@ function componentRule(opts) {
3442
3991
  location: c.file,
3443
3992
  ...b.line > 0 ? { line: b.line } : {},
3444
3993
  message: b.message,
3445
- recommendation: opts.recommendation,
3446
- docsUrl: docsUrl7,
3994
+ recommendation: recommendation8,
3995
+ docsUrl: docsUrl8,
3447
3996
  ...opts.fix ? { fix: { ...opts.fix } } : {}
3448
3997
  });
3449
3998
  }
@@ -3526,12 +4075,12 @@ var correctnessPropMutation = componentRule({
3526
4075
  title: "Mutated non-bindable prop",
3527
4076
  category: "correctness",
3528
4077
  label: "Prop mutation",
3529
- recommendation: "Clone the value before mutating it, communicate the change via a callback prop, or declare the prop $bindable if the parent and child should share it.",
3530
- rationale: "Svelte's docs say plainly: don't mutate props unless they are $bindable. A plain-object prop mutation is a silent no-op (the object isn't a state proxy); a reactive-state-proxy prop mutation works but triggers the ownership_invalid_mutation dev warning only when that code path actually runs. Neither is caught by the compiler, so this rule catches both statically.",
4078
+ recommendation: "Runes mode: clone the value before mutating it, communicate the change via a callback prop, or declare the prop $bindable if the parent and child should share it. Legacy mode: reassign the prop after mutating it (e.g. `list = list`) so Svelte's assignment-based reactivity picks up the change.",
4079
+ rationale: "Svelte's docs say plainly: don't mutate props unless they are $bindable. A plain-object prop mutation is a silent no-op (the object isn't a state proxy); a reactive-state-proxy prop mutation works but triggers the ownership_invalid_mutation dev warning only when that code path actually runs. In legacy mode, mutating methods like .push()/.splice() never trigger an update on their own \u2014 Svelte's reactivity there is based on assignments, not mutations. Neither case is caught by the compiler, so this rule catches both statically.",
3531
4080
  applies: (c) => c.mutatedProps.length > 0,
3532
4081
  bad: (c) => c.mutatedProps.map((m) => ({
3533
4082
  line: m.line,
3534
- message: `Prop "${m.name}" is mutated, but it is not declared $bindable`
4083
+ message: m.legacy ? `Prop "${m.name}" is mutated directly \u2014 Svelte's legacy-mode reactivity is assignment-based, so this alone will not update the UI. Reassign it after mutating (e.g. "${m.name} = ${m.name}").` : `Prop "${m.name}" is mutated, but it is not declared $bindable`
3535
4084
  }))
3536
4085
  });
3537
4086
 
@@ -3542,15 +4091,53 @@ var correctnessStalePropDerivation = componentRule({
3542
4091
  category: "correctness",
3543
4092
  severity: "warning",
3544
4093
  label: "Props derived reactively",
3545
- recommendation: "Wrap the computation in $derived(...), or $derived.by(() => ...) when it needs a function body.",
3546
- rationale: "Svelte's guidance is to treat props as though they will change: a plain `let color = type === 'danger' ? 'red' : 'green'` freezes the first render's value, so the UI silently stops tracking the parent when the prop changes. $derived keeps the computation live at no cost.",
4094
+ recommendation: "Wrap the computation in $derived(...) (or $derived.by(() => ...) for a function body) in runes-mode components; prefix the assignment with $: in legacy-mode components.",
4095
+ rationale: "Svelte's guidance is to treat props as though they will change: a plain `let color = type === 'danger' ? 'red' : 'green'` freezes the first render's value, so the UI silently stops tracking the parent when the prop changes. In runes mode, $derived keeps the computation live at no cost; in legacy mode (export let props), a $: reactive statement does the same job.",
3547
4096
  fix: {
3548
- description: "Wrap the prop-derived computation in $derived(...) (or $derived.by(() => ...) for a function body), keeping the same expression."
4097
+ description: "Wrap the prop-derived computation in $derived(...) (or $derived.by(() => ...) for a function body) in runes mode, or prefix the assignment with $: in legacy mode, keeping the same expression."
3549
4098
  },
3550
4099
  applies: (c) => c.stalePropDerivations.length > 0,
3551
4100
  bad: (c) => c.stalePropDerivations.map((s) => ({
3552
4101
  line: s.line,
3553
- message: `"${s.name}" is computed from a prop once, at initialization \u2014 it will not update when the prop changes. Wrap it in $derived.`
4102
+ message: s.legacy ? `"${s.name}" is computed from a prop once, at initialization \u2014 it will not update when the prop changes. Prefix the assignment with $: to make it a reactive statement.` : `"${s.name}" is computed from a prop once, at initialization \u2014 it will not update when the prop changes. Wrap it in $derived.`
4103
+ }))
4104
+ });
4105
+
4106
+ // src/rules/correctness/nonreactive-builtin-state.ts
4107
+ var correctnessNonreactiveBuiltinState = componentRule({
4108
+ id: "correctness/nonreactive-builtin-state",
4109
+ title: "Non-reactive built-in in $state",
4110
+ category: "correctness",
4111
+ severity: "warning",
4112
+ label: "Reactive collections in $state",
4113
+ recommendation: "Import the reactive equivalent from 'svelte/reactivity' (SvelteMap, SvelteSet, SvelteDate, SvelteURL, SvelteURLSearchParams) and construct that instead.",
4114
+ rationale: "$state deep-proxies plain objects and arrays only; built-in collection, date, and URL instances stay untracked, so property-level changes never reach effects, deriveds, or the template. Svelte's own answer is the drop-in classes in svelte/reactivity.",
4115
+ fix: {
4116
+ description: "Import Svelte<Type> from 'svelte/reactivity' and replace new <Type>(...) with new Svelte<Type>(...) \u2014 the API is identical."
4117
+ },
4118
+ applies: (c) => c.nonreactiveBuiltinStates.length > 0,
4119
+ bad: (c) => c.nonreactiveBuiltinStates.map((s) => ({
4120
+ line: s.line,
4121
+ message: `"${s.name}" is a plain ${s.type} in $state \u2014 its mutations are not tracked, so the UI silently stops updating when it changes. Use Svelte${s.type} from 'svelte/reactivity'.`
4122
+ }))
4123
+ });
4124
+
4125
+ // src/rules/correctness/checkable-bind-value.ts
4126
+ var correctnessCheckableBindValue = componentRule({
4127
+ id: "correctness/checkable-bind-value",
4128
+ title: "bind:value on a checkable input",
4129
+ category: "correctness",
4130
+ severity: "warning",
4131
+ label: "bind:checked / bind:group on checkable inputs",
4132
+ recommendation: "Replace bind:value with bind:checked (single checkbox) or bind:group (checkbox list / radio group).",
4133
+ rationale: "bind:value binds the DOM value property. A checkbox/radio's user interaction toggles checkedness, which bind:value never observes \u2014 the bound state is frozen at its initial value. Svelte's checked/grouped bindings (bind:checked, bind:group) are built for exactly this.",
4134
+ fix: {
4135
+ description: "For a single checkbox, replace bind:value={x} with bind:checked={x} (x becomes a boolean). For a checkbox list or radio group, replace bind:value={x} with bind:group={x} on every input sharing the group, keeping each input's static value attribute to identify the option."
4136
+ },
4137
+ applies: (c) => c.checkableBindValues.length > 0,
4138
+ bad: (c) => c.checkableBindValues.map((v) => ({
4139
+ line: v.line,
4140
+ message: v.kind === "checkbox" ? "bind:value on a checkbox does not track its checked state \u2014 the bound value silently never updates when the user toggles it. Use bind:checked (single checkbox) or bind:group (checkbox list) instead." : "bind:value on a radio input does not track which option is selected \u2014 the bound value silently never updates when the user picks one. Use bind:group with a shared group variable across the radio inputs instead."
3554
4141
  }))
3555
4142
  });
3556
4143
 
@@ -3653,24 +4240,35 @@ var correctnessOrphanLifecycle = {
3653
4240
  }
3654
4241
  };
3655
4242
 
3656
- // src/rules/correctness/server-browser-global.ts
4243
+ // src/rules/correctness/base-path-navigation.ts
3657
4244
  var PENALIZED5 = { presence: "none", value: "absent" };
3658
4245
  var PASS5 = { presence: "own", value: "static" };
3659
- var ID2 = "correctness/server-browser-global";
4246
+ var ID2 = "correctness/base-path-navigation";
3660
4247
  var DOCS_URL2 = docsUrlFor(ID2);
3661
- var LABEL2 = "Server-safe module code";
3662
- 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).";
3663
- 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"`;
4248
+ var LABEL2 = "Base-path-aware navigation";
4249
+ var RECOMMENDATION2 = "Wrap root-relative paths in resolve() from '$app/paths' so they resolve against kit.paths.base.";
4250
+ var FIX7 = {
4251
+ description: "Import { resolve } from '$app/paths' and wrap the path: href={resolve('/about')}, goto(resolve('/about')), redirect(303, resolve('/login'))."
4252
+ };
4253
+ function messageFor2(link) {
4254
+ if (link.kind === "href") {
4255
+ return `<a href="${link.path}"> is root-relative \u2014 under this project's kit.paths.base it points at the domain root, outside the app, and 404s in production. Use resolve('${link.path}') from '$app/paths'.`;
4256
+ }
4257
+ if (link.kind === "goto") {
4258
+ return `goto('${link.path}') is root-relative \u2014 it navigates outside this project's kit.paths.base and 404s in production. Use goto(resolve('${link.path}')) with resolve from '$app/paths'.`;
4259
+ }
4260
+ return `redirect(\u2026, '${link.path}') is root-relative \u2014 the Location header points outside this project's kit.paths.base and 404s in production. Use resolve('${link.path}') from '$app/paths'.`;
4261
+ }
3664
4262
  function isSuppressed4(suppressions, line) {
3665
4263
  return (suppressions ?? []).some((s) => s.line === line && (!s.ruleIds || s.ruleIds.includes(ID2)));
3666
4264
  }
3667
- function emitFile2(out, file, issues, suppressions) {
3668
- const bad = issues.filter((b) => !(b.line > 0 && isSuppressed4(suppressions, b.line)));
4265
+ function emitFile2(out, file, links, suppressions) {
4266
+ const bad = links.filter((l) => !(l.line > 0 && isSuppressed4(suppressions, l.line)));
3669
4267
  if (bad.length === 0) {
3670
4268
  out.push({
3671
4269
  id: ID2,
3672
4270
  category: "correctness",
3673
- severity: "critical",
4271
+ severity: "warning",
3674
4272
  detection: PASS5,
3675
4273
  route: file,
3676
4274
  message: LABEL2,
@@ -3679,23 +4277,90 @@ function emitFile2(out, file, issues, suppressions) {
3679
4277
  });
3680
4278
  return;
3681
4279
  }
3682
- for (const b of bad) {
4280
+ for (const l of bad) {
3683
4281
  out.push({
3684
4282
  id: ID2,
3685
4283
  category: "correctness",
3686
- severity: "critical",
4284
+ severity: "warning",
3687
4285
  detection: PENALIZED5,
3688
4286
  route: file,
3689
4287
  location: file,
4288
+ ...l.line > 0 ? { line: l.line } : {},
4289
+ message: messageFor2(l),
4290
+ recommendation: RECOMMENDATION2,
4291
+ docsUrl: DOCS_URL2,
4292
+ fix: { ...FIX7 }
4293
+ });
4294
+ }
4295
+ }
4296
+ var correctnessBasePathNavigation = {
4297
+ id: ID2,
4298
+ title: "Root-relative navigation under a base path",
4299
+ category: "correctness",
4300
+ severity: "warning",
4301
+ scope: "component",
4302
+ rationale: "A root-relative literal resolves against the domain root, not kit.paths.base, so navigation lands outside an app served from a sub-path. The break only appears once the app is deployed under its base \u2014 locally base is usually empty, so every such link works.",
4303
+ fix: { ...FIX7 },
4304
+ async check(ctx) {
4305
+ if (!ctx.project.kitPathsBase) return [];
4306
+ const out = [];
4307
+ for (const c of ctx.components ?? []) {
4308
+ const links = c.basePathLinks ?? [];
4309
+ if (links.length === 0) continue;
4310
+ emitFile2(out, c.file, links, c.suppressions);
4311
+ }
4312
+ for (const m of ctx.kitModules ?? []) {
4313
+ const links = m.basePathLinks ?? [];
4314
+ if (links.length === 0) continue;
4315
+ emitFile2(out, m.file, links, m.suppressions);
4316
+ }
4317
+ return out;
4318
+ }
4319
+ };
4320
+
4321
+ // src/rules/correctness/server-browser-global.ts
4322
+ var PENALIZED6 = { presence: "none", value: "absent" };
4323
+ var PASS6 = { presence: "own", value: "static" };
4324
+ var ID3 = "correctness/server-browser-global";
4325
+ var DOCS_URL3 = docsUrlFor(ID3);
4326
+ var LABEL3 = "Server-safe module code";
4327
+ var RECOMMENDATION3 = "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).";
4328
+ 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"`;
4329
+ function isSuppressed5(suppressions, line) {
4330
+ return (suppressions ?? []).some((s) => s.line === line && (!s.ruleIds || s.ruleIds.includes(ID3)));
4331
+ }
4332
+ function emitFile3(out, file, issues, suppressions) {
4333
+ const bad = issues.filter((b) => !(b.line > 0 && isSuppressed5(suppressions, b.line)));
4334
+ if (bad.length === 0) {
4335
+ out.push({
4336
+ id: ID3,
4337
+ category: "correctness",
4338
+ severity: "critical",
4339
+ detection: PASS6,
4340
+ route: file,
4341
+ message: LABEL3,
4342
+ recommendation: RECOMMENDATION3,
4343
+ docsUrl: DOCS_URL3
4344
+ });
4345
+ return;
4346
+ }
4347
+ for (const b of bad) {
4348
+ out.push({
4349
+ id: ID3,
4350
+ category: "correctness",
4351
+ severity: "critical",
4352
+ detection: PENALIZED6,
4353
+ route: file,
4354
+ location: file,
3690
4355
  ...b.line > 0 ? { line: b.line } : {},
3691
4356
  message: b.message,
3692
- recommendation: RECOMMENDATION2,
3693
- docsUrl: DOCS_URL2
4357
+ recommendation: RECOMMENDATION3,
4358
+ docsUrl: DOCS_URL3
3694
4359
  });
3695
4360
  }
3696
4361
  }
3697
4362
  var correctnessServerBrowserGlobal = {
3698
- id: ID2,
4363
+ id: ID3,
3699
4364
  title: "Browser global in server module code",
3700
4365
  category: "correctness",
3701
4366
  severity: "critical",
@@ -3706,7 +4371,7 @@ var correctnessServerBrowserGlobal = {
3706
4371
  for (const c of ctx.components ?? []) {
3707
4372
  const refs = (c.browserGlobalRefs ?? []).filter((r) => r.context === "module");
3708
4373
  if (refs.length === 0) continue;
3709
- emitFile2(
4374
+ emitFile3(
3710
4375
  out,
3711
4376
  c.file,
3712
4377
  refs.map((r) => ({ line: r.line, message: moduleMessage(r.name) })),
@@ -3716,7 +4381,7 @@ var correctnessServerBrowserGlobal = {
3716
4381
  for (const m of ctx.kitModules ?? []) {
3717
4382
  const refs = m.browserGlobalRefs ?? [];
3718
4383
  if (refs.length === 0) continue;
3719
- emitFile2(
4384
+ emitFile3(
3720
4385
  out,
3721
4386
  m.file,
3722
4387
  refs.map((r) => ({
@@ -3832,35 +4497,151 @@ var securitySharedStateImport = kitModuleRule({
3832
4497
  });
3833
4498
 
3834
4499
  // src/rules/architecture/component-size.ts
3835
- var MAX_LOC = 400;
4500
+ var MAX_LOC = 200;
3836
4501
  var architectureComponentSize = componentRule({
3837
4502
  id: "architecture/component-size",
3838
4503
  title: "Component size",
3839
4504
  category: "architecture",
3840
4505
  severity: "info",
3841
4506
  label: "Component size",
3842
- recommendation: `Split components over ${MAX_LOC} lines into smaller, focused pieces.`,
4507
+ options: { max: { kind: "integer", default: MAX_LOC, min: 1 } },
4508
+ recommendation: (o) => `Split components over ${intOption(o, "max", MAX_LOC)} lines into smaller, focused pieces.`,
3843
4509
  rationale: "A very large component is hard to read, test, and reuse, and is a common sign that several responsibilities should be split out.",
3844
4510
  applies: (c) => c.loc > 0,
3845
4511
  // skip unanalyzable files (loc 0 = read/parse failure), don't PASS them
3846
- bad: (c) => c.loc > MAX_LOC ? [{ line: 1, message: `Component is ${c.loc} lines (over ${MAX_LOC})` }] : []
4512
+ bad: (c, o) => {
4513
+ const max = intOption(o, "max", MAX_LOC);
4514
+ return c.loc > max ? [{ line: 1, message: `Component is ${c.loc} lines (over ${max})` }] : [];
4515
+ }
3847
4516
  });
3848
4517
 
3849
4518
  // src/rules/architecture/prop-count.ts
3850
- var MAX_PROPS = 10;
4519
+ var MAX_PROPS = 6;
3851
4520
  var architecturePropCount = componentRule({
3852
4521
  id: "architecture/prop-count",
3853
4522
  title: "Prop count",
3854
4523
  category: "architecture",
3855
4524
  severity: "info",
3856
4525
  label: "Prop count",
3857
- recommendation: `Group related props into an object, or split the component, when it takes more than ${MAX_PROPS} props.`,
4526
+ options: { max: { kind: "integer", default: MAX_PROPS, min: 1 } },
4527
+ recommendation: (o) => `Group related props into an object, or split the component, when it takes more than ${intOption(o, "max", MAX_PROPS)} props.`,
3858
4528
  rationale: "A component taking many props is usually doing too much; grouping or splitting keeps its API understandable.",
3859
4529
  applies: (c) => c.propCount > 0,
3860
4530
  // only components whose props we could count
3861
- bad: (c) => c.propCount > MAX_PROPS ? [{ line: 1, message: `Component takes ${c.propCount} props (over ${MAX_PROPS})` }] : []
4531
+ bad: (c, o) => {
4532
+ const max = intOption(o, "max", MAX_PROPS);
4533
+ return c.propCount > max ? [{ line: 1, message: `Component takes ${c.propCount} props (over ${max})` }] : [];
4534
+ }
3862
4535
  });
3863
4536
 
4537
+ // src/rules/architecture/private-scope-import.ts
4538
+ var docsUrl7 = docsUrlFor("architecture/private-scope-import");
4539
+ var recommendation7 = "Move the unit to the directory shared by all of its importers, or import it only from inside its own scope.";
4540
+ var OPTIONS2 = { scopes: { kind: "string-list", default: [] } };
4541
+ function ancestorDirs(file) {
4542
+ const segments = file.split("/");
4543
+ const out = [];
4544
+ for (let i = segments.length - 1; i > 0; i--) out.push(segments.slice(0, i).join("/"));
4545
+ return out;
4546
+ }
4547
+ function privateScopeOf(target, patterns) {
4548
+ for (const dir of ancestorDirs(target)) {
4549
+ if (!patterns.some((p) => p.test(dir))) continue;
4550
+ const cut = dir.lastIndexOf("/");
4551
+ return cut === -1 ? "" : dir.slice(0, cut);
4552
+ }
4553
+ return void 0;
4554
+ }
4555
+ function isInside(file, boundary) {
4556
+ return boundary === "" || file.startsWith(`${boundary}/`);
4557
+ }
4558
+ var architecturePrivateScopeImport = {
4559
+ id: "architecture/private-scope-import",
4560
+ title: "Private-scope import",
4561
+ category: "architecture",
4562
+ severity: "info",
4563
+ scope: "component",
4564
+ rationale: "A unit placed inside a private directory is written for one owner; importing it from elsewhere couples two parts of the tree that were meant to move independently, and the unit belongs higher up instead.",
4565
+ fix: {
4566
+ description: "Move this unit out of its private scope, to the directory shared by all of its importers, and update this import."
4567
+ },
4568
+ options: OPTIONS2,
4569
+ async check(ctx) {
4570
+ const out = [];
4571
+ const compiled = compileOverrides(ctx.config);
4572
+ const patternCache = /* @__PURE__ */ new Map();
4573
+ const compileScopes = (scopes) => {
4574
+ const key = JSON.stringify(scopes);
4575
+ let patterns = patternCache.get(key);
4576
+ if (patterns === void 0) {
4577
+ patterns = scopes.map((scope) => {
4578
+ const marker = scope.endsWith("/**") ? scope.slice(0, -3) : scope;
4579
+ return routeGlobToRegExp(marker);
4580
+ });
4581
+ patternCache.set(key, patterns);
4582
+ }
4583
+ return patterns;
4584
+ };
4585
+ for (const c of ctx.components ?? []) {
4586
+ const o = resolveRuleOptions(
4587
+ "architecture/private-scope-import",
4588
+ OPTIONS2,
4589
+ ctx.config,
4590
+ { route: c.file, file: c.file },
4591
+ compiled
4592
+ );
4593
+ const scopes = listOption(o, "scopes");
4594
+ if (scopes.length === 0) continue;
4595
+ const patterns = compileScopes(scopes);
4596
+ const spans = c.importSpans ?? c.imports.map((source) => ({ source, line: 0 }));
4597
+ let sawScopedImport = false;
4598
+ const violations = [];
4599
+ for (const { source, line } of spans) {
4600
+ const target = resolveRepoLocalPath(source, c.file);
4601
+ if (target === void 0) continue;
4602
+ const boundary = privateScopeOf(target, patterns);
4603
+ if (boundary === void 0) continue;
4604
+ sawScopedImport = true;
4605
+ if (isInside(c.file, boundary)) continue;
4606
+ violations.push({ line, message: `${target} is private to ${boundary}` });
4607
+ }
4608
+ if (!sawScopedImport) continue;
4609
+ const visible = violations.filter(
4610
+ (v) => !(v.line > 0 && isSuppressed2(c, "architecture/private-scope-import", v.line))
4611
+ );
4612
+ if (visible.length === 0) {
4613
+ out.push({
4614
+ id: "architecture/private-scope-import",
4615
+ category: "architecture",
4616
+ severity: "info",
4617
+ detection: { presence: "own", value: "static" },
4618
+ route: c.file,
4619
+ message: "No private-scope imports",
4620
+ recommendation: recommendation7,
4621
+ docsUrl: docsUrl7
4622
+ });
4623
+ continue;
4624
+ }
4625
+ for (const v of visible) {
4626
+ out.push({
4627
+ id: "architecture/private-scope-import",
4628
+ category: "architecture",
4629
+ severity: "info",
4630
+ detection: { presence: "none", value: "absent" },
4631
+ route: c.file,
4632
+ location: c.file,
4633
+ ...v.line > 0 ? { line: v.line } : {},
4634
+ message: v.message,
4635
+ recommendation: recommendation7,
4636
+ docsUrl: docsUrl7,
4637
+ fix: { ...architecturePrivateScopeImport.fix }
4638
+ });
4639
+ }
4640
+ }
4641
+ return out;
4642
+ }
4643
+ };
4644
+
3864
4645
  // src/rules/perf/heavy-import.ts
3865
4646
  var HEAVY_PACKAGES = {
3866
4647
  lodash: "import a submodule (lodash/debounce) or use lodash-es for tree-shaking",
@@ -3874,18 +4655,20 @@ var performanceHeavyImport = componentRule({
3874
4655
  label: "No heavy imports",
3875
4656
  recommendation: "Import a submodule or switch to a lighter, tree-shakeable alternative.",
3876
4657
  rationale: "Importing a large, non-tree-shakeable package pulls its whole weight into the bundle even when only a fraction is used, slowing load.",
4658
+ options: { packages: { kind: "string-map", default: HEAVY_PACKAGES } },
3877
4659
  // ComponentFacts is a public @svelte-vitals/core export — an external caller compiled
3878
4660
  // against an older version may still construct one without importSpans. Fall back to the
3879
4661
  // line-less `imports` (line: 0, the pre-fix behavior) instead of crashing on `undefined`.
3880
4662
  applies: (c) => (c.importSpans ?? c.imports).length > 0,
3881
- bad: (c) => {
4663
+ bad: (c, o) => {
4664
+ const packages = mapOption(o, "packages");
3882
4665
  const seen = /* @__PURE__ */ new Set();
3883
4666
  const out = [];
3884
4667
  const spans = c.importSpans ?? c.imports.map((source) => ({ source, line: 0 }));
3885
4668
  for (const { source: src, line } of spans) {
3886
- if (!Object.hasOwn(HEAVY_PACKAGES, src) || seen.has(src)) continue;
4669
+ if (!Object.hasOwn(packages, src) || seen.has(src)) continue;
3887
4670
  seen.add(src);
3888
- out.push({ line, message: `Heavy import "${src}" \u2014 ${HEAVY_PACKAGES[src]}` });
4671
+ out.push({ line, message: `Heavy import "${src}" \u2014 ${packages[src]}` });
3889
4672
  }
3890
4673
  return out;
3891
4674
  }
@@ -3915,13 +4698,13 @@ var performanceNamespaceImport = componentRule({
3915
4698
  });
3916
4699
 
3917
4700
  // src/rules/perf/minify-disabled.ts
3918
- var PENALIZED6 = { presence: "none", value: "absent" };
4701
+ var PENALIZED7 = { presence: "none", value: "absent" };
3919
4702
  var MINIFY_DISABLED_FIX = {
3920
4703
  description: "Remove the minify: false override from vite.config (Vite minifies with esbuild by default), or scope it to non-production builds.",
3921
4704
  snippet: "export default defineConfig({\n build: {\n minify: 'esbuild'\n }\n});",
3922
4705
  lang: "ts"
3923
4706
  };
3924
- var RECOMMENDATION3 = "Remove build.minify: false from vite.config, or scope it to non-production builds if it is intentional.";
4707
+ var RECOMMENDATION4 = "Remove build.minify: false from vite.config, or scope it to non-production builds if it is intentional.";
3925
4708
  var performanceMinifyDisabled = {
3926
4709
  id: "performance/minify-disabled",
3927
4710
  title: "Minification disabled",
@@ -3939,11 +4722,11 @@ var performanceMinifyDisabled = {
3939
4722
  id: "performance/minify-disabled",
3940
4723
  category: "performance",
3941
4724
  severity: "warning",
3942
- detection: PENALIZED6,
4725
+ detection: PENALIZED7,
3943
4726
  ...hit.file !== void 0 ? { location: hit.file } : {},
3944
4727
  ...hit.line !== void 0 ? { line: hit.line } : {},
3945
4728
  message: "JS/CSS minification is disabled (build.minify: false) \u2014 production bundles ship unminified and several times larger." + provenance,
3946
- recommendation: RECOMMENDATION3,
4729
+ recommendation: RECOMMENDATION4,
3947
4730
  docsUrl: docsUrlFor("performance/minify-disabled"),
3948
4731
  fix: { ...MINIFY_DISABLED_FIX }
3949
4732
  }
@@ -4056,8 +4839,11 @@ var allRules = [
4056
4839
  correctnessUnmutatedState,
4057
4840
  correctnessPropMutation,
4058
4841
  correctnessStalePropDerivation,
4842
+ correctnessNonreactiveBuiltinState,
4843
+ correctnessCheckableBindValue,
4059
4844
  correctnessOrphanEffect,
4060
4845
  correctnessOrphanLifecycle,
4846
+ correctnessBasePathNavigation,
4061
4847
  correctnessServerBrowserGlobal,
4062
4848
  correctnessInstanceBrowserGlobal,
4063
4849
  securityRawHtml,
@@ -4067,6 +4853,7 @@ var allRules = [
4067
4853
  securitySharedStateImport,
4068
4854
  architectureComponentSize,
4069
4855
  architecturePropCount,
4856
+ architecturePrivateScopeImport,
4070
4857
  performanceHeavyImport,
4071
4858
  performanceNamespaceImport,
4072
4859
  performanceMinifyDisabled,
@@ -4074,6 +4861,15 @@ var allRules = [
4074
4861
  performanceSequentialAwaits,
4075
4862
  performanceStateRaw
4076
4863
  ];
4864
+ function optionInfos(spec) {
4865
+ return Object.entries(spec).map(([name, s]) => ({
4866
+ name,
4867
+ kind: s.kind,
4868
+ default: s.default,
4869
+ ...s.kind === "integer" && s.min !== void 0 ? { min: s.min } : {},
4870
+ ...s.kind === "integer" && s.max !== void 0 ? { max: s.max } : {}
4871
+ }));
4872
+ }
4077
4873
  function explainRule(id) {
4078
4874
  const rule = allRules.find((r) => r.id === id);
4079
4875
  if (!rule) return void 0;
@@ -4084,7 +4880,8 @@ function explainRule(id) {
4084
4880
  severity: rule.severity,
4085
4881
  rationale: rule.rationale,
4086
4882
  docsUrl: docsUrlFor(rule.id),
4087
- ...rule.fix ? { fix: rule.fix } : {}
4883
+ ...rule.fix ? { fix: rule.fix } : {},
4884
+ ...rule.options ? { options: optionInfos(rule.options) } : {}
4088
4885
  };
4089
4886
  }
4090
4887
 
@@ -5303,60 +6100,21 @@ function safeHref(url) {
5303
6100
  const normalized = url.replace(/\s/g, "").toLowerCase();
5304
6101
  return /^https?:\/\//.test(normalized) ? url : null;
5305
6102
  }
5306
-
5307
- // src/config-apply.ts
5308
- function selectRules(rules, config) {
5309
- return rules.filter((rule) => config.rules[rule.id] !== "off");
5310
- }
5311
- function applyRuleSeverities(results, config) {
5312
- return results.map((result) => {
5313
- const setting = config.rules[result.id];
5314
- return setting && setting !== "off" ? { ...result, severity: setting } : result;
5315
- });
5316
- }
5317
- function routeGlobToRegExp(pattern) {
5318
- const body = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "\0").replace(/\*/g, "[^/]*").split("\0").join(".*");
5319
- const source = body.endsWith("/.*") ? `${body.slice(0, -3)}(/.*)?` : body;
5320
- return new RegExp(`^${source}$`);
5321
- }
5322
- function toPatterns(globs) {
5323
- if (globs === void 0) return [];
5324
- return (Array.isArray(globs) ? globs : [globs]).map(routeGlobToRegExp);
5325
- }
5326
- function applyOverrides(results, config) {
5327
- const overrides = config.overrides;
5328
- if (!overrides || overrides.length === 0) return results;
5329
- const compiled = overrides.map((o) => ({
5330
- routes: toPatterns(o.route),
5331
- files: toPatterns(o.files),
5332
- rules: o.rules
5333
- }));
5334
- const out = [];
5335
- for (const result of results) {
5336
- const { route, location } = result;
5337
- let setting;
5338
- for (const o of compiled) {
5339
- const matched = route !== void 0 && o.routes.some((p) => p.test(route)) || location !== void 0 && o.files.some((p) => p.test(location));
5340
- if (!matched) continue;
5341
- const s = o.rules[result.id] ?? o.rules[result.category ?? "seo"];
5342
- if (s !== void 0) setting = s;
5343
- }
5344
- if (setting === void 0) out.push(result);
5345
- else if (setting !== "off") out.push({ ...result, severity: setting });
5346
- }
5347
- return out;
5348
- }
5349
6103
  export {
5350
6104
  APP_SCRIPT,
5351
6105
  APP_STYLE,
5352
6106
  BAND_COLOR,
6107
+ CATEGORIES,
5353
6108
  CHILD_NODE_KEYS,
5354
6109
  ROBOTS_SOURCE_PATHS,
5355
6110
  SITEMAP_SOURCE_PATHS,
6111
+ SVELTE_CONFIG_FILES,
6112
+ VITE_CONFIG_FILES,
5356
6113
  allRules,
5357
6114
  applyOverrides,
5358
6115
  applyRuleSeverities,
5359
6116
  architectureComponentSize,
6117
+ architecturePrivateScopeImport,
5360
6118
  architecturePropCount,
5361
6119
  attrText,
5362
6120
  attrTextOf,
@@ -5367,13 +6125,17 @@ export {
5367
6125
  classify,
5368
6126
  collectComponentFacts,
5369
6127
  collectKitModuleFacts,
6128
+ compileOverrides,
5370
6129
  computeHealth,
5371
6130
  computeScore,
6131
+ correctnessBasePathNavigation,
6132
+ correctnessCheckableBindValue,
5372
6133
  correctnessEachIndexKey,
5373
6134
  correctnessEachKey,
5374
6135
  correctnessEffectAsDerived,
5375
6136
  correctnessEffectAsOnMount,
5376
6137
  correctnessInstanceBrowserGlobal,
6138
+ correctnessNonreactiveBuiltinState,
5377
6139
  correctnessOrphanEffect,
5378
6140
  correctnessOrphanLifecycle,
5379
6141
  correctnessPropMutation,
@@ -5390,6 +6152,8 @@ export {
5390
6152
  escapeHtml,
5391
6153
  explainRule,
5392
6154
  findAttr,
6155
+ findKitPathsBaseInSvelteConfig,
6156
+ findKitPathsBaseInViteConfig,
5393
6157
  findMinifyDisabled,
5394
6158
  formatAgentReport,
5395
6159
  formatConsoleReport,
@@ -5401,10 +6165,14 @@ export {
5401
6165
  hasFailureAtOrAbove,
5402
6166
  headTagRule,
5403
6167
  imageRule,
6168
+ intOption,
5404
6169
  isPenalized,
5405
6170
  lineOf,
5406
6171
  linkRule,
6172
+ listOption,
6173
+ mapOption,
5407
6174
  noColorPalette,
6175
+ overrideMatches,
5408
6176
  parseComponentFacts,
5409
6177
  parseKitModuleFacts,
5410
6178
  performanceFontPreloadCrossorigin,
@@ -5422,6 +6190,8 @@ export {
5422
6190
  performanceSequentialAwaits,
5423
6191
  performanceStateRaw,
5424
6192
  renderAppShell,
6193
+ resolveKitPathsBase,
6194
+ resolveRuleOptions,
5425
6195
  resolveRunesModuleSpecifier,
5426
6196
  runRules,
5427
6197
  safeHref,
@@ -5465,7 +6235,12 @@ export {
5465
6235
  seoTitlePresence,
5466
6236
  seoTwitterCard,
5467
6237
  seoViewport,
6238
+ settingOptions,
6239
+ settingSeverity,
6240
+ shouldSkipRangeCheck,
5468
6241
  summarize,
5469
6242
  textFromNodes,
6243
+ validateRuleOptions,
6244
+ validateRuleSetting,
5470
6245
  valueFromNodes
5471
6246
  };