@svelte-vitals/core 0.29.0 → 0.31.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 +491 -25
  2. package/dist/index.js +1477 -138
  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",
@@ -264,6 +270,12 @@ function scopeIntroducedNames(node) {
264
270
  } else if (node.type === "AwaitBlock") {
265
271
  if (node.value) addBoundNames(node.value, introduced);
266
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
+ }
267
279
  }
268
280
  return introduced;
269
281
  }
@@ -671,6 +683,7 @@ function bodyIsEmpty(fn) {
671
683
  return false;
672
684
  }
673
685
  var URL_ATTRS = ["href", "src", "action", "formaction"];
686
+ var CHECKABLE_INPUT_TYPES = /* @__PURE__ */ new Set(["checkbox", "radio"]);
674
687
  function collectSecurityFacts(node, source, htmlTags, jsUrls) {
675
688
  if (Array.isArray(node)) {
676
689
  for (const child of node) collectSecurityFacts(child, source, htmlTags, jsUrls);
@@ -692,6 +705,59 @@ function collectSecurityFacts(node, source, htmlTags, jsUrls) {
692
705
  if (key in node) collectSecurityFacts(node[key], source, htmlTags, jsUrls);
693
706
  }
694
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
+ }
695
761
  function isPropsCall(node) {
696
762
  return node?.type === "CallExpression" && node.callee?.type === "Identifier" && node.callee.name === "$props";
697
763
  }
@@ -791,10 +857,19 @@ function countLines(source) {
791
857
  if (source.length === 0) return 0;
792
858
  return source.split("\n").length - (source.endsWith("\n") ? 1 : 0);
793
859
  }
860
+ function isTypeOnlyImport(n) {
861
+ if (n.importKind === "type") return true;
862
+ const specs = n.specifiers;
863
+ return Array.isArray(specs) && specs.length > 0 && specs.every((s) => s?.importKind === "type");
864
+ }
794
865
  function collectImportSources(program, source, acc) {
795
866
  walkEstree(program, (n) => {
796
867
  if (n.type === "ImportDeclaration" && typeof n.source?.value === "string") {
797
- acc.push({ source: n.source.value, line: lineOf(source, n.start) });
868
+ acc.push({
869
+ source: n.source.value,
870
+ line: lineOf(source, n.start),
871
+ ...isTypeOnlyImport(n) ? { type: true } : {}
872
+ });
798
873
  }
799
874
  });
800
875
  }
@@ -977,20 +1052,25 @@ var BROWSER_GLOBALS = /* @__PURE__ */ new Set([
977
1052
  "confirm",
978
1053
  "prompt"
979
1054
  ]);
980
- function collectBrowserGuardImports(program) {
1055
+ function collectNamedImportAliases(program, moduleSource, names) {
981
1056
  const out = /* @__PURE__ */ new Set();
982
1057
  for (const stmt of program.body ?? []) {
983
- if (stmt?.type !== "ImportDeclaration" || stmt.importKind === "type" || stmt.source?.value !== "$app/environment")
1058
+ if (stmt?.type !== "ImportDeclaration" || stmt.importKind === "type" || stmt.source?.value !== moduleSource) {
984
1059
  continue;
1060
+ }
985
1061
  for (const s of stmt.specifiers ?? []) {
986
1062
  if (s?.importKind === "type" || s?.local?.type !== "Identifier") continue;
987
- if (s.type === "ImportSpecifier" && s.imported?.type === "Identifier" && s.imported.name === "browser") {
1063
+ if (s.type === "ImportSpecifier" && s.imported?.type === "Identifier" && names.has(s.imported.name)) {
988
1064
  out.add(s.local.name);
989
1065
  }
990
1066
  }
991
1067
  }
992
1068
  return out;
993
1069
  }
1070
+ var BROWSER_GUARD_NAMES = /* @__PURE__ */ new Set(["browser"]);
1071
+ function collectBrowserGuardImports(program) {
1072
+ return collectNamedImportAliases(program, "$app/environment", BROWSER_GUARD_NAMES);
1073
+ }
994
1074
  function collectProgramBindings(program) {
995
1075
  const bound = /* @__PURE__ */ new Set();
996
1076
  for (const stmt of program.body ?? []) {
@@ -1168,6 +1248,14 @@ function parseModuleFacts(source, filename) {
1168
1248
  const orphanLifecycleCalls = program ? collectOrphanLifecycleCalls(program, wrapped).map((f) => ({ ...f, line: shift(f.line) })) : [];
1169
1249
  const browserGlobalRefs = program ? collectBrowserGlobalRefs(program, wrapped).map((r) => ({ ...r, line: shift(r.line), context: "module" })) : [];
1170
1250
  const moduleStateDecls = program ? collectModuleStateDecls(program, wrapped).map((d) => ({ ...d, line: shift(d.line) })) : [];
1251
+ const basePathLinks = [];
1252
+ if (program) {
1253
+ const locals = collectNamedImportAliases(program, "$app/navigation", GOTO_NAMES);
1254
+ const raw = [];
1255
+ collectGotoLinks(locals, [program], wrapped, raw);
1256
+ for (const l of raw) basePathLinks.push({ ...l, line: shift(l.line) });
1257
+ basePathLinks.sort((a, b) => a.line - b.line);
1258
+ }
1171
1259
  return {
1172
1260
  eachBlocks: [],
1173
1261
  effects: [],
@@ -1183,6 +1271,8 @@ function parseModuleFacts(source, filename) {
1183
1271
  stalePropDerivations: [],
1184
1272
  rawableStates: [],
1185
1273
  nonreactiveBuiltinStates: [],
1274
+ checkableBindValues: [],
1275
+ basePathLinks,
1186
1276
  suppressions: collectSuppressions(source),
1187
1277
  orphanEffects,
1188
1278
  orphanLifecycleCalls,
@@ -1198,6 +1288,18 @@ function parseComponentFacts(source, filename) {
1198
1288
  const htmlTags = [];
1199
1289
  const javascriptUrls = [];
1200
1290
  collectSecurityFacts(ast.fragment ?? ast, source, htmlTags, javascriptUrls);
1291
+ const checkableBindValues = [];
1292
+ collectCheckableBindValues(ast.fragment ?? ast, source, checkableBindValues);
1293
+ const basePathLinks = [];
1294
+ collectHrefLinks(ast.fragment ?? ast, source, basePathLinks);
1295
+ const gotoPrograms = [ast.module?.content, ast.instance?.content].filter(Boolean);
1296
+ const gotoLocals = /* @__PURE__ */ new Set();
1297
+ for (const p of gotoPrograms)
1298
+ for (const n of collectNamedImportAliases(p, "$app/navigation", GOTO_NAMES)) {
1299
+ gotoLocals.add(n);
1300
+ }
1301
+ collectGotoLinks(gotoLocals, [...gotoPrograms, ast.fragment], source, basePathLinks);
1302
+ basePathLinks.sort((a, b) => a.line - b.line);
1201
1303
  const loc = countLines(source);
1202
1304
  const suppressions = collectSuppressions(source);
1203
1305
  const moduleProgram = ast.module?.content;
@@ -1372,6 +1474,8 @@ function parseComponentFacts(source, filename) {
1372
1474
  stalePropDerivations,
1373
1475
  rawableStates,
1374
1476
  nonreactiveBuiltinStates,
1477
+ checkableBindValues,
1478
+ basePathLinks,
1375
1479
  orphanEffects,
1376
1480
  orphanLifecycleCalls,
1377
1481
  browserGlobalRefs,
@@ -1398,6 +1502,8 @@ function emptyComponentFacts(file) {
1398
1502
  stalePropDerivations: [],
1399
1503
  rawableStates: [],
1400
1504
  nonreactiveBuiltinStates: [],
1505
+ checkableBindValues: [],
1506
+ basePathLinks: [],
1401
1507
  orphanEffects: [],
1402
1508
  orphanLifecycleCalls: [],
1403
1509
  browserGlobalRefs: [],
@@ -1419,6 +1525,12 @@ async function collectComponentFacts(rt, cwd) {
1419
1525
  );
1420
1526
  }
1421
1527
 
1528
+ // src/source-files.ts
1529
+ async function collectSourceFiles(rt, cwd) {
1530
+ const files = await rt.glob("src/**/*", cwd);
1531
+ return files.slice().sort();
1532
+ }
1533
+
1422
1534
  // src/kit-module-parse.ts
1423
1535
  var HANDLER_NAMES = /* @__PURE__ */ new Set([
1424
1536
  "load",
@@ -1534,6 +1646,22 @@ function collectAwaits(node, out = []) {
1534
1646
  }
1535
1647
  return out;
1536
1648
  }
1649
+ var REDIRECT_NAMES = /* @__PURE__ */ new Set(["redirect"]);
1650
+ function collectRedirectCalls(node, locals, out = []) {
1651
+ if (Array.isArray(node)) {
1652
+ for (const child of node) collectRedirectCalls(child, locals, out);
1653
+ return out;
1654
+ }
1655
+ if (!node || typeof node !== "object" || typeof node.type !== "string") return out;
1656
+ if (node.type === "CallExpression" && node.callee?.type === "Identifier" && locals.has(node.callee.name)) {
1657
+ out.push(node);
1658
+ }
1659
+ for (const key of Object.keys(node)) {
1660
+ if (WALK_IGNORED_KEYS.has(key)) continue;
1661
+ collectRedirectCalls(node[key], locals, out);
1662
+ }
1663
+ return out;
1664
+ }
1537
1665
  function isParentCall(arg) {
1538
1666
  const e = unwrapTs(arg);
1539
1667
  if (e?.type !== "CallExpression") return false;
@@ -1683,28 +1811,45 @@ function normalizePosix(path) {
1683
1811
  }
1684
1812
  return out.join("/");
1685
1813
  }
1686
- function resolveRepoLocalPath(spec, importerFile) {
1814
+ var DEFAULT_KIT_ALIASES = [{ find: "$lib", replacement: "src/lib", match: "prefix" }];
1815
+ function aliasMatches(entry, spec) {
1816
+ if (entry.match === "exact") return spec === entry.find;
1817
+ if (spec.startsWith(`${entry.find}/`)) return true;
1818
+ return entry.match === "prefix" && spec === entry.find;
1819
+ }
1820
+ function resolveRepoLocalPath(spec, importerFile, aliases = DEFAULT_KIT_ALIASES) {
1687
1821
  let path;
1688
- if (spec.startsWith("$lib/")) path = `src/lib/${spec.slice("$lib/".length)}`;
1689
- else if (spec.startsWith("./") || spec.startsWith("../")) {
1822
+ if (spec.startsWith("./") || spec.startsWith("../")) {
1690
1823
  const dir = importerFile.split("/").slice(0, -1).join("/");
1691
1824
  path = `${dir}/${spec}`;
1692
- } else return void 0;
1825
+ } else {
1826
+ const entry = aliases.find((a) => aliasMatches(a, spec));
1827
+ if (entry?.replacement == null) return void 0;
1828
+ if (entry.replacement.startsWith("/") || /^[A-Za-z]:\//.test(entry.replacement)) return void 0;
1829
+ path = entry.replacement + spec.slice(entry.find.length);
1830
+ }
1693
1831
  return normalizePosix(path);
1694
1832
  }
1695
- function resolveRunesModuleSpecifier(spec, importerFile) {
1696
- const path = resolveRepoLocalPath(spec, importerFile);
1833
+ function resolveRunesModuleSpecifier(spec, importerFile, aliases) {
1834
+ const path = resolveRepoLocalPath(spec, importerFile, aliases);
1697
1835
  if (path === void 0) return void 0;
1698
1836
  if (/\.svelte\.(ts|js)$/.test(path)) return path;
1699
1837
  if (path.endsWith(".svelte")) return `${path}.ts`;
1700
1838
  return void 0;
1701
1839
  }
1702
- function isLocalStateSpecifier(spec, importerFile) {
1703
- const path = resolveRepoLocalPath(spec, importerFile);
1840
+ function libServerRoot(aliases) {
1841
+ const lib = aliases?.find((a) => a.find === "$lib");
1842
+ if (lib && lib.replacement === null) return void 0;
1843
+ return `${lib?.replacement ?? "src/lib"}/server`;
1844
+ }
1845
+ function isLocalStateSpecifier(spec, importerFile, aliases) {
1846
+ const serverRoot = libServerRoot(aliases);
1847
+ if (serverRoot === void 0) return false;
1848
+ const path = resolveRepoLocalPath(spec, importerFile, aliases);
1704
1849
  if (path === void 0) return false;
1705
- return path !== "src/lib/server" && !path.startsWith("src/lib/server/");
1850
+ return path !== serverRoot && !path.startsWith(`${serverRoot}/`);
1706
1851
  }
1707
- function parseKitModuleFacts(source, filename) {
1852
+ function parseKitModuleFacts(source, filename, aliases) {
1708
1853
  const suppressions = collectSuppressions(source);
1709
1854
  const { program, wrapped } = parseModuleProgram(source, filename);
1710
1855
  const moduleStateReassignments = [];
@@ -1721,6 +1866,7 @@ function parseKitModuleFacts(source, filename) {
1721
1866
  runesModuleImports,
1722
1867
  lifecycleCalls,
1723
1868
  browserGlobalRefs,
1869
+ basePathLinks: [],
1724
1870
  suppressions
1725
1871
  };
1726
1872
  }
@@ -1736,7 +1882,7 @@ function parseKitModuleFacts(source, filename) {
1736
1882
  importedSpecifiers.set(s.local.name, spec);
1737
1883
  }
1738
1884
  if (names.length === 0) continue;
1739
- const resolved = resolveRunesModuleSpecifier(spec, filename);
1885
+ const resolved = resolveRunesModuleSpecifier(spec, filename, aliases);
1740
1886
  if (resolved) runesModuleImports.push({ source: spec, resolved, names, line: line(stmt.start) });
1741
1887
  }
1742
1888
  const moduleLets = /* @__PURE__ */ new Set();
@@ -1810,7 +1956,8 @@ function parseKitModuleFacts(source, filename) {
1810
1956
  const method = n.callee.property?.type === "Identifier" ? n.callee.property.name : void 0;
1811
1957
  if (method === "set" || method === "update") {
1812
1958
  const r = importedRoot(n.callee.object);
1813
- if (r && isLocalStateSpecifier(importedSpecifiers.get(r), filename)) write = { name: r, via: "set-call" };
1959
+ if (r && isLocalStateSpecifier(importedSpecifiers.get(r), filename, aliases))
1960
+ write = { name: r, via: "set-call" };
1814
1961
  }
1815
1962
  } else if (n.type === "AssignmentExpression" && (n.left?.type === "ObjectPattern" || n.left?.type === "ArrayPattern")) {
1816
1963
  const scanPatternTargets = (pat) => {
@@ -1845,6 +1992,15 @@ function parseKitModuleFacts(source, filename) {
1845
1992
  }
1846
1993
  });
1847
1994
  const byLine = (arr) => arr.sort((a, b) => a.line - b.line);
1995
+ const basePathLinks = [];
1996
+ const redirectLocals = collectNamedImportAliases(program, "@sveltejs/kit", REDIRECT_NAMES);
1997
+ if (redirectLocals.size > 0) {
1998
+ for (const call of collectRedirectCalls(program, redirectLocals)) {
1999
+ const arg = call.arguments?.[1];
2000
+ if (arg?.type !== "Literal" || typeof arg.value !== "string" || !isRootRelativePath(arg.value)) continue;
2001
+ basePathLinks.push({ kind: "redirect", path: arg.value, line: line(call.start) });
2002
+ }
2003
+ }
1848
2004
  return {
1849
2005
  moduleStateReassignments: byLine(moduleStateReassignments),
1850
2006
  importedStateWrites: byLine(importedStateWrites),
@@ -1852,6 +2008,7 @@ function parseKitModuleFacts(source, filename) {
1852
2008
  runesModuleImports: byLine(runesModuleImports),
1853
2009
  lifecycleCalls: byLine(lifecycleCalls),
1854
2010
  browserGlobalRefs: byLine(browserGlobalRefs),
2011
+ basePathLinks: byLine(basePathLinks),
1855
2012
  ...ssrOptOut ? { ssrDisabled: { line: Math.max(0, ssrOptOut.line - 1) } } : {},
1856
2013
  ...csrOptOut ? { csrDisabled: { line: Math.max(0, csrOptOut.line - 1) } } : {},
1857
2014
  ...waterfalls.dependentLines.length > 0 || waterfalls.independentLines.length > 0 ? { loadWaterfalls: waterfalls } : {},
@@ -1870,6 +2027,7 @@ function emptyKitModuleFacts(file, kind) {
1870
2027
  runesModuleImports: [],
1871
2028
  lifecycleCalls: [],
1872
2029
  browserGlobalRefs: [],
2030
+ basePathLinks: [],
1873
2031
  suppressions: []
1874
2032
  };
1875
2033
  }
@@ -1877,7 +2035,7 @@ function kindOf(file) {
1877
2035
  const base = file.split("/").pop() ?? file;
1878
2036
  return base.includes(".server.") || base.startsWith("+server.") ? "server" : "universal";
1879
2037
  }
1880
- async function collectKitModuleFacts(rt, cwd) {
2038
+ async function collectKitModuleFacts(rt, cwd, aliases) {
1881
2039
  const patterns = [
1882
2040
  "src/routes/**/+{page,layout}.server.{ts,js}",
1883
2041
  "src/routes/**/+{page,layout}.{ts,js}",
@@ -1891,7 +2049,7 @@ async function collectKitModuleFacts(rt, cwd) {
1891
2049
  const kind = kindOf(rel);
1892
2050
  try {
1893
2051
  const source = await rt.readFile(rt.join(cwd, rel));
1894
- return { file: rel, kind, ...parseKitModuleFacts(source, rel) };
2052
+ return { file: rel, kind, ...parseKitModuleFacts(source, rel, aliases) };
1895
2053
  } catch {
1896
2054
  return emptyKitModuleFacts(rel, kind);
1897
2055
  }
@@ -1899,7 +2057,7 @@ async function collectKitModuleFacts(rt, cwd) {
1899
2057
  );
1900
2058
  }
1901
2059
 
1902
- // src/vite-config-parse.ts
2060
+ // src/config-object.ts
1903
2061
  function propOf(obj, name) {
1904
2062
  let found;
1905
2063
  for (const p of obj.properties) {
@@ -1954,6 +2112,8 @@ function resolveConfigObject(program) {
1954
2112
  if (!exported) return void 0;
1955
2113
  return unwrapToObjectExpression(exported, collectTopLevelBindings(program));
1956
2114
  }
2115
+
2116
+ // src/vite-config-parse.ts
1957
2117
  function findMinifyDisabled(source) {
1958
2118
  let program;
1959
2119
  let wrapped;
@@ -1974,6 +2134,152 @@ function findMinifyDisabled(source) {
1974
2134
  return { line: Math.max(0, lineOf(wrapped, minify.start) - 1) };
1975
2135
  }
1976
2136
 
2137
+ // src/svelte-config-parse.ts
2138
+ function basePathOf(kitConfig, bindings) {
2139
+ const paths = propOf(kitConfig, "paths");
2140
+ const pathsObj = paths ? unwrapToObjectExpression(paths.value, bindings) : void 0;
2141
+ if (!pathsObj) return void 0;
2142
+ const base = propOf(pathsObj, "base");
2143
+ if (!base) return void 0;
2144
+ const value = unwrapTs(base.value);
2145
+ if (value.type === "Literal") {
2146
+ return typeof value.value === "string" && value.value !== "" ? { value: value.value } : void 0;
2147
+ }
2148
+ return {};
2149
+ }
2150
+ function keyNameOf(p) {
2151
+ if (p.computed) return void 0;
2152
+ if (p.key.type === "Identifier") return p.key.name;
2153
+ if (p.key.type === "Literal" && typeof p.key.value === "string") return p.key.value;
2154
+ return void 0;
2155
+ }
2156
+ function stringValueOf(p) {
2157
+ const v = unwrapTs(p.value);
2158
+ return v.type === "Literal" && typeof v.value === "string" ? v.value : void 0;
2159
+ }
2160
+ function aliasEntriesOf(kitConfig, bindings) {
2161
+ const alias = propOf(kitConfig, "alias");
2162
+ if (!alias) return [];
2163
+ const obj = unwrapToObjectExpression(alias.value, bindings);
2164
+ if (!obj) return void 0;
2165
+ const out = [];
2166
+ const at = /* @__PURE__ */ new Map();
2167
+ for (const p of obj.properties) {
2168
+ if (p.type !== "Property") return void 0;
2169
+ const key = keyNameOf(p);
2170
+ if (key === void 0) return void 0;
2171
+ const entry = { key, value: stringValueOf(p) ?? null };
2172
+ const seen = at.get(key);
2173
+ if (seen === void 0) {
2174
+ at.set(key, out.length);
2175
+ out.push(entry);
2176
+ } else out[seen] = entry;
2177
+ }
2178
+ return out;
2179
+ }
2180
+ function filesLibOf(kitConfig, bindings) {
2181
+ const files = propOf(kitConfig, "files");
2182
+ const obj = files ? unwrapToObjectExpression(files.value, bindings) : void 0;
2183
+ const lib = obj ? propOf(obj, "lib") : void 0;
2184
+ if (!lib) return void 0;
2185
+ return stringValueOf(lib) ?? null;
2186
+ }
2187
+ function findKitAliasesInSvelteConfig(source) {
2188
+ const program = programOf(source, "svelte.config.js");
2189
+ const config = program ? resolveConfigObject(program) : void 0;
2190
+ if (!program || !config) return { entries: [] };
2191
+ const bindings = collectTopLevelBindings(program);
2192
+ const kit = propOf(config, "kit");
2193
+ const kitObj = kit ? unwrapToObjectExpression(kit.value, bindings) : void 0;
2194
+ if (!kitObj) return { entries: [] };
2195
+ const filesLib = filesLibOf(kitObj, bindings);
2196
+ return { entries: aliasEntriesOf(kitObj, bindings), ...filesLib !== void 0 ? { filesLib } : {} };
2197
+ }
2198
+ function normalizeAliasValue(value) {
2199
+ const posix = value.replace(/\\/g, "/");
2200
+ const noStar = posix.endsWith("/*") ? posix.slice(0, -2) : posix;
2201
+ return noStar.replace(/\/+$/, "");
2202
+ }
2203
+ function compileKitAliases(raw) {
2204
+ const filesLib = raw.filesLib === null ? null : normalizeAliasValue(raw.filesLib ?? "src/lib");
2205
+ const out = [{ find: "$lib", replacement: filesLib, match: "prefix" }];
2206
+ const entries = raw.entries ?? [];
2207
+ const declared = new Set(entries.map((e) => e.key));
2208
+ for (const { key, value } of entries) {
2209
+ const star = key.endsWith("/*");
2210
+ out.push({
2211
+ find: star ? key.slice(0, -2) : key,
2212
+ replacement: value === null ? null : normalizeAliasValue(value),
2213
+ match: star ? "contents" : declared.has(`${key}/*`) ? "exact" : "prefix"
2214
+ });
2215
+ }
2216
+ return out;
2217
+ }
2218
+ function resolveKitAliases(viteConfig, svelteConfig) {
2219
+ if (viteConfig && findKitPathsBaseInViteConfig(viteConfig.source).kind !== "no-plugin-config") return void 0;
2220
+ if (!svelteConfig) return void 0;
2221
+ return compileKitAliases(findKitAliasesInSvelteConfig(svelteConfig.source));
2222
+ }
2223
+ function programOf(source, filename) {
2224
+ try {
2225
+ return parseModuleProgram(source, filename).program ?? void 0;
2226
+ } catch {
2227
+ return void 0;
2228
+ }
2229
+ }
2230
+ function findKitPathsBaseInSvelteConfig(source) {
2231
+ const program = programOf(source, "svelte.config.js");
2232
+ if (!program) return void 0;
2233
+ const config = resolveConfigObject(program);
2234
+ if (!config) return void 0;
2235
+ const bindings = collectTopLevelBindings(program);
2236
+ const kit = propOf(config, "kit");
2237
+ const kitObj = kit ? unwrapToObjectExpression(kit.value, bindings) : void 0;
2238
+ return kitObj ? basePathOf(kitObj, bindings) : void 0;
2239
+ }
2240
+ function sveltekitLocalNames(program) {
2241
+ const out = collectNamedImportAliases(program, "@sveltejs/kit/vite", /* @__PURE__ */ new Set(["sveltekit"]));
2242
+ if (out.size === 0) out.add("sveltekit");
2243
+ return out;
2244
+ }
2245
+ function findKitPathsBaseInViteConfig(source) {
2246
+ const none = { kind: "no-plugin-config" };
2247
+ const program = programOf(source, "vite.config.ts");
2248
+ if (!program) return none;
2249
+ const config = resolveConfigObject(program);
2250
+ if (!config) return none;
2251
+ const bindings = collectTopLevelBindings(program);
2252
+ const plugins = propOf(config, "plugins");
2253
+ const pluginsValue = plugins ? unwrapTs(plugins.value) : void 0;
2254
+ if (pluginsValue?.type !== "ArrayExpression") return none;
2255
+ const locals = sveltekitLocalNames(program);
2256
+ for (const el of pluginsValue.elements) {
2257
+ if (!el || el.type === "SpreadElement") continue;
2258
+ const call = unwrapTs(el);
2259
+ if (call.type !== "CallExpression") continue;
2260
+ if (call.callee.type !== "Identifier" || !locals.has(call.callee.name)) continue;
2261
+ const arg = call.arguments[0];
2262
+ if (arg === void 0) return none;
2263
+ const kitConfig = unwrapToObjectExpression(arg, bindings);
2264
+ if (!kitConfig) return { kind: "unresolvable" };
2265
+ const base = basePathOf(kitConfig, bindings);
2266
+ return base ? { kind: "resolved", base } : { kind: "resolved" };
2267
+ }
2268
+ return none;
2269
+ }
2270
+ function resolveKitPathsBase(viteConfig, svelteConfig) {
2271
+ if (viteConfig) {
2272
+ const result = findKitPathsBaseInViteConfig(viteConfig.source);
2273
+ if (result.kind === "unresolvable") return void 0;
2274
+ if (result.kind === "resolved") {
2275
+ return result.base ? { ...result.base, file: viteConfig.file } : void 0;
2276
+ }
2277
+ }
2278
+ if (!svelteConfig) return void 0;
2279
+ const base = findKitPathsBaseInSvelteConfig(svelteConfig.source);
2280
+ return base ? { ...base, file: svelteConfig.file } : void 0;
2281
+ }
2282
+
1977
2283
  // src/project-paths.ts
1978
2284
  var ROBOTS_SOURCE_PATHS = [
1979
2285
  "static/robots.txt",
@@ -1985,6 +2291,15 @@ var SITEMAP_SOURCE_PATHS = [
1985
2291
  "src/routes/sitemap.xml/+server.ts",
1986
2292
  "src/routes/sitemap.xml/+server.js"
1987
2293
  ];
2294
+ var VITE_CONFIG_FILES = [
2295
+ "vite.config.js",
2296
+ "vite.config.mjs",
2297
+ "vite.config.ts",
2298
+ "vite.config.cjs",
2299
+ "vite.config.mts",
2300
+ "vite.config.cts"
2301
+ ];
2302
+ var SVELTE_CONFIG_FILES = ["svelte.config.js", "svelte.config.ts"];
1988
2303
 
1989
2304
  // src/rule.ts
1990
2305
  function docsUrlFor(id) {
@@ -2054,7 +2369,7 @@ function detect(head, match) {
2054
2369
  return tag ? { presence: tag.presence, value: tag.value } : { presence: "none", value: "absent" };
2055
2370
  }
2056
2371
  function headTagRule(opts) {
2057
- const docsUrl7 = docsUrlFor(opts.id);
2372
+ const docsUrl11 = docsUrlFor(opts.id);
2058
2373
  return {
2059
2374
  id: opts.id,
2060
2375
  title: opts.title,
@@ -2077,7 +2392,7 @@ function headTagRule(opts) {
2077
2392
  location: head.file,
2078
2393
  message,
2079
2394
  recommendation: opts.recommendation,
2080
- docsUrl: docsUrl7,
2395
+ docsUrl: docsUrl11,
2081
2396
  // Copy per finding: opts.fix is a rule-level template shared across all
2082
2397
  // results this rule emits; a fresh object keeps findings independent.
2083
2398
  ...opts.fix ? { fix: { ...opts.fix } } : {}
@@ -2270,7 +2585,7 @@ var seoHtmlLang = {
2270
2585
 
2271
2586
  // src/rules/perf/image-rule.ts
2272
2587
  function imageRule(opts) {
2273
- const docsUrl7 = docsUrlFor(opts.id);
2588
+ const docsUrl11 = docsUrlFor(opts.id);
2274
2589
  const category = opts.category ?? "performance";
2275
2590
  return {
2276
2591
  id: opts.id,
@@ -2294,7 +2609,7 @@ function imageRule(opts) {
2294
2609
  route: route.route,
2295
2610
  message: opts.label,
2296
2611
  recommendation: opts.recommendation,
2297
- docsUrl: docsUrl7
2612
+ docsUrl: docsUrl11
2298
2613
  });
2299
2614
  continue;
2300
2615
  }
@@ -2309,7 +2624,7 @@ function imageRule(opts) {
2309
2624
  ...img.line > 0 ? { line: img.line } : {},
2310
2625
  message: `Missing ${opts.label}`,
2311
2626
  recommendation: opts.recommendation,
2312
- docsUrl: docsUrl7,
2627
+ docsUrl: docsUrl11,
2313
2628
  ...opts.fix ? { fix: { ...opts.fix } } : {}
2314
2629
  });
2315
2630
  }
@@ -2369,7 +2684,7 @@ var performanceResponsiveImage = imageRule({
2369
2684
 
2370
2685
  // src/rules/perf/link-rule.ts
2371
2686
  function linkRule(opts) {
2372
- const docsUrl7 = docsUrlFor(opts.id);
2687
+ const docsUrl11 = docsUrlFor(opts.id);
2373
2688
  return {
2374
2689
  id: opts.id,
2375
2690
  title: opts.title,
@@ -2393,7 +2708,7 @@ function linkRule(opts) {
2393
2708
  route: head.route,
2394
2709
  message: opts.label,
2395
2710
  recommendation: opts.recommendation,
2396
- docsUrl: docsUrl7
2711
+ docsUrl: docsUrl11
2397
2712
  });
2398
2713
  continue;
2399
2714
  }
@@ -2410,7 +2725,7 @@ function linkRule(opts) {
2410
2725
  location: tag.file ?? head.file,
2411
2726
  message: `Missing ${opts.label}`,
2412
2727
  recommendation: opts.recommendation,
2413
- docsUrl: docsUrl7,
2728
+ docsUrl: docsUrl11,
2414
2729
  ...opts.fix ? { fix: { ...opts.fix } } : {}
2415
2730
  });
2416
2731
  }
@@ -2557,10 +2872,201 @@ var performanceRenderBlockingScript = {
2557
2872
  }
2558
2873
  };
2559
2874
 
2875
+ // src/config-apply.ts
2876
+ function settingSeverity(setting) {
2877
+ if (setting === void 0) return void 0;
2878
+ if (typeof setting === "string") return setting;
2879
+ return setting.severity;
2880
+ }
2881
+ function settingOptions(setting) {
2882
+ return setting !== void 0 && typeof setting !== "string" ? setting.options : void 0;
2883
+ }
2884
+ function selectRules(rules, config) {
2885
+ return rules.filter((rule) => settingSeverity(config.rules[rule.id]) !== "off");
2886
+ }
2887
+ function applyRuleSeverities(results, config) {
2888
+ return results.map((result) => {
2889
+ const severity = settingSeverity(config.rules[result.id]);
2890
+ return severity !== void 0 && severity !== "off" ? { ...result, severity } : result;
2891
+ });
2892
+ }
2893
+ function routeGlobToRegExp(pattern) {
2894
+ const body = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "\0").replace(/\*/g, "[^/]*").split("\0").join(".*");
2895
+ const source = body.endsWith("/.*") ? `${body.slice(0, -3)}(/.*)?` : body;
2896
+ return new RegExp(`^${source}$`);
2897
+ }
2898
+ function toPatterns(globs) {
2899
+ if (globs === void 0) return [];
2900
+ return (Array.isArray(globs) ? globs : [globs]).map(routeGlobToRegExp);
2901
+ }
2902
+ function compileOverrides(config) {
2903
+ return (config.overrides ?? []).map((o) => ({
2904
+ routes: toPatterns(o.route),
2905
+ files: toPatterns(o.files),
2906
+ rules: o.rules
2907
+ }));
2908
+ }
2909
+ function overrideMatches(o, target) {
2910
+ const { route, file } = target;
2911
+ return route !== void 0 && o.routes.some((p) => p.test(route)) || file !== void 0 && o.files.some((p) => p.test(file));
2912
+ }
2913
+ function applyOverrides(results, config) {
2914
+ const compiled = compileOverrides(config);
2915
+ if (compiled.length === 0) return results;
2916
+ const out = [];
2917
+ for (const result of results) {
2918
+ let severity;
2919
+ for (const o of compiled) {
2920
+ if (!overrideMatches(o, { route: result.route, file: result.location })) continue;
2921
+ const sev = settingSeverity(o.rules[result.id]) ?? settingSeverity(o.rules[result.category ?? "seo"]);
2922
+ if (sev !== void 0) severity = sev;
2923
+ }
2924
+ if (severity === void 0) out.push(result);
2925
+ else if (severity !== "off") out.push({ ...result, severity });
2926
+ }
2927
+ return out;
2928
+ }
2929
+
2930
+ // src/rule-options.ts
2931
+ var RULE_SETTING_VALUES = ["off", "critical", "warning", "info"];
2932
+ function defaultsOf(spec) {
2933
+ const out = {};
2934
+ for (const [key, s] of Object.entries(spec)) {
2935
+ out[key] = s.kind === "integer" ? s.default : s.kind === "string-list" ? [...s.default] : { ...s.default };
2936
+ }
2937
+ return out;
2938
+ }
2939
+ function intOption(options, key, fallback = 0) {
2940
+ const v = options[key];
2941
+ return typeof v === "number" ? v : fallback;
2942
+ }
2943
+ function listOption(options, key) {
2944
+ const v = options[key];
2945
+ return Array.isArray(v) ? v : [];
2946
+ }
2947
+ function mapOption(options, key) {
2948
+ const v = options[key];
2949
+ return typeof v === "object" && v !== null && !Array.isArray(v) ? v : {};
2950
+ }
2951
+ function isMentionedAnywhere(config, ruleId) {
2952
+ if (Object.hasOwn(config.rules, ruleId)) return true;
2953
+ return (config.overrides ?? []).some((entry) => entry.rules !== void 0 && Object.hasOwn(entry.rules, ruleId));
2954
+ }
2955
+ function resolveRuleOptions(ruleId, spec, config, target, compiled) {
2956
+ if (!spec) return {};
2957
+ const out = defaultsOf(spec);
2958
+ const layers = [settingOptions(config.rules[ruleId])];
2959
+ if (target) {
2960
+ for (const o of compiled ?? compileOverrides(config)) {
2961
+ if (overrideMatches(o, target)) layers.push(settingOptions(o.rules[ruleId]));
2962
+ }
2963
+ }
2964
+ for (const layer of layers) {
2965
+ if (!layer) continue;
2966
+ for (const [key, value] of Object.entries(layer)) {
2967
+ const s = spec[key];
2968
+ if (!s) continue;
2969
+ if (s.kind === "integer") out[key] = value;
2970
+ else if (s.kind === "string-list") out[key] = [...out[key], ...value];
2971
+ else out[key] = { ...out[key], ...value };
2972
+ }
2973
+ }
2974
+ return out;
2975
+ }
2976
+ function validateRuleOptions(ruleId, spec, options, baseline, skipRangeCheck) {
2977
+ if (!spec) return Object.keys(options).length === 0 ? [] : [`${ruleId} takes no options.`];
2978
+ const errors = [];
2979
+ const badKeys = /* @__PURE__ */ new Set();
2980
+ const isNonEmptyString = (v) => typeof v === "string" && v.length > 0;
2981
+ for (const [key, value] of Object.entries(options)) {
2982
+ const s = spec[key];
2983
+ if (!s) {
2984
+ errors.push(`${ruleId}: unknown option '${key}'. Known options: ${Object.keys(spec).join(", ")}.`);
2985
+ continue;
2986
+ }
2987
+ if (s.kind === "integer") {
2988
+ if (typeof value !== "number" || !Number.isInteger(value)) {
2989
+ errors.push(`${ruleId}.${key} must be an integer.`);
2990
+ badKeys.add(key);
2991
+ } else if (s.min !== void 0 && value < s.min) {
2992
+ errors.push(`${ruleId}.${key} must be >= ${s.min}.`);
2993
+ badKeys.add(key);
2994
+ } else if (s.max !== void 0 && value > s.max) {
2995
+ errors.push(`${ruleId}.${key} must be <= ${s.max}.`);
2996
+ badKeys.add(key);
2997
+ }
2998
+ } else if (s.kind === "string-list") {
2999
+ if (!Array.isArray(value) || !value.every(isNonEmptyString)) {
3000
+ errors.push(`${ruleId}.${key} must be an array of non-empty strings.`);
3001
+ }
3002
+ } else if (typeof value !== "object" || value === null || Array.isArray(value) || !Object.values(value).every(isNonEmptyString)) {
3003
+ errors.push(`${ruleId}.${key} must be an object of string \u2192 non-empty string.`);
3004
+ }
3005
+ }
3006
+ const minSpec = spec.min;
3007
+ const maxSpec = spec.max;
3008
+ if (minSpec?.kind === "integer" && maxSpec?.kind === "integer" && !badKeys.has("min") && !badKeys.has("max") && !skipRangeCheck) {
3009
+ const base = baseline ?? defaultsOf(spec);
3010
+ const minVal = "min" in options ? options.min : base.min;
3011
+ const maxVal = "max" in options ? options.max : base.max;
3012
+ if (typeof minVal === "number" && typeof maxVal === "number" && minVal > maxVal) {
3013
+ errors.push(`${ruleId}: min (${minVal}) must be <= max (${maxVal}).`);
3014
+ }
3015
+ }
3016
+ return errors;
3017
+ }
3018
+ function isPlainObject(value) {
3019
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3020
+ }
3021
+ function otherOverrideNarrowsOppositeSide(overrides, selfIndex, key, side) {
3022
+ return overrides.some((entry, i) => {
3023
+ if (i === selfIndex || !isPlainObject(entry) || !isPlainObject(entry.rules)) return false;
3024
+ const setting = entry.rules[key];
3025
+ return isPlainObject(setting) && isPlainObject(setting.options) && side in setting.options;
3026
+ });
3027
+ }
3028
+ function shouldSkipRangeCheck(overrides, selfIndex, key, setting) {
3029
+ if (!isPlainObject(setting) || !isPlainObject(setting.options)) return false;
3030
+ const setsMin = "min" in setting.options;
3031
+ const setsMax = "max" in setting.options;
3032
+ if (setsMin === setsMax) return false;
3033
+ return otherOverrideNarrowsOppositeSide(overrides, selfIndex, key, setsMin ? "max" : "min");
3034
+ }
3035
+ function validateRuleSetting(label, ruleId, setting, spec, opts) {
3036
+ const expected = RULE_SETTING_VALUES.join("|");
3037
+ if (typeof setting === "string") {
3038
+ return RULE_SETTING_VALUES.includes(setting) ? [] : [`${label}: invalid setting '${setting}'; expected ${expected}.`];
3039
+ }
3040
+ if (!isPlainObject(setting)) {
3041
+ return [`${label}: must be ${expected} or an object with 'severity' and/or 'options'.`];
3042
+ }
3043
+ const errors = [];
3044
+ const unknownKeys = Object.keys(setting).filter((k) => k !== "severity" && k !== "options");
3045
+ if (unknownKeys.length > 0) {
3046
+ errors.push(`${label}: unknown key(s) ${unknownKeys.join(", ")}; expected severity, options.`);
3047
+ }
3048
+ if (setting.severity !== void 0 && !RULE_SETTING_VALUES.includes(setting.severity)) {
3049
+ errors.push(`${label}.severity: invalid setting '${String(setting.severity)}'; expected ${expected}.`);
3050
+ }
3051
+ if (setting.options === void 0) return errors;
3052
+ if (!opts.allowOptions) {
3053
+ errors.push(`${label}: options are not allowed on a category key.`);
3054
+ return errors;
3055
+ }
3056
+ if (!isPlainObject(setting.options)) {
3057
+ errors.push(`${label}.options: must be an object.`);
3058
+ return errors;
3059
+ }
3060
+ const optionErrors = validateRuleOptions(ruleId, spec, setting.options, opts.baseline, opts.skipRangeCheck);
3061
+ if (optionErrors.length > 0) errors.push(`${label}: ${optionErrors.join(" ")}`);
3062
+ return errors;
3063
+ }
3064
+
2560
3065
  // src/rules/perf/preconnect.ts
2561
3066
  var docsUrl3 = docsUrlFor("performance/preconnect");
2562
3067
  var recommendation3 = 'Add <link rel="preconnect"> (or dns-prefetch) for the third-party origin so the connection is set up early.';
2563
3068
  var THIRD_PARTY_ORIGINS = /* @__PURE__ */ new Set(["fonts.googleapis.com", "fonts.gstatic.com"]);
3069
+ var OPTIONS = { origins: { kind: "string-list", default: [...THIRD_PARTY_ORIGINS] } };
2564
3070
  function hostOf(href) {
2565
3071
  const m = /^(?:https?:)?\/\/([^/?#]+)/i.exec(href);
2566
3072
  return m ? m[1].toLowerCase() : void 0;
@@ -2577,17 +3083,30 @@ var performancePreconnect = {
2577
3083
  snippet: '<link rel="preconnect" href="https://fonts.googleapis.com" />',
2578
3084
  lang: "html"
2579
3085
  },
3086
+ options: OPTIONS,
2580
3087
  async check(ctx) {
2581
3088
  const out = [];
3089
+ const compiled = compileOverrides(ctx.config);
2582
3090
  for (const head of ctx.heads) {
2583
3091
  const referenced = /* @__PURE__ */ new Map();
2584
3092
  const covered = /* @__PURE__ */ new Set();
2585
3093
  for (const tag of head.tags) {
2586
3094
  if (tag.kind !== "link" && tag.kind !== "script" || typeof tag.href !== "string") continue;
2587
3095
  const host = hostOf(tag.href);
2588
- if (!host || !THIRD_PARTY_ORIGINS.has(host)) continue;
2589
- if (tag.kind === "link" && (tag.rel === "preconnect" || tag.rel === "dns-prefetch")) covered.add(host);
2590
- else if (!referenced.has(host)) referenced.set(host, tag.file);
3096
+ if (!host) continue;
3097
+ if (tag.kind === "link" && (tag.rel === "preconnect" || tag.rel === "dns-prefetch")) {
3098
+ covered.add(host);
3099
+ continue;
3100
+ }
3101
+ const o = resolveRuleOptions(
3102
+ "performance/preconnect",
3103
+ OPTIONS,
3104
+ ctx.config,
3105
+ { route: head.route, file: tag.file ?? head.file },
3106
+ compiled
3107
+ );
3108
+ if (!listOption(o, "origins").includes(host)) continue;
3109
+ if (!referenced.has(host)) referenced.set(host, tag.file);
2591
3110
  }
2592
3111
  if (referenced.size === 0) continue;
2593
3112
  const missing = [...referenced].filter(([host]) => !covered.has(host));
@@ -2638,7 +3157,7 @@ var seoIndexability = {
2638
3157
  rationale: "A noindex directive removes the page from search results; an accidental noindex on a public route silently deindexes it.",
2639
3158
  fix: FIX5,
2640
3159
  async check(ctx) {
2641
- const docsUrl7 = docsUrlFor("seo/indexability");
3160
+ const docsUrl11 = docsUrlFor("seo/indexability");
2642
3161
  const out = [];
2643
3162
  for (const head of ctx.heads) {
2644
3163
  const noindexed = head.tags.some((t) => t.kind === "meta" && t.name === "robots" && t.noindex === true);
@@ -2653,7 +3172,7 @@ var seoIndexability = {
2653
3172
  location: head.file,
2654
3173
  message: "Route is noindex \u2014 verify this is intentional",
2655
3174
  recommendation: 'If this route should be indexed, remove noindex from its <meta name="robots">.',
2656
- docsUrl: docsUrl7,
3175
+ docsUrl: docsUrl11,
2657
3176
  fix: { ...FIX5 }
2658
3177
  });
2659
3178
  }
@@ -2904,7 +3423,7 @@ function jsonldTags(head) {
2904
3423
  return head.tags.filter((t) => t.kind === "jsonld" && typeof t.jsonld === "string");
2905
3424
  }
2906
3425
  function jsonldRule(opts) {
2907
- const docsUrl7 = docsUrlFor(opts.id);
3426
+ const docsUrl11 = docsUrlFor(opts.id);
2908
3427
  return {
2909
3428
  id: opts.id,
2910
3429
  title: opts.title,
@@ -2932,7 +3451,7 @@ function jsonldRule(opts) {
2932
3451
  location: head.file,
2933
3452
  message: problem,
2934
3453
  recommendation: opts.recommendation,
2935
- docsUrl: docsUrl7,
3454
+ docsUrl: docsUrl11,
2936
3455
  ...opts.fix ? { fix: { ...opts.fix } } : {}
2937
3456
  } : {
2938
3457
  id: opts.id,
@@ -2942,7 +3461,7 @@ function jsonldRule(opts) {
2942
3461
  route: head.route,
2943
3462
  message: opts.label,
2944
3463
  recommendation: opts.recommendation,
2945
- docsUrl: docsUrl7
3464
+ docsUrl: docsUrl11
2946
3465
  }
2947
3466
  );
2948
3467
  }
@@ -2966,7 +3485,7 @@ var seoJsonLdValidity = {
2966
3485
  lang: "svelte"
2967
3486
  },
2968
3487
  async check(ctx) {
2969
- const docsUrl7 = docsUrlFor("seo/json-ld-validity");
3488
+ const docsUrl11 = docsUrlFor("seo/json-ld-validity");
2970
3489
  const out = [];
2971
3490
  for (const head of ctx.heads) {
2972
3491
  for (const tag of jsonldTags(head)) {
@@ -2985,7 +3504,7 @@ var seoJsonLdValidity = {
2985
3504
  location: head.file,
2986
3505
  message: problem,
2987
3506
  recommendation: "Make the JSON-LD valid JSON with both @context and @type.",
2988
- docsUrl: docsUrl7,
3507
+ docsUrl: docsUrl11,
2989
3508
  fix: { ...seoJsonLdValidity.fix }
2990
3509
  } : {
2991
3510
  id: "seo/json-ld-validity",
@@ -2995,7 +3514,7 @@ var seoJsonLdValidity = {
2995
3514
  route: head.route,
2996
3515
  message: "JSON-LD validity",
2997
3516
  recommendation: "Make the JSON-LD valid JSON with both @context and @type.",
2998
- docsUrl: docsUrl7
3517
+ docsUrl: docsUrl11
2999
3518
  }
3000
3519
  );
3001
3520
  }
@@ -3106,7 +3625,11 @@ function visibleLength(s) {
3106
3625
 
3107
3626
  // src/rules/seo/length-rule.ts
3108
3627
  function lengthRule(opts) {
3109
- const docsUrl7 = docsUrlFor(opts.id);
3628
+ const docsUrl11 = docsUrlFor(opts.id);
3629
+ const spec = {
3630
+ min: { kind: "integer", default: opts.min, min: 0 },
3631
+ max: { kind: "integer", default: opts.max, min: 1 }
3632
+ };
3110
3633
  return {
3111
3634
  id: opts.id,
3112
3635
  title: opts.title,
@@ -3114,15 +3637,22 @@ function lengthRule(opts) {
3114
3637
  severity: "info",
3115
3638
  scope: "route",
3116
3639
  rationale: opts.rationale,
3640
+ options: spec,
3117
3641
  async check(ctx) {
3118
3642
  const out = [];
3643
+ const compiled = compileOverrides(ctx.config);
3119
3644
  for (const head of ctx.heads) {
3120
3645
  const tag = head.tags.find(opts.match);
3121
3646
  if (!tag || typeof tag.text !== "string") continue;
3647
+ const location = tag.file ?? head.file;
3648
+ const o = resolveRuleOptions(opts.id, spec, ctx.config, { route: head.route, file: location }, compiled);
3649
+ const min = intOption(o, "min", opts.min);
3650
+ const max = intOption(o, "max", opts.max);
3651
+ const recommendation11 = typeof opts.recommendation === "function" ? opts.recommendation(o) : opts.recommendation;
3122
3652
  const len = visibleLength(tag.text);
3123
3653
  let problem;
3124
- if (len < opts.min) problem = `${opts.noun} is too short (${len} chars; aim for ${opts.min}\u2013${opts.max})`;
3125
- else if (len > opts.max) problem = `${opts.noun} is too long (${len} chars; aim for ${opts.min}\u2013${opts.max})`;
3654
+ if (len < min) problem = `${opts.noun} is too short (${len} chars; aim for ${min}\u2013${max})`;
3655
+ else if (len > max) problem = `${opts.noun} is too long (${len} chars; aim for ${min}\u2013${max})`;
3126
3656
  out.push(
3127
3657
  problem ? {
3128
3658
  id: opts.id,
@@ -3130,10 +3660,10 @@ function lengthRule(opts) {
3130
3660
  severity: "info",
3131
3661
  detection: PENALIZED,
3132
3662
  route: head.route,
3133
- location: tag.file ?? head.file,
3663
+ location,
3134
3664
  message: problem,
3135
- recommendation: opts.recommendation,
3136
- docsUrl: docsUrl7
3665
+ recommendation: recommendation11,
3666
+ docsUrl: docsUrl11
3137
3667
  } : {
3138
3668
  id: opts.id,
3139
3669
  category: "seo",
@@ -3141,8 +3671,8 @@ function lengthRule(opts) {
3141
3671
  detection: PASS,
3142
3672
  route: head.route,
3143
3673
  message: opts.label,
3144
- recommendation: opts.recommendation,
3145
- docsUrl: docsUrl7
3674
+ recommendation: recommendation11,
3675
+ docsUrl: docsUrl11
3146
3676
  }
3147
3677
  );
3148
3678
  }
@@ -3152,28 +3682,32 @@ function lengthRule(opts) {
3152
3682
  }
3153
3683
 
3154
3684
  // src/rules/seo/title-length.ts
3685
+ var MIN = 30;
3686
+ var MAX = 60;
3155
3687
  var seoTitleLength = lengthRule({
3156
3688
  id: "seo/title-length",
3157
3689
  title: "Title length",
3158
3690
  label: "Title length",
3159
3691
  noun: "Title",
3160
3692
  match: (t) => t.kind === "title",
3161
- min: 30,
3162
- max: 60,
3163
- recommendation: "Aim for a title of 30\u201360 characters so it is not truncated in search results.",
3693
+ min: MIN,
3694
+ max: MAX,
3695
+ 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.`,
3164
3696
  rationale: "A title that is too short wastes the strongest on-page signal; one that is too long is truncated in the SERP."
3165
3697
  });
3166
3698
 
3167
3699
  // src/rules/seo/description-length.ts
3700
+ var MIN2 = 70;
3701
+ var MAX2 = 160;
3168
3702
  var seoDescriptionLength = lengthRule({
3169
3703
  id: "seo/description-length",
3170
3704
  title: "Description length",
3171
3705
  label: "Description length",
3172
3706
  noun: "Description",
3173
3707
  match: (t) => t.kind === "meta" && t.name === "description",
3174
- min: 70,
3175
- max: 160,
3176
- recommendation: "Aim for a meta description of 70\u2013160 characters so it is not truncated in search results.",
3708
+ min: MIN2,
3709
+ max: MAX2,
3710
+ 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.`,
3177
3711
  rationale: "A description that is too short under-uses the SERP snippet; one that is too long is truncated by search engines."
3178
3712
  });
3179
3713
 
@@ -3323,7 +3857,7 @@ var seoSingleH1 = {
3323
3857
 
3324
3858
  // src/rules/seo/uniqueness-rule.ts
3325
3859
  function uniquenessRule(opts) {
3326
- const docsUrl7 = docsUrlFor(opts.id);
3860
+ const docsUrl11 = docsUrlFor(opts.id);
3327
3861
  return {
3328
3862
  id: opts.id,
3329
3863
  title: opts.title,
@@ -3353,7 +3887,7 @@ function uniquenessRule(opts) {
3353
3887
  location: e.file,
3354
3888
  message: `${opts.noun} is duplicated across ${n} routes`,
3355
3889
  recommendation: opts.recommendation,
3356
- docsUrl: docsUrl7
3890
+ docsUrl: docsUrl11
3357
3891
  } : {
3358
3892
  id: opts.id,
3359
3893
  category: "seo",
@@ -3362,7 +3896,7 @@ function uniquenessRule(opts) {
3362
3896
  route: e.route,
3363
3897
  message: opts.label,
3364
3898
  recommendation: opts.recommendation,
3365
- docsUrl: docsUrl7
3899
+ docsUrl: docsUrl11
3366
3900
  };
3367
3901
  });
3368
3902
  }
@@ -3450,7 +3984,7 @@ function isSuppressed(m, ruleId, line) {
3450
3984
  return (m.suppressions ?? []).some((s) => s.line === line && (!s.ruleIds || s.ruleIds.includes(ruleId)));
3451
3985
  }
3452
3986
  function kitModuleRule(opts) {
3453
- const docsUrl7 = docsUrlFor(opts.id);
3987
+ const docsUrl11 = docsUrlFor(opts.id);
3454
3988
  const severity = opts.severity ?? "warning";
3455
3989
  return {
3456
3990
  id: opts.id,
@@ -3474,7 +4008,7 @@ function kitModuleRule(opts) {
3474
4008
  route: m.file,
3475
4009
  message: opts.label,
3476
4010
  recommendation: opts.recommendation,
3477
- docsUrl: docsUrl7
4011
+ docsUrl: docsUrl11
3478
4012
  });
3479
4013
  continue;
3480
4014
  }
@@ -3489,7 +4023,7 @@ function kitModuleRule(opts) {
3489
4023
  ...b.line > 0 ? { line: b.line } : {},
3490
4024
  message: b.message,
3491
4025
  recommendation: opts.recommendation,
3492
- docsUrl: docsUrl7,
4026
+ docsUrl: docsUrl11,
3493
4027
  ...opts.fix ? { fix: { ...opts.fix } } : {}
3494
4028
  });
3495
4029
  }
@@ -3525,7 +4059,7 @@ function isSuppressed2(c, ruleId, line) {
3525
4059
  return (c.suppressions ?? []).some((s) => s.line === line && (!s.ruleIds || s.ruleIds.includes(ruleId)));
3526
4060
  }
3527
4061
  function componentRule(opts) {
3528
- const docsUrl7 = docsUrlFor(opts.id);
4062
+ const docsUrl11 = docsUrlFor(opts.id);
3529
4063
  const severity = opts.severity ?? "warning";
3530
4064
  return {
3531
4065
  id: opts.id,
@@ -3535,11 +4069,15 @@ function componentRule(opts) {
3535
4069
  scope: "component",
3536
4070
  rationale: opts.rationale,
3537
4071
  ...opts.fix ? { fix: opts.fix } : {},
4072
+ ...opts.options ? { options: opts.options } : {},
3538
4073
  async check(ctx) {
3539
4074
  const out = [];
4075
+ const compiled = compileOverrides(ctx.config);
3540
4076
  for (const c of ctx.components ?? []) {
3541
- if (!opts.applies(c)) continue;
3542
- const bad = opts.bad(c).filter((b) => !(b.line > 0 && isSuppressed2(c, opts.id, b.line)));
4077
+ const o = resolveRuleOptions(opts.id, opts.options, ctx.config, { route: c.file, file: c.file }, compiled);
4078
+ const recommendation11 = typeof opts.recommendation === "function" ? opts.recommendation(o) : opts.recommendation;
4079
+ if (!opts.applies(c, o, ctx)) continue;
4080
+ const bad = opts.bad(c, o, ctx).filter((b) => !(b.line > 0 && isSuppressed2(c, opts.id, b.line)));
3543
4081
  if (bad.length === 0) {
3544
4082
  out.push({
3545
4083
  id: opts.id,
@@ -3548,8 +4086,8 @@ function componentRule(opts) {
3548
4086
  detection: PASS3,
3549
4087
  route: c.file,
3550
4088
  message: opts.label,
3551
- recommendation: opts.recommendation,
3552
- docsUrl: docsUrl7
4089
+ recommendation: recommendation11,
4090
+ docsUrl: docsUrl11
3553
4091
  });
3554
4092
  continue;
3555
4093
  }
@@ -3563,8 +4101,8 @@ function componentRule(opts) {
3563
4101
  location: c.file,
3564
4102
  ...b.line > 0 ? { line: b.line } : {},
3565
4103
  message: b.message,
3566
- recommendation: opts.recommendation,
3567
- docsUrl: docsUrl7,
4104
+ recommendation: recommendation11,
4105
+ docsUrl: docsUrl11,
3568
4106
  ...opts.fix ? { fix: { ...opts.fix } } : {}
3569
4107
  });
3570
4108
  }
@@ -3694,6 +4232,25 @@ var correctnessNonreactiveBuiltinState = componentRule({
3694
4232
  }))
3695
4233
  });
3696
4234
 
4235
+ // src/rules/correctness/checkable-bind-value.ts
4236
+ var correctnessCheckableBindValue = componentRule({
4237
+ id: "correctness/checkable-bind-value",
4238
+ title: "bind:value on a checkable input",
4239
+ category: "correctness",
4240
+ severity: "warning",
4241
+ label: "bind:checked / bind:group on checkable inputs",
4242
+ recommendation: "Replace bind:value with bind:checked (single checkbox) or bind:group (checkbox list / radio group).",
4243
+ 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.",
4244
+ fix: {
4245
+ 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."
4246
+ },
4247
+ applies: (c) => c.checkableBindValues.length > 0,
4248
+ bad: (c) => c.checkableBindValues.map((v) => ({
4249
+ line: v.line,
4250
+ 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."
4251
+ }))
4252
+ });
4253
+
3697
4254
  // src/rules/correctness/orphan-effect.ts
3698
4255
  var correctnessOrphanEffect = componentRule({
3699
4256
  id: "correctness/orphan-effect",
@@ -3793,24 +4350,35 @@ var correctnessOrphanLifecycle = {
3793
4350
  }
3794
4351
  };
3795
4352
 
3796
- // src/rules/correctness/server-browser-global.ts
4353
+ // src/rules/correctness/base-path-navigation.ts
3797
4354
  var PENALIZED5 = { presence: "none", value: "absent" };
3798
4355
  var PASS5 = { presence: "own", value: "static" };
3799
- var ID2 = "correctness/server-browser-global";
4356
+ var ID2 = "correctness/base-path-navigation";
3800
4357
  var DOCS_URL2 = docsUrlFor(ID2);
3801
- var LABEL2 = "Server-safe module code";
3802
- 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).";
3803
- 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"`;
4358
+ var LABEL2 = "Base-path-aware navigation";
4359
+ var RECOMMENDATION2 = "Wrap root-relative paths in resolve() from '$app/paths' so they resolve against kit.paths.base.";
4360
+ var FIX7 = {
4361
+ description: "Import { resolve } from '$app/paths' and wrap the path: href={resolve('/about')}, goto(resolve('/about')), redirect(303, resolve('/login'))."
4362
+ };
4363
+ function messageFor2(link) {
4364
+ if (link.kind === "href") {
4365
+ 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'.`;
4366
+ }
4367
+ if (link.kind === "goto") {
4368
+ 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'.`;
4369
+ }
4370
+ 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'.`;
4371
+ }
3804
4372
  function isSuppressed4(suppressions, line) {
3805
4373
  return (suppressions ?? []).some((s) => s.line === line && (!s.ruleIds || s.ruleIds.includes(ID2)));
3806
4374
  }
3807
- function emitFile2(out, file, issues, suppressions) {
3808
- const bad = issues.filter((b) => !(b.line > 0 && isSuppressed4(suppressions, b.line)));
4375
+ function emitFile2(out, file, links, suppressions) {
4376
+ const bad = links.filter((l) => !(l.line > 0 && isSuppressed4(suppressions, l.line)));
3809
4377
  if (bad.length === 0) {
3810
4378
  out.push({
3811
4379
  id: ID2,
3812
4380
  category: "correctness",
3813
- severity: "critical",
4381
+ severity: "warning",
3814
4382
  detection: PASS5,
3815
4383
  route: file,
3816
4384
  message: LABEL2,
@@ -3819,23 +4387,90 @@ function emitFile2(out, file, issues, suppressions) {
3819
4387
  });
3820
4388
  return;
3821
4389
  }
3822
- for (const b of bad) {
4390
+ for (const l of bad) {
3823
4391
  out.push({
3824
4392
  id: ID2,
3825
4393
  category: "correctness",
3826
- severity: "critical",
4394
+ severity: "warning",
3827
4395
  detection: PENALIZED5,
3828
4396
  route: file,
3829
4397
  location: file,
4398
+ ...l.line > 0 ? { line: l.line } : {},
4399
+ message: messageFor2(l),
4400
+ recommendation: RECOMMENDATION2,
4401
+ docsUrl: DOCS_URL2,
4402
+ fix: { ...FIX7 }
4403
+ });
4404
+ }
4405
+ }
4406
+ var correctnessBasePathNavigation = {
4407
+ id: ID2,
4408
+ title: "Root-relative navigation under a base path",
4409
+ category: "correctness",
4410
+ severity: "warning",
4411
+ scope: "component",
4412
+ 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.",
4413
+ fix: { ...FIX7 },
4414
+ async check(ctx) {
4415
+ if (!ctx.project.kitPathsBase) return [];
4416
+ const out = [];
4417
+ for (const c of ctx.components ?? []) {
4418
+ const links = c.basePathLinks ?? [];
4419
+ if (links.length === 0) continue;
4420
+ emitFile2(out, c.file, links, c.suppressions);
4421
+ }
4422
+ for (const m of ctx.kitModules ?? []) {
4423
+ const links = m.basePathLinks ?? [];
4424
+ if (links.length === 0) continue;
4425
+ emitFile2(out, m.file, links, m.suppressions);
4426
+ }
4427
+ return out;
4428
+ }
4429
+ };
4430
+
4431
+ // src/rules/correctness/server-browser-global.ts
4432
+ var PENALIZED6 = { presence: "none", value: "absent" };
4433
+ var PASS6 = { presence: "own", value: "static" };
4434
+ var ID3 = "correctness/server-browser-global";
4435
+ var DOCS_URL3 = docsUrlFor(ID3);
4436
+ var LABEL3 = "Server-safe module code";
4437
+ 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).";
4438
+ 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"`;
4439
+ function isSuppressed5(suppressions, line) {
4440
+ return (suppressions ?? []).some((s) => s.line === line && (!s.ruleIds || s.ruleIds.includes(ID3)));
4441
+ }
4442
+ function emitFile3(out, file, issues, suppressions) {
4443
+ const bad = issues.filter((b) => !(b.line > 0 && isSuppressed5(suppressions, b.line)));
4444
+ if (bad.length === 0) {
4445
+ out.push({
4446
+ id: ID3,
4447
+ category: "correctness",
4448
+ severity: "critical",
4449
+ detection: PASS6,
4450
+ route: file,
4451
+ message: LABEL3,
4452
+ recommendation: RECOMMENDATION3,
4453
+ docsUrl: DOCS_URL3
4454
+ });
4455
+ return;
4456
+ }
4457
+ for (const b of bad) {
4458
+ out.push({
4459
+ id: ID3,
4460
+ category: "correctness",
4461
+ severity: "critical",
4462
+ detection: PENALIZED6,
4463
+ route: file,
4464
+ location: file,
3830
4465
  ...b.line > 0 ? { line: b.line } : {},
3831
4466
  message: b.message,
3832
- recommendation: RECOMMENDATION2,
3833
- docsUrl: DOCS_URL2
4467
+ recommendation: RECOMMENDATION3,
4468
+ docsUrl: DOCS_URL3
3834
4469
  });
3835
4470
  }
3836
4471
  }
3837
4472
  var correctnessServerBrowserGlobal = {
3838
- id: ID2,
4473
+ id: ID3,
3839
4474
  title: "Browser global in server module code",
3840
4475
  category: "correctness",
3841
4476
  severity: "critical",
@@ -3846,7 +4481,7 @@ var correctnessServerBrowserGlobal = {
3846
4481
  for (const c of ctx.components ?? []) {
3847
4482
  const refs = (c.browserGlobalRefs ?? []).filter((r) => r.context === "module");
3848
4483
  if (refs.length === 0) continue;
3849
- emitFile2(
4484
+ emitFile3(
3850
4485
  out,
3851
4486
  c.file,
3852
4487
  refs.map((r) => ({ line: r.line, message: moduleMessage(r.name) })),
@@ -3856,7 +4491,7 @@ var correctnessServerBrowserGlobal = {
3856
4491
  for (const m of ctx.kitModules ?? []) {
3857
4492
  const refs = m.browserGlobalRefs ?? [];
3858
4493
  if (refs.length === 0) continue;
3859
- emitFile2(
4494
+ emitFile3(
3860
4495
  out,
3861
4496
  m.file,
3862
4497
  refs.map((r) => ({
@@ -3972,33 +4607,733 @@ var securitySharedStateImport = kitModuleRule({
3972
4607
  });
3973
4608
 
3974
4609
  // src/rules/architecture/component-size.ts
3975
- var MAX_LOC = 400;
4610
+ var MAX_LOC = 200;
3976
4611
  var architectureComponentSize = componentRule({
3977
4612
  id: "architecture/component-size",
3978
4613
  title: "Component size",
3979
4614
  category: "architecture",
3980
4615
  severity: "info",
3981
4616
  label: "Component size",
3982
- recommendation: `Split components over ${MAX_LOC} lines into smaller, focused pieces.`,
4617
+ options: { max: { kind: "integer", default: MAX_LOC, min: 1 } },
4618
+ recommendation: (o) => `Split components over ${intOption(o, "max", MAX_LOC)} lines into smaller, focused pieces.`,
3983
4619
  rationale: "A very large component is hard to read, test, and reuse, and is a common sign that several responsibilities should be split out.",
3984
4620
  applies: (c) => c.loc > 0,
3985
4621
  // skip unanalyzable files (loc 0 = read/parse failure), don't PASS them
3986
- bad: (c) => c.loc > MAX_LOC ? [{ line: 1, message: `Component is ${c.loc} lines (over ${MAX_LOC})` }] : []
4622
+ bad: (c, o) => {
4623
+ const max = intOption(o, "max", MAX_LOC);
4624
+ return c.loc > max ? [{ line: 1, message: `Component is ${c.loc} lines (over ${max})` }] : [];
4625
+ }
3987
4626
  });
3988
4627
 
3989
4628
  // src/rules/architecture/prop-count.ts
3990
- var MAX_PROPS = 10;
4629
+ var MAX_PROPS = 6;
3991
4630
  var architecturePropCount = componentRule({
3992
4631
  id: "architecture/prop-count",
3993
4632
  title: "Prop count",
3994
4633
  category: "architecture",
3995
4634
  severity: "info",
3996
4635
  label: "Prop count",
3997
- recommendation: `Group related props into an object, or split the component, when it takes more than ${MAX_PROPS} props.`,
4636
+ options: { max: { kind: "integer", default: MAX_PROPS, min: 1 } },
4637
+ recommendation: (o) => `Group related props into an object, or split the component, when it takes more than ${intOption(o, "max", MAX_PROPS)} props.`,
3998
4638
  rationale: "A component taking many props is usually doing too much; grouping or splitting keeps its API understandable.",
3999
4639
  applies: (c) => c.propCount > 0,
4000
4640
  // only components whose props we could count
4001
- bad: (c) => c.propCount > MAX_PROPS ? [{ line: 1, message: `Component takes ${c.propCount} props (over ${MAX_PROPS})` }] : []
4641
+ bad: (c, o) => {
4642
+ const max = intOption(o, "max", MAX_PROPS);
4643
+ return c.propCount > max ? [{ line: 1, message: `Component takes ${c.propCount} props (over ${max})` }] : [];
4644
+ }
4645
+ });
4646
+
4647
+ // src/rules/architecture/private-scope-import.ts
4648
+ var docsUrl7 = docsUrlFor("architecture/private-scope-import");
4649
+ var recommendation7 = "Move the unit to the directory shared by all of its importers, or import it only from inside its own scope.";
4650
+ var OPTIONS2 = { scopes: { kind: "string-list", default: [] } };
4651
+ function ancestorDirs(file) {
4652
+ const segments = file.split("/");
4653
+ const out = [];
4654
+ for (let i = segments.length - 1; i > 0; i--) out.push(segments.slice(0, i).join("/"));
4655
+ return out;
4656
+ }
4657
+ function privateScopeOf(target, patterns) {
4658
+ for (const dir of ancestorDirs(target)) {
4659
+ if (!patterns.some((p) => p.test(dir))) continue;
4660
+ const cut = dir.lastIndexOf("/");
4661
+ return cut === -1 ? "" : dir.slice(0, cut);
4662
+ }
4663
+ return void 0;
4664
+ }
4665
+ function isInside(file, boundary) {
4666
+ return boundary === "" || file.startsWith(`${boundary}/`);
4667
+ }
4668
+ var architecturePrivateScopeImport = {
4669
+ id: "architecture/private-scope-import",
4670
+ title: "Private-scope import",
4671
+ category: "architecture",
4672
+ severity: "info",
4673
+ scope: "component",
4674
+ 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.",
4675
+ fix: {
4676
+ description: "Move this unit out of its private scope, to the directory shared by all of its importers, and update this import."
4677
+ },
4678
+ options: OPTIONS2,
4679
+ async check(ctx) {
4680
+ const out = [];
4681
+ const compiled = compileOverrides(ctx.config);
4682
+ const patternCache = /* @__PURE__ */ new Map();
4683
+ const compileScopes = (scopes) => {
4684
+ const key = JSON.stringify(scopes);
4685
+ let patterns = patternCache.get(key);
4686
+ if (patterns === void 0) {
4687
+ patterns = scopes.map((scope) => {
4688
+ const marker = scope.endsWith("/**") ? scope.slice(0, -3) : scope;
4689
+ return routeGlobToRegExp(marker);
4690
+ });
4691
+ patternCache.set(key, patterns);
4692
+ }
4693
+ return patterns;
4694
+ };
4695
+ for (const c of ctx.components ?? []) {
4696
+ const o = resolveRuleOptions(
4697
+ "architecture/private-scope-import",
4698
+ OPTIONS2,
4699
+ ctx.config,
4700
+ { route: c.file, file: c.file },
4701
+ compiled
4702
+ );
4703
+ const scopes = listOption(o, "scopes");
4704
+ if (scopes.length === 0) continue;
4705
+ const patterns = compileScopes(scopes);
4706
+ const spans = c.importSpans ?? c.imports.map((source) => ({ source, line: 0 }));
4707
+ let sawScopedImport = false;
4708
+ const violations = [];
4709
+ for (const { source, line } of spans) {
4710
+ const target = resolveRepoLocalPath(source, c.file, ctx.project.kitAliases);
4711
+ if (target === void 0) continue;
4712
+ const boundary = privateScopeOf(target, patterns);
4713
+ if (boundary === void 0) continue;
4714
+ sawScopedImport = true;
4715
+ if (isInside(c.file, boundary)) continue;
4716
+ violations.push({ line, message: `${target} is private to ${boundary}` });
4717
+ }
4718
+ if (!sawScopedImport) continue;
4719
+ const visible = violations.filter(
4720
+ (v) => !(v.line > 0 && isSuppressed2(c, "architecture/private-scope-import", v.line))
4721
+ );
4722
+ if (visible.length === 0) {
4723
+ out.push({
4724
+ id: "architecture/private-scope-import",
4725
+ category: "architecture",
4726
+ severity: "info",
4727
+ detection: { presence: "own", value: "static" },
4728
+ route: c.file,
4729
+ message: "No private-scope imports",
4730
+ recommendation: recommendation7,
4731
+ docsUrl: docsUrl7
4732
+ });
4733
+ continue;
4734
+ }
4735
+ for (const v of visible) {
4736
+ out.push({
4737
+ id: "architecture/private-scope-import",
4738
+ category: "architecture",
4739
+ severity: "info",
4740
+ detection: { presence: "none", value: "absent" },
4741
+ route: c.file,
4742
+ location: c.file,
4743
+ ...v.line > 0 ? { line: v.line } : {},
4744
+ message: v.message,
4745
+ recommendation: recommendation7,
4746
+ docsUrl: docsUrl7,
4747
+ fix: { ...architecturePrivateScopeImport.fix }
4748
+ });
4749
+ }
4750
+ }
4751
+ return out;
4752
+ }
4753
+ };
4754
+
4755
+ // src/rules/architecture/declarations.ts
4756
+ function ancestorDirs2(file) {
4757
+ const segments = file.split("/");
4758
+ const out = [];
4759
+ for (let i = 1; i < segments.length; i++) out.push(segments.slice(0, i).join("/"));
4760
+ return out;
4761
+ }
4762
+ function baseName(dir) {
4763
+ const cut = dir.lastIndexOf("/");
4764
+ return cut === -1 ? dir : dir.slice(cut + 1);
4765
+ }
4766
+ function childDirs(dirs) {
4767
+ const out = /* @__PURE__ */ new Map();
4768
+ for (const dir of dirs) {
4769
+ const cut = dir.lastIndexOf("/");
4770
+ if (cut === -1) continue;
4771
+ const parent = dir.slice(0, cut);
4772
+ let kids = out.get(parent);
4773
+ if (kids === void 0) out.set(parent, kids = []);
4774
+ kids.push(dir);
4775
+ }
4776
+ for (const kids of out.values()) kids.sort();
4777
+ return out;
4778
+ }
4779
+ function childFiles(files) {
4780
+ const out = /* @__PURE__ */ new Map();
4781
+ for (const file of files) {
4782
+ const cut = file.lastIndexOf("/");
4783
+ if (cut === -1) continue;
4784
+ const dir = file.slice(0, cut);
4785
+ let own = out.get(dir);
4786
+ if (own === void 0) out.set(dir, own = []);
4787
+ own.push(file.slice(cut + 1));
4788
+ }
4789
+ for (const own of out.values()) own.sort();
4790
+ return out;
4791
+ }
4792
+ function splitNames(value) {
4793
+ const out = [];
4794
+ for (const raw of value.split("|")) {
4795
+ const token = raw.trim();
4796
+ if (token.length > 0) out.push(token);
4797
+ }
4798
+ return out;
4799
+ }
4800
+ function keyShape(key) {
4801
+ const parts = key.split("/");
4802
+ let doubleStars = 0;
4803
+ for (const p of parts) if (p === "**") doubleStars++;
4804
+ return { segments: parts.length, doubleStars };
4805
+ }
4806
+ function createKeyCompiler() {
4807
+ const cache = /* @__PURE__ */ new Map();
4808
+ return (globs, bareGuard = false) => {
4809
+ const cacheKey = JSON.stringify([globs, bareGuard]);
4810
+ let entry = cache.get(cacheKey);
4811
+ if (entry === void 0) {
4812
+ entry = globs.map((key) => ({
4813
+ key,
4814
+ re: routeGlobToRegExp(key),
4815
+ ...keyShape(key),
4816
+ ...bareGuard && key.endsWith("/**") ? { barePrefixRe: routeGlobToRegExp(key.slice(0, -3)) } : {}
4817
+ }));
4818
+ cache.set(cacheKey, entry);
4819
+ }
4820
+ return entry;
4821
+ };
4822
+ }
4823
+ function matchKeys(dir, compiled) {
4824
+ const matched = [];
4825
+ let best;
4826
+ for (const entry of compiled) {
4827
+ if (entry.barePrefixRe?.test(dir)) continue;
4828
+ if (!entry.re.test(dir)) continue;
4829
+ matched.push(entry.key);
4830
+ if (best === void 0 || moreSpecificShaped(entry, best)) best = entry;
4831
+ }
4832
+ return best === void 0 ? { matched } : { matched, best: best.key };
4833
+ }
4834
+ function moreSpecificShaped(a, b) {
4835
+ if (a.segments !== b.segments) return a.segments > b.segments;
4836
+ if (a.doubleStars !== b.doubleStars) return a.doubleStars < b.doubleStars;
4837
+ if (a.key.length !== b.key.length) return a.key.length > b.key.length;
4838
+ return a.key < b.key;
4839
+ }
4840
+ function moreSpecificGlob(a, b) {
4841
+ return moreSpecificShaped({ key: a, ...keyShape(a) }, { key: b, ...keyShape(b) });
4842
+ }
4843
+ function reportAt(dir, files) {
4844
+ const prefix = `${dir}/`;
4845
+ const under = files.filter((f) => f.startsWith(prefix)).sort();
4846
+ return under.find((f) => !f.slice(prefix.length).includes("/")) ?? under[0];
4847
+ }
4848
+ function isExcluded(dir, ancestors, excluded) {
4849
+ return excluded.some(({ re }) => re.test(dir) || ancestors.some((a) => re.test(a)));
4850
+ }
4851
+ function keysMatchingAny(keys, dirs, compile) {
4852
+ const hit = /* @__PURE__ */ new Set();
4853
+ if (keys.length === 0 || dirs.length === 0) return hit;
4854
+ for (const { key, re, barePrefixRe } of compile(keys, true)) {
4855
+ if (dirs.some((d) => !barePrefixRe?.test(d) && re.test(d))) hit.add(key);
4856
+ }
4857
+ return hit;
4858
+ }
4859
+ function classifyUnusedKeys(unused, excludedDirs, compile) {
4860
+ const out = /* @__PURE__ */ new Map();
4861
+ if (unused.length === 0) return out;
4862
+ const shadowed = keysMatchingAny(unused, excludedDirs, compile);
4863
+ for (const key of unused) out.set(key, shadowed.has(key) ? "only-excluded" : "no-match");
4864
+ return out;
4865
+ }
4866
+
4867
+ // src/rules/architecture/unit-entry-file.ts
4868
+ var ID4 = "architecture/unit-entry-file";
4869
+ var docsUrl8 = docsUrlFor(ID4);
4870
+ var recommendation8 = "Give every declared unit directory a file named after it, or stop declaring that directory a unit.";
4871
+ var OPTIONS3 = {
4872
+ units: { kind: "string-map", default: {} },
4873
+ pascalCaseUnits: { kind: "string-map", default: {} },
4874
+ exclude: { kind: "string-list", default: [] }
4875
+ };
4876
+ function isPascalCase(name) {
4877
+ const c = name.charCodeAt(0);
4878
+ return c >= 65 && c <= 90;
4879
+ }
4880
+ var architectureUnitEntryFile = {
4881
+ id: ID4,
4882
+ title: "Unit entry file",
4883
+ category: "architecture",
4884
+ severity: "info",
4885
+ scope: "component",
4886
+ rationale: "A directory named after a unit but missing that unit's entry file is either an incomplete unit or a grouping wearing the wrong name; either way the tree no longer says what it means, and tooling that resolves by convention starts guessing.",
4887
+ fix: {
4888
+ description: "Make the directory and its entry file agree \u2014 add the entry file, or stop declaring this directory a unit."
4889
+ },
4890
+ options: OPTIONS3,
4891
+ async check(ctx) {
4892
+ const files = ctx.sourceFiles;
4893
+ if (files === void 0) return [];
4894
+ if (!isMentionedAnywhere(ctx.config, ID4)) return [];
4895
+ const compiledOverrides = compileOverrides(ctx.config);
4896
+ const dirs = /* @__PURE__ */ new Set();
4897
+ for (const f of files) for (const d of ancestorDirs2(f)) dirs.add(d);
4898
+ const fileSet = new Set(files);
4899
+ const compile = createKeyCompiler();
4900
+ const out = [];
4901
+ const globalOptions = resolveRuleOptions(ID4, OPTIONS3, ctx.config);
4902
+ const globalKeys = /* @__PURE__ */ new Set([
4903
+ ...Object.keys(mapOption(globalOptions, "units")),
4904
+ ...Object.keys(mapOption(globalOptions, "pascalCaseUnits"))
4905
+ ]);
4906
+ const usedKeys = /* @__PURE__ */ new Set();
4907
+ const excludedDirs = [];
4908
+ const matchedSurviving = /* @__PURE__ */ new Set();
4909
+ for (const dir of [...dirs].sort()) {
4910
+ const o = resolveRuleOptions(ID4, OPTIONS3, ctx.config, { route: dir, file: dir }, compiledOverrides);
4911
+ const units = mapOption(o, "units");
4912
+ const pascalUnits = mapOption(o, "pascalCaseUnits");
4913
+ if (Object.keys(units).length === 0 && Object.keys(pascalUnits).length === 0) continue;
4914
+ const excluded = compile(listOption(o, "exclude"));
4915
+ const ancestors = ancestorDirs2(dir);
4916
+ if (isExcluded(dir, ancestors, excluded)) {
4917
+ excludedDirs.push(dir);
4918
+ continue;
4919
+ }
4920
+ const byPath = matchKeys(dir, compile(Object.keys(units), true));
4921
+ const byCasing = matchKeys(dir, compile(Object.keys(pascalUnits), true));
4922
+ for (const k of byPath.matched) if (globalKeys.has(k)) usedKeys.add(k);
4923
+ for (const k of byPath.matched) if (globalKeys.has(k)) matchedSurviving.add(k);
4924
+ for (const k of byCasing.matched) if (globalKeys.has(k)) matchedSurviving.add(k);
4925
+ if (isPascalCase(baseName(dir))) {
4926
+ for (const k of byCasing.matched) if (globalKeys.has(k)) usedKeys.add(k);
4927
+ }
4928
+ let ext = byPath.best === void 0 ? void 0 : units[byPath.best];
4929
+ const viaUnits = ext !== void 0;
4930
+ if (ext === void 0 && isPascalCase(baseName(dir))) {
4931
+ ext = byCasing.best === void 0 ? void 0 : pascalUnits[byCasing.best];
4932
+ }
4933
+ if (ext === void 0) continue;
4934
+ const expected = `${dir}/${baseName(dir)}${ext}`;
4935
+ if (fileSet.has(expected)) {
4936
+ out.push({
4937
+ id: ID4,
4938
+ category: "architecture",
4939
+ severity: "info",
4940
+ detection: { presence: "own", value: "static" },
4941
+ route: expected,
4942
+ message: "Unit entry file",
4943
+ recommendation: recommendation8,
4944
+ docsUrl: docsUrl8
4945
+ });
4946
+ continue;
4947
+ }
4948
+ const at = reportAt(dir, files);
4949
+ if (at === void 0) continue;
4950
+ out.push({
4951
+ id: ID4,
4952
+ category: "architecture",
4953
+ severity: "info",
4954
+ detection: { presence: "none", value: "absent" },
4955
+ route: dir,
4956
+ location: at,
4957
+ message: `${dir} declares a unit but has no ${expected}`,
4958
+ recommendation: recommendation8,
4959
+ docsUrl: docsUrl8,
4960
+ // Which declaration matched decides the wording: a `units` match like functions/getFoo/
4961
+ // is already camelCase, so telling its author to rename it would be nonsense.
4962
+ fix: {
4963
+ description: viaUnits ? `Add ${baseName(dir)}${ext} to this directory, or remove it from the units declaration.` : `Add the same-named entry file, or rename the directory to camelCase if it is a grouping.`
4964
+ }
4965
+ });
4966
+ }
4967
+ const inertKeys = [...globalKeys].filter((key) => !usedKeys.has(key)).sort();
4968
+ if (inertKeys.length > 0) {
4969
+ const shadowed = inertKeys.filter((k) => !matchedSurviving.has(k));
4970
+ const reasons = classifyUnusedKeys(shadowed, excludedDirs, compile);
4971
+ const why = (k) => reasons.get(k) === "only-excluded" ? "matched only excluded directories" : "matched no directory";
4972
+ const message = inertKeys.length === 1 ? `The declaration '${inertKeys[0]}' ${why(inertKeys[0])}, so it checks nothing.` : `These declarations check nothing: ${inertKeys.map((k) => `'${k}' (${why(k)})`).join(", ")}.`;
4973
+ out.push({
4974
+ id: ID4,
4975
+ category: "architecture",
4976
+ severity: "info",
4977
+ detection: { presence: "none", value: "absent" },
4978
+ message,
4979
+ recommendation: "Correct the glob, or remove the declaration.",
4980
+ docsUrl: docsUrl8
4981
+ });
4982
+ }
4983
+ return out;
4984
+ }
4985
+ };
4986
+
4987
+ // src/rules/architecture/casing.ts
4988
+ var CASINGS = {
4989
+ camelCase: /^[a-z][a-zA-Z0-9]*$/,
4990
+ PascalCase: /^[A-Z][a-zA-Z0-9]*$/,
4991
+ "kebab-case": /^[a-z0-9]+(-[a-z0-9]+)*$/,
4992
+ snake_case: /^[a-z0-9]+(_[a-z0-9]+)*$/
4993
+ };
4994
+ function parseCasings(value) {
4995
+ const known = [];
4996
+ const unknown = [];
4997
+ for (const name of splitNames(value)) {
4998
+ if (Object.hasOwn(CASINGS, name)) known.push(name);
4999
+ else unknown.push(name);
5000
+ }
5001
+ return { known, unknown };
5002
+ }
5003
+ function decodeSegment(name) {
5004
+ let inner = name;
5005
+ if (inner.length > 2 && inner.startsWith("(") && inner.endsWith(")")) inner = inner.slice(1, -1);
5006
+ else if (inner.length > 4 && inner.startsWith("[[") && inner.endsWith("]]")) inner = inner.slice(2, -2);
5007
+ else if (inner.length > 2 && inner.startsWith("[") && inner.endsWith("]")) inner = inner.slice(1, -1);
5008
+ if (inner.startsWith("...")) inner = inner.slice(3);
5009
+ const eq = inner.indexOf("=");
5010
+ if (eq !== -1) inner = inner.slice(0, eq);
5011
+ if (inner.length === 0 || /[[\]()]/.test(inner)) return void 0;
5012
+ return inner;
5013
+ }
5014
+ function satisfiesCasing(name, allowed) {
5015
+ if (!/[a-zA-Z]/.test(name)) return true;
5016
+ return allowed.some((c) => Object.hasOwn(CASINGS, c) && CASINGS[c].test(name));
5017
+ }
5018
+
5019
+ // src/rules/architecture/directory-naming.ts
5020
+ var ID5 = "architecture/directory-naming";
5021
+ var docsUrl9 = docsUrlFor(ID5);
5022
+ var recommendation9 = "Name each directory in the casing its location declares, or narrow the declaration.";
5023
+ var OPTIONS4 = {
5024
+ directories: { kind: "string-map", default: {} },
5025
+ exclude: { kind: "string-list", default: [] }
5026
+ };
5027
+ var architectureDirectoryNaming = {
5028
+ id: ID5,
5029
+ title: "Directory naming",
5030
+ category: "architecture",
5031
+ severity: "info",
5032
+ scope: "component",
5033
+ rationale: "A directory whose name breaks the convention its location declares stops carrying the meaning the convention gave it, and every reader \u2014 human or agent \u2014 has to open the directory to learn what it is.",
5034
+ fix: {
5035
+ description: "Rename the directory to the declared casing, or narrow the declaration that governs it."
5036
+ },
5037
+ options: OPTIONS4,
5038
+ async check(ctx) {
5039
+ const files = ctx.sourceFiles;
5040
+ if (files === void 0) return [];
5041
+ if (!isMentionedAnywhere(ctx.config, ID5)) return [];
5042
+ const compiledOverrides = compileOverrides(ctx.config);
5043
+ const dirs = /* @__PURE__ */ new Set();
5044
+ for (const f of files) for (const d of ancestorDirs2(f)) dirs.add(d);
5045
+ const compile = createKeyCompiler();
5046
+ const parsed = /* @__PURE__ */ new Map();
5047
+ const casingsOf = (value) => {
5048
+ let p = parsed.get(value);
5049
+ if (p === void 0) parsed.set(value, p = parseCasings(value));
5050
+ return p;
5051
+ };
5052
+ const out = [];
5053
+ const globalOptions = resolveRuleOptions(ID5, OPTIONS4, ctx.config);
5054
+ const globalMap = mapOption(globalOptions, "directories");
5055
+ const globalKeys = new Set(Object.keys(globalMap));
5056
+ const usedKeys = /* @__PURE__ */ new Set();
5057
+ const excludedDirs = [];
5058
+ for (const dir of [...dirs].sort()) {
5059
+ const o = resolveRuleOptions(ID5, OPTIONS4, ctx.config, { route: dir, file: dir }, compiledOverrides);
5060
+ const declared = mapOption(o, "directories");
5061
+ if (Object.keys(declared).length === 0) continue;
5062
+ const excluded = compile(listOption(o, "exclude"));
5063
+ if (isExcluded(dir, ancestorDirs2(dir), excluded)) {
5064
+ excludedDirs.push(dir);
5065
+ continue;
5066
+ }
5067
+ const live = Object.keys(declared).filter((k) => casingsOf(declared[k]).known.length > 0);
5068
+ const m = matchKeys(dir, compile(live, true));
5069
+ for (const k of m.matched) if (globalKeys.has(k)) usedKeys.add(k);
5070
+ if (m.best === void 0) continue;
5071
+ const decoded = decodeSegment(baseName(dir));
5072
+ if (decoded === void 0) continue;
5073
+ const allowed = casingsOf(declared[m.best]).known;
5074
+ if (satisfiesCasing(decoded, allowed)) continue;
5075
+ const at = reportAt(dir, files);
5076
+ if (at === void 0) continue;
5077
+ out.push({
5078
+ id: ID5,
5079
+ category: "architecture",
5080
+ severity: "info",
5081
+ detection: { presence: "none", value: "absent" },
5082
+ route: dir,
5083
+ location: at,
5084
+ message: `${dir} must be ${allowed.join(" or ")}.`,
5085
+ recommendation: recommendation9,
5086
+ docsUrl: docsUrl9,
5087
+ fix: { description: "Rename the directory, or narrow the declaration that governs it." }
5088
+ });
5089
+ }
5090
+ const notes = /* @__PURE__ */ new Map();
5091
+ for (const key of globalKeys) {
5092
+ const { known, unknown } = casingsOf(globalMap[key]);
5093
+ if (known.length > 0 && unknown.length === 0) continue;
5094
+ const names = unknown.map((u) => `'${u}'`).join(", ");
5095
+ notes.set(
5096
+ key,
5097
+ known.length === 0 ? unknown.length === 0 ? "the value names no casing at all, so it checks nothing" : `unknown casing name ${names}, so it checks nothing` : `unknown casing name ${names}; the rest of the value still applies`
5098
+ );
5099
+ }
5100
+ const unclassified = [...globalKeys].filter(
5101
+ (key) => !notes.has(key) && !usedKeys.has(key) && casingsOf(globalMap[key]).known.length > 0
5102
+ );
5103
+ const reasons = classifyUnusedKeys(unclassified, excludedDirs, compile);
5104
+ for (const [key, reason] of reasons) {
5105
+ notes.set(key, reason === "only-excluded" ? "matched only excluded directories" : "matched no directory");
5106
+ }
5107
+ const reported = [...notes.keys()].sort();
5108
+ if (reported.length > 0) {
5109
+ const message = reported.length === 1 ? `The declaration '${reported[0]}' does not check what it says: ${notes.get(reported[0])}.` : `These declarations do not check what they say: ${reported.map((k) => `'${k}' (${notes.get(k)})`).join(", ")}.`;
5110
+ out.push({
5111
+ id: ID5,
5112
+ category: "architecture",
5113
+ severity: "info",
5114
+ detection: { presence: "none", value: "absent" },
5115
+ message,
5116
+ recommendation: "Correct the glob or the casing name, or remove the declaration.",
5117
+ docsUrl: docsUrl9
5118
+ });
5119
+ }
5120
+ return out;
5121
+ }
5122
+ };
5123
+
5124
+ // src/rules/architecture/reserved-directory-names.ts
5125
+ var ID6 = "architecture/reserved-directory-names";
5126
+ var docsUrl10 = docsUrlFor(ID6);
5127
+ var recommendation10 = "Use one of the names this location declares, or add the new name to the declaration.";
5128
+ var OPTIONS5 = {
5129
+ scopes: { kind: "string-map", default: {} },
5130
+ unitScopes: { kind: "string-map", default: {} },
5131
+ exclude: { kind: "string-list", default: [] }
5132
+ };
5133
+ function stem(file) {
5134
+ const dot = file.indexOf(".");
5135
+ return dot === -1 ? file : file.slice(0, dot);
5136
+ }
5137
+ function isUnitDir(dir, filesIn) {
5138
+ const name = baseName(dir);
5139
+ const first = name.charCodeAt(0);
5140
+ if (!(first >= 65 && first <= 90)) return false;
5141
+ const own = filesIn.get(dir);
5142
+ return own !== void 0 && own.some((f) => stem(f) === name);
5143
+ }
5144
+ var architectureReservedDirectoryNames = {
5145
+ id: ID6,
5146
+ title: "Reserved directory names",
5147
+ category: "architecture",
5148
+ severity: "info",
5149
+ scope: "component",
5150
+ rationale: "A closed set of directory names is only worth writing down if it stays closed: one directory outside it and the table stops describing the tree, so every reader has to open a directory to learn what it holds.",
5151
+ fix: {
5152
+ description: "Rename the directory to a declared name, move it under one of them, or add its name to the declaration."
5153
+ },
5154
+ options: OPTIONS5,
5155
+ async check(ctx) {
5156
+ const files = ctx.sourceFiles;
5157
+ if (files === void 0) return [];
5158
+ if (!isMentionedAnywhere(ctx.config, ID6)) return [];
5159
+ const compiledOverrides = compileOverrides(ctx.config);
5160
+ const dirs = /* @__PURE__ */ new Set();
5161
+ for (const f of files) for (const d of ancestorDirs2(f)) dirs.add(d);
5162
+ const kids = childDirs(dirs);
5163
+ const filesIn = childFiles(files);
5164
+ const compile = createKeyCompiler();
5165
+ const parsed = /* @__PURE__ */ new Map();
5166
+ const namesOf = (value) => {
5167
+ let n = parsed.get(value);
5168
+ if (n === void 0) parsed.set(value, n = splitNames(value));
5169
+ return n;
5170
+ };
5171
+ const out = [];
5172
+ const globalOptions = resolveRuleOptions(ID6, OPTIONS5, ctx.config);
5173
+ const globalScopes = mapOption(globalOptions, "scopes");
5174
+ const globalUnits = mapOption(globalOptions, "unitScopes");
5175
+ const globalKeys = /* @__PURE__ */ new Set([...Object.keys(globalScopes), ...Object.keys(globalUnits)]);
5176
+ const usedKeys = /* @__PURE__ */ new Set();
5177
+ const excludedDirs = [];
5178
+ const nonUnitDirs = [];
5179
+ const collisions = /* @__PURE__ */ new Set();
5180
+ const noteCollisions = (scopesMap, unitMap) => {
5181
+ for (const key of Object.keys(scopesMap)) {
5182
+ if (!Object.hasOwn(unitMap, key)) continue;
5183
+ if (namesOf(scopesMap[key]).length === 0) continue;
5184
+ if (namesOf(unitMap[key]).length === 0) continue;
5185
+ collisions.add(key);
5186
+ }
5187
+ };
5188
+ noteCollisions(globalScopes, globalUnits);
5189
+ for (const dir of [...dirs].sort()) {
5190
+ const o = resolveRuleOptions(ID6, OPTIONS5, ctx.config, { route: dir, file: dir }, compiledOverrides);
5191
+ const scopes = mapOption(o, "scopes");
5192
+ const unitScopes = mapOption(o, "unitScopes");
5193
+ if (Object.keys(scopes).length === 0 && Object.keys(unitScopes).length === 0) continue;
5194
+ noteCollisions(scopes, unitScopes);
5195
+ const excluded = compile(listOption(o, "exclude"));
5196
+ if (isExcluded(dir, ancestorDirs2(dir), excluded)) {
5197
+ excludedDirs.push(dir);
5198
+ continue;
5199
+ }
5200
+ const liveScopes = Object.keys(scopes).filter((k) => namesOf(scopes[k]).length > 0);
5201
+ const isUnit = isUnitDir(dir, filesIn);
5202
+ const liveUnits = isUnit ? Object.keys(unitScopes).filter((k) => namesOf(unitScopes[k]).length > 0) : [];
5203
+ if (!isUnit) nonUnitDirs.push(dir);
5204
+ const byPosition = matchKeys(dir, compile(liveScopes, true));
5205
+ const byUnit = matchKeys(dir, compile(liveUnits, true));
5206
+ for (const k of byPosition.matched) if (globalKeys.has(k)) usedKeys.add(k);
5207
+ for (const k of byUnit.matched) if (globalKeys.has(k)) usedKeys.add(k);
5208
+ let governing;
5209
+ if (byPosition.best !== void 0 && byUnit.best !== void 0) {
5210
+ governing = moreSpecificGlob(byUnit.best, byPosition.best) ? namesOf(unitScopes[byUnit.best]) : namesOf(scopes[byPosition.best]);
5211
+ } else if (byPosition.best !== void 0) {
5212
+ governing = namesOf(scopes[byPosition.best]);
5213
+ } else if (byUnit.best !== void 0) {
5214
+ governing = namesOf(unitScopes[byUnit.best]);
5215
+ }
5216
+ if (governing === void 0) continue;
5217
+ const allowed = new Set(governing);
5218
+ for (const child of kids.get(dir) ?? []) {
5219
+ if (allowed.has(baseName(child))) continue;
5220
+ if (isExcluded(child, ancestorDirs2(child), excluded)) continue;
5221
+ const childOptions = resolveRuleOptions(
5222
+ ID6,
5223
+ OPTIONS5,
5224
+ ctx.config,
5225
+ { route: child, file: child },
5226
+ compiledOverrides
5227
+ );
5228
+ if (isExcluded(child, ancestorDirs2(child), compile(listOption(childOptions, "exclude")))) continue;
5229
+ const at = reportAt(child, files);
5230
+ if (at === void 0) continue;
5231
+ out.push({
5232
+ id: ID6,
5233
+ category: "architecture",
5234
+ severity: "info",
5235
+ detection: { presence: "none", value: "absent" },
5236
+ route: child,
5237
+ location: at,
5238
+ message: `${child} is not one of the names declared here: ${governing.join(", ")}.`,
5239
+ recommendation: recommendation10,
5240
+ docsUrl: docsUrl10,
5241
+ fix: {
5242
+ description: "Rename it to a declared name, move it under one of them, or add its name to the declaration."
5243
+ }
5244
+ });
5245
+ }
5246
+ }
5247
+ const notes = /* @__PURE__ */ new Map();
5248
+ for (const key of collisions) {
5249
+ notes.set(key, "declared in both scopes and unitScopes, so the scopes entry wins wherever both apply");
5250
+ }
5251
+ for (const key of globalKeys) {
5252
+ if (notes.has(key)) continue;
5253
+ const scopesEmpty = Object.hasOwn(globalScopes, key) && namesOf(globalScopes[key]).length === 0;
5254
+ const unitsEmpty = Object.hasOwn(globalUnits, key) && namesOf(globalUnits[key]).length === 0;
5255
+ if (scopesEmpty || unitsEmpty) {
5256
+ notes.set(key, "names no directory name at all");
5257
+ }
5258
+ }
5259
+ const unused = [...globalKeys].filter((k) => !notes.has(k) && !usedKeys.has(k));
5260
+ const unitOnly = unused.filter((k) => Object.hasOwn(globalUnits, k) && !Object.hasOwn(globalScopes, k));
5261
+ for (const key of keysMatchingAny(unitOnly, nonUnitDirs, compile)) {
5262
+ notes.set(key, "matched directories but never a unit");
5263
+ }
5264
+ for (const [key, reason] of classifyUnusedKeys(
5265
+ unused.filter((k) => !notes.has(k)),
5266
+ excludedDirs,
5267
+ compile
5268
+ )) {
5269
+ notes.set(key, reason === "only-excluded" ? "matched only excluded directories" : "matched no directory");
5270
+ }
5271
+ const reported = [...notes.keys()].sort();
5272
+ if (reported.length > 0) {
5273
+ const message = reported.length === 1 ? `The declaration '${reported[0]}' does not check what it says: ${notes.get(reported[0])}.` : `These declarations do not check what they say: ${reported.map((k) => `'${k}' (${notes.get(k)})`).join(", ")}.`;
5274
+ out.push({
5275
+ id: ID6,
5276
+ category: "architecture",
5277
+ severity: "info",
5278
+ detection: { presence: "none", value: "absent" },
5279
+ message,
5280
+ recommendation: "Correct the glob or the names, or remove the declaration.",
5281
+ docsUrl: docsUrl10
5282
+ });
5283
+ }
5284
+ return out;
5285
+ }
5286
+ };
5287
+
5288
+ // src/rules/architecture/route-component-import.ts
5289
+ var ID7 = "architecture/route-component-import";
5290
+ var EXEMPT_IMPORTERS = ["**/*.stories.svelte", "**/*.test.svelte", "**/*.spec.svelte"];
5291
+ var ROUTES_DIR = "src/routes/";
5292
+ var ROUTE_ENTRY = /^\+(page|layout)(@.*)?\.svelte$/;
5293
+ function isRouteEntry(path) {
5294
+ if (!path.startsWith(ROUTES_DIR)) return false;
5295
+ const base = path.slice(path.lastIndexOf("/") + 1);
5296
+ return base === "+error.svelte" || ROUTE_ENTRY.test(base);
5297
+ }
5298
+ function routeEntryImports(c, ctx) {
5299
+ const out = [];
5300
+ for (const { source, line, type } of c.importSpans ?? []) {
5301
+ if (type) continue;
5302
+ const target = resolveRepoLocalPath(source, c.file, ctx.project.kitAliases);
5303
+ if (target !== void 0 && isRouteEntry(target)) out.push({ line, target });
5304
+ }
5305
+ return out;
5306
+ }
5307
+ var routeEntryImportsCache = /* @__PURE__ */ new WeakMap();
5308
+ function cachedRouteEntryImports(c, ctx) {
5309
+ const cached = routeEntryImportsCache.get(c);
5310
+ if (cached !== void 0 && cached.aliases === ctx.project.kitAliases) return cached.result;
5311
+ const result = routeEntryImports(c, ctx);
5312
+ routeEntryImportsCache.set(c, { aliases: ctx.project.kitAliases, result });
5313
+ return result;
5314
+ }
5315
+ var architectureRouteComponentImport = componentRule({
5316
+ id: ID7,
5317
+ title: "Route component import",
5318
+ category: "architecture",
5319
+ severity: "info",
5320
+ label: "Route component imports",
5321
+ options: { exemptImporters: { kind: "string-list", default: EXEMPT_IMPORTERS } },
5322
+ recommendation: "Extract the shared markup into a component under $lib and import that from both places, leaving the route entry to SvelteKit.",
5323
+ rationale: "A route entry is written on the assumption that SvelteKit renders it: Kit hands a page its data and params, and an error page its page.error and page.status. Imported from somewhere else it receives none of that and renders against nothing, or against the importing page data standing in for its own.",
5324
+ // Signal present = this file imports a route entry, exempt or not. An exempt file therefore
5325
+ // reaches `bad` and earns a PASS, rather than being called signal-free.
5326
+ // `_o`: this rule's option is read only in `bad`, but `ctx` is the third parameter, so the
5327
+ // second cannot simply be omitted the way every other component rule omits what it does not use.
5328
+ applies: (c, _o, ctx) => cachedRouteEntryImports(c, ctx).length > 0,
5329
+ bad: (c, o, ctx) => {
5330
+ const exempt = listOption(o, "exemptImporters").map(routeGlobToRegExp);
5331
+ if (exempt.some((re) => re.test(c.file))) return [];
5332
+ return cachedRouteEntryImports(c, ctx).map(({ line, target }) => ({
5333
+ line,
5334
+ message: `${target} is a SvelteKit route entry \u2014 imported here it renders without the data Kit would give it`
5335
+ }));
5336
+ }
4002
5337
  });
4003
5338
 
4004
5339
  // src/rules/perf/heavy-import.ts
@@ -4014,18 +5349,20 @@ var performanceHeavyImport = componentRule({
4014
5349
  label: "No heavy imports",
4015
5350
  recommendation: "Import a submodule or switch to a lighter, tree-shakeable alternative.",
4016
5351
  rationale: "Importing a large, non-tree-shakeable package pulls its whole weight into the bundle even when only a fraction is used, slowing load.",
5352
+ options: { packages: { kind: "string-map", default: HEAVY_PACKAGES } },
4017
5353
  // ComponentFacts is a public @svelte-vitals/core export — an external caller compiled
4018
5354
  // against an older version may still construct one without importSpans. Fall back to the
4019
5355
  // line-less `imports` (line: 0, the pre-fix behavior) instead of crashing on `undefined`.
4020
5356
  applies: (c) => (c.importSpans ?? c.imports).length > 0,
4021
- bad: (c) => {
5357
+ bad: (c, o) => {
5358
+ const packages = mapOption(o, "packages");
4022
5359
  const seen = /* @__PURE__ */ new Set();
4023
5360
  const out = [];
4024
5361
  const spans = c.importSpans ?? c.imports.map((source) => ({ source, line: 0 }));
4025
- for (const { source: src, line } of spans) {
4026
- if (!Object.hasOwn(HEAVY_PACKAGES, src) || seen.has(src)) continue;
5362
+ for (const { source: src, line, type } of spans) {
5363
+ if (type || !Object.hasOwn(packages, src) || seen.has(src)) continue;
4027
5364
  seen.add(src);
4028
- out.push({ line, message: `Heavy import "${src}" \u2014 ${HEAVY_PACKAGES[src]}` });
5365
+ out.push({ line, message: `Heavy import "${src}" \u2014 ${packages[src]}` });
4029
5366
  }
4030
5367
  return out;
4031
5368
  }
@@ -4055,13 +5392,13 @@ var performanceNamespaceImport = componentRule({
4055
5392
  });
4056
5393
 
4057
5394
  // src/rules/perf/minify-disabled.ts
4058
- var PENALIZED6 = { presence: "none", value: "absent" };
5395
+ var PENALIZED7 = { presence: "none", value: "absent" };
4059
5396
  var MINIFY_DISABLED_FIX = {
4060
5397
  description: "Remove the minify: false override from vite.config (Vite minifies with esbuild by default), or scope it to non-production builds.",
4061
5398
  snippet: "export default defineConfig({\n build: {\n minify: 'esbuild'\n }\n});",
4062
5399
  lang: "ts"
4063
5400
  };
4064
- var RECOMMENDATION3 = "Remove build.minify: false from vite.config, or scope it to non-production builds if it is intentional.";
5401
+ var RECOMMENDATION4 = "Remove build.minify: false from vite.config, or scope it to non-production builds if it is intentional.";
4065
5402
  var performanceMinifyDisabled = {
4066
5403
  id: "performance/minify-disabled",
4067
5404
  title: "Minification disabled",
@@ -4079,11 +5416,11 @@ var performanceMinifyDisabled = {
4079
5416
  id: "performance/minify-disabled",
4080
5417
  category: "performance",
4081
5418
  severity: "warning",
4082
- detection: PENALIZED6,
5419
+ detection: PENALIZED7,
4083
5420
  ...hit.file !== void 0 ? { location: hit.file } : {},
4084
5421
  ...hit.line !== void 0 ? { line: hit.line } : {},
4085
5422
  message: "JS/CSS minification is disabled (build.minify: false) \u2014 production bundles ship unminified and several times larger." + provenance,
4086
- recommendation: RECOMMENDATION3,
5423
+ recommendation: RECOMMENDATION4,
4087
5424
  docsUrl: docsUrlFor("performance/minify-disabled"),
4088
5425
  fix: { ...MINIFY_DISABLED_FIX }
4089
5426
  }
@@ -4197,8 +5534,10 @@ var allRules = [
4197
5534
  correctnessPropMutation,
4198
5535
  correctnessStalePropDerivation,
4199
5536
  correctnessNonreactiveBuiltinState,
5537
+ correctnessCheckableBindValue,
4200
5538
  correctnessOrphanEffect,
4201
5539
  correctnessOrphanLifecycle,
5540
+ correctnessBasePathNavigation,
4202
5541
  correctnessServerBrowserGlobal,
4203
5542
  correctnessInstanceBrowserGlobal,
4204
5543
  securityRawHtml,
@@ -4208,6 +5547,11 @@ var allRules = [
4208
5547
  securitySharedStateImport,
4209
5548
  architectureComponentSize,
4210
5549
  architecturePropCount,
5550
+ architecturePrivateScopeImport,
5551
+ architectureUnitEntryFile,
5552
+ architectureDirectoryNaming,
5553
+ architectureReservedDirectoryNames,
5554
+ architectureRouteComponentImport,
4211
5555
  performanceHeavyImport,
4212
5556
  performanceNamespaceImport,
4213
5557
  performanceMinifyDisabled,
@@ -4215,6 +5559,15 @@ var allRules = [
4215
5559
  performanceSequentialAwaits,
4216
5560
  performanceStateRaw
4217
5561
  ];
5562
+ function optionInfos(spec) {
5563
+ return Object.entries(spec).map(([name, s]) => ({
5564
+ name,
5565
+ kind: s.kind,
5566
+ default: s.default,
5567
+ ...s.kind === "integer" && s.min !== void 0 ? { min: s.min } : {},
5568
+ ...s.kind === "integer" && s.max !== void 0 ? { max: s.max } : {}
5569
+ }));
5570
+ }
4218
5571
  function explainRule(id) {
4219
5572
  const rule = allRules.find((r) => r.id === id);
4220
5573
  if (!rule) return void 0;
@@ -4225,7 +5578,8 @@ function explainRule(id) {
4225
5578
  severity: rule.severity,
4226
5579
  rationale: rule.rationale,
4227
5580
  docsUrl: docsUrlFor(rule.id),
4228
- ...rule.fix ? { fix: rule.fix } : {}
5581
+ ...rule.fix ? { fix: rule.fix } : {},
5582
+ ...rule.options ? { options: optionInfos(rule.options) } : {}
4229
5583
  };
4230
5584
  }
4231
5585
 
@@ -5444,61 +6798,26 @@ function safeHref(url) {
5444
6798
  const normalized = url.replace(/\s/g, "").toLowerCase();
5445
6799
  return /^https?:\/\//.test(normalized) ? url : null;
5446
6800
  }
5447
-
5448
- // src/config-apply.ts
5449
- function selectRules(rules, config) {
5450
- return rules.filter((rule) => config.rules[rule.id] !== "off");
5451
- }
5452
- function applyRuleSeverities(results, config) {
5453
- return results.map((result) => {
5454
- const setting = config.rules[result.id];
5455
- return setting && setting !== "off" ? { ...result, severity: setting } : result;
5456
- });
5457
- }
5458
- function routeGlobToRegExp(pattern) {
5459
- const body = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "\0").replace(/\*/g, "[^/]*").split("\0").join(".*");
5460
- const source = body.endsWith("/.*") ? `${body.slice(0, -3)}(/.*)?` : body;
5461
- return new RegExp(`^${source}$`);
5462
- }
5463
- function toPatterns(globs) {
5464
- if (globs === void 0) return [];
5465
- return (Array.isArray(globs) ? globs : [globs]).map(routeGlobToRegExp);
5466
- }
5467
- function applyOverrides(results, config) {
5468
- const overrides = config.overrides;
5469
- if (!overrides || overrides.length === 0) return results;
5470
- const compiled = overrides.map((o) => ({
5471
- routes: toPatterns(o.route),
5472
- files: toPatterns(o.files),
5473
- rules: o.rules
5474
- }));
5475
- const out = [];
5476
- for (const result of results) {
5477
- const { route, location } = result;
5478
- let setting;
5479
- for (const o of compiled) {
5480
- const matched = route !== void 0 && o.routes.some((p) => p.test(route)) || location !== void 0 && o.files.some((p) => p.test(location));
5481
- if (!matched) continue;
5482
- const s = o.rules[result.id] ?? o.rules[result.category ?? "seo"];
5483
- if (s !== void 0) setting = s;
5484
- }
5485
- if (setting === void 0) out.push(result);
5486
- else if (setting !== "off") out.push({ ...result, severity: setting });
5487
- }
5488
- return out;
5489
- }
5490
6801
  export {
5491
6802
  APP_SCRIPT,
5492
6803
  APP_STYLE,
5493
6804
  BAND_COLOR,
6805
+ CATEGORIES,
5494
6806
  CHILD_NODE_KEYS,
5495
6807
  ROBOTS_SOURCE_PATHS,
5496
6808
  SITEMAP_SOURCE_PATHS,
6809
+ SVELTE_CONFIG_FILES,
6810
+ VITE_CONFIG_FILES,
5497
6811
  allRules,
5498
6812
  applyOverrides,
5499
6813
  applyRuleSeverities,
5500
6814
  architectureComponentSize,
6815
+ architectureDirectoryNaming,
6816
+ architecturePrivateScopeImport,
5501
6817
  architecturePropCount,
6818
+ architectureReservedDirectoryNames,
6819
+ architectureRouteComponentImport,
6820
+ architectureUnitEntryFile,
5502
6821
  attrText,
5503
6822
  attrTextOf,
5504
6823
  attrValue,
@@ -5508,8 +6827,12 @@ export {
5508
6827
  classify,
5509
6828
  collectComponentFacts,
5510
6829
  collectKitModuleFacts,
6830
+ collectSourceFiles,
6831
+ compileOverrides,
5511
6832
  computeHealth,
5512
6833
  computeScore,
6834
+ correctnessBasePathNavigation,
6835
+ correctnessCheckableBindValue,
5513
6836
  correctnessEachIndexKey,
5514
6837
  correctnessEachKey,
5515
6838
  correctnessEffectAsDerived,
@@ -5532,6 +6855,9 @@ export {
5532
6855
  escapeHtml,
5533
6856
  explainRule,
5534
6857
  findAttr,
6858
+ findKitAliasesInSvelteConfig,
6859
+ findKitPathsBaseInSvelteConfig,
6860
+ findKitPathsBaseInViteConfig,
5535
6861
  findMinifyDisabled,
5536
6862
  formatAgentReport,
5537
6863
  formatConsoleReport,
@@ -5543,10 +6869,15 @@ export {
5543
6869
  hasFailureAtOrAbove,
5544
6870
  headTagRule,
5545
6871
  imageRule,
6872
+ intOption,
6873
+ isMentionedAnywhere,
5546
6874
  isPenalized,
5547
6875
  lineOf,
5548
6876
  linkRule,
6877
+ listOption,
6878
+ mapOption,
5549
6879
  noColorPalette,
6880
+ overrideMatches,
5550
6881
  parseComponentFacts,
5551
6882
  parseKitModuleFacts,
5552
6883
  performanceFontPreloadCrossorigin,
@@ -5564,6 +6895,9 @@ export {
5564
6895
  performanceSequentialAwaits,
5565
6896
  performanceStateRaw,
5566
6897
  renderAppShell,
6898
+ resolveKitAliases,
6899
+ resolveKitPathsBase,
6900
+ resolveRuleOptions,
5567
6901
  resolveRunesModuleSpecifier,
5568
6902
  runRules,
5569
6903
  safeHref,
@@ -5607,7 +6941,12 @@ export {
5607
6941
  seoTitlePresence,
5608
6942
  seoTwitterCard,
5609
6943
  seoViewport,
6944
+ settingOptions,
6945
+ settingSeverity,
6946
+ shouldSkipRangeCheck,
5610
6947
  summarize,
5611
6948
  textFromNodes,
6949
+ validateRuleOptions,
6950
+ validateRuleSetting,
5612
6951
  valueFromNodes
5613
6952
  };