@svelte-vitals/core 0.30.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.
package/dist/index.js CHANGED
@@ -857,10 +857,19 @@ function countLines(source) {
857
857
  if (source.length === 0) return 0;
858
858
  return source.split("\n").length - (source.endsWith("\n") ? 1 : 0);
859
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
+ }
860
865
  function collectImportSources(program, source, acc) {
861
866
  walkEstree(program, (n) => {
862
867
  if (n.type === "ImportDeclaration" && typeof n.source?.value === "string") {
863
- 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
+ });
864
873
  }
865
874
  });
866
875
  }
@@ -1516,6 +1525,12 @@ async function collectComponentFacts(rt, cwd) {
1516
1525
  );
1517
1526
  }
1518
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
+
1519
1534
  // src/kit-module-parse.ts
1520
1535
  var HANDLER_NAMES = /* @__PURE__ */ new Set([
1521
1536
  "load",
@@ -1796,28 +1811,45 @@ function normalizePosix(path) {
1796
1811
  }
1797
1812
  return out.join("/");
1798
1813
  }
1799
- 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) {
1800
1821
  let path;
1801
- if (spec.startsWith("$lib/")) path = `src/lib/${spec.slice("$lib/".length)}`;
1802
- else if (spec.startsWith("./") || spec.startsWith("../")) {
1822
+ if (spec.startsWith("./") || spec.startsWith("../")) {
1803
1823
  const dir = importerFile.split("/").slice(0, -1).join("/");
1804
1824
  path = `${dir}/${spec}`;
1805
- } 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
+ }
1806
1831
  return normalizePosix(path);
1807
1832
  }
1808
- function resolveRunesModuleSpecifier(spec, importerFile) {
1809
- const path = resolveRepoLocalPath(spec, importerFile);
1833
+ function resolveRunesModuleSpecifier(spec, importerFile, aliases) {
1834
+ const path = resolveRepoLocalPath(spec, importerFile, aliases);
1810
1835
  if (path === void 0) return void 0;
1811
1836
  if (/\.svelte\.(ts|js)$/.test(path)) return path;
1812
1837
  if (path.endsWith(".svelte")) return `${path}.ts`;
1813
1838
  return void 0;
1814
1839
  }
1815
- function isLocalStateSpecifier(spec, importerFile) {
1816
- 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);
1817
1849
  if (path === void 0) return false;
1818
- return path !== "src/lib/server" && !path.startsWith("src/lib/server/");
1850
+ return path !== serverRoot && !path.startsWith(`${serverRoot}/`);
1819
1851
  }
1820
- function parseKitModuleFacts(source, filename) {
1852
+ function parseKitModuleFacts(source, filename, aliases) {
1821
1853
  const suppressions = collectSuppressions(source);
1822
1854
  const { program, wrapped } = parseModuleProgram(source, filename);
1823
1855
  const moduleStateReassignments = [];
@@ -1850,7 +1882,7 @@ function parseKitModuleFacts(source, filename) {
1850
1882
  importedSpecifiers.set(s.local.name, spec);
1851
1883
  }
1852
1884
  if (names.length === 0) continue;
1853
- const resolved = resolveRunesModuleSpecifier(spec, filename);
1885
+ const resolved = resolveRunesModuleSpecifier(spec, filename, aliases);
1854
1886
  if (resolved) runesModuleImports.push({ source: spec, resolved, names, line: line(stmt.start) });
1855
1887
  }
1856
1888
  const moduleLets = /* @__PURE__ */ new Set();
@@ -1924,7 +1956,8 @@ function parseKitModuleFacts(source, filename) {
1924
1956
  const method = n.callee.property?.type === "Identifier" ? n.callee.property.name : void 0;
1925
1957
  if (method === "set" || method === "update") {
1926
1958
  const r = importedRoot(n.callee.object);
1927
- 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" };
1928
1961
  }
1929
1962
  } else if (n.type === "AssignmentExpression" && (n.left?.type === "ObjectPattern" || n.left?.type === "ArrayPattern")) {
1930
1963
  const scanPatternTargets = (pat) => {
@@ -2002,7 +2035,7 @@ function kindOf(file) {
2002
2035
  const base = file.split("/").pop() ?? file;
2003
2036
  return base.includes(".server.") || base.startsWith("+server.") ? "server" : "universal";
2004
2037
  }
2005
- async function collectKitModuleFacts(rt, cwd) {
2038
+ async function collectKitModuleFacts(rt, cwd, aliases) {
2006
2039
  const patterns = [
2007
2040
  "src/routes/**/+{page,layout}.server.{ts,js}",
2008
2041
  "src/routes/**/+{page,layout}.{ts,js}",
@@ -2016,7 +2049,7 @@ async function collectKitModuleFacts(rt, cwd) {
2016
2049
  const kind = kindOf(rel);
2017
2050
  try {
2018
2051
  const source = await rt.readFile(rt.join(cwd, rel));
2019
- return { file: rel, kind, ...parseKitModuleFacts(source, rel) };
2052
+ return { file: rel, kind, ...parseKitModuleFacts(source, rel, aliases) };
2020
2053
  } catch {
2021
2054
  return emptyKitModuleFacts(rel, kind);
2022
2055
  }
@@ -2114,6 +2147,79 @@ function basePathOf(kitConfig, bindings) {
2114
2147
  }
2115
2148
  return {};
2116
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
+ }
2117
2223
  function programOf(source, filename) {
2118
2224
  try {
2119
2225
  return parseModuleProgram(source, filename).program ?? void 0;
@@ -2263,7 +2369,7 @@ function detect(head, match) {
2263
2369
  return tag ? { presence: tag.presence, value: tag.value } : { presence: "none", value: "absent" };
2264
2370
  }
2265
2371
  function headTagRule(opts) {
2266
- const docsUrl8 = docsUrlFor(opts.id);
2372
+ const docsUrl11 = docsUrlFor(opts.id);
2267
2373
  return {
2268
2374
  id: opts.id,
2269
2375
  title: opts.title,
@@ -2286,7 +2392,7 @@ function headTagRule(opts) {
2286
2392
  location: head.file,
2287
2393
  message,
2288
2394
  recommendation: opts.recommendation,
2289
- docsUrl: docsUrl8,
2395
+ docsUrl: docsUrl11,
2290
2396
  // Copy per finding: opts.fix is a rule-level template shared across all
2291
2397
  // results this rule emits; a fresh object keeps findings independent.
2292
2398
  ...opts.fix ? { fix: { ...opts.fix } } : {}
@@ -2479,7 +2585,7 @@ var seoHtmlLang = {
2479
2585
 
2480
2586
  // src/rules/perf/image-rule.ts
2481
2587
  function imageRule(opts) {
2482
- const docsUrl8 = docsUrlFor(opts.id);
2588
+ const docsUrl11 = docsUrlFor(opts.id);
2483
2589
  const category = opts.category ?? "performance";
2484
2590
  return {
2485
2591
  id: opts.id,
@@ -2503,7 +2609,7 @@ function imageRule(opts) {
2503
2609
  route: route.route,
2504
2610
  message: opts.label,
2505
2611
  recommendation: opts.recommendation,
2506
- docsUrl: docsUrl8
2612
+ docsUrl: docsUrl11
2507
2613
  });
2508
2614
  continue;
2509
2615
  }
@@ -2518,7 +2624,7 @@ function imageRule(opts) {
2518
2624
  ...img.line > 0 ? { line: img.line } : {},
2519
2625
  message: `Missing ${opts.label}`,
2520
2626
  recommendation: opts.recommendation,
2521
- docsUrl: docsUrl8,
2627
+ docsUrl: docsUrl11,
2522
2628
  ...opts.fix ? { fix: { ...opts.fix } } : {}
2523
2629
  });
2524
2630
  }
@@ -2578,7 +2684,7 @@ var performanceResponsiveImage = imageRule({
2578
2684
 
2579
2685
  // src/rules/perf/link-rule.ts
2580
2686
  function linkRule(opts) {
2581
- const docsUrl8 = docsUrlFor(opts.id);
2687
+ const docsUrl11 = docsUrlFor(opts.id);
2582
2688
  return {
2583
2689
  id: opts.id,
2584
2690
  title: opts.title,
@@ -2602,7 +2708,7 @@ function linkRule(opts) {
2602
2708
  route: head.route,
2603
2709
  message: opts.label,
2604
2710
  recommendation: opts.recommendation,
2605
- docsUrl: docsUrl8
2711
+ docsUrl: docsUrl11
2606
2712
  });
2607
2713
  continue;
2608
2714
  }
@@ -2619,7 +2725,7 @@ function linkRule(opts) {
2619
2725
  location: tag.file ?? head.file,
2620
2726
  message: `Missing ${opts.label}`,
2621
2727
  recommendation: opts.recommendation,
2622
- docsUrl: docsUrl8,
2728
+ docsUrl: docsUrl11,
2623
2729
  ...opts.fix ? { fix: { ...opts.fix } } : {}
2624
2730
  });
2625
2731
  }
@@ -2842,6 +2948,10 @@ function mapOption(options, key) {
2842
2948
  const v = options[key];
2843
2949
  return typeof v === "object" && v !== null && !Array.isArray(v) ? v : {};
2844
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
+ }
2845
2955
  function resolveRuleOptions(ruleId, spec, config, target, compiled) {
2846
2956
  if (!spec) return {};
2847
2957
  const out = defaultsOf(spec);
@@ -3047,7 +3157,7 @@ var seoIndexability = {
3047
3157
  rationale: "A noindex directive removes the page from search results; an accidental noindex on a public route silently deindexes it.",
3048
3158
  fix: FIX5,
3049
3159
  async check(ctx) {
3050
- const docsUrl8 = docsUrlFor("seo/indexability");
3160
+ const docsUrl11 = docsUrlFor("seo/indexability");
3051
3161
  const out = [];
3052
3162
  for (const head of ctx.heads) {
3053
3163
  const noindexed = head.tags.some((t) => t.kind === "meta" && t.name === "robots" && t.noindex === true);
@@ -3062,7 +3172,7 @@ var seoIndexability = {
3062
3172
  location: head.file,
3063
3173
  message: "Route is noindex \u2014 verify this is intentional",
3064
3174
  recommendation: 'If this route should be indexed, remove noindex from its <meta name="robots">.',
3065
- docsUrl: docsUrl8,
3175
+ docsUrl: docsUrl11,
3066
3176
  fix: { ...FIX5 }
3067
3177
  });
3068
3178
  }
@@ -3313,7 +3423,7 @@ function jsonldTags(head) {
3313
3423
  return head.tags.filter((t) => t.kind === "jsonld" && typeof t.jsonld === "string");
3314
3424
  }
3315
3425
  function jsonldRule(opts) {
3316
- const docsUrl8 = docsUrlFor(opts.id);
3426
+ const docsUrl11 = docsUrlFor(opts.id);
3317
3427
  return {
3318
3428
  id: opts.id,
3319
3429
  title: opts.title,
@@ -3341,7 +3451,7 @@ function jsonldRule(opts) {
3341
3451
  location: head.file,
3342
3452
  message: problem,
3343
3453
  recommendation: opts.recommendation,
3344
- docsUrl: docsUrl8,
3454
+ docsUrl: docsUrl11,
3345
3455
  ...opts.fix ? { fix: { ...opts.fix } } : {}
3346
3456
  } : {
3347
3457
  id: opts.id,
@@ -3351,7 +3461,7 @@ function jsonldRule(opts) {
3351
3461
  route: head.route,
3352
3462
  message: opts.label,
3353
3463
  recommendation: opts.recommendation,
3354
- docsUrl: docsUrl8
3464
+ docsUrl: docsUrl11
3355
3465
  }
3356
3466
  );
3357
3467
  }
@@ -3375,7 +3485,7 @@ var seoJsonLdValidity = {
3375
3485
  lang: "svelte"
3376
3486
  },
3377
3487
  async check(ctx) {
3378
- const docsUrl8 = docsUrlFor("seo/json-ld-validity");
3488
+ const docsUrl11 = docsUrlFor("seo/json-ld-validity");
3379
3489
  const out = [];
3380
3490
  for (const head of ctx.heads) {
3381
3491
  for (const tag of jsonldTags(head)) {
@@ -3394,7 +3504,7 @@ var seoJsonLdValidity = {
3394
3504
  location: head.file,
3395
3505
  message: problem,
3396
3506
  recommendation: "Make the JSON-LD valid JSON with both @context and @type.",
3397
- docsUrl: docsUrl8,
3507
+ docsUrl: docsUrl11,
3398
3508
  fix: { ...seoJsonLdValidity.fix }
3399
3509
  } : {
3400
3510
  id: "seo/json-ld-validity",
@@ -3404,7 +3514,7 @@ var seoJsonLdValidity = {
3404
3514
  route: head.route,
3405
3515
  message: "JSON-LD validity",
3406
3516
  recommendation: "Make the JSON-LD valid JSON with both @context and @type.",
3407
- docsUrl: docsUrl8
3517
+ docsUrl: docsUrl11
3408
3518
  }
3409
3519
  );
3410
3520
  }
@@ -3515,7 +3625,7 @@ function visibleLength(s) {
3515
3625
 
3516
3626
  // src/rules/seo/length-rule.ts
3517
3627
  function lengthRule(opts) {
3518
- const docsUrl8 = docsUrlFor(opts.id);
3628
+ const docsUrl11 = docsUrlFor(opts.id);
3519
3629
  const spec = {
3520
3630
  min: { kind: "integer", default: opts.min, min: 0 },
3521
3631
  max: { kind: "integer", default: opts.max, min: 1 }
@@ -3538,7 +3648,7 @@ function lengthRule(opts) {
3538
3648
  const o = resolveRuleOptions(opts.id, spec, ctx.config, { route: head.route, file: location }, compiled);
3539
3649
  const min = intOption(o, "min", opts.min);
3540
3650
  const max = intOption(o, "max", opts.max);
3541
- const recommendation8 = typeof opts.recommendation === "function" ? opts.recommendation(o) : opts.recommendation;
3651
+ const recommendation11 = typeof opts.recommendation === "function" ? opts.recommendation(o) : opts.recommendation;
3542
3652
  const len = visibleLength(tag.text);
3543
3653
  let problem;
3544
3654
  if (len < min) problem = `${opts.noun} is too short (${len} chars; aim for ${min}\u2013${max})`;
@@ -3552,8 +3662,8 @@ function lengthRule(opts) {
3552
3662
  route: head.route,
3553
3663
  location,
3554
3664
  message: problem,
3555
- recommendation: recommendation8,
3556
- docsUrl: docsUrl8
3665
+ recommendation: recommendation11,
3666
+ docsUrl: docsUrl11
3557
3667
  } : {
3558
3668
  id: opts.id,
3559
3669
  category: "seo",
@@ -3561,8 +3671,8 @@ function lengthRule(opts) {
3561
3671
  detection: PASS,
3562
3672
  route: head.route,
3563
3673
  message: opts.label,
3564
- recommendation: recommendation8,
3565
- docsUrl: docsUrl8
3674
+ recommendation: recommendation11,
3675
+ docsUrl: docsUrl11
3566
3676
  }
3567
3677
  );
3568
3678
  }
@@ -3747,7 +3857,7 @@ var seoSingleH1 = {
3747
3857
 
3748
3858
  // src/rules/seo/uniqueness-rule.ts
3749
3859
  function uniquenessRule(opts) {
3750
- const docsUrl8 = docsUrlFor(opts.id);
3860
+ const docsUrl11 = docsUrlFor(opts.id);
3751
3861
  return {
3752
3862
  id: opts.id,
3753
3863
  title: opts.title,
@@ -3777,7 +3887,7 @@ function uniquenessRule(opts) {
3777
3887
  location: e.file,
3778
3888
  message: `${opts.noun} is duplicated across ${n} routes`,
3779
3889
  recommendation: opts.recommendation,
3780
- docsUrl: docsUrl8
3890
+ docsUrl: docsUrl11
3781
3891
  } : {
3782
3892
  id: opts.id,
3783
3893
  category: "seo",
@@ -3786,7 +3896,7 @@ function uniquenessRule(opts) {
3786
3896
  route: e.route,
3787
3897
  message: opts.label,
3788
3898
  recommendation: opts.recommendation,
3789
- docsUrl: docsUrl8
3899
+ docsUrl: docsUrl11
3790
3900
  };
3791
3901
  });
3792
3902
  }
@@ -3874,7 +3984,7 @@ function isSuppressed(m, ruleId, line) {
3874
3984
  return (m.suppressions ?? []).some((s) => s.line === line && (!s.ruleIds || s.ruleIds.includes(ruleId)));
3875
3985
  }
3876
3986
  function kitModuleRule(opts) {
3877
- const docsUrl8 = docsUrlFor(opts.id);
3987
+ const docsUrl11 = docsUrlFor(opts.id);
3878
3988
  const severity = opts.severity ?? "warning";
3879
3989
  return {
3880
3990
  id: opts.id,
@@ -3898,7 +4008,7 @@ function kitModuleRule(opts) {
3898
4008
  route: m.file,
3899
4009
  message: opts.label,
3900
4010
  recommendation: opts.recommendation,
3901
- docsUrl: docsUrl8
4011
+ docsUrl: docsUrl11
3902
4012
  });
3903
4013
  continue;
3904
4014
  }
@@ -3913,7 +4023,7 @@ function kitModuleRule(opts) {
3913
4023
  ...b.line > 0 ? { line: b.line } : {},
3914
4024
  message: b.message,
3915
4025
  recommendation: opts.recommendation,
3916
- docsUrl: docsUrl8,
4026
+ docsUrl: docsUrl11,
3917
4027
  ...opts.fix ? { fix: { ...opts.fix } } : {}
3918
4028
  });
3919
4029
  }
@@ -3949,7 +4059,7 @@ function isSuppressed2(c, ruleId, line) {
3949
4059
  return (c.suppressions ?? []).some((s) => s.line === line && (!s.ruleIds || s.ruleIds.includes(ruleId)));
3950
4060
  }
3951
4061
  function componentRule(opts) {
3952
- const docsUrl8 = docsUrlFor(opts.id);
4062
+ const docsUrl11 = docsUrlFor(opts.id);
3953
4063
  const severity = opts.severity ?? "warning";
3954
4064
  return {
3955
4065
  id: opts.id,
@@ -3965,9 +4075,9 @@ function componentRule(opts) {
3965
4075
  const compiled = compileOverrides(ctx.config);
3966
4076
  for (const c of ctx.components ?? []) {
3967
4077
  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)));
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)));
3971
4081
  if (bad.length === 0) {
3972
4082
  out.push({
3973
4083
  id: opts.id,
@@ -3976,8 +4086,8 @@ function componentRule(opts) {
3976
4086
  detection: PASS3,
3977
4087
  route: c.file,
3978
4088
  message: opts.label,
3979
- recommendation: recommendation8,
3980
- docsUrl: docsUrl8
4089
+ recommendation: recommendation11,
4090
+ docsUrl: docsUrl11
3981
4091
  });
3982
4092
  continue;
3983
4093
  }
@@ -3991,8 +4101,8 @@ function componentRule(opts) {
3991
4101
  location: c.file,
3992
4102
  ...b.line > 0 ? { line: b.line } : {},
3993
4103
  message: b.message,
3994
- recommendation: recommendation8,
3995
- docsUrl: docsUrl8,
4104
+ recommendation: recommendation11,
4105
+ docsUrl: docsUrl11,
3996
4106
  ...opts.fix ? { fix: { ...opts.fix } } : {}
3997
4107
  });
3998
4108
  }
@@ -4597,7 +4707,7 @@ var architecturePrivateScopeImport = {
4597
4707
  let sawScopedImport = false;
4598
4708
  const violations = [];
4599
4709
  for (const { source, line } of spans) {
4600
- const target = resolveRepoLocalPath(source, c.file);
4710
+ const target = resolveRepoLocalPath(source, c.file, ctx.project.kitAliases);
4601
4711
  if (target === void 0) continue;
4602
4712
  const boundary = privateScopeOf(target, patterns);
4603
4713
  if (boundary === void 0) continue;
@@ -4642,6 +4752,590 @@ var architecturePrivateScopeImport = {
4642
4752
  }
4643
4753
  };
4644
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
+ }
5337
+ });
5338
+
4645
5339
  // src/rules/perf/heavy-import.ts
4646
5340
  var HEAVY_PACKAGES = {
4647
5341
  lodash: "import a submodule (lodash/debounce) or use lodash-es for tree-shaking",
@@ -4665,8 +5359,8 @@ var performanceHeavyImport = componentRule({
4665
5359
  const seen = /* @__PURE__ */ new Set();
4666
5360
  const out = [];
4667
5361
  const spans = c.importSpans ?? c.imports.map((source) => ({ source, line: 0 }));
4668
- for (const { source: src, line } of spans) {
4669
- if (!Object.hasOwn(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;
4670
5364
  seen.add(src);
4671
5365
  out.push({ line, message: `Heavy import "${src}" \u2014 ${packages[src]}` });
4672
5366
  }
@@ -4854,6 +5548,10 @@ var allRules = [
4854
5548
  architectureComponentSize,
4855
5549
  architecturePropCount,
4856
5550
  architecturePrivateScopeImport,
5551
+ architectureUnitEntryFile,
5552
+ architectureDirectoryNaming,
5553
+ architectureReservedDirectoryNames,
5554
+ architectureRouteComponentImport,
4857
5555
  performanceHeavyImport,
4858
5556
  performanceNamespaceImport,
4859
5557
  performanceMinifyDisabled,
@@ -6114,8 +6812,12 @@ export {
6114
6812
  applyOverrides,
6115
6813
  applyRuleSeverities,
6116
6814
  architectureComponentSize,
6815
+ architectureDirectoryNaming,
6117
6816
  architecturePrivateScopeImport,
6118
6817
  architecturePropCount,
6818
+ architectureReservedDirectoryNames,
6819
+ architectureRouteComponentImport,
6820
+ architectureUnitEntryFile,
6119
6821
  attrText,
6120
6822
  attrTextOf,
6121
6823
  attrValue,
@@ -6125,6 +6827,7 @@ export {
6125
6827
  classify,
6126
6828
  collectComponentFacts,
6127
6829
  collectKitModuleFacts,
6830
+ collectSourceFiles,
6128
6831
  compileOverrides,
6129
6832
  computeHealth,
6130
6833
  computeScore,
@@ -6152,6 +6855,7 @@ export {
6152
6855
  escapeHtml,
6153
6856
  explainRule,
6154
6857
  findAttr,
6858
+ findKitAliasesInSvelteConfig,
6155
6859
  findKitPathsBaseInSvelteConfig,
6156
6860
  findKitPathsBaseInViteConfig,
6157
6861
  findMinifyDisabled,
@@ -6166,6 +6870,7 @@ export {
6166
6870
  headTagRule,
6167
6871
  imageRule,
6168
6872
  intOption,
6873
+ isMentionedAnywhere,
6169
6874
  isPenalized,
6170
6875
  lineOf,
6171
6876
  linkRule,
@@ -6190,6 +6895,7 @@ export {
6190
6895
  performanceSequentialAwaits,
6191
6896
  performanceStateRaw,
6192
6897
  renderAppShell,
6898
+ resolveKitAliases,
6193
6899
  resolveKitPathsBase,
6194
6900
  resolveRuleOptions,
6195
6901
  resolveRunesModuleSpecifier,