@svelte-vitals/core 0.42.0 → 0.43.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
@@ -4,7 +4,14 @@ var defaultProject = {
4
4
  hasSitemap: false,
5
5
  htmlLang: { presence: "none", value: "absent" }
6
6
  };
7
- var CATEGORIES = ["seo", "performance", "correctness", "security", "architecture"];
7
+ var CATEGORIES = [
8
+ "seo",
9
+ "performance",
10
+ "correctness",
11
+ "security",
12
+ "architecture",
13
+ "a11y"
14
+ ];
8
15
  var defaultConfig = {
9
16
  treatDynamicAs: "pass",
10
17
  metaComponents: [],
@@ -15,6 +22,71 @@ function defineConfig(config = {}) {
15
22
  return { ...defaultConfig, ...config };
16
23
  }
17
24
 
25
+ // src/a11y.ts
26
+ function foldOccurrences(nodes) {
27
+ const byKey = /* @__PURE__ */ new Map();
28
+ for (const node of nodes) {
29
+ if (node.repeatable) continue;
30
+ const list = byKey.get(node.key);
31
+ if (list) list.push(node);
32
+ else byKey.set(node.key, [node]);
33
+ }
34
+ const folded = /* @__PURE__ */ new Map();
35
+ for (const [key, list] of byKey) folded.set(key, foldAt(list, 0));
36
+ return folded;
37
+ }
38
+ function foldAt(nodes, depth) {
39
+ const unconditional = [];
40
+ const groups = /* @__PURE__ */ new Map();
41
+ for (const node of nodes) {
42
+ const step = node.path[depth];
43
+ if (!step) {
44
+ unconditional.push(node);
45
+ continue;
46
+ }
47
+ let branches = groups.get(step.group);
48
+ if (!branches) groups.set(step.group, branches = /* @__PURE__ */ new Map());
49
+ const list = branches.get(step.branch);
50
+ if (list) list.push(node);
51
+ else branches.set(step.branch, [node]);
52
+ }
53
+ const representatives = [...unconditional];
54
+ for (const branches of groups.values()) {
55
+ let best = [];
56
+ let bestBranch = Number.POSITIVE_INFINITY;
57
+ for (const [branch, list] of branches) {
58
+ const arm = foldAt(list, depth + 1);
59
+ if (arm.length > best.length || arm.length === best.length && branch < bestBranch) {
60
+ best = arm;
61
+ bestBranch = branch;
62
+ }
63
+ }
64
+ representatives.push(...best);
65
+ }
66
+ return representatives;
67
+ }
68
+ function decodeFragmentId(fragment) {
69
+ try {
70
+ return decodeURIComponent(fragment);
71
+ } catch {
72
+ return fragment;
73
+ }
74
+ }
75
+ function splitTokens(value) {
76
+ return value ? value.trim().split(/\s+/).filter(Boolean) : [];
77
+ }
78
+ var LANDMARK_ROLES = /* @__PURE__ */ new Set(["main", "banner", "contentinfo", "complementary"]);
79
+ var IDREF_ATTRS = [
80
+ "for",
81
+ "aria-labelledby",
82
+ "aria-describedby",
83
+ "aria-controls",
84
+ "aria-activedescendant"
85
+ ];
86
+ function isTopFragment(id) {
87
+ return id.toLowerCase() === "top";
88
+ }
89
+
18
90
  // src/component-parse.ts
19
91
  import { parse } from "svelte/compiler";
20
92
 
@@ -82,6 +154,57 @@ function attrTextOf(attr) {
82
154
  return Array.isArray(v) ? textFromNodes(v) : void 0;
83
155
  }
84
156
 
157
+ // src/rules/a11y/interactive.ts
158
+ var ALWAYS_INTERACTIVE_TAGS = /* @__PURE__ */ new Set(["button", "select", "textarea", "summary", "embed", "iframe"]);
159
+ var INTERACTIVE_ROLES = /* @__PURE__ */ new Set([
160
+ "button",
161
+ "link",
162
+ "checkbox",
163
+ "radio",
164
+ "switch",
165
+ "tab",
166
+ "menuitem",
167
+ "menuitemcheckbox",
168
+ "menuitemradio",
169
+ "option",
170
+ "slider",
171
+ "spinbutton",
172
+ "textbox",
173
+ "combobox",
174
+ "searchbox",
175
+ "scrollbar",
176
+ "gridcell"
177
+ ]);
178
+ function literalOf(attrs, name) {
179
+ return attrs.find((a) => a.name === name)?.literal;
180
+ }
181
+ function hasInteractiveRole(attrs) {
182
+ const role = splitTokens(literalOf(attrs, "role"))[0];
183
+ return role !== void 0 && INTERACTIVE_ROLES.has(role);
184
+ }
185
+ function isInteractiveElement(tag, attrs) {
186
+ if (ALWAYS_INTERACTIVE_TAGS.has(tag)) return true;
187
+ if (tag === "a" && literalOf(attrs, "href") !== void 0) return true;
188
+ if (tag === "input") {
189
+ const typeAttr = attrs.find((a) => a.name === "type");
190
+ if (!typeAttr) return true;
191
+ if (typeAttr.literal !== void 0) return typeAttr.literal.toLowerCase() !== "hidden";
192
+ return false;
193
+ }
194
+ if ((tag === "audio" || tag === "video") && attrs.some((a) => a.name === "controls")) return true;
195
+ const tabindex = literalOf(attrs, "tabindex")?.trim();
196
+ if (tabindex) {
197
+ const n = Number(tabindex);
198
+ if (Number.isFinite(n) && n >= 0) return true;
199
+ }
200
+ return hasInteractiveRole(attrs);
201
+ }
202
+ function isInteractiveContainer(tag, attrs) {
203
+ if (tag === "button") return true;
204
+ if (tag === "a") return literalOf(attrs, "href") !== void 0;
205
+ return hasInteractiveRole(attrs);
206
+ }
207
+
85
208
  // src/component-parse.ts
86
209
  function unwrapTs(expr) {
87
210
  let cur = expr;
@@ -715,6 +838,304 @@ function collectCheckableBindValues(node, source, acc) {
715
838
  if (key in node) collectCheckableBindValues(node[key], source, acc);
716
839
  }
717
840
  }
841
+ function classifyAttrValue(value) {
842
+ if (value === true) return { literal: "" };
843
+ if (Array.isArray(value) && value.length === 1 && value[0]?.type === "Text") {
844
+ return { literal: String(value[0].data ?? "") };
845
+ }
846
+ return { expression: true };
847
+ }
848
+ function collectAriaElements(node, source, acc) {
849
+ if (Array.isArray(node)) {
850
+ for (const child of node) collectAriaElements(child, source, acc);
851
+ return;
852
+ }
853
+ if (!node || typeof node !== "object") return;
854
+ if (node.type === "RegularElement" && Array.isArray(node.attributes)) {
855
+ const roleAttr = findAttr(node.attributes, "role");
856
+ const ariaAttrs = node.attributes.filter(
857
+ (a) => a?.type === "Attribute" && typeof a.name === "string" && a.name.startsWith("aria-")
858
+ );
859
+ if (roleAttr || ariaAttrs.length > 0) {
860
+ const inputType = node.name === "input" ? attrText(node.attributes, "type") : void 0;
861
+ const hasSpread = node.attributes.some((a) => a?.type === "SpreadAttribute");
862
+ acc.push({
863
+ tag: node.name,
864
+ line: lineOf(source, node.start),
865
+ ...roleAttr ? { role: classifyAttrValue(roleAttr.value) } : {},
866
+ ...inputType !== void 0 ? { inputType: inputType.toLowerCase() } : {},
867
+ ...hasSpread ? { hasSpread: true } : {},
868
+ aria: ariaAttrs.map((a) => ({
869
+ name: a.name,
870
+ line: lineOf(source, a.start ?? node.start),
871
+ ...classifyAttrValue(a.value)
872
+ }))
873
+ });
874
+ }
875
+ }
876
+ for (const key of CHILD_NODE_KEYS) {
877
+ if (key in node) collectAriaElements(node[key], source, acc);
878
+ }
879
+ }
880
+ function elementAttrs(attributes) {
881
+ return attributes.filter((a) => a?.type === "Attribute" && typeof a.name === "string").map((a) => ({ name: a.name, ...classifyAttrValue(a.value) }));
882
+ }
883
+ function collectInteractiveNestings(node, source, acc, stack) {
884
+ if (Array.isArray(node)) {
885
+ for (const child of node) collectInteractiveNestings(child, source, acc, stack);
886
+ return;
887
+ }
888
+ if (!node || typeof node !== "object") return;
889
+ if (node.type === "SnippetBlock") {
890
+ for (const key of CHILD_NODE_KEYS) {
891
+ if (key in node) collectInteractiveNestings(node[key], source, acc, []);
892
+ }
893
+ return;
894
+ }
895
+ let opened = false;
896
+ if (node.type === "RegularElement" && Array.isArray(node.attributes)) {
897
+ const attrs = elementAttrs(node.attributes);
898
+ if (stack.length > 0 && isInteractiveElement(node.name, attrs)) {
899
+ acc.push({
900
+ containerTag: stack[stack.length - 1],
901
+ descendantTag: node.name,
902
+ line: lineOf(source, node.start)
903
+ });
904
+ }
905
+ if (isInteractiveContainer(node.name, attrs)) {
906
+ stack.push(node.name);
907
+ opened = true;
908
+ }
909
+ }
910
+ for (const key of CHILD_NODE_KEYS) {
911
+ if (key in node) collectInteractiveNestings(node[key], source, acc, stack);
912
+ }
913
+ if (opened) stack.pop();
914
+ }
915
+ function hasNamingAttr(attributes) {
916
+ return ["aria-label", "aria-labelledby", "title"].some((name) => {
917
+ const attr = findAttr(attributes, name);
918
+ if (!attr) return false;
919
+ const v = classifyAttrValue(attr.value);
920
+ return "expression" in v || v.literal !== void 0 && v.literal.trim().length > 0;
921
+ });
922
+ }
923
+ function scanAccessibleNameSubtree(node) {
924
+ if (Array.isArray(node)) {
925
+ const acc2 = { named: false, unknowable: false };
926
+ for (const child of node) {
927
+ const r = scanAccessibleNameSubtree(child);
928
+ acc2.named ||= r.named;
929
+ acc2.unknowable ||= r.unknowable;
930
+ }
931
+ return acc2;
932
+ }
933
+ if (!node || typeof node !== "object") return { named: false, unknowable: false };
934
+ if (node.type === "SnippetBlock") return { named: false, unknowable: false };
935
+ if (node.type === "Text") return { named: String(node.data ?? "").trim().length > 0, unknowable: false };
936
+ if (node.type === "ExpressionTag" || node.type === "RenderTag" || node.type === "HtmlTag" || COMPONENT_LIKE_TYPES.has(node.type)) {
937
+ return { named: false, unknowable: true };
938
+ }
939
+ if (node.type === "RegularElement" && node.name === "img" && Array.isArray(node.attributes)) {
940
+ const alt = attrText(node.attributes, "alt");
941
+ if (alt !== void 0 && alt.trim().length > 0) return { named: true, unknowable: false };
942
+ }
943
+ const acc = { named: false, unknowable: false };
944
+ for (const key of CHILD_NODE_KEYS) {
945
+ if (key in node) {
946
+ const r = scanAccessibleNameSubtree(node[key]);
947
+ acc.named ||= r.named;
948
+ acc.unknowable ||= r.unknowable;
949
+ }
950
+ }
951
+ return acc;
952
+ }
953
+ function accessibleNameTarget(node) {
954
+ if (node.name === "button") return "button";
955
+ if (node.name === "a") return attrText(node.attributes, "href") !== void 0 ? "a" : void 0;
956
+ if (node.name === "input") {
957
+ const type = attrText(node.attributes, "type");
958
+ return type?.toLowerCase() === "image" ? "input" : void 0;
959
+ }
960
+ return void 0;
961
+ }
962
+ function collectUnnamedInteractive(node, source, acc) {
963
+ if (Array.isArray(node)) {
964
+ for (const child of node) collectUnnamedInteractive(child, source, acc);
965
+ return;
966
+ }
967
+ if (!node || typeof node !== "object") return;
968
+ if (node.type === "RegularElement" && Array.isArray(node.attributes)) {
969
+ const target = accessibleNameTarget(node);
970
+ const hasSpread = node.attributes.some((a) => a?.type === "SpreadAttribute");
971
+ if (target && !hasSpread) {
972
+ if (target === "input") {
973
+ const alt = attrText(node.attributes, "alt");
974
+ if (!hasNamingAttr(node.attributes) && !(alt !== void 0 && alt.trim().length > 0)) {
975
+ acc.push({ tag: node.name, line: lineOf(source, node.start) });
976
+ }
977
+ } else {
978
+ const scan = scanAccessibleNameSubtree(node);
979
+ if (!hasNamingAttr(node.attributes) && !scan.named && !scan.unknowable) {
980
+ acc.push({ tag: node.name, line: lineOf(source, node.start) });
981
+ }
982
+ }
983
+ }
984
+ }
985
+ for (const key of CHILD_NODE_KEYS) {
986
+ if (key in node) collectUnnamedInteractive(node[key], source, acc);
987
+ }
988
+ }
989
+ var LABELABLE_TAGS = /* @__PURE__ */ new Set(["input", "select", "textarea", "button", "meter", "output", "progress"]);
990
+ function isLabelableDescendant(node) {
991
+ if (node.type !== "RegularElement" || !LABELABLE_TAGS.has(node.name)) return false;
992
+ if (node.name !== "input") return true;
993
+ return attrText(node.attributes ?? [], "type")?.toLowerCase() !== "hidden";
994
+ }
995
+ function scanLabelSubtree(node) {
996
+ if (Array.isArray(node)) {
997
+ const acc2 = { hasControl: false, unknowable: false };
998
+ for (const child of node) {
999
+ const r = scanLabelSubtree(child);
1000
+ acc2.hasControl ||= r.hasControl;
1001
+ acc2.unknowable ||= r.unknowable;
1002
+ }
1003
+ return acc2;
1004
+ }
1005
+ if (!node || typeof node !== "object") return { hasControl: false, unknowable: false };
1006
+ if (node.type === "SnippetBlock") return { hasControl: false, unknowable: false };
1007
+ if (node.type === "ExpressionTag" || node.type === "RenderTag" || node.type === "HtmlTag" || COMPONENT_LIKE_TYPES.has(node.type)) {
1008
+ return { hasControl: false, unknowable: true };
1009
+ }
1010
+ if (isLabelableDescendant(node)) return { hasControl: true, unknowable: false };
1011
+ const acc = { hasControl: false, unknowable: false };
1012
+ for (const key of CHILD_NODE_KEYS) {
1013
+ if (key in node) {
1014
+ const r = scanLabelSubtree(node[key]);
1015
+ acc.hasControl ||= r.hasControl;
1016
+ acc.unknowable ||= r.unknowable;
1017
+ }
1018
+ }
1019
+ return acc;
1020
+ }
1021
+ function collectUnassociatedLabels(node, source, acc) {
1022
+ if (Array.isArray(node)) {
1023
+ for (const child of node) collectUnassociatedLabels(child, source, acc);
1024
+ return;
1025
+ }
1026
+ if (!node || typeof node !== "object") return;
1027
+ if (node.type === "RegularElement" && node.name === "label" && Array.isArray(node.attributes)) {
1028
+ const hasFor = findAttr(node.attributes, "for") !== void 0;
1029
+ const hasSpread = node.attributes.some((a) => a?.type === "SpreadAttribute");
1030
+ if (!hasFor && !hasSpread) {
1031
+ const scan = scanLabelSubtree(node);
1032
+ if (!scan.hasControl && !scan.unknowable) {
1033
+ acc.push({ line: lineOf(source, node.start) });
1034
+ }
1035
+ }
1036
+ }
1037
+ for (const key of CHILD_NODE_KEYS) {
1038
+ if (key in node) collectUnassociatedLabels(node[key], source, acc);
1039
+ }
1040
+ }
1041
+ var BULLET_TEXT_RE = /^[•・·\-*]\s/;
1042
+ function collectBulletTexts(node, source, acc, insideLi) {
1043
+ if (Array.isArray(node)) {
1044
+ for (const child of node) collectBulletTexts(child, source, acc, insideLi);
1045
+ return;
1046
+ }
1047
+ if (!node || typeof node !== "object") return;
1048
+ if (node.type === "Text") {
1049
+ const trimmed = String(node.data ?? "").trim();
1050
+ if (!insideLi && BULLET_TEXT_RE.test(trimmed)) {
1051
+ acc.push({ line: lineOf(source, node.start), char: trimmed[0] });
1052
+ }
1053
+ return;
1054
+ }
1055
+ if (node.type === "SnippetBlock") return;
1056
+ const nowInsideLi = insideLi || node.type === "RegularElement" && node.name === "li";
1057
+ for (const key of CHILD_NODE_KEYS) {
1058
+ if (key in node) collectBulletTexts(node[key], source, acc, nowInsideLi);
1059
+ }
1060
+ }
1061
+ function selectNeedsPlaceholder(attributes) {
1062
+ const requiredAttr = findAttr(attributes, "required");
1063
+ if (!requiredAttr || attrValueOf(requiredAttr) === "dynamic") return false;
1064
+ if (findAttr(attributes, "multiple")) return false;
1065
+ const sizeAttr = findAttr(attributes, "size");
1066
+ if (sizeAttr) {
1067
+ if (attrValueOf(sizeAttr) === "dynamic") return false;
1068
+ const size = Number(attrText(attributes, "size"));
1069
+ if (!(Number.isFinite(size) && size <= 1)) return false;
1070
+ }
1071
+ return true;
1072
+ }
1073
+ function firstSignificantChild(nodes) {
1074
+ for (const child of nodes ?? []) {
1075
+ if (!child) continue;
1076
+ if (child.type === "Comment") continue;
1077
+ if (child.type === "Text" && !String(child.data ?? "").trim()) continue;
1078
+ return child;
1079
+ }
1080
+ return void 0;
1081
+ }
1082
+ function isPlaceholderOption(option) {
1083
+ const attributes = option.attributes ?? [];
1084
+ if (findAttr(attributes, "value")) {
1085
+ const literal = attrText(attributes, "value");
1086
+ return literal === void 0 || literal === "";
1087
+ }
1088
+ return textFromNodes(option.fragment?.nodes ?? []) === void 0;
1089
+ }
1090
+ function collectSelectsMissingPlaceholder(node, source, acc) {
1091
+ if (Array.isArray(node)) {
1092
+ for (const child of node) collectSelectsMissingPlaceholder(child, source, acc);
1093
+ return;
1094
+ }
1095
+ if (!node || typeof node !== "object") return;
1096
+ if (node.type === "RegularElement" && node.name === "select" && Array.isArray(node.attributes)) {
1097
+ const hasSpread = node.attributes.some((a) => a?.type === "SpreadAttribute");
1098
+ if (!hasSpread && selectNeedsPlaceholder(node.attributes)) {
1099
+ const first = firstSignificantChild(node.fragment?.nodes);
1100
+ if (!first) {
1101
+ acc.push({ line: lineOf(source, node.start) });
1102
+ } else if (first.type === "RegularElement" && first.name === "option") {
1103
+ const firstHasSpread = (first.attributes ?? []).some((a) => a?.type === "SpreadAttribute");
1104
+ if (!firstHasSpread && !isPlaceholderOption(first)) acc.push({ line: lineOf(source, node.start) });
1105
+ }
1106
+ }
1107
+ }
1108
+ for (const key of CHILD_NODE_KEYS) {
1109
+ if (key in node) collectSelectsMissingPlaceholder(node[key], source, acc);
1110
+ }
1111
+ }
1112
+ var MACHINE_READABLE_TIME = [
1113
+ /^\d{4}(-\d{2}){0,2}$/,
1114
+ /^\d{2}:\d{2}(:\d{2}(\.\d+)?)?$/,
1115
+ /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}/,
1116
+ /^\d{2}-\d{2}$/,
1117
+ /^P(?=\d|T)/i
1118
+ ];
1119
+ function collectTimesMissingDatetime(node, source, acc) {
1120
+ if (Array.isArray(node)) {
1121
+ for (const child of node) collectTimesMissingDatetime(child, source, acc);
1122
+ return;
1123
+ }
1124
+ if (!node || typeof node !== "object") return;
1125
+ if (node.type === "RegularElement" && node.name === "time" && findAttr(node.attributes ?? [], "datetime") === void 0 && !(node.attributes ?? []).some((a) => a?.type === "SpreadAttribute")) {
1126
+ const nodes = node.fragment?.nodes ?? [];
1127
+ if (nodes.length > 0 && nodes.every((n) => n?.type === "Text")) {
1128
+ const text = nodes.map((n) => String(n.data ?? "")).join("");
1129
+ const trimmed = text.trim();
1130
+ if (trimmed.length > 0 && !MACHINE_READABLE_TIME.some((re) => re.test(trimmed))) {
1131
+ acc.push({ line: lineOf(source, node.start), text: trimmed });
1132
+ }
1133
+ }
1134
+ }
1135
+ for (const key of CHILD_NODE_KEYS) {
1136
+ if (key in node) collectTimesMissingDatetime(node[key], source, acc);
1137
+ }
1138
+ }
718
1139
  function collectHrefLinks(node, source, acc) {
719
1140
  if (Array.isArray(node)) {
720
1141
  for (const child of node) collectHrefLinks(child, source, acc);
@@ -1336,6 +1757,20 @@ function parseComponentFacts(source, filename) {
1336
1757
  collectSecurityFacts(ast.fragment ?? ast, source, htmlTags, javascriptUrls);
1337
1758
  const checkableBindValues = [];
1338
1759
  collectCheckableBindValues(ast.fragment ?? ast, source, checkableBindValues);
1760
+ const ariaElements = [];
1761
+ collectAriaElements(ast.fragment ?? ast, source, ariaElements);
1762
+ const interactiveNestings = [];
1763
+ collectInteractiveNestings(ast.fragment ?? ast, source, interactiveNestings, []);
1764
+ const unnamedInteractive = [];
1765
+ collectUnnamedInteractive(ast.fragment ?? ast, source, unnamedInteractive);
1766
+ const unassociatedLabels = [];
1767
+ collectUnassociatedLabels(ast.fragment ?? ast, source, unassociatedLabels);
1768
+ const bulletTexts = [];
1769
+ collectBulletTexts(ast.fragment ?? ast, source, bulletTexts, false);
1770
+ const selectsMissingPlaceholder = [];
1771
+ collectSelectsMissingPlaceholder(ast.fragment ?? ast, source, selectsMissingPlaceholder);
1772
+ const timesMissingDatetime = [];
1773
+ collectTimesMissingDatetime(ast.fragment ?? ast, source, timesMissingDatetime);
1339
1774
  const basePathLinks = [];
1340
1775
  collectHrefLinks(ast.fragment ?? ast, source, basePathLinks);
1341
1776
  const gotoPrograms = [ast.module?.content, ast.instance?.content].filter(Boolean);
@@ -1538,7 +1973,14 @@ function parseComponentFacts(source, filename) {
1538
1973
  browserGlobalRefs,
1539
1974
  moduleStateDecls: [],
1540
1975
  suppressions,
1541
- commentLinks: collectCommentLinks(source)
1976
+ commentLinks: collectCommentLinks(source),
1977
+ ariaElements,
1978
+ interactiveNestings,
1979
+ unnamedInteractive,
1980
+ unassociatedLabels,
1981
+ bulletTexts,
1982
+ selectsMissingPlaceholder,
1983
+ timesMissingDatetime
1542
1984
  };
1543
1985
  }
1544
1986
 
@@ -2408,10 +2850,10 @@ function findKitPathsBaseInViteConfig(source) {
2408
2850
  }
2409
2851
  function resolveKitPathsBase(viteConfig, svelteConfig) {
2410
2852
  if (viteConfig) {
2411
- const result = findKitPathsBaseInViteConfig(viteConfig.source);
2412
- if (result.kind === "unresolvable") return void 0;
2413
- if (result.kind === "resolved") {
2414
- return result.base ? { ...result.base, file: viteConfig.file } : void 0;
2853
+ const result3 = findKitPathsBaseInViteConfig(viteConfig.source);
2854
+ if (result3.kind === "unresolvable") return void 0;
2855
+ if (result3.kind === "resolved") {
2856
+ return result3.base ? { ...result3.base, file: viteConfig.file } : void 0;
2415
2857
  }
2416
2858
  }
2417
2859
  if (!svelteConfig) return void 0;
@@ -3047,9 +3489,9 @@ function formatFailedRuleWarning(f) {
3047
3489
  return `rule ${f.id} failed and was skipped: ${f.message.split("\n")[0]}`;
3048
3490
  }
3049
3491
  function applyRuleSeverities(results, config) {
3050
- return results.map((result) => {
3051
- const severity = settingSeverity(config.rules[result.id]);
3052
- return severity !== void 0 && severity !== "off" ? { ...result, severity } : result;
3492
+ return results.map((result3) => {
3493
+ const severity = settingSeverity(config.rules[result3.id]);
3494
+ return severity !== void 0 && severity !== "off" ? { ...result3, severity } : result3;
3053
3495
  });
3054
3496
  }
3055
3497
  function routeGlobToRegExp(pattern) {
@@ -3076,15 +3518,15 @@ function applyOverrides(results, config) {
3076
3518
  const compiled = compileOverrides(config);
3077
3519
  if (compiled.length === 0) return results;
3078
3520
  const out = [];
3079
- for (const result of results) {
3521
+ for (const result3 of results) {
3080
3522
  let severity;
3081
3523
  for (const o of compiled) {
3082
- if (!overrideMatches(o, { route: result.route, file: result.location })) continue;
3083
- const sev = settingSeverity(o.rules[result.id]) ?? settingSeverity(o.rules[result.category ?? "seo"]);
3524
+ if (!overrideMatches(o, { route: result3.route, file: result3.location })) continue;
3525
+ const sev = settingSeverity(o.rules[result3.id]) ?? settingSeverity(o.rules[result3.category ?? "seo"]);
3084
3526
  if (sev !== void 0) severity = sev;
3085
3527
  }
3086
- if (severity === void 0) out.push(result);
3087
- else if (severity !== "off") out.push({ ...result, severity });
3528
+ if (severity === void 0) out.push(result3);
3529
+ else if (severity !== "off") out.push({ ...result3, severity });
3088
3530
  }
3089
3531
  return out;
3090
3532
  }
@@ -3922,7 +4364,7 @@ function lengthRule(opts) {
3922
4364
  const o = resolveRuleOptions(opts.id, spec, ctx.config, { route: head.route, file: location }, compiled);
3923
4365
  const min = intOption(o, "min", opts.min);
3924
4366
  const max = intOption(o, "max", opts.max);
3925
- const recommendation10 = typeof opts.recommendation === "function" ? opts.recommendation(o) : opts.recommendation;
4367
+ const recommendation12 = typeof opts.recommendation === "function" ? opts.recommendation(o) : opts.recommendation;
3926
4368
  const len = visibleLength(tag.text);
3927
4369
  let problem;
3928
4370
  if (len < min) problem = `${opts.noun} is too short (${len} chars; aim for ${min}\u2013${max})`;
@@ -3936,7 +4378,7 @@ function lengthRule(opts) {
3936
4378
  route: head.route,
3937
4379
  location,
3938
4380
  message: problem,
3939
- recommendation: recommendation10,
4381
+ recommendation: recommendation12,
3940
4382
  docsUrl: docsUrl12
3941
4383
  } : {
3942
4384
  id: opts.id,
@@ -3950,7 +4392,7 @@ function lengthRule(opts) {
3950
4392
  // it to also apply `severity: 'off'`.
3951
4393
  location,
3952
4394
  message: opts.label,
3953
- recommendation: recommendation10,
4395
+ recommendation: recommendation12,
3954
4396
  docsUrl: docsUrl12
3955
4397
  }
3956
4398
  );
@@ -4304,7 +4746,7 @@ function fileRule(spec) {
4304
4746
  for (const f of spec.facts(ctx) ?? []) {
4305
4747
  const o = resolveRuleOptions(spec.id, spec.options, ctx.config, { route: f.file, file: f.file }, compiled);
4306
4748
  if (!spec.applies(f, o, ctx)) continue;
4307
- const recommendation10 = typeof spec.recommendation === "function" ? spec.recommendation(o) : spec.recommendation;
4749
+ const recommendation12 = typeof spec.recommendation === "function" ? spec.recommendation(o) : spec.recommendation;
4308
4750
  const bad = spec.bad(f, o, ctx).filter((b) => !(b.line > 0 && isSuppressed(f.suppressions, spec.id, b.line)));
4309
4751
  if (bad.length === 0) {
4310
4752
  out.push({
@@ -4315,7 +4757,7 @@ function fileRule(spec) {
4315
4757
  route: f.file,
4316
4758
  location: f.file,
4317
4759
  message: spec.label,
4318
- recommendation: recommendation10,
4760
+ recommendation: recommendation12,
4319
4761
  docsUrl: docsUrl12
4320
4762
  });
4321
4763
  continue;
@@ -4330,7 +4772,7 @@ function fileRule(spec) {
4330
4772
  location: f.file,
4331
4773
  ...b.line > 0 ? { line: b.line } : {},
4332
4774
  message: b.message,
4333
- recommendation: recommendation10,
4775
+ recommendation: recommendation12,
4334
4776
  docsUrl: docsUrl12,
4335
4777
  ...spec.fix ? { fix: { ...spec.fix } } : {}
4336
4778
  });
@@ -5704,7 +6146,7 @@ var architectureReservedNamePlacement = {
5704
6146
  const inCapUnits = Object.hasOwn(capUnits, name);
5705
6147
  const inAnyUnits = Object.hasOwn(anyUnits, name);
5706
6148
  if (!inPlacements && !inCapUnits && !inAnyUnits) continue;
5707
- const emptyValue = (present3, value) => present3 && globsOf(value ?? "").length === 0;
6149
+ const emptyValue = (present4, value) => present4 && globsOf(value ?? "").length === 0;
5708
6150
  if (emptyValue(inPlacements, placements[name]) || emptyValue(inCapUnits, capUnits[name]) || emptyValue(inAnyUnits, anyUnits[name])) {
5709
6151
  continue;
5710
6152
  }
@@ -5821,9 +6263,9 @@ var routeEntryImportsCache = /* @__PURE__ */ new WeakMap();
5821
6263
  function cachedRouteEntryImports(c, ctx) {
5822
6264
  const cached = routeEntryImportsCache.get(c);
5823
6265
  if (cached !== void 0 && cached.aliases === ctx.project.kitAliases) return cached.result;
5824
- const result = routeEntryImports(c, ctx);
5825
- routeEntryImportsCache.set(c, { aliases: ctx.project.kitAliases, result });
5826
- return result;
6266
+ const result3 = routeEntryImports(c, ctx);
6267
+ routeEntryImportsCache.set(c, { aliases: ctx.project.kitAliases, result: result3 });
6268
+ return result3;
5827
6269
  }
5828
6270
  var architectureRouteComponentImport = componentRule({
5829
6271
  id: ID8,
@@ -6052,6 +6494,386 @@ var performanceStateRaw = componentRule({
6052
6494
  }))
6053
6495
  });
6054
6496
 
6497
+ // src/rules/a11y/aria-data.ts
6498
+ import { roles, aria } from "aria-query";
6499
+ function isKnownRole(role) {
6500
+ return roles.has(role);
6501
+ }
6502
+ function isAbstractRole(role) {
6503
+ return roles.get(role)?.abstract === true;
6504
+ }
6505
+ function isKnownAriaAttribute(name) {
6506
+ return aria.has(name);
6507
+ }
6508
+ function requiredAriaProps(role) {
6509
+ const def = roles.get(role);
6510
+ return def ? Object.keys(def.requiredProps) : [];
6511
+ }
6512
+ function ariaValueKind(name) {
6513
+ const def = aria.get(name);
6514
+ if (!def) return void 0;
6515
+ return { type: def.type, ...def.values ? { values: def.values.map(String) } : {} };
6516
+ }
6517
+
6518
+ // src/rules/a11y/invalid-role.ts
6519
+ var a11yInvalidRole = componentRule({
6520
+ id: "a11y/invalid-role",
6521
+ title: "Invalid ARIA role",
6522
+ category: "a11y",
6523
+ label: "ARIA roles",
6524
+ rationale: "A role that does not exist in WAI-ARIA (or is abstract, reserved for the spec itself) is ignored or misread by assistive technology, silently breaking the element\u2019s announced semantics.",
6525
+ recommendation: "Use a concrete WAI-ARIA role; abstract roles and typos are ignored by assistive technology.",
6526
+ applies: (c) => (c.ariaElements ?? []).some((e) => e.role?.literal !== void 0),
6527
+ bad: (c) => (c.ariaElements ?? []).flatMap((e) => {
6528
+ const literal = e.role?.literal;
6529
+ if (literal === void 0) return [];
6530
+ const tokens = splitTokens(literal);
6531
+ const badTokens = tokens.filter((t) => !isKnownRole(t) || isAbstractRole(t));
6532
+ if (badTokens.length === 0) return [];
6533
+ return [
6534
+ {
6535
+ line: e.line,
6536
+ message: `role="${literal}" on <${e.tag}> is ${isAbstractRole(badTokens[0]) ? "an abstract role" : "not a WAI-ARIA role"}`
6537
+ }
6538
+ ];
6539
+ })
6540
+ });
6541
+
6542
+ // src/rules/a11y/unknown-aria-attribute.ts
6543
+ var a11yUnknownAriaAttribute = componentRule({
6544
+ id: "a11y/unknown-aria-attribute",
6545
+ title: "Unknown ARIA attribute",
6546
+ category: "a11y",
6547
+ label: "Known ARIA attributes",
6548
+ rationale: "An `aria-*` name that does not exist in WAI-ARIA is not recognized by assistive technology, so the attribute is silently ignored instead of doing what the author intended.",
6549
+ recommendation: "Use a spec-defined `aria-*` attribute; unknown names are ignored by assistive technology.",
6550
+ applies: (c) => (c.ariaElements ?? []).some((e) => e.aria.length > 0),
6551
+ bad: (c) => (c.ariaElements ?? []).flatMap(
6552
+ (e) => e.aria.filter((a) => !isKnownAriaAttribute(a.name)).map((a) => ({ line: a.line, message: `\`${a.name}\` is not a WAI-ARIA attribute` }))
6553
+ )
6554
+ });
6555
+
6556
+ // src/rules/a11y/required-aria-props.ts
6557
+ var HOST_SUPPLIED = {
6558
+ "aria-checked": (e) => e.tag === "input" && (e.inputType === "checkbox" || e.inputType === "radio"),
6559
+ "aria-selected": (e) => e.tag === "option",
6560
+ "aria-level": (e) => /^h[1-6]$/.test(e.tag),
6561
+ "aria-valuenow": (e) => e.tag === "input" && e.inputType === "range" || e.tag === "progress" || e.tag === "meter"
6562
+ };
6563
+ var a11yRequiredAriaProps = componentRule({
6564
+ id: "a11y/required-aria-props",
6565
+ title: "Missing required ARIA props",
6566
+ category: "a11y",
6567
+ label: "Required ARIA props",
6568
+ rationale: 'Some WAI-ARIA roles are unusable to assistive technology without their required state/property attributes \u2014 a role="checkbox" with no way to know checked/unchecked announces a control with no discoverable state.',
6569
+ recommendation: "Add the role\u2019s required `aria-*` attribute(s), or rely on native host semantics that already supply them.",
6570
+ applies: (c) => (c.ariaElements ?? []).some((e) => e.role?.literal !== void 0 && !e.role.literal.includes(" ")),
6571
+ bad: (c) => (c.ariaElements ?? []).flatMap((e) => {
6572
+ if (e.hasSpread) return [];
6573
+ const literal = e.role?.literal;
6574
+ if (literal === void 0 || literal.includes(" ")) return [];
6575
+ const required = requiredAriaProps(literal);
6576
+ if (required.length === 0) return [];
6577
+ const present4 = new Set(e.aria.map((a) => a.name));
6578
+ const missing = required.filter((p) => !present4.has(p) && !HOST_SUPPLIED[p]?.(e));
6579
+ if (missing.length === 0) return [];
6580
+ return [{ line: e.line, message: `role="${literal}" on <${e.tag}> is missing required ${missing.join(", ")}` }];
6581
+ })
6582
+ });
6583
+
6584
+ // src/rules/a11y/invalid-aria-value.ts
6585
+ function isValid(type, values, literal) {
6586
+ switch (type) {
6587
+ case "boolean":
6588
+ return literal === "true" || literal === "false";
6589
+ case "tristate":
6590
+ return literal === "true" || literal === "false" || literal === "mixed";
6591
+ case "token":
6592
+ return (values ?? []).includes(literal);
6593
+ case "tokenlist":
6594
+ return splitTokens(literal).every((t) => (values ?? []).includes(t));
6595
+ case "integer":
6596
+ return /^-?\d+$/.test(literal);
6597
+ case "number":
6598
+ return literal.trim() !== "" && Number.isFinite(Number(literal));
6599
+ default:
6600
+ return true;
6601
+ }
6602
+ }
6603
+ var a11yInvalidAriaValue = componentRule({
6604
+ id: "a11y/invalid-aria-value",
6605
+ title: "Invalid ARIA attribute value",
6606
+ category: "a11y",
6607
+ label: "ARIA attribute values",
6608
+ rationale: "An `aria-*` attribute whose value does not match its spec-defined type (e.g. a boolean given a non-`true`/`false` literal) is misread or ignored by assistive technology.",
6609
+ recommendation: "Use a value matching the attribute\u2019s WAI-ARIA type \u2014 see the spec for allowed values.",
6610
+ applies: (c) => (c.ariaElements ?? []).some((e) => e.aria.some((a) => a.literal !== void 0)),
6611
+ bad: (c) => (c.ariaElements ?? []).flatMap(
6612
+ (e) => e.aria.flatMap((a) => {
6613
+ if (a.literal === void 0) return [];
6614
+ const kind = ariaValueKind(a.name);
6615
+ if (kind === void 0) return [];
6616
+ if (isValid(kind.type, kind.values, a.literal)) return [];
6617
+ return [{ line: a.line, message: `\`${a.name}="${a.literal}"\` is not a valid ${kind.type} value` }];
6618
+ })
6619
+ )
6620
+ });
6621
+
6622
+ // src/rules/a11y/interactive-nesting.ts
6623
+ var a11yInteractiveNesting = componentRule({
6624
+ id: "a11y/interactive-nesting",
6625
+ title: "Interactive element nested in an interactive element",
6626
+ category: "a11y",
6627
+ label: "Interactive nesting",
6628
+ rationale: "A control nested inside another interactive element is unreachable or misannounced by keyboard and assistive technology, and violates the HTML content model.",
6629
+ recommendation: "Restructure the markup so each interactive control is a sibling, not a descendant, of another.",
6630
+ applies: (c) => (c.interactiveNestings ?? []).length > 0,
6631
+ bad: (c) => (c.interactiveNestings ?? []).map((f) => ({
6632
+ line: f.line,
6633
+ message: `<${f.descendantTag}> is nested inside interactive <${f.containerTag}>`
6634
+ }))
6635
+ });
6636
+
6637
+ // src/rules/a11y/accessible-name.ts
6638
+ var a11yAccessibleName = componentRule({
6639
+ id: "a11y/accessible-name",
6640
+ title: "Interactive element has no accessible name",
6641
+ category: "a11y",
6642
+ label: "Accessible names",
6643
+ rationale: 'A button, link, or image button with no accessible name is announced by assistive technology as its bare role ("button", "link") with nothing to distinguish it from any other control on the page.',
6644
+ recommendation: "Give the element visible text, an aria-label/aria-labelledby/title, or an alt on its icon image.",
6645
+ applies: (c) => (c.unnamedInteractive ?? []).length > 0,
6646
+ bad: (c) => (c.unnamedInteractive ?? []).map((f) => ({ line: f.line, message: `<${f.tag}> has no accessible name` }))
6647
+ });
6648
+
6649
+ // src/rules/a11y/label-has-control.ts
6650
+ var a11yLabelHasControl = componentRule({
6651
+ id: "a11y/label-has-control",
6652
+ title: "<label> has no associated control",
6653
+ category: "a11y",
6654
+ label: "Label associations",
6655
+ rationale: "A `<label>` with no associated control is announced by assistive technology as plain text \u2014 clicking or tapping it does not focus the field, and a screen reader gives no relationship between the label and its control.",
6656
+ recommendation: "Add a `for` attribute pointing at the control's `id`, or wrap the control inside the `<label>`.",
6657
+ applies: (c) => (c.unassociatedLabels ?? []).length > 0,
6658
+ bad: (c) => (c.unassociatedLabels ?? []).map((f) => ({ line: f.line, message: "<label> has no associated control" }))
6659
+ });
6660
+
6661
+ // src/rules/a11y/use-list.ts
6662
+ var a11yUseList = componentRule({
6663
+ id: "a11y/use-list",
6664
+ title: "Bullet text should be a list",
6665
+ category: "a11y",
6666
+ severity: "info",
6667
+ label: "List structure",
6668
+ rationale: "A screen reader announces a real `<ul>`/`<ol>` as a list \u2014 item count, position, and boundaries. A bullet character typed into plain text carries none of that, so the visual structure is lost on assistive technology.",
6669
+ recommendation: "Use a `<ul>`/`<ol>` with `<li>` items instead of a bullet character in plain text.",
6670
+ applies: (c) => (c.bulletTexts ?? []).length > 0,
6671
+ bad: (c) => (c.bulletTexts ?? []).map((b) => ({
6672
+ line: b.line,
6673
+ message: `Text starts with a bullet character ('${b.char}') \u2014 use a list element`
6674
+ }))
6675
+ });
6676
+
6677
+ // src/rules/a11y/placeholder-label-option.ts
6678
+ var a11yPlaceholderLabelOption = componentRule({
6679
+ id: "a11y/placeholder-label-option",
6680
+ title: "Missing placeholder label option",
6681
+ category: "a11y",
6682
+ label: "Select placeholder",
6683
+ rationale: "A required, single-selection `<select>` initially shows its first option as the chosen value \u2014 if that option is not an empty placeholder, users can submit the form without ever having made a real choice, and assistive technology announces a value as already selected.",
6684
+ recommendation: 'Make the first `<option>` a placeholder: an empty `value=""`, or no `value` attribute and no text.',
6685
+ applies: (c) => (c.selectsMissingPlaceholder ?? []).length > 0,
6686
+ bad: (c) => (c.selectsMissingPlaceholder ?? []).map((f) => ({
6687
+ line: f.line,
6688
+ message: "<select required> is missing a placeholder label option"
6689
+ }))
6690
+ });
6691
+
6692
+ // src/rules/a11y/require-datetime.ts
6693
+ var a11yRequireDatetime = componentRule({
6694
+ id: "a11y/require-datetime",
6695
+ title: "Missing datetime attribute",
6696
+ category: "a11y",
6697
+ label: "Time elements",
6698
+ rationale: 'A `<time>` element with no `datetime` attribute exposes its text content as the machine-readable value \u2014 text like "last Tuesday" cannot be parsed by assistive technology or user agents into an actual date, so its meaning is lost to anything that isn\'t a sighted reader.',
6699
+ recommendation: 'Add a `datetime` attribute with a machine-readable value, e.g. `<time datetime="2026-08-14">Aug 14</time>`.',
6700
+ applies: (c) => (c.timesMissingDatetime ?? []).length > 0,
6701
+ bad: (c) => (c.timesMissingDatetime ?? []).map((f) => ({
6702
+ line: f.line,
6703
+ message: `<time> content "${f.text}" is not machine-readable and has no datetime attribute`
6704
+ }))
6705
+ });
6706
+
6707
+ // src/rules/a11y/doctype.ts
6708
+ var present3 = { presence: "own", value: "static" };
6709
+ var absent3 = { presence: "none", value: "absent" };
6710
+ var FIX8 = {
6711
+ description: "Add <!doctype html> as the first line of src/app.html.",
6712
+ snippet: "<!doctype html>",
6713
+ lang: "html"
6714
+ };
6715
+ var a11yDoctype = {
6716
+ id: "a11y/doctype",
6717
+ title: "Doctype",
6718
+ category: "a11y",
6719
+ severity: "warning",
6720
+ scope: "project",
6721
+ rationale: "Without a doctype browsers render in quirks mode, breaking CSS and accessibility tree behavior.",
6722
+ fix: FIX8,
6723
+ async check(ctx) {
6724
+ const { appHtmlDoctype } = ctx.project;
6725
+ if (appHtmlDoctype === void 0) return [];
6726
+ return [
6727
+ {
6728
+ id: "a11y/doctype",
6729
+ category: "a11y",
6730
+ severity: "warning",
6731
+ detection: appHtmlDoctype ? present3 : absent3,
6732
+ location: "src/app.html",
6733
+ message: appHtmlDoctype ? "<!doctype html>" : "src/app.html is missing <!doctype html>",
6734
+ recommendation: "Add <!doctype html> as the first line of src/app.html.",
6735
+ docsUrl: docsUrlFor("a11y/doctype"),
6736
+ fix: { ...FIX8 }
6737
+ }
6738
+ ];
6739
+ }
6740
+ };
6741
+
6742
+ // src/rules/a11y/route-rule.ts
6743
+ function resultFactory(id, recommendation12) {
6744
+ const docsUrl12 = docsUrlFor(id);
6745
+ return (route, detection, occ, message) => ({
6746
+ id,
6747
+ category: "a11y",
6748
+ severity: "warning",
6749
+ detection,
6750
+ route,
6751
+ location: occ.file,
6752
+ ...occ.line > 0 ? { line: occ.line } : {},
6753
+ message,
6754
+ recommendation: recommendation12,
6755
+ docsUrl: docsUrl12
6756
+ });
6757
+ }
6758
+ function surplusRule(spec) {
6759
+ const result3 = resultFactory(spec.id, spec.recommendation);
6760
+ return {
6761
+ id: spec.id,
6762
+ title: spec.title,
6763
+ category: "a11y",
6764
+ severity: "warning",
6765
+ scope: "route",
6766
+ rationale: spec.rationale,
6767
+ async check(ctx) {
6768
+ const out = [];
6769
+ for (const route of ctx.a11y ?? []) {
6770
+ let first;
6771
+ let surplus = false;
6772
+ for (const [key, reps] of spec.map(route)) {
6773
+ first ??= reps[0];
6774
+ for (let i = 1; i < reps.length; i++) {
6775
+ surplus = true;
6776
+ out.push(result3(route.route, PENALIZED, reps[i], spec.message(key, i, reps.length)));
6777
+ }
6778
+ }
6779
+ if (first && !surplus) out.push(result3(route.route, PASS, { file: first.file, line: 0 }, spec.passMessage));
6780
+ }
6781
+ return out;
6782
+ }
6783
+ };
6784
+ }
6785
+
6786
+ // src/rules/a11y/duplicate-landmark.ts
6787
+ var KINDS = ["main", "banner", "contentinfo"];
6788
+ var a11yDuplicateLandmark = surplusRule({
6789
+ id: "a11y/duplicate-landmark",
6790
+ title: "Duplicate landmark",
6791
+ rationale: "Assistive tech users jump between landmarks to skip repeated content; more than one main, banner, or contentinfo per page leaves them guessing which one is the real one.",
6792
+ recommendation: "A route should have at most one main, banner, and contentinfo landmark.",
6793
+ map: (route) => KINDS.flatMap((kind) => {
6794
+ const reps = route.landmarks[kind];
6795
+ return reps?.length ? [[kind, reps]] : [];
6796
+ }),
6797
+ message: (kind, i, n) => `Duplicate ${kind} landmark (${i + 1} of ${n})`,
6798
+ passMessage: "No duplicate landmarks"
6799
+ });
6800
+
6801
+ // src/rules/a11y/top-level-landmark.ts
6802
+ var recommendation10 = "A banner, main, complementary, or contentinfo landmark should not be nested inside another landmark.";
6803
+ var result = resultFactory("a11y/top-level-landmark", recommendation10);
6804
+ var KINDS2 = ["main", "banner", "complementary", "contentinfo"];
6805
+ var a11yTopLevelLandmark = {
6806
+ id: "a11y/top-level-landmark",
6807
+ title: "Top-level landmark",
6808
+ category: "a11y",
6809
+ severity: "warning",
6810
+ scope: "route",
6811
+ rationale: "Assistive tech landmark navigation expects banner/main/complementary/contentinfo at the top level; nesting one inside another hides it from that navigation.",
6812
+ async check(ctx) {
6813
+ const out = [];
6814
+ for (const route of ctx.a11y ?? []) {
6815
+ for (const nested of route.nestedLandmarks) {
6816
+ out.push(result(route.route, PENALIZED, nested, `${nested.kind} landmark is nested inside ${nested.within}`));
6817
+ }
6818
+ if (route.nestedLandmarks.length === 0) {
6819
+ const first = KINDS2.map((kind) => route.landmarks[kind]?.[0]).find((rep) => rep !== void 0);
6820
+ if (first) out.push(result(route.route, PASS, { file: first.file, line: 0 }, "No nested landmarks"));
6821
+ }
6822
+ }
6823
+ return out;
6824
+ }
6825
+ };
6826
+
6827
+ // src/rules/a11y/id-duplication.ts
6828
+ var a11yIdDuplication = surplusRule({
6829
+ id: "a11y/id-duplication",
6830
+ title: "Id duplication",
6831
+ rationale: "A duplicate id breaks label/aria-labelledby associations and in-page fragment navigation: assistive tech resolves the first match, which may not be the one the author intended.",
6832
+ recommendation: "Every id in a route should be unique.",
6833
+ // Entries ordered by each id's first representative (file, then line): content-derived and
6834
+ // stable — a Record's own-key enumeration would pull integer-like ids ("1") to the front.
6835
+ map: (route) => Object.entries(route.ids).sort(([, a], [, b]) => a[0].file.localeCompare(b[0].file) || a[0].line - b[0].line),
6836
+ message: (id) => `Duplicate id "${id}"`,
6837
+ passMessage: "No duplicate ids"
6838
+ });
6839
+
6840
+ // src/rules/a11y/no-missing-id-ref.ts
6841
+ var recommendation11 = "An id reference should point to an id that exists somewhere in the composed route.";
6842
+ var result2 = resultFactory("a11y/no-missing-id-ref", recommendation11);
6843
+ var a11yNoMissingIdRef = {
6844
+ id: "a11y/no-missing-id-ref",
6845
+ title: "No missing id ref",
6846
+ category: "a11y",
6847
+ severity: "warning",
6848
+ scope: "route",
6849
+ rationale: 'A `for`/`aria-labelledby`/`aria-describedby`/`aria-controls`/`aria-activedescendant`/`href="#\u2026"` pointing at an id that does not exist leaves assistive tech with a broken association or the browser with a dead in-page link.',
6850
+ async check(ctx) {
6851
+ const out = [];
6852
+ for (const route of ctx.a11y ?? []) {
6853
+ if (!route.fullyResolved || route.idRefs.length === 0) continue;
6854
+ const candidates = new Set(route.idCandidates);
6855
+ let hasMissing = false;
6856
+ for (const ref of route.idRefs) {
6857
+ if (candidates.has(ref.id)) continue;
6858
+ hasMissing = true;
6859
+ out.push(
6860
+ result2(
6861
+ route.route,
6862
+ PENALIZED,
6863
+ ref,
6864
+ `${ref.attr}="${ref.attr === "href" ? "#" : ""}${ref.id}" references a missing id`
6865
+ )
6866
+ );
6867
+ }
6868
+ if (!hasMissing) {
6869
+ const first = route.idRefs[0];
6870
+ out.push(result2(route.route, PASS, { file: first.file, line: 0 }, "No missing id references"));
6871
+ }
6872
+ }
6873
+ return out;
6874
+ }
6875
+ };
6876
+
6055
6877
  // src/rules/index.ts
6056
6878
  var allRules = [
6057
6879
  seoTitlePresence,
@@ -6126,7 +6948,22 @@ var allRules = [
6126
6948
  performanceMinifyDisabled,
6127
6949
  performanceLoadWaterfall,
6128
6950
  performanceSequentialAwaits,
6129
- performanceStateRaw
6951
+ performanceStateRaw,
6952
+ a11yInvalidRole,
6953
+ a11yUnknownAriaAttribute,
6954
+ a11yRequiredAriaProps,
6955
+ a11yInvalidAriaValue,
6956
+ a11yInteractiveNesting,
6957
+ a11yAccessibleName,
6958
+ a11yLabelHasControl,
6959
+ a11yUseList,
6960
+ a11yPlaceholderLabelOption,
6961
+ a11yRequireDatetime,
6962
+ a11yDoctype,
6963
+ a11yDuplicateLandmark,
6964
+ a11yTopLevelLandmark,
6965
+ a11yIdDuplication,
6966
+ a11yNoMissingIdRef
6130
6967
  ];
6131
6968
  function optionInfos(spec) {
6132
6969
  return Object.entries(spec).map(([name, s]) => ({
@@ -6153,21 +6990,21 @@ function explainRule(id) {
6153
6990
  }
6154
6991
 
6155
6992
  // src/summary.ts
6156
- function classify(result, config) {
6157
- if (isPenalized(result.detection, config.treatDynamicAs)) return "fail";
6158
- if (result.detection.value === "dynamic") return "dynamic";
6993
+ function classify(result3, config) {
6994
+ if (isPenalized(result3.detection, config.treatDynamicAs)) return "fail";
6995
+ if (result3.detection.value === "dynamic") return "dynamic";
6159
6996
  return "pass";
6160
6997
  }
6161
- function effectiveSeverity(result, config) {
6162
- if (result.detection.value === "dynamic" && config.treatDynamicAs === "warn") return "warning";
6163
- return result.severity;
6998
+ function effectiveSeverity(result3, config) {
6999
+ if (result3.detection.value === "dynamic" && config.treatDynamicAs === "warn") return "warning";
7000
+ return result3.severity;
6164
7001
  }
6165
7002
  function summarize(results, config) {
6166
7003
  const summary = { critical: 0, warning: 0, info: 0, passed: 0, dynamic: 0 };
6167
- for (const result of results) {
6168
- const cls = classify(result, config);
7004
+ for (const result3 of results) {
7005
+ const cls = classify(result3, config);
6169
7006
  if (cls === "fail") {
6170
- summary[effectiveSeverity(result, config)] += 1;
7007
+ summary[effectiveSeverity(result3, config)] += 1;
6171
7008
  } else {
6172
7009
  summary.passed += 1;
6173
7010
  if (cls === "dynamic") summary.dynamic += 1;
@@ -6341,7 +7178,16 @@ var SEVERITY_TITLE = {
6341
7178
  warning: "Warnings",
6342
7179
  info: "Info"
6343
7180
  };
6344
- var categoryLabel = (c) => c === "seo" ? "SEO" : c.charAt(0).toUpperCase() + c.slice(1);
7181
+ var CATEGORY_LABEL = {
7182
+ seo: "SEO",
7183
+ performance: "Performance",
7184
+ correctness: "Correctness",
7185
+ security: "Security",
7186
+ architecture: "Architecture",
7187
+ a11y: "Accessibility"
7188
+ };
7189
+ var CATEGORY_ORDER = Object.keys(CATEGORY_LABEL);
7190
+ var categoryLabel = (c) => CATEGORY_LABEL[c];
6345
7191
  var MAX_RULE_GROUPS_PER_BUCKET = 5;
6346
7192
  function groupByRule(results) {
6347
7193
  const groups = /* @__PURE__ */ new Map();
@@ -6396,7 +7242,7 @@ function formatConsoleReport(results, config, options = {}) {
6396
7242
  const p = options.palette ?? noColorPalette;
6397
7243
  const summary = summarize(results, config);
6398
7244
  const { health, categories: byCat } = computeHealth(results, config);
6399
- const present3 = CATEGORIES.filter((c) => byCat[c] !== void 0);
7245
+ const present4 = CATEGORY_ORDER.filter((c) => byCat[c] !== void 0);
6400
7246
  const lines = [];
6401
7247
  if (!options.omitHeader) {
6402
7248
  lines.push(
@@ -6405,7 +7251,7 @@ function formatConsoleReport(results, config, options = {}) {
6405
7251
  `${p.bold("Health:")} ${scoreColor(p, health)(`${health}/100`)}`
6406
7252
  );
6407
7253
  }
6408
- for (const c of present3) {
7254
+ for (const c of present4) {
6409
7255
  lines.push(scoreLine(p, categoryLabel(c), byCat[c]));
6410
7256
  }
6411
7257
  lines.push("");
@@ -6465,17 +7311,17 @@ function formatConsoleReport(results, config, options = {}) {
6465
7311
  }
6466
7312
 
6467
7313
  // src/reporter/json.ts
6468
- function issueOf(result) {
7314
+ function issueOf(result3) {
6469
7315
  return {
6470
- id: result.id,
6471
- category: result.category ?? "seo",
6472
- title: result.message,
6473
- detection: result.detection,
6474
- location: result.location,
6475
- ...result.line !== void 0 ? { line: result.line } : {},
6476
- recommendation: result.recommendation,
6477
- ...result.docsUrl ? { docsUrl: result.docsUrl } : {},
6478
- ...result.fix ? { fix: result.fix } : {}
7316
+ id: result3.id,
7317
+ category: result3.category ?? "seo",
7318
+ title: result3.message,
7319
+ detection: result3.detection,
7320
+ location: result3.location,
7321
+ ...result3.line !== void 0 ? { line: result3.line } : {},
7322
+ recommendation: result3.recommendation,
7323
+ ...result3.docsUrl ? { docsUrl: result3.docsUrl } : {},
7324
+ ...result3.fix ? { fix: result3.fix } : {}
6479
7325
  };
6480
7326
  }
6481
7327
  function ruleEvidence(results, config, ruleIds) {
@@ -6543,8 +7389,8 @@ function severityToSarifLevel(sev) {
6543
7389
  function severityToGithubLevel(sev) {
6544
7390
  return sev === "critical" ? "error" : sev === "warning" ? "warning" : "notice";
6545
7391
  }
6546
- function messageText(result) {
6547
- return result.recommendation ? `${result.message} ${result.recommendation}` : result.message;
7392
+ function messageText(result3) {
7393
+ return result3.recommendation ? `${result3.message} ${result3.recommendation}` : result3.message;
6548
7394
  }
6549
7395
  var RULE_META = new Map(
6550
7396
  allRules.map((r) => [r.id, { title: r.title, severity: r.severity, docsUrl: docsUrlFor(r.id) }])
@@ -6614,7 +7460,7 @@ function formatSarifReport(results, config, meta) {
6614
7460
  defaultConfiguration: { level: severityToSarifLevel(m?.severity ?? r.severity) }
6615
7461
  });
6616
7462
  }
6617
- const result = {
7463
+ const result3 = {
6618
7464
  ruleId: r.id,
6619
7465
  ruleIndex: ruleIndex.get(r.id),
6620
7466
  level: severityToSarifLevel(effectiveSeverity(r, config)),
@@ -6624,7 +7470,7 @@ function formatSarifReport(results, config, meta) {
6624
7470
  }
6625
7471
  };
6626
7472
  if (r.location) {
6627
- result.locations = [
7473
+ result3.locations = [
6628
7474
  {
6629
7475
  physicalLocation: {
6630
7476
  artifactLocation: { uri: r.location },
@@ -6633,7 +7479,7 @@ function formatSarifReport(results, config, meta) {
6633
7479
  }
6634
7480
  ];
6635
7481
  }
6636
- return result;
7482
+ return result3;
6637
7483
  });
6638
7484
  const log = {
6639
7485
  $schema: "https://json.schemastore.org/sarif-2.1.0.json",
@@ -6899,6 +7745,7 @@ body{background:var(--ground);color:var(--ink);font-family:var(--sans);line-heig
6899
7745
  var APP_SCRIPT = `
6900
7746
  (function(){
6901
7747
  var BAND_COLOR = { good: '#2fa968', warn: '#e8a317', poor: '#e5484d' };
7748
+ var CATEGORY_NAMES = ${JSON.stringify(CATEGORY_LABEL)};
6902
7749
  function scoreBand(score) { return score >= 90 ? 'good' : score >= 50 ? 'warn' : 'poor'; }
6903
7750
 
6904
7751
  // Same mark as the docs site's hero wordmark (docs/public/wordmark.svg) \u2014 an inline
@@ -7245,7 +8092,7 @@ var APP_SCRIPT = `
7245
8092
  }, []);
7246
8093
  };
7247
8094
  var catChips = Object.keys(categories).map(function (cat) {
7248
- var name = cat === 'seo' ? 'SEO' : cat.charAt(0).toUpperCase() + cat.slice(1);
8095
+ var name = CATEGORY_NAMES[cat] || cat;
7249
8096
  return chip(cat, name);
7250
8097
  });
7251
8098
  return h('div', { class: 'dv-filters', role: 'group', 'aria-label': 'Filter findings' },
@@ -7317,7 +8164,7 @@ var APP_SCRIPT = `
7317
8164
  var c = s.report.categories[cat];
7318
8165
  var band = scoreBand(c.score);
7319
8166
  var weight = s.report.weights[cat];
7320
- var name = cat === 'seo' ? 'SEO' : cat.charAt(0).toUpperCase() + cat.slice(1);
8167
+ var name = CATEGORY_NAMES[cat] || cat;
7321
8168
  // keys/affectedKeys are absent on hand-built snapshots (older fixtures, tests) \u2014
7322
8169
  // render nothing rather than "undefined of undefined". 0 affected of N keys is still
7323
8170
  // rendered: on a real project that's the signal a thin score can't give, that the
@@ -7458,10 +8305,27 @@ export {
7458
8305
  BAND_COLOR,
7459
8306
  CATEGORIES,
7460
8307
  CHILD_NODE_KEYS,
8308
+ IDREF_ATTRS,
8309
+ LANDMARK_ROLES,
7461
8310
  ROBOTS_SOURCE_PATHS,
7462
8311
  SITEMAP_SOURCE_PATHS,
7463
8312
  SVELTE_CONFIG_FILES,
7464
8313
  VITE_CONFIG_FILES,
8314
+ a11yAccessibleName,
8315
+ a11yDoctype,
8316
+ a11yDuplicateLandmark,
8317
+ a11yIdDuplication,
8318
+ a11yInteractiveNesting,
8319
+ a11yInvalidAriaValue,
8320
+ a11yInvalidRole,
8321
+ a11yLabelHasControl,
8322
+ a11yNoMissingIdRef,
8323
+ a11yPlaceholderLabelOption,
8324
+ a11yRequireDatetime,
8325
+ a11yRequiredAriaProps,
8326
+ a11yTopLevelLandmark,
8327
+ a11yUnknownAriaAttribute,
8328
+ a11yUseList,
7465
8329
  allRules,
7466
8330
  applyOverrides,
7467
8331
  applyRuleSeverities,
@@ -7501,6 +8365,7 @@ export {
7501
8365
  correctnessServerBrowserGlobal,
7502
8366
  correctnessStalePropDerivation,
7503
8367
  correctnessUnmutatedState,
8368
+ decodeFragmentId,
7504
8369
  defaultConfig,
7505
8370
  defaultProject,
7506
8371
  defineConfig,
@@ -7515,6 +8380,7 @@ export {
7515
8380
  findKitPathsBaseInSvelteConfig,
7516
8381
  findKitPathsBaseInViteConfig,
7517
8382
  findMinifyDisabled,
8383
+ foldOccurrences,
7518
8384
  formatAgentReport,
7519
8385
  formatConsoleReport,
7520
8386
  formatFailedRuleWarning,
@@ -7529,6 +8395,7 @@ export {
7529
8395
  intOption,
7530
8396
  isMentionedAnywhere,
7531
8397
  isPenalized,
8398
+ isTopFragment,
7532
8399
  lineOf,
7533
8400
  linkRule,
7534
8401
  listOption,
@@ -7602,6 +8469,7 @@ export {
7602
8469
  settingOptions,
7603
8470
  settingSeverity,
7604
8471
  shouldSkipRangeCheck,
8472
+ splitTokens,
7605
8473
  summarize,
7606
8474
  terminalSafe,
7607
8475
  textFromNodes,