@svelte-vitals/core 0.29.0 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.d.ts +305 -18
  2. package/dist/index.js +755 -122
  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
  }
@@ -977,20 +1043,25 @@ var BROWSER_GLOBALS = /* @__PURE__ */ new Set([
977
1043
  "confirm",
978
1044
  "prompt"
979
1045
  ]);
980
- function collectBrowserGuardImports(program) {
1046
+ function collectNamedImportAliases(program, moduleSource, names) {
981
1047
  const out = /* @__PURE__ */ new Set();
982
1048
  for (const stmt of program.body ?? []) {
983
- if (stmt?.type !== "ImportDeclaration" || stmt.importKind === "type" || stmt.source?.value !== "$app/environment")
1049
+ if (stmt?.type !== "ImportDeclaration" || stmt.importKind === "type" || stmt.source?.value !== moduleSource) {
984
1050
  continue;
1051
+ }
985
1052
  for (const s of stmt.specifiers ?? []) {
986
1053
  if (s?.importKind === "type" || s?.local?.type !== "Identifier") continue;
987
- if (s.type === "ImportSpecifier" && s.imported?.type === "Identifier" && s.imported.name === "browser") {
1054
+ if (s.type === "ImportSpecifier" && s.imported?.type === "Identifier" && names.has(s.imported.name)) {
988
1055
  out.add(s.local.name);
989
1056
  }
990
1057
  }
991
1058
  }
992
1059
  return out;
993
1060
  }
1061
+ var BROWSER_GUARD_NAMES = /* @__PURE__ */ new Set(["browser"]);
1062
+ function collectBrowserGuardImports(program) {
1063
+ return collectNamedImportAliases(program, "$app/environment", BROWSER_GUARD_NAMES);
1064
+ }
994
1065
  function collectProgramBindings(program) {
995
1066
  const bound = /* @__PURE__ */ new Set();
996
1067
  for (const stmt of program.body ?? []) {
@@ -1168,6 +1239,14 @@ function parseModuleFacts(source, filename) {
1168
1239
  const orphanLifecycleCalls = program ? collectOrphanLifecycleCalls(program, wrapped).map((f) => ({ ...f, line: shift(f.line) })) : [];
1169
1240
  const browserGlobalRefs = program ? collectBrowserGlobalRefs(program, wrapped).map((r) => ({ ...r, line: shift(r.line), context: "module" })) : [];
1170
1241
  const moduleStateDecls = program ? collectModuleStateDecls(program, wrapped).map((d) => ({ ...d, line: shift(d.line) })) : [];
1242
+ const basePathLinks = [];
1243
+ if (program) {
1244
+ const locals = collectNamedImportAliases(program, "$app/navigation", GOTO_NAMES);
1245
+ const raw = [];
1246
+ collectGotoLinks(locals, [program], wrapped, raw);
1247
+ for (const l of raw) basePathLinks.push({ ...l, line: shift(l.line) });
1248
+ basePathLinks.sort((a, b) => a.line - b.line);
1249
+ }
1171
1250
  return {
1172
1251
  eachBlocks: [],
1173
1252
  effects: [],
@@ -1183,6 +1262,8 @@ function parseModuleFacts(source, filename) {
1183
1262
  stalePropDerivations: [],
1184
1263
  rawableStates: [],
1185
1264
  nonreactiveBuiltinStates: [],
1265
+ checkableBindValues: [],
1266
+ basePathLinks,
1186
1267
  suppressions: collectSuppressions(source),
1187
1268
  orphanEffects,
1188
1269
  orphanLifecycleCalls,
@@ -1198,6 +1279,18 @@ function parseComponentFacts(source, filename) {
1198
1279
  const htmlTags = [];
1199
1280
  const javascriptUrls = [];
1200
1281
  collectSecurityFacts(ast.fragment ?? ast, source, htmlTags, javascriptUrls);
1282
+ const checkableBindValues = [];
1283
+ collectCheckableBindValues(ast.fragment ?? ast, source, checkableBindValues);
1284
+ const basePathLinks = [];
1285
+ collectHrefLinks(ast.fragment ?? ast, source, basePathLinks);
1286
+ const gotoPrograms = [ast.module?.content, ast.instance?.content].filter(Boolean);
1287
+ const gotoLocals = /* @__PURE__ */ new Set();
1288
+ for (const p of gotoPrograms)
1289
+ for (const n of collectNamedImportAliases(p, "$app/navigation", GOTO_NAMES)) {
1290
+ gotoLocals.add(n);
1291
+ }
1292
+ collectGotoLinks(gotoLocals, [...gotoPrograms, ast.fragment], source, basePathLinks);
1293
+ basePathLinks.sort((a, b) => a.line - b.line);
1201
1294
  const loc = countLines(source);
1202
1295
  const suppressions = collectSuppressions(source);
1203
1296
  const moduleProgram = ast.module?.content;
@@ -1372,6 +1465,8 @@ function parseComponentFacts(source, filename) {
1372
1465
  stalePropDerivations,
1373
1466
  rawableStates,
1374
1467
  nonreactiveBuiltinStates,
1468
+ checkableBindValues,
1469
+ basePathLinks,
1375
1470
  orphanEffects,
1376
1471
  orphanLifecycleCalls,
1377
1472
  browserGlobalRefs,
@@ -1398,6 +1493,8 @@ function emptyComponentFacts(file) {
1398
1493
  stalePropDerivations: [],
1399
1494
  rawableStates: [],
1400
1495
  nonreactiveBuiltinStates: [],
1496
+ checkableBindValues: [],
1497
+ basePathLinks: [],
1401
1498
  orphanEffects: [],
1402
1499
  orphanLifecycleCalls: [],
1403
1500
  browserGlobalRefs: [],
@@ -1534,6 +1631,22 @@ function collectAwaits(node, out = []) {
1534
1631
  }
1535
1632
  return out;
1536
1633
  }
1634
+ var REDIRECT_NAMES = /* @__PURE__ */ new Set(["redirect"]);
1635
+ function collectRedirectCalls(node, locals, out = []) {
1636
+ if (Array.isArray(node)) {
1637
+ for (const child of node) collectRedirectCalls(child, locals, out);
1638
+ return out;
1639
+ }
1640
+ if (!node || typeof node !== "object" || typeof node.type !== "string") return out;
1641
+ if (node.type === "CallExpression" && node.callee?.type === "Identifier" && locals.has(node.callee.name)) {
1642
+ out.push(node);
1643
+ }
1644
+ for (const key of Object.keys(node)) {
1645
+ if (WALK_IGNORED_KEYS.has(key)) continue;
1646
+ collectRedirectCalls(node[key], locals, out);
1647
+ }
1648
+ return out;
1649
+ }
1537
1650
  function isParentCall(arg) {
1538
1651
  const e = unwrapTs(arg);
1539
1652
  if (e?.type !== "CallExpression") return false;
@@ -1721,6 +1834,7 @@ function parseKitModuleFacts(source, filename) {
1721
1834
  runesModuleImports,
1722
1835
  lifecycleCalls,
1723
1836
  browserGlobalRefs,
1837
+ basePathLinks: [],
1724
1838
  suppressions
1725
1839
  };
1726
1840
  }
@@ -1845,6 +1959,15 @@ function parseKitModuleFacts(source, filename) {
1845
1959
  }
1846
1960
  });
1847
1961
  const byLine = (arr) => arr.sort((a, b) => a.line - b.line);
1962
+ const basePathLinks = [];
1963
+ const redirectLocals = collectNamedImportAliases(program, "@sveltejs/kit", REDIRECT_NAMES);
1964
+ if (redirectLocals.size > 0) {
1965
+ for (const call of collectRedirectCalls(program, redirectLocals)) {
1966
+ const arg = call.arguments?.[1];
1967
+ if (arg?.type !== "Literal" || typeof arg.value !== "string" || !isRootRelativePath(arg.value)) continue;
1968
+ basePathLinks.push({ kind: "redirect", path: arg.value, line: line(call.start) });
1969
+ }
1970
+ }
1848
1971
  return {
1849
1972
  moduleStateReassignments: byLine(moduleStateReassignments),
1850
1973
  importedStateWrites: byLine(importedStateWrites),
@@ -1852,6 +1975,7 @@ function parseKitModuleFacts(source, filename) {
1852
1975
  runesModuleImports: byLine(runesModuleImports),
1853
1976
  lifecycleCalls: byLine(lifecycleCalls),
1854
1977
  browserGlobalRefs: byLine(browserGlobalRefs),
1978
+ basePathLinks: byLine(basePathLinks),
1855
1979
  ...ssrOptOut ? { ssrDisabled: { line: Math.max(0, ssrOptOut.line - 1) } } : {},
1856
1980
  ...csrOptOut ? { csrDisabled: { line: Math.max(0, csrOptOut.line - 1) } } : {},
1857
1981
  ...waterfalls.dependentLines.length > 0 || waterfalls.independentLines.length > 0 ? { loadWaterfalls: waterfalls } : {},
@@ -1870,6 +1994,7 @@ function emptyKitModuleFacts(file, kind) {
1870
1994
  runesModuleImports: [],
1871
1995
  lifecycleCalls: [],
1872
1996
  browserGlobalRefs: [],
1997
+ basePathLinks: [],
1873
1998
  suppressions: []
1874
1999
  };
1875
2000
  }
@@ -1899,7 +2024,7 @@ async function collectKitModuleFacts(rt, cwd) {
1899
2024
  );
1900
2025
  }
1901
2026
 
1902
- // src/vite-config-parse.ts
2027
+ // src/config-object.ts
1903
2028
  function propOf(obj, name) {
1904
2029
  let found;
1905
2030
  for (const p of obj.properties) {
@@ -1954,6 +2079,8 @@ function resolveConfigObject(program) {
1954
2079
  if (!exported) return void 0;
1955
2080
  return unwrapToObjectExpression(exported, collectTopLevelBindings(program));
1956
2081
  }
2082
+
2083
+ // src/vite-config-parse.ts
1957
2084
  function findMinifyDisabled(source) {
1958
2085
  let program;
1959
2086
  let wrapped;
@@ -1974,6 +2101,79 @@ function findMinifyDisabled(source) {
1974
2101
  return { line: Math.max(0, lineOf(wrapped, minify.start) - 1) };
1975
2102
  }
1976
2103
 
2104
+ // src/svelte-config-parse.ts
2105
+ function basePathOf(kitConfig, bindings) {
2106
+ const paths = propOf(kitConfig, "paths");
2107
+ const pathsObj = paths ? unwrapToObjectExpression(paths.value, bindings) : void 0;
2108
+ if (!pathsObj) return void 0;
2109
+ const base = propOf(pathsObj, "base");
2110
+ if (!base) return void 0;
2111
+ const value = unwrapTs(base.value);
2112
+ if (value.type === "Literal") {
2113
+ return typeof value.value === "string" && value.value !== "" ? { value: value.value } : void 0;
2114
+ }
2115
+ return {};
2116
+ }
2117
+ function programOf(source, filename) {
2118
+ try {
2119
+ return parseModuleProgram(source, filename).program ?? void 0;
2120
+ } catch {
2121
+ return void 0;
2122
+ }
2123
+ }
2124
+ function findKitPathsBaseInSvelteConfig(source) {
2125
+ const program = programOf(source, "svelte.config.js");
2126
+ if (!program) return void 0;
2127
+ const config = resolveConfigObject(program);
2128
+ if (!config) return void 0;
2129
+ const bindings = collectTopLevelBindings(program);
2130
+ const kit = propOf(config, "kit");
2131
+ const kitObj = kit ? unwrapToObjectExpression(kit.value, bindings) : void 0;
2132
+ return kitObj ? basePathOf(kitObj, bindings) : void 0;
2133
+ }
2134
+ function sveltekitLocalNames(program) {
2135
+ const out = collectNamedImportAliases(program, "@sveltejs/kit/vite", /* @__PURE__ */ new Set(["sveltekit"]));
2136
+ if (out.size === 0) out.add("sveltekit");
2137
+ return out;
2138
+ }
2139
+ function findKitPathsBaseInViteConfig(source) {
2140
+ const none = { kind: "no-plugin-config" };
2141
+ const program = programOf(source, "vite.config.ts");
2142
+ if (!program) return none;
2143
+ const config = resolveConfigObject(program);
2144
+ if (!config) return none;
2145
+ const bindings = collectTopLevelBindings(program);
2146
+ const plugins = propOf(config, "plugins");
2147
+ const pluginsValue = plugins ? unwrapTs(plugins.value) : void 0;
2148
+ if (pluginsValue?.type !== "ArrayExpression") return none;
2149
+ const locals = sveltekitLocalNames(program);
2150
+ for (const el of pluginsValue.elements) {
2151
+ if (!el || el.type === "SpreadElement") continue;
2152
+ const call = unwrapTs(el);
2153
+ if (call.type !== "CallExpression") continue;
2154
+ if (call.callee.type !== "Identifier" || !locals.has(call.callee.name)) continue;
2155
+ const arg = call.arguments[0];
2156
+ if (arg === void 0) return none;
2157
+ const kitConfig = unwrapToObjectExpression(arg, bindings);
2158
+ if (!kitConfig) return { kind: "unresolvable" };
2159
+ const base = basePathOf(kitConfig, bindings);
2160
+ return base ? { kind: "resolved", base } : { kind: "resolved" };
2161
+ }
2162
+ return none;
2163
+ }
2164
+ function resolveKitPathsBase(viteConfig, svelteConfig) {
2165
+ if (viteConfig) {
2166
+ const result = findKitPathsBaseInViteConfig(viteConfig.source);
2167
+ if (result.kind === "unresolvable") return void 0;
2168
+ if (result.kind === "resolved") {
2169
+ return result.base ? { ...result.base, file: viteConfig.file } : void 0;
2170
+ }
2171
+ }
2172
+ if (!svelteConfig) return void 0;
2173
+ const base = findKitPathsBaseInSvelteConfig(svelteConfig.source);
2174
+ return base ? { ...base, file: svelteConfig.file } : void 0;
2175
+ }
2176
+
1977
2177
  // src/project-paths.ts
1978
2178
  var ROBOTS_SOURCE_PATHS = [
1979
2179
  "static/robots.txt",
@@ -1985,6 +2185,15 @@ var SITEMAP_SOURCE_PATHS = [
1985
2185
  "src/routes/sitemap.xml/+server.ts",
1986
2186
  "src/routes/sitemap.xml/+server.js"
1987
2187
  ];
2188
+ var VITE_CONFIG_FILES = [
2189
+ "vite.config.js",
2190
+ "vite.config.mjs",
2191
+ "vite.config.ts",
2192
+ "vite.config.cjs",
2193
+ "vite.config.mts",
2194
+ "vite.config.cts"
2195
+ ];
2196
+ var SVELTE_CONFIG_FILES = ["svelte.config.js", "svelte.config.ts"];
1988
2197
 
1989
2198
  // src/rule.ts
1990
2199
  function docsUrlFor(id) {
@@ -2054,7 +2263,7 @@ function detect(head, match) {
2054
2263
  return tag ? { presence: tag.presence, value: tag.value } : { presence: "none", value: "absent" };
2055
2264
  }
2056
2265
  function headTagRule(opts) {
2057
- const docsUrl7 = docsUrlFor(opts.id);
2266
+ const docsUrl8 = docsUrlFor(opts.id);
2058
2267
  return {
2059
2268
  id: opts.id,
2060
2269
  title: opts.title,
@@ -2077,7 +2286,7 @@ function headTagRule(opts) {
2077
2286
  location: head.file,
2078
2287
  message,
2079
2288
  recommendation: opts.recommendation,
2080
- docsUrl: docsUrl7,
2289
+ docsUrl: docsUrl8,
2081
2290
  // Copy per finding: opts.fix is a rule-level template shared across all
2082
2291
  // results this rule emits; a fresh object keeps findings independent.
2083
2292
  ...opts.fix ? { fix: { ...opts.fix } } : {}
@@ -2270,7 +2479,7 @@ var seoHtmlLang = {
2270
2479
 
2271
2480
  // src/rules/perf/image-rule.ts
2272
2481
  function imageRule(opts) {
2273
- const docsUrl7 = docsUrlFor(opts.id);
2482
+ const docsUrl8 = docsUrlFor(opts.id);
2274
2483
  const category = opts.category ?? "performance";
2275
2484
  return {
2276
2485
  id: opts.id,
@@ -2294,7 +2503,7 @@ function imageRule(opts) {
2294
2503
  route: route.route,
2295
2504
  message: opts.label,
2296
2505
  recommendation: opts.recommendation,
2297
- docsUrl: docsUrl7
2506
+ docsUrl: docsUrl8
2298
2507
  });
2299
2508
  continue;
2300
2509
  }
@@ -2309,7 +2518,7 @@ function imageRule(opts) {
2309
2518
  ...img.line > 0 ? { line: img.line } : {},
2310
2519
  message: `Missing ${opts.label}`,
2311
2520
  recommendation: opts.recommendation,
2312
- docsUrl: docsUrl7,
2521
+ docsUrl: docsUrl8,
2313
2522
  ...opts.fix ? { fix: { ...opts.fix } } : {}
2314
2523
  });
2315
2524
  }
@@ -2369,7 +2578,7 @@ var performanceResponsiveImage = imageRule({
2369
2578
 
2370
2579
  // src/rules/perf/link-rule.ts
2371
2580
  function linkRule(opts) {
2372
- const docsUrl7 = docsUrlFor(opts.id);
2581
+ const docsUrl8 = docsUrlFor(opts.id);
2373
2582
  return {
2374
2583
  id: opts.id,
2375
2584
  title: opts.title,
@@ -2393,7 +2602,7 @@ function linkRule(opts) {
2393
2602
  route: head.route,
2394
2603
  message: opts.label,
2395
2604
  recommendation: opts.recommendation,
2396
- docsUrl: docsUrl7
2605
+ docsUrl: docsUrl8
2397
2606
  });
2398
2607
  continue;
2399
2608
  }
@@ -2410,7 +2619,7 @@ function linkRule(opts) {
2410
2619
  location: tag.file ?? head.file,
2411
2620
  message: `Missing ${opts.label}`,
2412
2621
  recommendation: opts.recommendation,
2413
- docsUrl: docsUrl7,
2622
+ docsUrl: docsUrl8,
2414
2623
  ...opts.fix ? { fix: { ...opts.fix } } : {}
2415
2624
  });
2416
2625
  }
@@ -2557,10 +2766,197 @@ var performanceRenderBlockingScript = {
2557
2766
  }
2558
2767
  };
2559
2768
 
2769
+ // src/config-apply.ts
2770
+ function settingSeverity(setting) {
2771
+ if (setting === void 0) return void 0;
2772
+ if (typeof setting === "string") return setting;
2773
+ return setting.severity;
2774
+ }
2775
+ function settingOptions(setting) {
2776
+ return setting !== void 0 && typeof setting !== "string" ? setting.options : void 0;
2777
+ }
2778
+ function selectRules(rules, config) {
2779
+ return rules.filter((rule) => settingSeverity(config.rules[rule.id]) !== "off");
2780
+ }
2781
+ function applyRuleSeverities(results, config) {
2782
+ return results.map((result) => {
2783
+ const severity = settingSeverity(config.rules[result.id]);
2784
+ return severity !== void 0 && severity !== "off" ? { ...result, severity } : result;
2785
+ });
2786
+ }
2787
+ function routeGlobToRegExp(pattern) {
2788
+ const body = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "\0").replace(/\*/g, "[^/]*").split("\0").join(".*");
2789
+ const source = body.endsWith("/.*") ? `${body.slice(0, -3)}(/.*)?` : body;
2790
+ return new RegExp(`^${source}$`);
2791
+ }
2792
+ function toPatterns(globs) {
2793
+ if (globs === void 0) return [];
2794
+ return (Array.isArray(globs) ? globs : [globs]).map(routeGlobToRegExp);
2795
+ }
2796
+ function compileOverrides(config) {
2797
+ return (config.overrides ?? []).map((o) => ({
2798
+ routes: toPatterns(o.route),
2799
+ files: toPatterns(o.files),
2800
+ rules: o.rules
2801
+ }));
2802
+ }
2803
+ function overrideMatches(o, target) {
2804
+ const { route, file } = target;
2805
+ return route !== void 0 && o.routes.some((p) => p.test(route)) || file !== void 0 && o.files.some((p) => p.test(file));
2806
+ }
2807
+ function applyOverrides(results, config) {
2808
+ const compiled = compileOverrides(config);
2809
+ if (compiled.length === 0) return results;
2810
+ const out = [];
2811
+ for (const result of results) {
2812
+ let severity;
2813
+ for (const o of compiled) {
2814
+ if (!overrideMatches(o, { route: result.route, file: result.location })) continue;
2815
+ const sev = settingSeverity(o.rules[result.id]) ?? settingSeverity(o.rules[result.category ?? "seo"]);
2816
+ if (sev !== void 0) severity = sev;
2817
+ }
2818
+ if (severity === void 0) out.push(result);
2819
+ else if (severity !== "off") out.push({ ...result, severity });
2820
+ }
2821
+ return out;
2822
+ }
2823
+
2824
+ // src/rule-options.ts
2825
+ var RULE_SETTING_VALUES = ["off", "critical", "warning", "info"];
2826
+ function defaultsOf(spec) {
2827
+ const out = {};
2828
+ for (const [key, s] of Object.entries(spec)) {
2829
+ out[key] = s.kind === "integer" ? s.default : s.kind === "string-list" ? [...s.default] : { ...s.default };
2830
+ }
2831
+ return out;
2832
+ }
2833
+ function intOption(options, key, fallback = 0) {
2834
+ const v = options[key];
2835
+ return typeof v === "number" ? v : fallback;
2836
+ }
2837
+ function listOption(options, key) {
2838
+ const v = options[key];
2839
+ return Array.isArray(v) ? v : [];
2840
+ }
2841
+ function mapOption(options, key) {
2842
+ const v = options[key];
2843
+ return typeof v === "object" && v !== null && !Array.isArray(v) ? v : {};
2844
+ }
2845
+ function resolveRuleOptions(ruleId, spec, config, target, compiled) {
2846
+ if (!spec) return {};
2847
+ const out = defaultsOf(spec);
2848
+ const layers = [settingOptions(config.rules[ruleId])];
2849
+ if (target) {
2850
+ for (const o of compiled ?? compileOverrides(config)) {
2851
+ if (overrideMatches(o, target)) layers.push(settingOptions(o.rules[ruleId]));
2852
+ }
2853
+ }
2854
+ for (const layer of layers) {
2855
+ if (!layer) continue;
2856
+ for (const [key, value] of Object.entries(layer)) {
2857
+ const s = spec[key];
2858
+ if (!s) continue;
2859
+ if (s.kind === "integer") out[key] = value;
2860
+ else if (s.kind === "string-list") out[key] = [...out[key], ...value];
2861
+ else out[key] = { ...out[key], ...value };
2862
+ }
2863
+ }
2864
+ return out;
2865
+ }
2866
+ function validateRuleOptions(ruleId, spec, options, baseline, skipRangeCheck) {
2867
+ if (!spec) return Object.keys(options).length === 0 ? [] : [`${ruleId} takes no options.`];
2868
+ const errors = [];
2869
+ const badKeys = /* @__PURE__ */ new Set();
2870
+ const isNonEmptyString = (v) => typeof v === "string" && v.length > 0;
2871
+ for (const [key, value] of Object.entries(options)) {
2872
+ const s = spec[key];
2873
+ if (!s) {
2874
+ errors.push(`${ruleId}: unknown option '${key}'. Known options: ${Object.keys(spec).join(", ")}.`);
2875
+ continue;
2876
+ }
2877
+ if (s.kind === "integer") {
2878
+ if (typeof value !== "number" || !Number.isInteger(value)) {
2879
+ errors.push(`${ruleId}.${key} must be an integer.`);
2880
+ badKeys.add(key);
2881
+ } else if (s.min !== void 0 && value < s.min) {
2882
+ errors.push(`${ruleId}.${key} must be >= ${s.min}.`);
2883
+ badKeys.add(key);
2884
+ } else if (s.max !== void 0 && value > s.max) {
2885
+ errors.push(`${ruleId}.${key} must be <= ${s.max}.`);
2886
+ badKeys.add(key);
2887
+ }
2888
+ } else if (s.kind === "string-list") {
2889
+ if (!Array.isArray(value) || !value.every(isNonEmptyString)) {
2890
+ errors.push(`${ruleId}.${key} must be an array of non-empty strings.`);
2891
+ }
2892
+ } else if (typeof value !== "object" || value === null || Array.isArray(value) || !Object.values(value).every(isNonEmptyString)) {
2893
+ errors.push(`${ruleId}.${key} must be an object of string \u2192 non-empty string.`);
2894
+ }
2895
+ }
2896
+ const minSpec = spec.min;
2897
+ const maxSpec = spec.max;
2898
+ if (minSpec?.kind === "integer" && maxSpec?.kind === "integer" && !badKeys.has("min") && !badKeys.has("max") && !skipRangeCheck) {
2899
+ const base = baseline ?? defaultsOf(spec);
2900
+ const minVal = "min" in options ? options.min : base.min;
2901
+ const maxVal = "max" in options ? options.max : base.max;
2902
+ if (typeof minVal === "number" && typeof maxVal === "number" && minVal > maxVal) {
2903
+ errors.push(`${ruleId}: min (${minVal}) must be <= max (${maxVal}).`);
2904
+ }
2905
+ }
2906
+ return errors;
2907
+ }
2908
+ function isPlainObject(value) {
2909
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2910
+ }
2911
+ function otherOverrideNarrowsOppositeSide(overrides, selfIndex, key, side) {
2912
+ return overrides.some((entry, i) => {
2913
+ if (i === selfIndex || !isPlainObject(entry) || !isPlainObject(entry.rules)) return false;
2914
+ const setting = entry.rules[key];
2915
+ return isPlainObject(setting) && isPlainObject(setting.options) && side in setting.options;
2916
+ });
2917
+ }
2918
+ function shouldSkipRangeCheck(overrides, selfIndex, key, setting) {
2919
+ if (!isPlainObject(setting) || !isPlainObject(setting.options)) return false;
2920
+ const setsMin = "min" in setting.options;
2921
+ const setsMax = "max" in setting.options;
2922
+ if (setsMin === setsMax) return false;
2923
+ return otherOverrideNarrowsOppositeSide(overrides, selfIndex, key, setsMin ? "max" : "min");
2924
+ }
2925
+ function validateRuleSetting(label, ruleId, setting, spec, opts) {
2926
+ const expected = RULE_SETTING_VALUES.join("|");
2927
+ if (typeof setting === "string") {
2928
+ return RULE_SETTING_VALUES.includes(setting) ? [] : [`${label}: invalid setting '${setting}'; expected ${expected}.`];
2929
+ }
2930
+ if (!isPlainObject(setting)) {
2931
+ return [`${label}: must be ${expected} or an object with 'severity' and/or 'options'.`];
2932
+ }
2933
+ const errors = [];
2934
+ const unknownKeys = Object.keys(setting).filter((k) => k !== "severity" && k !== "options");
2935
+ if (unknownKeys.length > 0) {
2936
+ errors.push(`${label}: unknown key(s) ${unknownKeys.join(", ")}; expected severity, options.`);
2937
+ }
2938
+ if (setting.severity !== void 0 && !RULE_SETTING_VALUES.includes(setting.severity)) {
2939
+ errors.push(`${label}.severity: invalid setting '${String(setting.severity)}'; expected ${expected}.`);
2940
+ }
2941
+ if (setting.options === void 0) return errors;
2942
+ if (!opts.allowOptions) {
2943
+ errors.push(`${label}: options are not allowed on a category key.`);
2944
+ return errors;
2945
+ }
2946
+ if (!isPlainObject(setting.options)) {
2947
+ errors.push(`${label}.options: must be an object.`);
2948
+ return errors;
2949
+ }
2950
+ const optionErrors = validateRuleOptions(ruleId, spec, setting.options, opts.baseline, opts.skipRangeCheck);
2951
+ if (optionErrors.length > 0) errors.push(`${label}: ${optionErrors.join(" ")}`);
2952
+ return errors;
2953
+ }
2954
+
2560
2955
  // src/rules/perf/preconnect.ts
2561
2956
  var docsUrl3 = docsUrlFor("performance/preconnect");
2562
2957
  var recommendation3 = 'Add <link rel="preconnect"> (or dns-prefetch) for the third-party origin so the connection is set up early.';
2563
2958
  var THIRD_PARTY_ORIGINS = /* @__PURE__ */ new Set(["fonts.googleapis.com", "fonts.gstatic.com"]);
2959
+ var OPTIONS = { origins: { kind: "string-list", default: [...THIRD_PARTY_ORIGINS] } };
2564
2960
  function hostOf(href) {
2565
2961
  const m = /^(?:https?:)?\/\/([^/?#]+)/i.exec(href);
2566
2962
  return m ? m[1].toLowerCase() : void 0;
@@ -2577,17 +2973,30 @@ var performancePreconnect = {
2577
2973
  snippet: '<link rel="preconnect" href="https://fonts.googleapis.com" />',
2578
2974
  lang: "html"
2579
2975
  },
2976
+ options: OPTIONS,
2580
2977
  async check(ctx) {
2581
2978
  const out = [];
2979
+ const compiled = compileOverrides(ctx.config);
2582
2980
  for (const head of ctx.heads) {
2583
2981
  const referenced = /* @__PURE__ */ new Map();
2584
2982
  const covered = /* @__PURE__ */ new Set();
2585
2983
  for (const tag of head.tags) {
2586
2984
  if (tag.kind !== "link" && tag.kind !== "script" || typeof tag.href !== "string") continue;
2587
2985
  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);
2986
+ if (!host) continue;
2987
+ if (tag.kind === "link" && (tag.rel === "preconnect" || tag.rel === "dns-prefetch")) {
2988
+ covered.add(host);
2989
+ continue;
2990
+ }
2991
+ const o = resolveRuleOptions(
2992
+ "performance/preconnect",
2993
+ OPTIONS,
2994
+ ctx.config,
2995
+ { route: head.route, file: tag.file ?? head.file },
2996
+ compiled
2997
+ );
2998
+ if (!listOption(o, "origins").includes(host)) continue;
2999
+ if (!referenced.has(host)) referenced.set(host, tag.file);
2591
3000
  }
2592
3001
  if (referenced.size === 0) continue;
2593
3002
  const missing = [...referenced].filter(([host]) => !covered.has(host));
@@ -2638,7 +3047,7 @@ var seoIndexability = {
2638
3047
  rationale: "A noindex directive removes the page from search results; an accidental noindex on a public route silently deindexes it.",
2639
3048
  fix: FIX5,
2640
3049
  async check(ctx) {
2641
- const docsUrl7 = docsUrlFor("seo/indexability");
3050
+ const docsUrl8 = docsUrlFor("seo/indexability");
2642
3051
  const out = [];
2643
3052
  for (const head of ctx.heads) {
2644
3053
  const noindexed = head.tags.some((t) => t.kind === "meta" && t.name === "robots" && t.noindex === true);
@@ -2653,7 +3062,7 @@ var seoIndexability = {
2653
3062
  location: head.file,
2654
3063
  message: "Route is noindex \u2014 verify this is intentional",
2655
3064
  recommendation: 'If this route should be indexed, remove noindex from its <meta name="robots">.',
2656
- docsUrl: docsUrl7,
3065
+ docsUrl: docsUrl8,
2657
3066
  fix: { ...FIX5 }
2658
3067
  });
2659
3068
  }
@@ -2904,7 +3313,7 @@ function jsonldTags(head) {
2904
3313
  return head.tags.filter((t) => t.kind === "jsonld" && typeof t.jsonld === "string");
2905
3314
  }
2906
3315
  function jsonldRule(opts) {
2907
- const docsUrl7 = docsUrlFor(opts.id);
3316
+ const docsUrl8 = docsUrlFor(opts.id);
2908
3317
  return {
2909
3318
  id: opts.id,
2910
3319
  title: opts.title,
@@ -2932,7 +3341,7 @@ function jsonldRule(opts) {
2932
3341
  location: head.file,
2933
3342
  message: problem,
2934
3343
  recommendation: opts.recommendation,
2935
- docsUrl: docsUrl7,
3344
+ docsUrl: docsUrl8,
2936
3345
  ...opts.fix ? { fix: { ...opts.fix } } : {}
2937
3346
  } : {
2938
3347
  id: opts.id,
@@ -2942,7 +3351,7 @@ function jsonldRule(opts) {
2942
3351
  route: head.route,
2943
3352
  message: opts.label,
2944
3353
  recommendation: opts.recommendation,
2945
- docsUrl: docsUrl7
3354
+ docsUrl: docsUrl8
2946
3355
  }
2947
3356
  );
2948
3357
  }
@@ -2966,7 +3375,7 @@ var seoJsonLdValidity = {
2966
3375
  lang: "svelte"
2967
3376
  },
2968
3377
  async check(ctx) {
2969
- const docsUrl7 = docsUrlFor("seo/json-ld-validity");
3378
+ const docsUrl8 = docsUrlFor("seo/json-ld-validity");
2970
3379
  const out = [];
2971
3380
  for (const head of ctx.heads) {
2972
3381
  for (const tag of jsonldTags(head)) {
@@ -2985,7 +3394,7 @@ var seoJsonLdValidity = {
2985
3394
  location: head.file,
2986
3395
  message: problem,
2987
3396
  recommendation: "Make the JSON-LD valid JSON with both @context and @type.",
2988
- docsUrl: docsUrl7,
3397
+ docsUrl: docsUrl8,
2989
3398
  fix: { ...seoJsonLdValidity.fix }
2990
3399
  } : {
2991
3400
  id: "seo/json-ld-validity",
@@ -2995,7 +3404,7 @@ var seoJsonLdValidity = {
2995
3404
  route: head.route,
2996
3405
  message: "JSON-LD validity",
2997
3406
  recommendation: "Make the JSON-LD valid JSON with both @context and @type.",
2998
- docsUrl: docsUrl7
3407
+ docsUrl: docsUrl8
2999
3408
  }
3000
3409
  );
3001
3410
  }
@@ -3106,7 +3515,11 @@ function visibleLength(s) {
3106
3515
 
3107
3516
  // src/rules/seo/length-rule.ts
3108
3517
  function lengthRule(opts) {
3109
- const docsUrl7 = docsUrlFor(opts.id);
3518
+ const docsUrl8 = docsUrlFor(opts.id);
3519
+ const spec = {
3520
+ min: { kind: "integer", default: opts.min, min: 0 },
3521
+ max: { kind: "integer", default: opts.max, min: 1 }
3522
+ };
3110
3523
  return {
3111
3524
  id: opts.id,
3112
3525
  title: opts.title,
@@ -3114,15 +3527,22 @@ function lengthRule(opts) {
3114
3527
  severity: "info",
3115
3528
  scope: "route",
3116
3529
  rationale: opts.rationale,
3530
+ options: spec,
3117
3531
  async check(ctx) {
3118
3532
  const out = [];
3533
+ const compiled = compileOverrides(ctx.config);
3119
3534
  for (const head of ctx.heads) {
3120
3535
  const tag = head.tags.find(opts.match);
3121
3536
  if (!tag || typeof tag.text !== "string") continue;
3537
+ const location = tag.file ?? head.file;
3538
+ const o = resolveRuleOptions(opts.id, spec, ctx.config, { route: head.route, file: location }, compiled);
3539
+ const min = intOption(o, "min", opts.min);
3540
+ const max = intOption(o, "max", opts.max);
3541
+ const recommendation8 = typeof opts.recommendation === "function" ? opts.recommendation(o) : opts.recommendation;
3122
3542
  const len = visibleLength(tag.text);
3123
3543
  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})`;
3544
+ if (len < min) problem = `${opts.noun} is too short (${len} chars; aim for ${min}\u2013${max})`;
3545
+ else if (len > max) problem = `${opts.noun} is too long (${len} chars; aim for ${min}\u2013${max})`;
3126
3546
  out.push(
3127
3547
  problem ? {
3128
3548
  id: opts.id,
@@ -3130,10 +3550,10 @@ function lengthRule(opts) {
3130
3550
  severity: "info",
3131
3551
  detection: PENALIZED,
3132
3552
  route: head.route,
3133
- location: tag.file ?? head.file,
3553
+ location,
3134
3554
  message: problem,
3135
- recommendation: opts.recommendation,
3136
- docsUrl: docsUrl7
3555
+ recommendation: recommendation8,
3556
+ docsUrl: docsUrl8
3137
3557
  } : {
3138
3558
  id: opts.id,
3139
3559
  category: "seo",
@@ -3141,8 +3561,8 @@ function lengthRule(opts) {
3141
3561
  detection: PASS,
3142
3562
  route: head.route,
3143
3563
  message: opts.label,
3144
- recommendation: opts.recommendation,
3145
- docsUrl: docsUrl7
3564
+ recommendation: recommendation8,
3565
+ docsUrl: docsUrl8
3146
3566
  }
3147
3567
  );
3148
3568
  }
@@ -3152,28 +3572,32 @@ function lengthRule(opts) {
3152
3572
  }
3153
3573
 
3154
3574
  // src/rules/seo/title-length.ts
3575
+ var MIN = 30;
3576
+ var MAX = 60;
3155
3577
  var seoTitleLength = lengthRule({
3156
3578
  id: "seo/title-length",
3157
3579
  title: "Title length",
3158
3580
  label: "Title length",
3159
3581
  noun: "Title",
3160
3582
  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.",
3583
+ min: MIN,
3584
+ max: MAX,
3585
+ recommendation: (o) => `Aim for a title of ${intOption(o, "min", MIN)}\u2013${intOption(o, "max", MAX)} characters so it is not truncated in search results.`,
3164
3586
  rationale: "A title that is too short wastes the strongest on-page signal; one that is too long is truncated in the SERP."
3165
3587
  });
3166
3588
 
3167
3589
  // src/rules/seo/description-length.ts
3590
+ var MIN2 = 70;
3591
+ var MAX2 = 160;
3168
3592
  var seoDescriptionLength = lengthRule({
3169
3593
  id: "seo/description-length",
3170
3594
  title: "Description length",
3171
3595
  label: "Description length",
3172
3596
  noun: "Description",
3173
3597
  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.",
3598
+ min: MIN2,
3599
+ max: MAX2,
3600
+ recommendation: (o) => `Aim for a meta description of ${intOption(o, "min", MIN2)}\u2013${intOption(o, "max", MAX2)} characters so it is not truncated in search results.`,
3177
3601
  rationale: "A description that is too short under-uses the SERP snippet; one that is too long is truncated by search engines."
3178
3602
  });
3179
3603
 
@@ -3323,7 +3747,7 @@ var seoSingleH1 = {
3323
3747
 
3324
3748
  // src/rules/seo/uniqueness-rule.ts
3325
3749
  function uniquenessRule(opts) {
3326
- const docsUrl7 = docsUrlFor(opts.id);
3750
+ const docsUrl8 = docsUrlFor(opts.id);
3327
3751
  return {
3328
3752
  id: opts.id,
3329
3753
  title: opts.title,
@@ -3353,7 +3777,7 @@ function uniquenessRule(opts) {
3353
3777
  location: e.file,
3354
3778
  message: `${opts.noun} is duplicated across ${n} routes`,
3355
3779
  recommendation: opts.recommendation,
3356
- docsUrl: docsUrl7
3780
+ docsUrl: docsUrl8
3357
3781
  } : {
3358
3782
  id: opts.id,
3359
3783
  category: "seo",
@@ -3362,7 +3786,7 @@ function uniquenessRule(opts) {
3362
3786
  route: e.route,
3363
3787
  message: opts.label,
3364
3788
  recommendation: opts.recommendation,
3365
- docsUrl: docsUrl7
3789
+ docsUrl: docsUrl8
3366
3790
  };
3367
3791
  });
3368
3792
  }
@@ -3450,7 +3874,7 @@ function isSuppressed(m, ruleId, line) {
3450
3874
  return (m.suppressions ?? []).some((s) => s.line === line && (!s.ruleIds || s.ruleIds.includes(ruleId)));
3451
3875
  }
3452
3876
  function kitModuleRule(opts) {
3453
- const docsUrl7 = docsUrlFor(opts.id);
3877
+ const docsUrl8 = docsUrlFor(opts.id);
3454
3878
  const severity = opts.severity ?? "warning";
3455
3879
  return {
3456
3880
  id: opts.id,
@@ -3474,7 +3898,7 @@ function kitModuleRule(opts) {
3474
3898
  route: m.file,
3475
3899
  message: opts.label,
3476
3900
  recommendation: opts.recommendation,
3477
- docsUrl: docsUrl7
3901
+ docsUrl: docsUrl8
3478
3902
  });
3479
3903
  continue;
3480
3904
  }
@@ -3489,7 +3913,7 @@ function kitModuleRule(opts) {
3489
3913
  ...b.line > 0 ? { line: b.line } : {},
3490
3914
  message: b.message,
3491
3915
  recommendation: opts.recommendation,
3492
- docsUrl: docsUrl7,
3916
+ docsUrl: docsUrl8,
3493
3917
  ...opts.fix ? { fix: { ...opts.fix } } : {}
3494
3918
  });
3495
3919
  }
@@ -3525,7 +3949,7 @@ function isSuppressed2(c, ruleId, line) {
3525
3949
  return (c.suppressions ?? []).some((s) => s.line === line && (!s.ruleIds || s.ruleIds.includes(ruleId)));
3526
3950
  }
3527
3951
  function componentRule(opts) {
3528
- const docsUrl7 = docsUrlFor(opts.id);
3952
+ const docsUrl8 = docsUrlFor(opts.id);
3529
3953
  const severity = opts.severity ?? "warning";
3530
3954
  return {
3531
3955
  id: opts.id,
@@ -3535,11 +3959,15 @@ function componentRule(opts) {
3535
3959
  scope: "component",
3536
3960
  rationale: opts.rationale,
3537
3961
  ...opts.fix ? { fix: opts.fix } : {},
3962
+ ...opts.options ? { options: opts.options } : {},
3538
3963
  async check(ctx) {
3539
3964
  const out = [];
3965
+ const compiled = compileOverrides(ctx.config);
3540
3966
  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)));
3967
+ const o = resolveRuleOptions(opts.id, opts.options, ctx.config, { route: c.file, file: c.file }, compiled);
3968
+ const recommendation8 = typeof opts.recommendation === "function" ? opts.recommendation(o) : opts.recommendation;
3969
+ if (!opts.applies(c, o)) continue;
3970
+ const bad = opts.bad(c, o).filter((b) => !(b.line > 0 && isSuppressed2(c, opts.id, b.line)));
3543
3971
  if (bad.length === 0) {
3544
3972
  out.push({
3545
3973
  id: opts.id,
@@ -3548,8 +3976,8 @@ function componentRule(opts) {
3548
3976
  detection: PASS3,
3549
3977
  route: c.file,
3550
3978
  message: opts.label,
3551
- recommendation: opts.recommendation,
3552
- docsUrl: docsUrl7
3979
+ recommendation: recommendation8,
3980
+ docsUrl: docsUrl8
3553
3981
  });
3554
3982
  continue;
3555
3983
  }
@@ -3563,8 +3991,8 @@ function componentRule(opts) {
3563
3991
  location: c.file,
3564
3992
  ...b.line > 0 ? { line: b.line } : {},
3565
3993
  message: b.message,
3566
- recommendation: opts.recommendation,
3567
- docsUrl: docsUrl7,
3994
+ recommendation: recommendation8,
3995
+ docsUrl: docsUrl8,
3568
3996
  ...opts.fix ? { fix: { ...opts.fix } } : {}
3569
3997
  });
3570
3998
  }
@@ -3694,6 +4122,25 @@ var correctnessNonreactiveBuiltinState = componentRule({
3694
4122
  }))
3695
4123
  });
3696
4124
 
4125
+ // src/rules/correctness/checkable-bind-value.ts
4126
+ var correctnessCheckableBindValue = componentRule({
4127
+ id: "correctness/checkable-bind-value",
4128
+ title: "bind:value on a checkable input",
4129
+ category: "correctness",
4130
+ severity: "warning",
4131
+ label: "bind:checked / bind:group on checkable inputs",
4132
+ recommendation: "Replace bind:value with bind:checked (single checkbox) or bind:group (checkbox list / radio group).",
4133
+ rationale: "bind:value binds the DOM value property. A checkbox/radio's user interaction toggles checkedness, which bind:value never observes \u2014 the bound state is frozen at its initial value. Svelte's checked/grouped bindings (bind:checked, bind:group) are built for exactly this.",
4134
+ fix: {
4135
+ description: "For a single checkbox, replace bind:value={x} with bind:checked={x} (x becomes a boolean). For a checkbox list or radio group, replace bind:value={x} with bind:group={x} on every input sharing the group, keeping each input's static value attribute to identify the option."
4136
+ },
4137
+ applies: (c) => c.checkableBindValues.length > 0,
4138
+ bad: (c) => c.checkableBindValues.map((v) => ({
4139
+ line: v.line,
4140
+ message: v.kind === "checkbox" ? "bind:value on a checkbox does not track its checked state \u2014 the bound value silently never updates when the user toggles it. Use bind:checked (single checkbox) or bind:group (checkbox list) instead." : "bind:value on a radio input does not track which option is selected \u2014 the bound value silently never updates when the user picks one. Use bind:group with a shared group variable across the radio inputs instead."
4141
+ }))
4142
+ });
4143
+
3697
4144
  // src/rules/correctness/orphan-effect.ts
3698
4145
  var correctnessOrphanEffect = componentRule({
3699
4146
  id: "correctness/orphan-effect",
@@ -3793,24 +4240,35 @@ var correctnessOrphanLifecycle = {
3793
4240
  }
3794
4241
  };
3795
4242
 
3796
- // src/rules/correctness/server-browser-global.ts
4243
+ // src/rules/correctness/base-path-navigation.ts
3797
4244
  var PENALIZED5 = { presence: "none", value: "absent" };
3798
4245
  var PASS5 = { presence: "own", value: "static" };
3799
- var ID2 = "correctness/server-browser-global";
4246
+ var ID2 = "correctness/base-path-navigation";
3800
4247
  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"`;
4248
+ var LABEL2 = "Base-path-aware navigation";
4249
+ var RECOMMENDATION2 = "Wrap root-relative paths in resolve() from '$app/paths' so they resolve against kit.paths.base.";
4250
+ var FIX7 = {
4251
+ description: "Import { resolve } from '$app/paths' and wrap the path: href={resolve('/about')}, goto(resolve('/about')), redirect(303, resolve('/login'))."
4252
+ };
4253
+ function messageFor2(link) {
4254
+ if (link.kind === "href") {
4255
+ return `<a href="${link.path}"> is root-relative \u2014 under this project's kit.paths.base it points at the domain root, outside the app, and 404s in production. Use resolve('${link.path}') from '$app/paths'.`;
4256
+ }
4257
+ if (link.kind === "goto") {
4258
+ return `goto('${link.path}') is root-relative \u2014 it navigates outside this project's kit.paths.base and 404s in production. Use goto(resolve('${link.path}')) with resolve from '$app/paths'.`;
4259
+ }
4260
+ return `redirect(\u2026, '${link.path}') is root-relative \u2014 the Location header points outside this project's kit.paths.base and 404s in production. Use resolve('${link.path}') from '$app/paths'.`;
4261
+ }
3804
4262
  function isSuppressed4(suppressions, line) {
3805
4263
  return (suppressions ?? []).some((s) => s.line === line && (!s.ruleIds || s.ruleIds.includes(ID2)));
3806
4264
  }
3807
- function emitFile2(out, file, issues, suppressions) {
3808
- const bad = issues.filter((b) => !(b.line > 0 && isSuppressed4(suppressions, b.line)));
4265
+ function emitFile2(out, file, links, suppressions) {
4266
+ const bad = links.filter((l) => !(l.line > 0 && isSuppressed4(suppressions, l.line)));
3809
4267
  if (bad.length === 0) {
3810
4268
  out.push({
3811
4269
  id: ID2,
3812
4270
  category: "correctness",
3813
- severity: "critical",
4271
+ severity: "warning",
3814
4272
  detection: PASS5,
3815
4273
  route: file,
3816
4274
  message: LABEL2,
@@ -3819,23 +4277,90 @@ function emitFile2(out, file, issues, suppressions) {
3819
4277
  });
3820
4278
  return;
3821
4279
  }
3822
- for (const b of bad) {
4280
+ for (const l of bad) {
3823
4281
  out.push({
3824
4282
  id: ID2,
3825
4283
  category: "correctness",
3826
- severity: "critical",
4284
+ severity: "warning",
3827
4285
  detection: PENALIZED5,
3828
4286
  route: file,
3829
4287
  location: file,
4288
+ ...l.line > 0 ? { line: l.line } : {},
4289
+ message: messageFor2(l),
4290
+ recommendation: RECOMMENDATION2,
4291
+ docsUrl: DOCS_URL2,
4292
+ fix: { ...FIX7 }
4293
+ });
4294
+ }
4295
+ }
4296
+ var correctnessBasePathNavigation = {
4297
+ id: ID2,
4298
+ title: "Root-relative navigation under a base path",
4299
+ category: "correctness",
4300
+ severity: "warning",
4301
+ scope: "component",
4302
+ rationale: "A root-relative literal resolves against the domain root, not kit.paths.base, so navigation lands outside an app served from a sub-path. The break only appears once the app is deployed under its base \u2014 locally base is usually empty, so every such link works.",
4303
+ fix: { ...FIX7 },
4304
+ async check(ctx) {
4305
+ if (!ctx.project.kitPathsBase) return [];
4306
+ const out = [];
4307
+ for (const c of ctx.components ?? []) {
4308
+ const links = c.basePathLinks ?? [];
4309
+ if (links.length === 0) continue;
4310
+ emitFile2(out, c.file, links, c.suppressions);
4311
+ }
4312
+ for (const m of ctx.kitModules ?? []) {
4313
+ const links = m.basePathLinks ?? [];
4314
+ if (links.length === 0) continue;
4315
+ emitFile2(out, m.file, links, m.suppressions);
4316
+ }
4317
+ return out;
4318
+ }
4319
+ };
4320
+
4321
+ // src/rules/correctness/server-browser-global.ts
4322
+ var PENALIZED6 = { presence: "none", value: "absent" };
4323
+ var PASS6 = { presence: "own", value: "static" };
4324
+ var ID3 = "correctness/server-browser-global";
4325
+ var DOCS_URL3 = docsUrlFor(ID3);
4326
+ var LABEL3 = "Server-safe module code";
4327
+ var RECOMMENDATION3 = "Move browser-only code into onMount or $effect (they never run on the server), or guard it with browser from $app/environment (or a typeof check).";
4328
+ var moduleMessage = (name) => `${name} is accessed at module scope \u2014 it does not exist on the server, so importing this file crashes SSR with "${name} is not defined"`;
4329
+ function isSuppressed5(suppressions, line) {
4330
+ return (suppressions ?? []).some((s) => s.line === line && (!s.ruleIds || s.ruleIds.includes(ID3)));
4331
+ }
4332
+ function emitFile3(out, file, issues, suppressions) {
4333
+ const bad = issues.filter((b) => !(b.line > 0 && isSuppressed5(suppressions, b.line)));
4334
+ if (bad.length === 0) {
4335
+ out.push({
4336
+ id: ID3,
4337
+ category: "correctness",
4338
+ severity: "critical",
4339
+ detection: PASS6,
4340
+ route: file,
4341
+ message: LABEL3,
4342
+ recommendation: RECOMMENDATION3,
4343
+ docsUrl: DOCS_URL3
4344
+ });
4345
+ return;
4346
+ }
4347
+ for (const b of bad) {
4348
+ out.push({
4349
+ id: ID3,
4350
+ category: "correctness",
4351
+ severity: "critical",
4352
+ detection: PENALIZED6,
4353
+ route: file,
4354
+ location: file,
3830
4355
  ...b.line > 0 ? { line: b.line } : {},
3831
4356
  message: b.message,
3832
- recommendation: RECOMMENDATION2,
3833
- docsUrl: DOCS_URL2
4357
+ recommendation: RECOMMENDATION3,
4358
+ docsUrl: DOCS_URL3
3834
4359
  });
3835
4360
  }
3836
4361
  }
3837
4362
  var correctnessServerBrowserGlobal = {
3838
- id: ID2,
4363
+ id: ID3,
3839
4364
  title: "Browser global in server module code",
3840
4365
  category: "correctness",
3841
4366
  severity: "critical",
@@ -3846,7 +4371,7 @@ var correctnessServerBrowserGlobal = {
3846
4371
  for (const c of ctx.components ?? []) {
3847
4372
  const refs = (c.browserGlobalRefs ?? []).filter((r) => r.context === "module");
3848
4373
  if (refs.length === 0) continue;
3849
- emitFile2(
4374
+ emitFile3(
3850
4375
  out,
3851
4376
  c.file,
3852
4377
  refs.map((r) => ({ line: r.line, message: moduleMessage(r.name) })),
@@ -3856,7 +4381,7 @@ var correctnessServerBrowserGlobal = {
3856
4381
  for (const m of ctx.kitModules ?? []) {
3857
4382
  const refs = m.browserGlobalRefs ?? [];
3858
4383
  if (refs.length === 0) continue;
3859
- emitFile2(
4384
+ emitFile3(
3860
4385
  out,
3861
4386
  m.file,
3862
4387
  refs.map((r) => ({
@@ -3972,35 +4497,151 @@ var securitySharedStateImport = kitModuleRule({
3972
4497
  });
3973
4498
 
3974
4499
  // src/rules/architecture/component-size.ts
3975
- var MAX_LOC = 400;
4500
+ var MAX_LOC = 200;
3976
4501
  var architectureComponentSize = componentRule({
3977
4502
  id: "architecture/component-size",
3978
4503
  title: "Component size",
3979
4504
  category: "architecture",
3980
4505
  severity: "info",
3981
4506
  label: "Component size",
3982
- recommendation: `Split components over ${MAX_LOC} lines into smaller, focused pieces.`,
4507
+ options: { max: { kind: "integer", default: MAX_LOC, min: 1 } },
4508
+ recommendation: (o) => `Split components over ${intOption(o, "max", MAX_LOC)} lines into smaller, focused pieces.`,
3983
4509
  rationale: "A very large component is hard to read, test, and reuse, and is a common sign that several responsibilities should be split out.",
3984
4510
  applies: (c) => c.loc > 0,
3985
4511
  // 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})` }] : []
4512
+ bad: (c, o) => {
4513
+ const max = intOption(o, "max", MAX_LOC);
4514
+ return c.loc > max ? [{ line: 1, message: `Component is ${c.loc} lines (over ${max})` }] : [];
4515
+ }
3987
4516
  });
3988
4517
 
3989
4518
  // src/rules/architecture/prop-count.ts
3990
- var MAX_PROPS = 10;
4519
+ var MAX_PROPS = 6;
3991
4520
  var architecturePropCount = componentRule({
3992
4521
  id: "architecture/prop-count",
3993
4522
  title: "Prop count",
3994
4523
  category: "architecture",
3995
4524
  severity: "info",
3996
4525
  label: "Prop count",
3997
- recommendation: `Group related props into an object, or split the component, when it takes more than ${MAX_PROPS} props.`,
4526
+ options: { max: { kind: "integer", default: MAX_PROPS, min: 1 } },
4527
+ recommendation: (o) => `Group related props into an object, or split the component, when it takes more than ${intOption(o, "max", MAX_PROPS)} props.`,
3998
4528
  rationale: "A component taking many props is usually doing too much; grouping or splitting keeps its API understandable.",
3999
4529
  applies: (c) => c.propCount > 0,
4000
4530
  // 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})` }] : []
4531
+ bad: (c, o) => {
4532
+ const max = intOption(o, "max", MAX_PROPS);
4533
+ return c.propCount > max ? [{ line: 1, message: `Component takes ${c.propCount} props (over ${max})` }] : [];
4534
+ }
4002
4535
  });
4003
4536
 
4537
+ // src/rules/architecture/private-scope-import.ts
4538
+ var docsUrl7 = docsUrlFor("architecture/private-scope-import");
4539
+ var recommendation7 = "Move the unit to the directory shared by all of its importers, or import it only from inside its own scope.";
4540
+ var OPTIONS2 = { scopes: { kind: "string-list", default: [] } };
4541
+ function ancestorDirs(file) {
4542
+ const segments = file.split("/");
4543
+ const out = [];
4544
+ for (let i = segments.length - 1; i > 0; i--) out.push(segments.slice(0, i).join("/"));
4545
+ return out;
4546
+ }
4547
+ function privateScopeOf(target, patterns) {
4548
+ for (const dir of ancestorDirs(target)) {
4549
+ if (!patterns.some((p) => p.test(dir))) continue;
4550
+ const cut = dir.lastIndexOf("/");
4551
+ return cut === -1 ? "" : dir.slice(0, cut);
4552
+ }
4553
+ return void 0;
4554
+ }
4555
+ function isInside(file, boundary) {
4556
+ return boundary === "" || file.startsWith(`${boundary}/`);
4557
+ }
4558
+ var architecturePrivateScopeImport = {
4559
+ id: "architecture/private-scope-import",
4560
+ title: "Private-scope import",
4561
+ category: "architecture",
4562
+ severity: "info",
4563
+ scope: "component",
4564
+ rationale: "A unit placed inside a private directory is written for one owner; importing it from elsewhere couples two parts of the tree that were meant to move independently, and the unit belongs higher up instead.",
4565
+ fix: {
4566
+ description: "Move this unit out of its private scope, to the directory shared by all of its importers, and update this import."
4567
+ },
4568
+ options: OPTIONS2,
4569
+ async check(ctx) {
4570
+ const out = [];
4571
+ const compiled = compileOverrides(ctx.config);
4572
+ const patternCache = /* @__PURE__ */ new Map();
4573
+ const compileScopes = (scopes) => {
4574
+ const key = JSON.stringify(scopes);
4575
+ let patterns = patternCache.get(key);
4576
+ if (patterns === void 0) {
4577
+ patterns = scopes.map((scope) => {
4578
+ const marker = scope.endsWith("/**") ? scope.slice(0, -3) : scope;
4579
+ return routeGlobToRegExp(marker);
4580
+ });
4581
+ patternCache.set(key, patterns);
4582
+ }
4583
+ return patterns;
4584
+ };
4585
+ for (const c of ctx.components ?? []) {
4586
+ const o = resolveRuleOptions(
4587
+ "architecture/private-scope-import",
4588
+ OPTIONS2,
4589
+ ctx.config,
4590
+ { route: c.file, file: c.file },
4591
+ compiled
4592
+ );
4593
+ const scopes = listOption(o, "scopes");
4594
+ if (scopes.length === 0) continue;
4595
+ const patterns = compileScopes(scopes);
4596
+ const spans = c.importSpans ?? c.imports.map((source) => ({ source, line: 0 }));
4597
+ let sawScopedImport = false;
4598
+ const violations = [];
4599
+ for (const { source, line } of spans) {
4600
+ const target = resolveRepoLocalPath(source, c.file);
4601
+ if (target === void 0) continue;
4602
+ const boundary = privateScopeOf(target, patterns);
4603
+ if (boundary === void 0) continue;
4604
+ sawScopedImport = true;
4605
+ if (isInside(c.file, boundary)) continue;
4606
+ violations.push({ line, message: `${target} is private to ${boundary}` });
4607
+ }
4608
+ if (!sawScopedImport) continue;
4609
+ const visible = violations.filter(
4610
+ (v) => !(v.line > 0 && isSuppressed2(c, "architecture/private-scope-import", v.line))
4611
+ );
4612
+ if (visible.length === 0) {
4613
+ out.push({
4614
+ id: "architecture/private-scope-import",
4615
+ category: "architecture",
4616
+ severity: "info",
4617
+ detection: { presence: "own", value: "static" },
4618
+ route: c.file,
4619
+ message: "No private-scope imports",
4620
+ recommendation: recommendation7,
4621
+ docsUrl: docsUrl7
4622
+ });
4623
+ continue;
4624
+ }
4625
+ for (const v of visible) {
4626
+ out.push({
4627
+ id: "architecture/private-scope-import",
4628
+ category: "architecture",
4629
+ severity: "info",
4630
+ detection: { presence: "none", value: "absent" },
4631
+ route: c.file,
4632
+ location: c.file,
4633
+ ...v.line > 0 ? { line: v.line } : {},
4634
+ message: v.message,
4635
+ recommendation: recommendation7,
4636
+ docsUrl: docsUrl7,
4637
+ fix: { ...architecturePrivateScopeImport.fix }
4638
+ });
4639
+ }
4640
+ }
4641
+ return out;
4642
+ }
4643
+ };
4644
+
4004
4645
  // src/rules/perf/heavy-import.ts
4005
4646
  var HEAVY_PACKAGES = {
4006
4647
  lodash: "import a submodule (lodash/debounce) or use lodash-es for tree-shaking",
@@ -4014,18 +4655,20 @@ var performanceHeavyImport = componentRule({
4014
4655
  label: "No heavy imports",
4015
4656
  recommendation: "Import a submodule or switch to a lighter, tree-shakeable alternative.",
4016
4657
  rationale: "Importing a large, non-tree-shakeable package pulls its whole weight into the bundle even when only a fraction is used, slowing load.",
4658
+ options: { packages: { kind: "string-map", default: HEAVY_PACKAGES } },
4017
4659
  // ComponentFacts is a public @svelte-vitals/core export — an external caller compiled
4018
4660
  // against an older version may still construct one without importSpans. Fall back to the
4019
4661
  // line-less `imports` (line: 0, the pre-fix behavior) instead of crashing on `undefined`.
4020
4662
  applies: (c) => (c.importSpans ?? c.imports).length > 0,
4021
- bad: (c) => {
4663
+ bad: (c, o) => {
4664
+ const packages = mapOption(o, "packages");
4022
4665
  const seen = /* @__PURE__ */ new Set();
4023
4666
  const out = [];
4024
4667
  const spans = c.importSpans ?? c.imports.map((source) => ({ source, line: 0 }));
4025
4668
  for (const { source: src, line } of spans) {
4026
- if (!Object.hasOwn(HEAVY_PACKAGES, src) || seen.has(src)) continue;
4669
+ if (!Object.hasOwn(packages, src) || seen.has(src)) continue;
4027
4670
  seen.add(src);
4028
- out.push({ line, message: `Heavy import "${src}" \u2014 ${HEAVY_PACKAGES[src]}` });
4671
+ out.push({ line, message: `Heavy import "${src}" \u2014 ${packages[src]}` });
4029
4672
  }
4030
4673
  return out;
4031
4674
  }
@@ -4055,13 +4698,13 @@ var performanceNamespaceImport = componentRule({
4055
4698
  });
4056
4699
 
4057
4700
  // src/rules/perf/minify-disabled.ts
4058
- var PENALIZED6 = { presence: "none", value: "absent" };
4701
+ var PENALIZED7 = { presence: "none", value: "absent" };
4059
4702
  var MINIFY_DISABLED_FIX = {
4060
4703
  description: "Remove the minify: false override from vite.config (Vite minifies with esbuild by default), or scope it to non-production builds.",
4061
4704
  snippet: "export default defineConfig({\n build: {\n minify: 'esbuild'\n }\n});",
4062
4705
  lang: "ts"
4063
4706
  };
4064
- var RECOMMENDATION3 = "Remove build.minify: false from vite.config, or scope it to non-production builds if it is intentional.";
4707
+ var RECOMMENDATION4 = "Remove build.minify: false from vite.config, or scope it to non-production builds if it is intentional.";
4065
4708
  var performanceMinifyDisabled = {
4066
4709
  id: "performance/minify-disabled",
4067
4710
  title: "Minification disabled",
@@ -4079,11 +4722,11 @@ var performanceMinifyDisabled = {
4079
4722
  id: "performance/minify-disabled",
4080
4723
  category: "performance",
4081
4724
  severity: "warning",
4082
- detection: PENALIZED6,
4725
+ detection: PENALIZED7,
4083
4726
  ...hit.file !== void 0 ? { location: hit.file } : {},
4084
4727
  ...hit.line !== void 0 ? { line: hit.line } : {},
4085
4728
  message: "JS/CSS minification is disabled (build.minify: false) \u2014 production bundles ship unminified and several times larger." + provenance,
4086
- recommendation: RECOMMENDATION3,
4729
+ recommendation: RECOMMENDATION4,
4087
4730
  docsUrl: docsUrlFor("performance/minify-disabled"),
4088
4731
  fix: { ...MINIFY_DISABLED_FIX }
4089
4732
  }
@@ -4197,8 +4840,10 @@ var allRules = [
4197
4840
  correctnessPropMutation,
4198
4841
  correctnessStalePropDerivation,
4199
4842
  correctnessNonreactiveBuiltinState,
4843
+ correctnessCheckableBindValue,
4200
4844
  correctnessOrphanEffect,
4201
4845
  correctnessOrphanLifecycle,
4846
+ correctnessBasePathNavigation,
4202
4847
  correctnessServerBrowserGlobal,
4203
4848
  correctnessInstanceBrowserGlobal,
4204
4849
  securityRawHtml,
@@ -4208,6 +4853,7 @@ var allRules = [
4208
4853
  securitySharedStateImport,
4209
4854
  architectureComponentSize,
4210
4855
  architecturePropCount,
4856
+ architecturePrivateScopeImport,
4211
4857
  performanceHeavyImport,
4212
4858
  performanceNamespaceImport,
4213
4859
  performanceMinifyDisabled,
@@ -4215,6 +4861,15 @@ var allRules = [
4215
4861
  performanceSequentialAwaits,
4216
4862
  performanceStateRaw
4217
4863
  ];
4864
+ function optionInfos(spec) {
4865
+ return Object.entries(spec).map(([name, s]) => ({
4866
+ name,
4867
+ kind: s.kind,
4868
+ default: s.default,
4869
+ ...s.kind === "integer" && s.min !== void 0 ? { min: s.min } : {},
4870
+ ...s.kind === "integer" && s.max !== void 0 ? { max: s.max } : {}
4871
+ }));
4872
+ }
4218
4873
  function explainRule(id) {
4219
4874
  const rule = allRules.find((r) => r.id === id);
4220
4875
  if (!rule) return void 0;
@@ -4225,7 +4880,8 @@ function explainRule(id) {
4225
4880
  severity: rule.severity,
4226
4881
  rationale: rule.rationale,
4227
4882
  docsUrl: docsUrlFor(rule.id),
4228
- ...rule.fix ? { fix: rule.fix } : {}
4883
+ ...rule.fix ? { fix: rule.fix } : {},
4884
+ ...rule.options ? { options: optionInfos(rule.options) } : {}
4229
4885
  };
4230
4886
  }
4231
4887
 
@@ -5444,60 +6100,21 @@ function safeHref(url) {
5444
6100
  const normalized = url.replace(/\s/g, "").toLowerCase();
5445
6101
  return /^https?:\/\//.test(normalized) ? url : null;
5446
6102
  }
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
6103
  export {
5491
6104
  APP_SCRIPT,
5492
6105
  APP_STYLE,
5493
6106
  BAND_COLOR,
6107
+ CATEGORIES,
5494
6108
  CHILD_NODE_KEYS,
5495
6109
  ROBOTS_SOURCE_PATHS,
5496
6110
  SITEMAP_SOURCE_PATHS,
6111
+ SVELTE_CONFIG_FILES,
6112
+ VITE_CONFIG_FILES,
5497
6113
  allRules,
5498
6114
  applyOverrides,
5499
6115
  applyRuleSeverities,
5500
6116
  architectureComponentSize,
6117
+ architecturePrivateScopeImport,
5501
6118
  architecturePropCount,
5502
6119
  attrText,
5503
6120
  attrTextOf,
@@ -5508,8 +6125,11 @@ export {
5508
6125
  classify,
5509
6126
  collectComponentFacts,
5510
6127
  collectKitModuleFacts,
6128
+ compileOverrides,
5511
6129
  computeHealth,
5512
6130
  computeScore,
6131
+ correctnessBasePathNavigation,
6132
+ correctnessCheckableBindValue,
5513
6133
  correctnessEachIndexKey,
5514
6134
  correctnessEachKey,
5515
6135
  correctnessEffectAsDerived,
@@ -5532,6 +6152,8 @@ export {
5532
6152
  escapeHtml,
5533
6153
  explainRule,
5534
6154
  findAttr,
6155
+ findKitPathsBaseInSvelteConfig,
6156
+ findKitPathsBaseInViteConfig,
5535
6157
  findMinifyDisabled,
5536
6158
  formatAgentReport,
5537
6159
  formatConsoleReport,
@@ -5543,10 +6165,14 @@ export {
5543
6165
  hasFailureAtOrAbove,
5544
6166
  headTagRule,
5545
6167
  imageRule,
6168
+ intOption,
5546
6169
  isPenalized,
5547
6170
  lineOf,
5548
6171
  linkRule,
6172
+ listOption,
6173
+ mapOption,
5549
6174
  noColorPalette,
6175
+ overrideMatches,
5550
6176
  parseComponentFacts,
5551
6177
  parseKitModuleFacts,
5552
6178
  performanceFontPreloadCrossorigin,
@@ -5564,6 +6190,8 @@ export {
5564
6190
  performanceSequentialAwaits,
5565
6191
  performanceStateRaw,
5566
6192
  renderAppShell,
6193
+ resolveKitPathsBase,
6194
+ resolveRuleOptions,
5567
6195
  resolveRunesModuleSpecifier,
5568
6196
  runRules,
5569
6197
  safeHref,
@@ -5607,7 +6235,12 @@ export {
5607
6235
  seoTitlePresence,
5608
6236
  seoTwitterCard,
5609
6237
  seoViewport,
6238
+ settingOptions,
6239
+ settingSeverity,
6240
+ shouldSkipRangeCheck,
5610
6241
  summarize,
5611
6242
  textFromNodes,
6243
+ validateRuleOptions,
6244
+ validateRuleSetting,
5612
6245
  valueFromNodes
5613
6246
  };