@bridge-ds/core 0.4.0 → 0.5.1

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.
@@ -306,7 +306,7 @@ const looseKey = (p) => p.startsWith("$") || p.startsWith("--") || !/^-?[a-z]+(-
306
306
  const KNOWN_PROPS = new Set(
307
307
  "color background background-color background-image border border-top border-right border-bottom border-left border-color border-width border-style border-radius border-top-left-radius border-top-right-radius border-bottom-left-radius border-bottom-right-radius border-top-color border-bottom-color border-left-color border-right-color border-top-width border-bottom-width border-left-width border-right-width border-block border-inline border-block-start border-block-end border-inline-start border-inline-end outline outline-color outline-width outline-offset fill stroke text-decoration text-decoration-color caret-color accent-color box-shadow text-shadow padding padding-top padding-right padding-bottom padding-left padding-block padding-inline padding-x padding-y padding-inline-start padding-inline-end padding-block-start padding-block-end margin margin-top margin-right margin-bottom margin-left margin-block margin-inline margin-x margin-y margin-inline-start margin-inline-end margin-block-start margin-block-end gap row-gap column-gap grid-gap inset top right bottom left width height min-width min-height max-width max-height flex-basis font font-family font-size font-weight line-height letter-spacing transition transition-duration transition-delay transition-timing-function animation animation-duration animation-delay animation-timing-function z-index opacity display position flex flex-direction flex-wrap flex-grow flex-shrink align-items justify-content overflow text-align white-space cursor content transform".split(" ")
308
308
  );
309
- function mask(value) {
309
+ function mask$1(value) {
310
310
  let out = value;
311
311
  for (const fn of ["var(", "url(", "env("]) {
312
312
  let i = out.indexOf(fn);
@@ -352,7 +352,7 @@ function scanCssValue(property, value) {
352
352
  out.themeRef = true;
353
353
  return out;
354
354
  }
355
- const masked = mask(value);
355
+ const masked = mask$1(value);
356
356
  if (prop === "font-family") {
357
357
  if (masked.trim() && !/^(inherit|initial|unset)$/i.test(v)) hits.push({ category: "font-family", text: v, index: value.indexOf(v) });
358
358
  return out;
@@ -572,6 +572,10 @@ const EQUIVALENTS = [
572
572
  [/^Badge$/, ["Badge"]],
573
573
  [/^(Chip|Tag|ChipField)$/, ["Tag", "Badge"]],
574
574
  [/^(Alert|AlertTitle|Callout)$/, ["Alert"]],
575
+ [/^(Banner|AnnouncementBar|TopBanner)$/, ["Banner", "Alert"]],
576
+ [/^(CodeBlock|Code|CodeSnippet|Snippet|Prism|SyntaxHighlighter|CopyBlock)$/, ["CodeBlock"]],
577
+ [/^(RadioCard|RadioCards|RadioTile|ChoiceCard|ChoiceCardGroup)$/, ["ChoiceCard", "RadioGroup"]],
578
+ [/^(Checklist|ChecklistItem|StepList)$/, ["Checklist"]],
575
579
  [/^(Snackbar|Toast|Toaster|Notification)$/, ["Toast"]],
576
580
  [/^(CircularProgress|Spinner|Loading|LoadingIndicator)$/, ["Spinner", "Progress"]],
577
581
  [/^(LinearProgress|Progress)$/, ["Progress"]],
@@ -851,6 +855,26 @@ const THEME_FNS = /^(createTheme|extendTheme|createMuiTheme|responsiveFontSizes|
851
855
  const CSS_TAGS = /^(styled(\.\w+|\(.*\)(\.attrs\(.*\))?)|css|keyframes|createGlobalStyle|injectGlobal|tw\.\w+)$/s;
852
856
  const MUI_SPACING_ATTR = /^([mp][trblxy]?|(margin|padding)(Top|Bottom|Left|Right|X|Y)?|gap|rowGap|columnGap|spacing)$/;
853
857
  const THEME_REF_RE = /\btheme\.(palette|spacing|shape|typography|shadows|zIndex|transitions|vars)\b/g;
858
+ const DOM_RENDERER_RE = /^([a-z][A-Za-z0-9]*?)(Html|HTML|HtmlAttributes|Attributes|ClassName)$/;
859
+ function domRendererComponent(fn, components) {
860
+ const m = DOM_RENDERER_RE.exec(fn);
861
+ if (!m) return void 0;
862
+ const pascal2 = m[1][0].toUpperCase() + m[1].slice(1);
863
+ if (components.has(pascal2)) return pascal2;
864
+ return [...components].filter((c) => pascal2.startsWith(c)).sort((a, b) => b.length - a.length)[0];
865
+ }
866
+ const LOOKALIKE_CLASSES = { pill: "Badge", badge: "Badge", chip: "Tag", tooltip: "Tooltip", tip: "Tooltip", "has-tip": "Tooltip" };
867
+ const LOOKALIKE_ATTRS = { "data-tip": "Tooltip" };
868
+ const HTML_CLASS_RE = /\bclass(?:Name)?\s*=\s*["']([^"'$]*)/g;
869
+ const HTML_ATTR_RE = /\s(data-tip)\s*=/g;
870
+ function lookalikesInClassList(list, components) {
871
+ const out = [];
872
+ for (const token of list.split(/\s+/)) {
873
+ const component = LOOKALIKE_CLASSES[token];
874
+ if (component && components.has(component) && !out.some((o) => o.component === component)) out.push({ token, component });
875
+ }
876
+ return out;
877
+ }
854
878
  function kindFor(path) {
855
879
  return path.endsWith(".tsx") ? ts.ScriptKind.TSX : path.endsWith(".jsx") ? ts.ScriptKind.JSX : path.endsWith(".ts") || path.endsWith(".mts") || path.endsWith(".cts") ? ts.ScriptKind.TS : ts.ScriptKind.JSX;
856
880
  }
@@ -870,6 +894,79 @@ function contextForDeclaration(name, typeText) {
870
894
  if (/^(styles?|sx|classes)$|Styles?$|Sx$/.test(name)) return /sx$/i.test(name) ? "sx" : "styled";
871
895
  return null;
872
896
  }
897
+ function importsOf(path, text) {
898
+ const sf = ts.createSourceFile(path, text, ts.ScriptTarget.Latest, false, kindFor(path));
899
+ const imports = /* @__PURE__ */ new Map();
900
+ for (const node of sf.statements) {
901
+ if (!ts.isImportDeclaration(node) || !ts.isStringLiteral(node.moduleSpecifier)) continue;
902
+ const mod = node.moduleSpecifier.text;
903
+ const c = node.importClause;
904
+ if (c?.name) imports.set(c.name.text, { module: mod, imported: "default" });
905
+ if (c?.namedBindings && ts.isNamespaceImport(c.namedBindings)) imports.set(c.namedBindings.name.text, { module: mod, imported: "*" });
906
+ if (c?.namedBindings && ts.isNamedImports(c.namedBindings)) for (const el of c.namedBindings.elements) imports.set(el.name.text, { module: mod, imported: (el.propertyName ?? el.name).text });
907
+ }
908
+ return imports;
909
+ }
910
+ function classifyTag(t, imports, ds) {
911
+ const { name } = t;
912
+ const at = { line: t.line, column: t.column };
913
+ if (/^[a-z][\w-]*$/.test(name)) {
914
+ if (!ds.nativeElements.includes(name)) return {};
915
+ if (name === "input" && t.type === "hidden") return {};
916
+ if (name === "a" && !t.hasHref) return {};
917
+ if (t.inRenderProp) return {};
918
+ const suggestion = nativeEquivalent(name, t.type, ds.components);
919
+ if (!suggestion) return {};
920
+ return { component: { ...at, name, kind: "native", library: "native", suggestion } };
921
+ }
922
+ const root = name.split(".")[0];
923
+ const imp = imports.get(root);
924
+ const ns = imp?.imported === "*";
925
+ const shown = imp && !ns && imp.imported !== "default" ? [imp.imported, ...name.split(".").slice(1)].join(".") : name;
926
+ const leaf = ns && name.includes(".") ? name.split(".").slice(1).join(".") : shown;
927
+ if (imp) {
928
+ const kind = classifyModule(imp.module, ds.packages, ds.foreignLibraries);
929
+ if (isNonVisual(leaf.split(".").pop())) return {};
930
+ if (kind === "ds") return { ds: leaf };
931
+ if (kind === "charts") return { component: { ...at, name: shown, kind: "foreign", library: libraryLabel(imp.module, kind) } };
932
+ if (kind === "other" && isRouterLink(imp.module, imp.imported === "default" ? "default" : leaf)) {
933
+ if (t.inRenderProp) return {};
934
+ const suggestion = ds.components.has("Link") ? "Link" : void 0;
935
+ return { component: { ...at, name: shown, kind: "foreign", library: libraryLabel(imp.module, kind), ...suggestion ? { suggestion } : {} } };
936
+ }
937
+ if (kind === "foreign" || kind === "shadcn") {
938
+ const suggestion = (ns ? dsEquivalent(root, ds.components) : void 0) ?? dsEquivalent(leaf, ds.components);
939
+ return { component: { ...at, name: shown, kind: "foreign", library: libraryLabel(imp.module, kind), ...suggestion ? { suggestion } : {} } };
940
+ }
941
+ if (kind === "local" && ds.components.has(leaf.split(".")[0])) return { component: { ...at, name: leaf, kind: "recreated", library: "local", suggestion: leaf.split(".")[0] } };
942
+ return {};
943
+ }
944
+ if (ds.components.has(root)) return { localCandidate: { ...at, name, kind: "recreated", library: "local", suggestion: root } };
945
+ return {};
946
+ }
947
+ function tailwindFindings(hits, offset, pos, ds) {
948
+ const findings = [];
949
+ let themeRefs = 0;
950
+ for (const h of hits) {
951
+ if (h.kind === "scale") {
952
+ themeRefs++;
953
+ continue;
954
+ }
955
+ if (h.kind === "token") continue;
956
+ let cat = h.category;
957
+ const v = h.value;
958
+ const isColor = Boolean(parseCssColor(v));
959
+ if (cat === "color-or-size") cat = isColor ? "color" : "font-size";
960
+ else if (cat === "color-or-width") cat = isColor ? "color" : "border-width";
961
+ else if (cat === "color-or-shadow") cat = isColor ? "color" : "shadow";
962
+ else if (cat === "font") cat = /^\d{3}$/.test(v) ? "font-weight" : "font-family";
963
+ if (cat === "color" && !isColor) continue;
964
+ const suggestion = ds.suggest(cat, v);
965
+ const cap = h.kind === "palette" && suggestion && suggestion.confidence > 0.6 ? { ...suggestion, confidence: 0.6, level: "medium" } : suggestion;
966
+ findings.push({ ...pos(offset + h.index), category: cat, value: h.cls, source: "tailwind", ...cap ? { suggestion: cap } : {} });
967
+ }
968
+ return { findings, themeRefs };
969
+ }
873
970
  function scanCode(path, text, ds) {
874
971
  const sf = ts.createSourceFile(path, text, ts.ScriptTarget.Latest, true, kindFor(path));
875
972
  const out = { findings: [], tokenUsages: 0, themeRefs: 0, ds: {}, components: [], definitions: [] };
@@ -884,24 +981,9 @@ function scanCode(path, text, ds) {
884
981
  out.findings.push({ ...pos(offset), category: h.category, ...property ? { property } : {}, value: h.text, source, ...suggestion ? { suggestion } : {} });
885
982
  };
886
983
  const addTailwind = (hits, offset) => {
887
- for (const h of hits) {
888
- if (h.kind === "scale") {
889
- out.themeRefs++;
890
- continue;
891
- }
892
- if (h.kind === "token") continue;
893
- let cat = h.category;
894
- const v = h.value;
895
- const isColor = Boolean(parseCssColor(v));
896
- if (cat === "color-or-size") cat = isColor ? "color" : "font-size";
897
- else if (cat === "color-or-width") cat = isColor ? "color" : "border-width";
898
- else if (cat === "color-or-shadow") cat = isColor ? "color" : "shadow";
899
- else if (cat === "font") cat = /^\d{3}$/.test(v) ? "font-weight" : "font-family";
900
- if (cat === "color" && !isColor) continue;
901
- const suggestion = ds.suggest(cat, v);
902
- const cap = h.kind === "palette" && suggestion && suggestion.confidence > 0.6 ? { ...suggestion, confidence: 0.6, level: "medium" } : suggestion;
903
- out.findings.push({ ...pos(offset + h.index), category: cat, value: h.cls, source: "tailwind", ...cap ? { suggestion: cap } : {} });
904
- }
984
+ const r = tailwindFindings(hits, offset, pos, ds);
985
+ out.themeRefs += r.themeRefs;
986
+ out.findings.push(...r.findings);
905
987
  };
906
988
  const styleValue = (key, node, ctx) => {
907
989
  const source = ctx === "sx" ? "sx" : ctx === "style" ? "style" : ctx === "theme" ? "theme" : "styled";
@@ -962,13 +1044,18 @@ function scanCode(path, text, ds) {
962
1044
  }
963
1045
  for (const a of scan.applies) addTailwind(a.hits, start + a.start);
964
1046
  };
1047
+ const lookalike = (list, offset) => {
1048
+ for (const l of lookalikesInClassList(list, ds.components)) out.components.push({ ...pos(offset), name: `.${l.token}`, kind: "recreated", library: "local", suggestion: l.component });
1049
+ };
965
1050
  const classLiteral = (node) => {
966
1051
  if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
967
1052
  handled.add(node);
1053
+ lookalike(node.text, node.getStart(sf) + 1);
968
1054
  addTailwind(scanClassString(node.text), node.getStart(sf) + 1);
969
1055
  } else if (ts.isTemplateExpression(node)) {
970
1056
  for (const p of [node.head, ...node.templateSpans.map((s) => s.literal)]) {
971
1057
  handled.add(p);
1058
+ lookalike(p.text, p.getStart(sf) + 1);
972
1059
  addTailwind(scanClassString(p.text), p.getStart(sf) + 1);
973
1060
  }
974
1061
  node.templateSpans.forEach((s) => visit(s.expression, null, true));
@@ -977,49 +1064,13 @@ function scanCode(path, text, ds) {
977
1064
  const jsxName = (n) => n.getText(sf);
978
1065
  const element = (node) => {
979
1066
  const name = jsxName(node.tagName);
980
- const at = pos(node.getStart(sf));
981
- if (/^[a-z][\w-]*$/.test(name)) {
982
- if (!ds.nativeElements.includes(name)) return;
983
- const typeAttr = node.attributes.properties.find((p) => ts.isJsxAttribute(p) && p.name.getText(sf) === "type");
984
- const type = typeAttr?.initializer && ts.isStringLiteral(typeAttr.initializer) ? typeAttr.initializer.text : void 0;
985
- if (name === "input" && type === "hidden") return;
986
- if (name === "a" && !node.attributes.properties.some((p) => ts.isJsxAttribute(p) && p.name.getText(sf) === "href")) return;
987
- if (isRenderElement(node)) return;
988
- const suggestion = nativeEquivalent(name, type, ds.components);
989
- out.components.push({ ...at, name, kind: "native", library: "native", ...suggestion ? { suggestion } : {} });
990
- return;
991
- }
992
- const root = name.split(".")[0];
993
- const imp = imports.get(root);
994
- const ns = imp?.imported === "*";
995
- const shown = imp && !ns && imp.imported !== "default" ? [imp.imported, ...name.split(".").slice(1)].join(".") : name;
996
- const leaf = ns && name.includes(".") ? name.split(".").slice(1).join(".") : shown;
997
- if (imp) {
998
- const kind = classifyModule(imp.module, ds.packages, ds.foreignLibraries);
999
- if (isNonVisual(leaf.split(".").pop())) return;
1000
- if (kind === "ds") {
1001
- out.ds[leaf] = (out.ds[leaf] ?? 0) + 1;
1002
- return;
1003
- }
1004
- if (kind === "charts") {
1005
- out.components.push({ ...at, name: shown, kind: "foreign", library: libraryLabel(imp.module, kind) });
1006
- return;
1007
- }
1008
- if (kind === "other" && isRouterLink(imp.module, imp.imported === "default" ? "default" : leaf)) {
1009
- if (isRenderElement(node)) return;
1010
- const suggestion = ds.components.has("Link") ? "Link" : void 0;
1011
- out.components.push({ ...at, name: shown, kind: "foreign", library: libraryLabel(imp.module, kind), ...suggestion ? { suggestion } : {} });
1012
- return;
1013
- }
1014
- if (kind === "foreign" || kind === "shadcn") {
1015
- const suggestion = (ns ? dsEquivalent(root, ds.components) : void 0) ?? dsEquivalent(leaf, ds.components);
1016
- out.components.push({ ...at, name: shown, kind: "foreign", library: libraryLabel(imp.module, kind), ...suggestion ? { suggestion } : {} });
1017
- return;
1018
- }
1019
- if (kind === "local" && ds.components.has(leaf.split(".")[0])) out.components.push({ ...at, name: leaf, kind: "recreated", library: "local", suggestion: leaf.split(".")[0] });
1020
- return;
1021
- }
1022
- if (ds.components.has(root)) pendingLocal.push({ ...at, name, kind: "recreated", library: "local", suggestion: root });
1067
+ const typeAttr = node.attributes.properties.find((p) => ts.isJsxAttribute(p) && p.name.getText(sf) === "type");
1068
+ const type = typeAttr?.initializer && ts.isStringLiteral(typeAttr.initializer) ? typeAttr.initializer.text : void 0;
1069
+ const hasHref = node.attributes.properties.some((p) => ts.isJsxAttribute(p) && p.name.getText(sf) === "href");
1070
+ const r = classifyTag({ name, ...pos(node.getStart(sf)), ...type !== void 0 ? { type } : {}, hasHref, inRenderProp: isRenderElement(node) }, imports, ds);
1071
+ if (r.ds) out.ds[r.ds] = (out.ds[r.ds] ?? 0) + 1;
1072
+ if (r.component) out.components.push(r.component);
1073
+ if (r.localCandidate) pendingLocal.push(r.localCandidate);
1023
1074
  };
1024
1075
  const pendingLocal = [];
1025
1076
  function visit(node, ctx, classCtx) {
@@ -1079,6 +1130,17 @@ function scanCode(path, text, ds) {
1079
1130
  cssTemplate(node.template);
1080
1131
  return;
1081
1132
  }
1133
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
1134
+ const imp = imports.get(node.expression.text);
1135
+ if (imp && imp.imported !== "*" && classifyModule(imp.module, ds.packages, ds.foreignLibraries) === "ds") {
1136
+ const comp = domRendererComponent(imp.imported === "default" ? node.expression.text : imp.imported, ds.components);
1137
+ if (comp) out.ds[comp] = (out.ds[comp] ?? 0) + 1;
1138
+ }
1139
+ }
1140
+ if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken && ts.isPropertyAccessExpression(node.left) && node.left.name.text === "className") {
1141
+ visit(node.left, ctx, classCtx);
1142
+ return classLiteral(node.right);
1143
+ }
1082
1144
  if (ts.isCallExpression(node)) {
1083
1145
  const callee = node.expression.getText(sf);
1084
1146
  if (CLASS_FNS.test(callee)) {
@@ -1123,6 +1185,14 @@ function scanCode(path, text, ds) {
1123
1185
  const literal = (node) => {
1124
1186
  if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node) || ts.isTemplateHead(node) || ts.isTemplateMiddle(node) || ts.isTemplateTail(node)) {
1125
1187
  const t = node.text;
1188
+ if (!handled.has(node) && (t.includes("<") || ts.isTemplateMiddle(node) || ts.isTemplateTail(node))) {
1189
+ const base = node.getStart(sf) + 1;
1190
+ for (const m of t.matchAll(HTML_CLASS_RE)) for (const l of lookalikesInClassList(m[1], ds.components)) out.components.push({ ...pos(base + m.index), name: `.${l.token}`, kind: "recreated", library: "local", suggestion: l.component });
1191
+ for (const m of t.matchAll(HTML_ATTR_RE)) {
1192
+ const component = LOOKALIKE_ATTRS[m[1]];
1193
+ if (component && ds.components.has(component)) out.components.push({ ...pos(base + m.index + 1), name: `[${m[1]}]`, kind: "recreated", library: "local", suggestion: component });
1194
+ }
1195
+ }
1126
1196
  for (const m of t.matchAll(/var\(\s*(--[\w-]+)/g)) {
1127
1197
  if (ds.isDsVar(m[1])) out.tokenUsages++;
1128
1198
  else out.themeRefs++;
@@ -1141,24 +1211,82 @@ function scanCode(path, text, ds) {
1141
1211
  out.components.sort((a, b) => a.line - b.line || a.column - b.column);
1142
1212
  return out;
1143
1213
  }
1144
- const AUDIT_EXTENSIONS = [".css", ".scss", ".sass", ".less", ".tsx", ".jsx", ".ts", ".js", ".mjs", ".cjs", ".mts", ".cts"];
1145
- const AUDIT_IGNORE_DIRS = ["node_modules", "dist", "build", "out", "coverage", ".git", ".next", ".nuxt", ".astro", ".turbo", ".cache", ".vercel", "storybook-static", "vendor", "public"];
1146
- const IGNORE_FILE = /\.d\.[cm]?ts$|\.(test|spec|stories|story)\.[cm]?[jt]sx?$|\.min\.(css|js)$|(^|\/)(vite|vitest|jest|webpack|rollup|eslint|postcss|tailwind|babel|next|astro|prettier|stylelint|tsup)\.config\.[cm]?[jt]s$/;
1147
- function isAuditable(path) {
1148
- if (!AUDIT_EXTENSIONS.some((e) => path.endsWith(e))) return false;
1149
- if (IGNORE_FILE.test(path)) return false;
1150
- return !path.split("/").some((seg) => AUDIT_IGNORE_DIRS.includes(seg));
1214
+ const IGNORE_DIRECTIVE = "bridge-audit-ignore-next-line";
1215
+ function ignoreDirectives(text) {
1216
+ const at = lineCol(text);
1217
+ const out = [];
1218
+ for (const m of text.matchAll(new RegExp(`${IGNORE_DIRECTIVE}\\b`, "g"))) {
1219
+ const start = m.index;
1220
+ const opener = /(<!--|\/\*|\/\/)(\s*)$/.exec(text.slice(Math.max(0, start - 200), start));
1221
+ const open = opener?.[1];
1222
+ if (!open || open === "//" && opener[2].includes("\n")) continue;
1223
+ const from = start + m[0].length;
1224
+ let end;
1225
+ if (open === "//") {
1226
+ end = text.indexOf("\n", from);
1227
+ if (end < 0) end = text.length;
1228
+ } else {
1229
+ end = text.indexOf(open === "<!--" ? "-->" : "*/", from);
1230
+ if (end < 0) continue;
1231
+ }
1232
+ const reason = text.slice(from, end).replace(/^\s*:?/, "").replace(/\s+/g, " ").trim();
1233
+ out.push({ line: at(start).line, target: at(end).line + 1, reason });
1234
+ }
1235
+ return out;
1236
+ }
1237
+ const item = (x, reason, via) => "category" in x ? { line: x.line, column: x.column, kind: "value", what: x.value, reason, via } : { line: x.line, column: x.column, kind: "component", what: x.name, reason, via };
1238
+ function applyIgnoreComments(text, findings, components) {
1239
+ const errors = [];
1240
+ const reasons = /* @__PURE__ */ new Map();
1241
+ for (const d of ignoreDirectives(text)) {
1242
+ if (!d.reason) {
1243
+ errors.push({ line: d.line, message: `${IGNORE_DIRECTIVE} needs a reason after a colon (${IGNORE_DIRECTIVE}: why this one stays); nothing was ignored` });
1244
+ continue;
1245
+ }
1246
+ if (!findings.some((f) => f.line === d.target) && !components.some((c) => c.line === d.target)) {
1247
+ errors.push({ line: d.line, message: `${IGNORE_DIRECTIVE} ignores nothing: line ${d.target} has no finding; remove the comment` });
1248
+ continue;
1249
+ }
1250
+ reasons.set(d.target, d.reason);
1251
+ }
1252
+ const ignored = [];
1253
+ const keep = (xs) => xs.filter((x) => {
1254
+ const r = reasons.get(x.line);
1255
+ if (r === void 0) return true;
1256
+ ignored.push(item(x, r, "comment"));
1257
+ return false;
1258
+ });
1259
+ return { findings: keep(findings), components: keep(components), ignored, errors };
1260
+ }
1261
+ function applyAllowlist(entries, findings, components) {
1262
+ const used = /* @__PURE__ */ new Set();
1263
+ const ignored = [];
1264
+ const keep = (xs) => xs.filter((x) => {
1265
+ const what = "category" in x ? x.value : x.name;
1266
+ const fits = (e) => e.line === x.line && (e.what === void 0 || e.what === what);
1267
+ let i = entries.findIndex((e, k) => fits(e) && !used.has(k));
1268
+ if (i < 0) i = entries.findIndex(fits);
1269
+ if (i < 0) return true;
1270
+ used.add(i);
1271
+ ignored.push(item(x, entries[i].reason, "allowlist"));
1272
+ return false;
1273
+ });
1274
+ return { findings: keep(findings), components: keep(components), ignored, used };
1275
+ }
1276
+ function parseAllowlist(json) {
1277
+ const fail = (msg) => {
1278
+ throw new Error(`allowlist: ${msg}`);
1279
+ };
1280
+ if (!json || typeof json !== "object" || !Array.isArray(json.entries)) fail('expected { "entries": [...] }');
1281
+ return json.entries.map((e, i) => {
1282
+ const x = e;
1283
+ if (typeof x.file !== "string" || !x.file) fail(`entry ${i} needs "file"`);
1284
+ if (typeof x.line !== "number" || !Number.isInteger(x.line) || x.line < 1) fail(`entry ${i} (${x.file}) needs a "line" number`);
1285
+ if (typeof x.reason !== "string" || !x.reason.trim()) fail(`entry ${i} (${x.file}:${x.line}) needs a "reason"`);
1286
+ if (x.what !== void 0 && typeof x.what !== "string") fail(`entry ${i} (${x.file}:${x.line}): "what" must be a string`);
1287
+ return { file: x.file, line: x.line, ...x.what !== void 0 ? { what: x.what } : {}, reason: x.reason.trim() };
1288
+ });
1151
1289
  }
1152
- const langOf = (path) => {
1153
- const ext = path.slice(path.lastIndexOf(".") + 1);
1154
- if (ext === "css") return "css";
1155
- if (ext === "scss" || ext === "sass") return "scss";
1156
- if (ext === "less") return "less";
1157
- if (ext === "tsx") return "tsx";
1158
- if (ext === "jsx") return "jsx";
1159
- if (/^[cm]?ts$/.test(ext)) return "ts";
1160
- return "js";
1161
- };
1162
1290
  function isSafeFix(h, s) {
1163
1291
  if (!s || s.level !== "exact") return false;
1164
1292
  switch (h.category) {
@@ -1183,49 +1311,338 @@ function isSafeFix(h, s) {
1183
1311
  return false;
1184
1312
  }
1185
1313
  }
1186
- function auditFile(path, text, ds) {
1187
- const lang = langOf(path);
1188
- if (lang === "css" || lang === "scss" || lang === "less") {
1189
- const at = lineCol(text);
1190
- const scan = scanCssText(text, lang);
1191
- const findings = [];
1192
- let tokenUsages = 0;
1193
- let themeRefs = 0;
1194
- for (const d of scan.decls) {
1195
- for (const v of d.scan.vars) ds.isDsVar(v) ? tokenUsages++ : themeRefs++;
1196
- if (d.scan.themeRef) themeRefs++;
1197
- else if (lang !== "css" && !d.property.startsWith("$")) themeRefs += (d.value.match(/\$[\w-]+/g) ?? []).length;
1198
- const property = d.property.toLowerCase();
1199
- for (const h of d.scan.hits) {
1200
- const suggestion = ds.suggest(h.category, h.text, property);
1201
- const p = at(d.valueStart + h.index);
1202
- findings.push({ ...p, category: h.category, property, value: h.text, source: "css", ...suggestion ? { suggestion } : {}, ...isSafeFix(h, suggestion) ? { autofix: true } : {} });
1314
+ function auditCssText(text, lang, ds, opts) {
1315
+ const base = opts.offset ?? 0;
1316
+ const source = opts.source ?? "css";
1317
+ const at = (o) => opts.pos(base + o);
1318
+ const scan = scanCssText(text, lang);
1319
+ const findings = [];
1320
+ let tokenUsages = 0;
1321
+ let themeRefs = 0;
1322
+ for (const d of scan.decls) {
1323
+ for (const v of d.scan.vars) ds.isDsVar(v) ? tokenUsages++ : themeRefs++;
1324
+ if (d.scan.themeRef) themeRefs++;
1325
+ else if (lang !== "css" && !d.property.startsWith("$")) themeRefs += (d.value.match(/\$[\w-]+/g) ?? []).length;
1326
+ const property = d.property.toLowerCase();
1327
+ for (const h of d.scan.hits) {
1328
+ const suggestion = ds.suggest(h.category, h.text, property);
1329
+ const autofix = source === "css" && isSafeFix(h, suggestion);
1330
+ findings.push({ ...at(d.valueStart + h.index), category: h.category, property, value: h.text, source, ...suggestion ? { suggestion } : {}, ...autofix ? { autofix: true } : {} });
1331
+ }
1332
+ }
1333
+ for (const a of scan.applies) {
1334
+ for (const h of a.hits) {
1335
+ if (h.kind === "scale") themeRefs++;
1336
+ else if (h.kind !== "token" && h.value) {
1337
+ const cat = parseCssColor(h.value) ? "color" : h.category === "color-or-size" ? "font-size" : h.category === "color-or-width" ? "border-width" : h.category === "color-or-shadow" ? "shadow" : h.category === "font" ? "font-family" : h.category;
1338
+ const suggestion = ds.suggest(cat, h.value);
1339
+ findings.push({ ...at(a.start + h.index), category: cat, value: h.cls, source: "tailwind", ...suggestion ? { suggestion } : {} });
1340
+ } else if (h.kind === "token" && h.value) {
1341
+ const v = /var\(\s*(--[\w-]+)/.exec(h.value)?.[1];
1342
+ if (v && ds.isDsVar(v)) tokenUsages++;
1343
+ else themeRefs++;
1203
1344
  }
1204
1345
  }
1205
- for (const a of scan.applies) {
1206
- for (const h of a.hits) {
1207
- if (h.kind === "scale") themeRefs++;
1208
- else if (h.kind !== "token" && h.value) {
1209
- const cat = parseCssColor(h.value) ? "color" : h.category === "color-or-size" ? "font-size" : h.category === "color-or-width" ? "border-width" : h.category === "color-or-shadow" ? "shadow" : h.category === "font" ? "font-family" : h.category;
1210
- const suggestion = ds.suggest(cat, h.value);
1211
- findings.push({ ...at(a.start + h.index), category: cat, value: h.cls, source: "tailwind", ...suggestion ? { suggestion } : {} });
1212
- } else if (h.kind === "token" && h.value) {
1213
- const v = /var\(\s*(--[\w-]+)/.exec(h.value)?.[1];
1214
- if (v && ds.isDsVar(v)) tokenUsages++;
1215
- else themeRefs++;
1216
- }
1346
+ }
1347
+ findings.sort((x, y) => x.line - y.line || x.column - y.column);
1348
+ return { findings, tokenUsages, themeRefs };
1349
+ }
1350
+ const MARKUP_EXTENSIONS = [".astro", ".vue", ".svelte"];
1351
+ function mask(text, ranges, keepInside) {
1352
+ const inside = new Uint8Array(text.length);
1353
+ for (const r of ranges) inside.fill(1, r.start, r.end);
1354
+ let out = "";
1355
+ for (let i = 0; i < text.length; i++) {
1356
+ const c = text[i];
1357
+ out += c === "\n" || Boolean(inside[i]) === keepInside ? c : " ";
1358
+ }
1359
+ return out;
1360
+ }
1361
+ const attrOf = (attrs, name) => new RegExp(`(?:^|\\s)${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s>]+))`, "i").exec(attrs)?.slice(1).find((v) => v !== void 0);
1362
+ const isCodeScript = (attrs) => {
1363
+ const type = attrOf(attrs, "type")?.toLowerCase();
1364
+ return !type || ["module", "text/javascript", "application/javascript", "text/typescript", "ts", "text/babel"].includes(type);
1365
+ };
1366
+ function markupBlocks(text, lang) {
1367
+ const nonTemplate = [];
1368
+ let frontmatter;
1369
+ let from = 0;
1370
+ if (lang === "astro") {
1371
+ const open = /^\s*---[^\S\n]*\n/.exec(text);
1372
+ if (open) {
1373
+ const close = /\n---[^\S\n]*(\n|$)/g;
1374
+ close.lastIndex = open[0].length - 1;
1375
+ const m = close.exec(text);
1376
+ if (m) {
1377
+ frontmatter = { start: open[0].length, end: m.index + 1 };
1378
+ from = m.index + m[0].length;
1379
+ nonTemplate.push({ start: 0, end: from });
1380
+ }
1381
+ }
1382
+ }
1383
+ const scripts = [];
1384
+ const styles = [];
1385
+ const re = /<(script|style)\b((?:[^>"']|"[^"]*"|'[^']*')*)>([\s\S]*?)<\/\1\s*>/gi;
1386
+ re.lastIndex = from;
1387
+ for (let m; m = re.exec(text); ) {
1388
+ const contentStart = m.index + m[0].indexOf(">", m[1].length + 1 + m[2].length) + 1;
1389
+ const block = { start: contentStart, end: contentStart + m[3].length, attrs: m[2] };
1390
+ (m[1].toLowerCase() === "script" ? scripts : styles).push(block);
1391
+ nonTemplate.push({ start: m.index, end: m.index + m[0].length });
1392
+ }
1393
+ for (const m of text.slice(from).matchAll(/<!--[\s\S]*?-->/g)) nonTemplate.push({ start: from + m.index, end: from + m.index + m[0].length });
1394
+ return { ...frontmatter ? { frontmatter } : {}, scripts, styles, nonTemplate };
1395
+ }
1396
+ function closeBrace(s, i) {
1397
+ let depth = 0;
1398
+ for (let k = i; k < s.length; k++) {
1399
+ const c = s[k];
1400
+ if (c === "{") depth++;
1401
+ else if (c === "}") {
1402
+ if (--depth === 0) return k + 1;
1403
+ } else if (c === '"' || c === "'") {
1404
+ const e = s.indexOf(c, k + 1);
1405
+ if (e < 0) return s.length;
1406
+ k = e;
1407
+ } else if (c === "`") {
1408
+ for (k++; k < s.length && s[k] !== "`"; k++) {
1409
+ if (s[k] === "\\") k++;
1410
+ else if (s[k] === "$" && s[k + 1] === "{") k = closeBrace(s, k + 1) - 1;
1411
+ }
1412
+ }
1413
+ }
1414
+ return s.length;
1415
+ }
1416
+ function readTag(s, i, lang) {
1417
+ const attrs = [];
1418
+ let k = i;
1419
+ while (k < s.length) {
1420
+ while (k < s.length && /\s/.test(s[k])) k++;
1421
+ const c = s[k];
1422
+ if (c === void 0) break;
1423
+ if (c === ">") return { attrs, end: k + 1, selfClosing: false };
1424
+ if (c === "/" && s[k + 1] === ">") return { attrs, end: k + 2, selfClosing: true };
1425
+ if (c === "{") {
1426
+ const e = closeBrace(s, k);
1427
+ const inner = s.slice(k + 1, e - 1).trim();
1428
+ attrs.push(inner.startsWith("...") ? { name: "", kind: "spread", value: inner, valueStart: k + 1 } : { name: inner, kind: "expr", value: inner, valueStart: k + 1 });
1429
+ k = e;
1430
+ continue;
1431
+ }
1432
+ const nm = /^[^\s=>/"'{]+/.exec(s.slice(k, k + 200));
1433
+ if (!nm) {
1434
+ k++;
1435
+ continue;
1436
+ }
1437
+ const name = nm[0];
1438
+ k += name.length;
1439
+ let j = k;
1440
+ while (j < s.length && /\s/.test(s[j])) j++;
1441
+ if (s[j] !== "=") {
1442
+ attrs.push({ name, kind: "bare", value: "", valueStart: k });
1443
+ continue;
1444
+ }
1445
+ j++;
1446
+ while (j < s.length && /\s/.test(s[j])) j++;
1447
+ const q = s[j];
1448
+ const bound = lang === "vue" && /^(:|v-bind:|v-on:|@|v-)/.test(name);
1449
+ if (q === '"' || q === "'") {
1450
+ const e = s.indexOf(q, j + 1);
1451
+ const end = e < 0 ? s.length : e;
1452
+ attrs.push({ name, kind: bound ? "expr" : "static", value: s.slice(j + 1, end), valueStart: j + 1 });
1453
+ k = end + 1;
1454
+ } else if (q === "{") {
1455
+ const e = closeBrace(s, j);
1456
+ attrs.push({ name, kind: "expr", value: s.slice(j + 1, e - 1), valueStart: j + 1 });
1457
+ k = e;
1458
+ } else {
1459
+ const m = /^[^\s>]+/.exec(s.slice(j));
1460
+ const v = m?.[0].replace(/\/$/, "") ?? "";
1461
+ attrs.push({ name, kind: "static", value: v, valueStart: j });
1462
+ k = j + v.length;
1463
+ }
1464
+ }
1465
+ return { attrs, end: s.length, selfClosing: false };
1466
+ }
1467
+ const attrName = (n) => n.replace(/^(v-bind:|:|bind:)/, "");
1468
+ const pascal = (kebab) => kebab.replace(/(^|-)([a-z0-9])/g, (_m, _d, c) => c.toUpperCase());
1469
+ const NO_COLOR_ATTR_TAGS = /* @__PURE__ */ new Set(["meta", "link"]);
1470
+ function scanMarkup(path, text, lang, ds) {
1471
+ const out = { findings: [], tokenUsages: 0, themeRefs: 0, ds: {}, components: [], definitions: [] };
1472
+ const pos = lineCol(text);
1473
+ const blocks = markupBlocks(text, lang);
1474
+ const imports = /* @__PURE__ */ new Map();
1475
+ const merge = (c) => {
1476
+ out.findings.push(...c.findings);
1477
+ out.tokenUsages += c.tokenUsages;
1478
+ out.themeRefs += c.themeRefs;
1479
+ for (const [n, k] of Object.entries(c.ds)) out.ds[n] = (out.ds[n] ?? 0) + k;
1480
+ out.components.push(...c.components);
1481
+ out.definitions.push(...c.definitions);
1482
+ };
1483
+ const codeRanges = [...blocks.frontmatter ? [{ ...blocks.frontmatter, attrs: "" }] : [], ...blocks.scripts.filter((b) => isCodeScript(b.attrs))];
1484
+ for (const r of codeRanges) {
1485
+ const jsx = /^(tsx|jsx)$/i.test(attrOf(r.attrs, "lang") ?? "");
1486
+ const code = mask(text, [r], true);
1487
+ const file = `${path}.${jsx ? "tsx" : "ts"}`;
1488
+ merge(scanCode(file, code, ds));
1489
+ for (const [k, v] of importsOf(file, code)) imports.set(k, v);
1490
+ }
1491
+ for (const b of blocks.styles) {
1492
+ const l = (attrOf(b.attrs, "lang") ?? "css").toLowerCase();
1493
+ const r = auditCssText(text.slice(b.start, b.end), l === "scss" || l === "sass" ? "scss" : l === "less" ? "less" : "css", ds, { pos, offset: b.start });
1494
+ out.findings.push(...r.findings);
1495
+ out.tokenUsages += r.tokenUsages;
1496
+ out.themeRefs += r.themeRefs;
1497
+ }
1498
+ const scanExpr = (prefix, expr, offset, suffix, keepLookalikes = false) => {
1499
+ const { line, column } = pos(offset);
1500
+ const src = line >= 2 ? `${"\n".repeat(line - 2)}${prefix}
1501
+ ${" ".repeat(column - 1)}${expr}${suffix}` : `${prefix}${expr}${suffix}`;
1502
+ const c = scanCode(`${path}.tsx`, src, ds);
1503
+ merge({ ...c, components: keepLookalikes ? c.components.filter((x) => x.kind === "recreated" && x.name.startsWith(".")) : [], definitions: [], ds: {} });
1504
+ };
1505
+ const styleAttr = (a) => {
1506
+ if (a.kind === "static") {
1507
+ const css = lang === "svelte" ? a.value.replace(/\{[^}]*\}/g, (m) => " ".repeat(m.length)) : a.value;
1508
+ const r = auditCssText(css, "css", ds, { pos, offset: a.valueStart, source: "style" });
1509
+ out.findings.push(...r.findings);
1510
+ out.tokenUsages += r.tokenUsages;
1511
+ out.themeRefs += r.themeRefs;
1512
+ return;
1513
+ }
1514
+ const str = /^(\s*)([`'"])([\s\S]*)\2\s*$/.exec(a.value);
1515
+ if (str) {
1516
+ const body = str[3].replace(/\$\{[^}]*\}/g, (m) => " ".repeat(m.length));
1517
+ const r = auditCssText(body, "css", ds, { pos, offset: a.valueStart + str[1].length + 1, source: "style" });
1518
+ out.findings.push(...r.findings);
1519
+ out.tokenUsages += r.tokenUsages;
1520
+ out.themeRefs += r.themeRefs;
1521
+ return;
1522
+ }
1523
+ scanExpr("const style: CSSProperties =", a.value, a.valueStart, ";");
1524
+ };
1525
+ const tpl = mask(text, blocks.nonTemplate, false);
1526
+ const pending = [];
1527
+ let noscript = 0;
1528
+ const TAG = /<(\/?)([A-Za-z][\w.:-]*)/g;
1529
+ for (let m; m = TAG.exec(tpl); ) {
1530
+ const raw = m[2];
1531
+ if (m[1]) {
1532
+ if (raw.toLowerCase() === "noscript") noscript = Math.max(0, noscript - 1);
1533
+ continue;
1534
+ }
1535
+ const tag = readTag(tpl, m.index + m[0].length, lang);
1536
+ TAG.lastIndex = tag.end;
1537
+ const lower = raw.toLowerCase();
1538
+ if (lower === "noscript" && !tag.selfClosing) {
1539
+ noscript++;
1540
+ continue;
1541
+ }
1542
+ const attrs = tag.attrs;
1543
+ const get = (n) => attrs.find((a) => attrName(a.name) === n);
1544
+ for (const a of attrs) {
1545
+ const n = attrName(a.name);
1546
+ if (n === "style") styleAttr(a);
1547
+ else if (lang === "svelte" && a.name.startsWith("style:") && a.kind === "static") {
1548
+ const prop = a.name.slice(6).split("|")[0];
1549
+ const r2 = auditCssText(`${prop}:${a.value}`, "css", ds, { pos, offset: a.valueStart - prop.length - 1, source: "style" });
1550
+ out.findings.push(...r2.findings);
1551
+ out.tokenUsages += r2.tokenUsages;
1552
+ out.themeRefs += r2.themeRefs;
1553
+ } else if (/^(class|className|class:list)$/.test(n)) {
1554
+ if (a.kind === "static") {
1555
+ for (const l of lookalikesInClassList(a.value, ds.components)) out.components.push({ ...pos(a.valueStart), name: `.${l.token}`, kind: "recreated", library: "local", suggestion: l.component });
1556
+ const r2 = tailwindFindings(scanClassString(a.value), a.valueStart, pos, ds);
1557
+ out.findings.push(...r2.findings);
1558
+ out.themeRefs += r2.themeRefs;
1559
+ } else if (a.kind === "expr") scanExpr("cn(", a.value, a.valueStart, ")", true);
1560
+ } else if (LOOKALIKE_ATTRS[n] && ds.components.has(LOOKALIKE_ATTRS[n])) {
1561
+ out.components.push({ ...pos(a.valueStart - a.name.length - 1), name: `[${n}]`, kind: "recreated", library: "local", suggestion: LOOKALIKE_ATTRS[n] });
1562
+ } else if (a.kind === "static" && !NO_COLOR_ATTR_TAGS.has(lower) && isColorLiteral(a.value)) {
1563
+ const suggestion = ds.suggest("color", a.value.trim(), n);
1564
+ out.findings.push({ ...pos(a.valueStart), category: "color", property: n, value: a.value.trim(), source: "jsx-attr", ...suggestion ? { suggestion } : {} });
1217
1565
  }
1218
1566
  }
1219
- findings.sort((x, y) => x.line - y.line || x.column - y.column);
1220
- return { path, lang, tokenUsages, themeRefs, hardcoded: findings.length, dsComponents: 0, otherComponents: 0, findings, components: [], definitions: [], dsByName: {} };
1567
+ let name = raw;
1568
+ if (raw.includes(":")) continue;
1569
+ if (raw.includes("-")) {
1570
+ if (!imports.has(pascal(raw))) continue;
1571
+ name = pascal(raw);
1572
+ }
1573
+ const isNative = /^[a-z]/.test(name) && !name.includes(".");
1574
+ if (isNative && noscript > 0) continue;
1575
+ const type = get("type");
1576
+ const r = classifyTag({ name, ...pos(m.index), ...type?.kind === "static" ? { type: type.value } : {}, hasHref: Boolean(get("href")) }, imports, ds);
1577
+ if (r.ds) out.ds[r.ds] = (out.ds[r.ds] ?? 0) + 1;
1578
+ if (r.component) out.components.push(r.component);
1579
+ if (r.localCandidate) pending.push(r.localCandidate);
1221
1580
  }
1222
- const c = scanCode(path, text, ds);
1581
+ const base = /([^/]+)\.(astro|vue|svelte)$/.exec(path)?.[1];
1582
+ if (base && /^[A-Z][A-Za-z0-9]*$/.test(base)) out.definitions.push({ name: base, line: 1 });
1583
+ const defined = new Set(out.definitions.map((d) => d.name));
1584
+ out.components.push(...pending.filter((f) => defined.has(f.suggestion)));
1585
+ out.findings.sort((a, b) => a.line - b.line || a.column - b.column);
1586
+ out.components.sort((a, b) => a.line - b.line || a.column - b.column);
1587
+ return out;
1588
+ }
1589
+ const AUDIT_EXTENSIONS = [".css", ".scss", ".sass", ".less", ".tsx", ".jsx", ".ts", ".js", ".mjs", ".cjs", ".mts", ".cts", ...MARKUP_EXTENSIONS];
1590
+ const AUDIT_IGNORE_DIRS = ["node_modules", "dist", "build", "out", "coverage", ".git", ".next", ".nuxt", ".astro", ".turbo", ".cache", ".vercel", "storybook-static", "vendor", "public"];
1591
+ const IGNORE_FILE = /\.d\.[cm]?ts$|\.(test|spec|stories|story)\.[cm]?[jt]sx?$|\.min\.(css|js)$|(^|\/)(vite|vitest|jest|webpack|rollup|eslint|postcss|tailwind|babel|next|astro|prettier|stylelint|tsup)\.config\.[cm]?[jt]s$/;
1592
+ function isAuditable(path) {
1593
+ if (!AUDIT_EXTENSIONS.some((e) => path.endsWith(e))) return false;
1594
+ if (IGNORE_FILE.test(path)) return false;
1595
+ return !path.split("/").some((seg) => AUDIT_IGNORE_DIRS.includes(seg));
1596
+ }
1597
+ const langOf = (path) => {
1598
+ const ext = path.slice(path.lastIndexOf(".") + 1);
1599
+ if (ext === "css") return "css";
1600
+ if (ext === "scss" || ext === "sass") return "scss";
1601
+ if (ext === "less") return "less";
1602
+ if (ext === "tsx") return "tsx";
1603
+ if (ext === "jsx") return "jsx";
1604
+ if (/^[cm]?ts$/.test(ext)) return "ts";
1605
+ if (ext === "astro" || ext === "vue" || ext === "svelte") return ext;
1606
+ return "js";
1607
+ };
1608
+ function auditFile(path, text, ds) {
1609
+ const lang = langOf(path);
1610
+ if (lang === "css" || lang === "scss" || lang === "less") {
1611
+ const r = auditCssText(text, lang, ds, { pos: lineCol(text) });
1612
+ return withIgnores(text, { path, lang, tokenUsages: r.tokenUsages, themeRefs: r.themeRefs, hardcoded: 0, dsComponents: 0, otherComponents: 0, findings: r.findings, components: [], definitions: [], dsByName: {} });
1613
+ }
1614
+ const c = lang === "astro" || lang === "vue" || lang === "svelte" ? scanMarkup(path, text, lang, ds) : scanCode(path, text, ds);
1223
1615
  const dsCount = Object.values(c.ds).reduce((a, b) => a + b, 0);
1224
- return { path, lang, tokenUsages: c.tokenUsages, themeRefs: c.themeRefs, hardcoded: c.findings.length, dsComponents: dsCount, otherComponents: c.components.length, findings: c.findings, components: c.components, definitions: c.definitions, dsByName: c.ds };
1616
+ return withIgnores(text, { path, lang, tokenUsages: c.tokenUsages, themeRefs: c.themeRefs, hardcoded: 0, dsComponents: dsCount, otherComponents: 0, findings: c.findings, components: c.components, definitions: c.definitions, dsByName: c.ds });
1617
+ }
1618
+ function withIgnores(text, f) {
1619
+ const r = applyIgnoreComments(text, f.findings, f.components);
1620
+ return {
1621
+ ...f,
1622
+ findings: r.findings,
1623
+ components: r.components,
1624
+ hardcoded: r.findings.length,
1625
+ otherComponents: r.components.length,
1626
+ ...r.ignored.length ? { ignored: r.ignored } : {},
1627
+ ...r.errors.length ? { ignoreErrors: r.errors } : {}
1628
+ };
1225
1629
  }
1226
1630
  const pct = (num2, den) => den > 0 ? Math.round(num2 / den * 1e3) / 10 : null;
1227
1631
  function buildAuditReport(input, ds) {
1228
- const reports = input.files.map((f) => auditFile(f.path, f.text, ds));
1632
+ const allow = input.allowlist ?? [];
1633
+ const usedAllow = /* @__PURE__ */ new Set();
1634
+ const reports = input.files.map((f) => {
1635
+ const r = auditFile(f.path, f.text, ds);
1636
+ const idx = allow.flatMap((e, i) => e.file === f.path ? [i] : []);
1637
+ if (!idx.length) return r;
1638
+ const a = applyAllowlist(idx.map((i) => allow[i]), r.findings, r.components);
1639
+ a.used.forEach((k) => usedAllow.add(idx[k]));
1640
+ if (!a.ignored.length) return r;
1641
+ const ignored = [...r.ignored ?? [], ...a.ignored].sort((x, y) => x.line - y.line || x.column - y.column);
1642
+ return { ...r, findings: a.findings, components: a.components, hardcoded: a.findings.length, otherComponents: a.components.length, ignored };
1643
+ });
1644
+ const ignoredItems = reports.flatMap((r) => (r.ignored ?? []).map((x) => ({ file: r.path, ...x })));
1645
+ const staleAllowlist = allow.filter((_e, i) => !usedAllow.has(i));
1229
1646
  const files = reports.map(({ definitions: _d, dsByName: _n, ...r }) => r);
1230
1647
  const byCategory = {};
1231
1648
  const fixes = /* @__PURE__ */ new Map();
@@ -1285,6 +1702,15 @@ function buildAuditReport(input, ds) {
1285
1702
  topFiles,
1286
1703
  topFixes,
1287
1704
  files: files.sort((a, b) => a.path.localeCompare(b.path)),
1705
+ ...ignoredItems.length || staleAllowlist.length ? {
1706
+ ignored: {
1707
+ total: ignoredItems.length,
1708
+ comment: ignoredItems.filter((x) => x.via === "comment").length,
1709
+ allowlist: ignoredItems.filter((x) => x.via === "allowlist").length,
1710
+ items: ignoredItems,
1711
+ staleAllowlist
1712
+ }
1713
+ } : {},
1288
1714
  history: []
1289
1715
  };
1290
1716
  }
@@ -1388,6 +1814,20 @@ function auditMarkdown(r, opts = {}) {
1388
1814
  for (const f of fixes) lines.push(`| \`${esc(f.value)}\` (${f.category}) | \`var(${f.suggestion.cssVar})\` ${esc(f.suggestion.value)}${f.suggestion.mode ? ` [${f.suggestion.mode}]` : ""} | ${f.suggestion.level} | ${f.count} |`);
1389
1815
  lines.push("");
1390
1816
  }
1817
+ const ig = r.ignored;
1818
+ if (ig && (ig.total || ig.staleAllowlist.length)) {
1819
+ lines.push("### Ignored on purpose", "", `${ig.total} item(s) left out of the counts: ${ig.comment} by \`bridge-audit-ignore-next-line\` comments, ${ig.allowlist} by the allowlist.`, "");
1820
+ const shown = ig.items.slice(0, opts.ignored ?? 50);
1821
+ if (shown.length) {
1822
+ lines.push("| Where | What | Reason |", "| --- | --- | --- |");
1823
+ for (const x of shown) lines.push(`| \`${esc(x.file)}:${x.line}\` | \`${esc(x.what)}\` | ${esc(x.reason)} (${x.via}) |`);
1824
+ if (ig.items.length > shown.length) lines.push(`| ... | ${ig.items.length - shown.length} more | |`);
1825
+ lines.push("");
1826
+ }
1827
+ if (ig.staleAllowlist.length) {
1828
+ lines.push(`Allowlist entries that match nothing any more (remove them): ${ig.staleAllowlist.map((e) => `\`${esc(e.file)}:${e.line}${e.what ? ` ${esc(e.what)}` : ""}\``).join(", ")}.`, "");
1829
+ }
1830
+ }
1391
1831
  if (b) {
1392
1832
  lines.push(`### Progress since ${b.label ?? b.generatedAt}`, "");
1393
1833
  lines.push(`- ${b.improved.length} files improved, ${b.regressed.length} regressed, ${b.newFiles.length} new, ${b.removedFiles.length} removed.`);
@@ -1415,40 +1855,56 @@ function planCssFixes(text, lang, ds) {
1415
1855
  }
1416
1856
  export {
1417
1857
  AUDIT_EXTENSIONS as A,
1418
- scanCssText as B,
1858
+ libraryLabel as B,
1419
1859
  COLOR_LITERAL_RE as C,
1420
1860
  DEFAULT_NATIVE_ELEMENTS as D,
1421
- scanCssValue as E,
1861
+ lineCol as E,
1422
1862
  FINDING_CATEGORIES as F,
1423
- scanJsStyle as G,
1424
- tailwindColor as H,
1425
- toOklab as I,
1863
+ lookalikesInClassList as G,
1864
+ markupBlocks as H,
1865
+ IGNORE_DIRECTIVE as I,
1866
+ nativeEquivalent as J,
1867
+ packageName as K,
1868
+ LOOKALIKE_ATTRS as L,
1869
+ MARKUP_EXTENSIONS as M,
1426
1870
  NAMED_COLORS as N,
1871
+ parseAllowlist as O,
1872
+ parseCssColor as P,
1873
+ planCssFixes as Q,
1874
+ scanClassString as R,
1875
+ scanCode as S,
1876
+ scanCssText as T,
1877
+ scanCssValue as U,
1878
+ scanJsStyle as V,
1879
+ scanMarkup as W,
1880
+ tailwindColor as X,
1881
+ tailwindFindings as Y,
1882
+ toOklab as Z,
1427
1883
  AUDIT_IGNORE_DIRS as a,
1428
1884
  AUDIT_SCHEMA_VERSION as b,
1429
1885
  DesignSystemIndex as c,
1430
- auditFile as d,
1431
- auditMarkdown as e,
1432
- blankComments as f,
1433
- buildAuditReport as g,
1434
- classifyModule as h,
1435
- compareWithBaseline as i,
1436
- cssPropertyOf as j,
1437
- deltaE as k,
1438
- dsEquivalent as l,
1439
- historyPoint as m,
1440
- isAuditable as n,
1441
- isColorLiteral as o,
1442
- isNonVisual as p,
1443
- isRouterLink as q,
1444
- isSafeFix as r,
1445
- libraryLabel as s,
1446
- lineCol as t,
1447
- nativeEquivalent as u,
1448
- packageName as v,
1449
- parseCssColor as w,
1450
- planCssFixes as x,
1451
- scanClassString as y,
1452
- scanCode as z
1886
+ LOOKALIKE_CLASSES as d,
1887
+ applyAllowlist as e,
1888
+ applyIgnoreComments as f,
1889
+ auditCssText as g,
1890
+ auditFile as h,
1891
+ auditMarkdown as i,
1892
+ blankComments as j,
1893
+ buildAuditReport as k,
1894
+ classifyModule as l,
1895
+ classifyTag as m,
1896
+ compareWithBaseline as n,
1897
+ cssPropertyOf as o,
1898
+ deltaE as p,
1899
+ domRendererComponent as q,
1900
+ dsEquivalent as r,
1901
+ historyPoint as s,
1902
+ ignoreDirectives as t,
1903
+ importsOf as u,
1904
+ isAuditable as v,
1905
+ isColorLiteral as w,
1906
+ isNonVisual as x,
1907
+ isRouterLink as y,
1908
+ isSafeFix as z
1453
1909
  };
1454
- //# sourceMappingURL=report-Bf2HEr9t.js.map
1910
+ //# sourceMappingURL=report-iPSnsjYi.js.map