@bridge-ds/core 0.4.0 → 0.5.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.
@@ -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,78 @@ 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
+ return { component: { ...at, name, kind: "native", library: "native", ...suggestion ? { suggestion } : {} } };
920
+ }
921
+ const root = name.split(".")[0];
922
+ const imp = imports.get(root);
923
+ const ns = imp?.imported === "*";
924
+ const shown = imp && !ns && imp.imported !== "default" ? [imp.imported, ...name.split(".").slice(1)].join(".") : name;
925
+ const leaf = ns && name.includes(".") ? name.split(".").slice(1).join(".") : shown;
926
+ if (imp) {
927
+ const kind = classifyModule(imp.module, ds.packages, ds.foreignLibraries);
928
+ if (isNonVisual(leaf.split(".").pop())) return {};
929
+ if (kind === "ds") return { ds: leaf };
930
+ if (kind === "charts") return { component: { ...at, name: shown, kind: "foreign", library: libraryLabel(imp.module, kind) } };
931
+ if (kind === "other" && isRouterLink(imp.module, imp.imported === "default" ? "default" : leaf)) {
932
+ if (t.inRenderProp) return {};
933
+ const suggestion = ds.components.has("Link") ? "Link" : void 0;
934
+ return { component: { ...at, name: shown, kind: "foreign", library: libraryLabel(imp.module, kind), ...suggestion ? { suggestion } : {} } };
935
+ }
936
+ if (kind === "foreign" || kind === "shadcn") {
937
+ const suggestion = (ns ? dsEquivalent(root, ds.components) : void 0) ?? dsEquivalent(leaf, ds.components);
938
+ return { component: { ...at, name: shown, kind: "foreign", library: libraryLabel(imp.module, kind), ...suggestion ? { suggestion } : {} } };
939
+ }
940
+ if (kind === "local" && ds.components.has(leaf.split(".")[0])) return { component: { ...at, name: leaf, kind: "recreated", library: "local", suggestion: leaf.split(".")[0] } };
941
+ return {};
942
+ }
943
+ if (ds.components.has(root)) return { localCandidate: { ...at, name, kind: "recreated", library: "local", suggestion: root } };
944
+ return {};
945
+ }
946
+ function tailwindFindings(hits, offset, pos, ds) {
947
+ const findings = [];
948
+ let themeRefs = 0;
949
+ for (const h of hits) {
950
+ if (h.kind === "scale") {
951
+ themeRefs++;
952
+ continue;
953
+ }
954
+ if (h.kind === "token") continue;
955
+ let cat = h.category;
956
+ const v = h.value;
957
+ const isColor = Boolean(parseCssColor(v));
958
+ if (cat === "color-or-size") cat = isColor ? "color" : "font-size";
959
+ else if (cat === "color-or-width") cat = isColor ? "color" : "border-width";
960
+ else if (cat === "color-or-shadow") cat = isColor ? "color" : "shadow";
961
+ else if (cat === "font") cat = /^\d{3}$/.test(v) ? "font-weight" : "font-family";
962
+ if (cat === "color" && !isColor) continue;
963
+ const suggestion = ds.suggest(cat, v);
964
+ const cap = h.kind === "palette" && suggestion && suggestion.confidence > 0.6 ? { ...suggestion, confidence: 0.6, level: "medium" } : suggestion;
965
+ findings.push({ ...pos(offset + h.index), category: cat, value: h.cls, source: "tailwind", ...cap ? { suggestion: cap } : {} });
966
+ }
967
+ return { findings, themeRefs };
968
+ }
873
969
  function scanCode(path, text, ds) {
874
970
  const sf = ts.createSourceFile(path, text, ts.ScriptTarget.Latest, true, kindFor(path));
875
971
  const out = { findings: [], tokenUsages: 0, themeRefs: 0, ds: {}, components: [], definitions: [] };
@@ -884,24 +980,9 @@ function scanCode(path, text, ds) {
884
980
  out.findings.push({ ...pos(offset), category: h.category, ...property ? { property } : {}, value: h.text, source, ...suggestion ? { suggestion } : {} });
885
981
  };
886
982
  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
- }
983
+ const r = tailwindFindings(hits, offset, pos, ds);
984
+ out.themeRefs += r.themeRefs;
985
+ out.findings.push(...r.findings);
905
986
  };
906
987
  const styleValue = (key, node, ctx) => {
907
988
  const source = ctx === "sx" ? "sx" : ctx === "style" ? "style" : ctx === "theme" ? "theme" : "styled";
@@ -962,13 +1043,18 @@ function scanCode(path, text, ds) {
962
1043
  }
963
1044
  for (const a of scan.applies) addTailwind(a.hits, start + a.start);
964
1045
  };
1046
+ const lookalike = (list, offset) => {
1047
+ for (const l of lookalikesInClassList(list, ds.components)) out.components.push({ ...pos(offset), name: `.${l.token}`, kind: "recreated", library: "local", suggestion: l.component });
1048
+ };
965
1049
  const classLiteral = (node) => {
966
1050
  if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
967
1051
  handled.add(node);
1052
+ lookalike(node.text, node.getStart(sf) + 1);
968
1053
  addTailwind(scanClassString(node.text), node.getStart(sf) + 1);
969
1054
  } else if (ts.isTemplateExpression(node)) {
970
1055
  for (const p of [node.head, ...node.templateSpans.map((s) => s.literal)]) {
971
1056
  handled.add(p);
1057
+ lookalike(p.text, p.getStart(sf) + 1);
972
1058
  addTailwind(scanClassString(p.text), p.getStart(sf) + 1);
973
1059
  }
974
1060
  node.templateSpans.forEach((s) => visit(s.expression, null, true));
@@ -977,49 +1063,13 @@ function scanCode(path, text, ds) {
977
1063
  const jsxName = (n) => n.getText(sf);
978
1064
  const element = (node) => {
979
1065
  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 });
1066
+ const typeAttr = node.attributes.properties.find((p) => ts.isJsxAttribute(p) && p.name.getText(sf) === "type");
1067
+ const type = typeAttr?.initializer && ts.isStringLiteral(typeAttr.initializer) ? typeAttr.initializer.text : void 0;
1068
+ const hasHref = node.attributes.properties.some((p) => ts.isJsxAttribute(p) && p.name.getText(sf) === "href");
1069
+ const r = classifyTag({ name, ...pos(node.getStart(sf)), ...type !== void 0 ? { type } : {}, hasHref, inRenderProp: isRenderElement(node) }, imports, ds);
1070
+ if (r.ds) out.ds[r.ds] = (out.ds[r.ds] ?? 0) + 1;
1071
+ if (r.component) out.components.push(r.component);
1072
+ if (r.localCandidate) pendingLocal.push(r.localCandidate);
1023
1073
  };
1024
1074
  const pendingLocal = [];
1025
1075
  function visit(node, ctx, classCtx) {
@@ -1079,6 +1129,17 @@ function scanCode(path, text, ds) {
1079
1129
  cssTemplate(node.template);
1080
1130
  return;
1081
1131
  }
1132
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
1133
+ const imp = imports.get(node.expression.text);
1134
+ if (imp && imp.imported !== "*" && classifyModule(imp.module, ds.packages, ds.foreignLibraries) === "ds") {
1135
+ const comp = domRendererComponent(imp.imported === "default" ? node.expression.text : imp.imported, ds.components);
1136
+ if (comp) out.ds[comp] = (out.ds[comp] ?? 0) + 1;
1137
+ }
1138
+ }
1139
+ if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken && ts.isPropertyAccessExpression(node.left) && node.left.name.text === "className") {
1140
+ visit(node.left, ctx, classCtx);
1141
+ return classLiteral(node.right);
1142
+ }
1082
1143
  if (ts.isCallExpression(node)) {
1083
1144
  const callee = node.expression.getText(sf);
1084
1145
  if (CLASS_FNS.test(callee)) {
@@ -1123,6 +1184,14 @@ function scanCode(path, text, ds) {
1123
1184
  const literal = (node) => {
1124
1185
  if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node) || ts.isTemplateHead(node) || ts.isTemplateMiddle(node) || ts.isTemplateTail(node)) {
1125
1186
  const t = node.text;
1187
+ if (!handled.has(node) && (t.includes("<") || ts.isTemplateMiddle(node) || ts.isTemplateTail(node))) {
1188
+ const base = node.getStart(sf) + 1;
1189
+ 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 });
1190
+ for (const m of t.matchAll(HTML_ATTR_RE)) {
1191
+ const component = LOOKALIKE_ATTRS[m[1]];
1192
+ if (component && ds.components.has(component)) out.components.push({ ...pos(base + m.index + 1), name: `[${m[1]}]`, kind: "recreated", library: "local", suggestion: component });
1193
+ }
1194
+ }
1126
1195
  for (const m of t.matchAll(/var\(\s*(--[\w-]+)/g)) {
1127
1196
  if (ds.isDsVar(m[1])) out.tokenUsages++;
1128
1197
  else out.themeRefs++;
@@ -1141,24 +1210,82 @@ function scanCode(path, text, ds) {
1141
1210
  out.components.sort((a, b) => a.line - b.line || a.column - b.column);
1142
1211
  return out;
1143
1212
  }
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));
1213
+ const IGNORE_DIRECTIVE = "bridge-audit-ignore-next-line";
1214
+ function ignoreDirectives(text) {
1215
+ const at = lineCol(text);
1216
+ const out = [];
1217
+ for (const m of text.matchAll(new RegExp(`${IGNORE_DIRECTIVE}\\b`, "g"))) {
1218
+ const start = m.index;
1219
+ const opener = /(<!--|\/\*|\/\/)(\s*)$/.exec(text.slice(Math.max(0, start - 200), start));
1220
+ const open = opener?.[1];
1221
+ if (!open || open === "//" && opener[2].includes("\n")) continue;
1222
+ const from = start + m[0].length;
1223
+ let end;
1224
+ if (open === "//") {
1225
+ end = text.indexOf("\n", from);
1226
+ if (end < 0) end = text.length;
1227
+ } else {
1228
+ end = text.indexOf(open === "<!--" ? "-->" : "*/", from);
1229
+ if (end < 0) continue;
1230
+ }
1231
+ const reason = text.slice(from, end).replace(/^\s*:?/, "").replace(/\s+/g, " ").trim();
1232
+ out.push({ line: at(start).line, target: at(end).line + 1, reason });
1233
+ }
1234
+ return out;
1235
+ }
1236
+ 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 };
1237
+ function applyIgnoreComments(text, findings, components) {
1238
+ const errors = [];
1239
+ const reasons = /* @__PURE__ */ new Map();
1240
+ for (const d of ignoreDirectives(text)) {
1241
+ if (!d.reason) {
1242
+ errors.push({ line: d.line, message: `${IGNORE_DIRECTIVE} needs a reason after a colon (${IGNORE_DIRECTIVE}: why this one stays); nothing was ignored` });
1243
+ continue;
1244
+ }
1245
+ if (!findings.some((f) => f.line === d.target) && !components.some((c) => c.line === d.target)) {
1246
+ errors.push({ line: d.line, message: `${IGNORE_DIRECTIVE} ignores nothing: line ${d.target} has no finding; remove the comment` });
1247
+ continue;
1248
+ }
1249
+ reasons.set(d.target, d.reason);
1250
+ }
1251
+ const ignored = [];
1252
+ const keep = (xs) => xs.filter((x) => {
1253
+ const r = reasons.get(x.line);
1254
+ if (r === void 0) return true;
1255
+ ignored.push(item(x, r, "comment"));
1256
+ return false;
1257
+ });
1258
+ return { findings: keep(findings), components: keep(components), ignored, errors };
1259
+ }
1260
+ function applyAllowlist(entries, findings, components) {
1261
+ const used = /* @__PURE__ */ new Set();
1262
+ const ignored = [];
1263
+ const keep = (xs) => xs.filter((x) => {
1264
+ const what = "category" in x ? x.value : x.name;
1265
+ const fits = (e) => e.line === x.line && (e.what === void 0 || e.what === what);
1266
+ let i = entries.findIndex((e, k) => fits(e) && !used.has(k));
1267
+ if (i < 0) i = entries.findIndex(fits);
1268
+ if (i < 0) return true;
1269
+ used.add(i);
1270
+ ignored.push(item(x, entries[i].reason, "allowlist"));
1271
+ return false;
1272
+ });
1273
+ return { findings: keep(findings), components: keep(components), ignored, used };
1274
+ }
1275
+ function parseAllowlist(json) {
1276
+ const fail = (msg) => {
1277
+ throw new Error(`allowlist: ${msg}`);
1278
+ };
1279
+ if (!json || typeof json !== "object" || !Array.isArray(json.entries)) fail('expected { "entries": [...] }');
1280
+ return json.entries.map((e, i) => {
1281
+ const x = e;
1282
+ if (typeof x.file !== "string" || !x.file) fail(`entry ${i} needs "file"`);
1283
+ if (typeof x.line !== "number" || !Number.isInteger(x.line) || x.line < 1) fail(`entry ${i} (${x.file}) needs a "line" number`);
1284
+ if (typeof x.reason !== "string" || !x.reason.trim()) fail(`entry ${i} (${x.file}:${x.line}) needs a "reason"`);
1285
+ if (x.what !== void 0 && typeof x.what !== "string") fail(`entry ${i} (${x.file}:${x.line}): "what" must be a string`);
1286
+ return { file: x.file, line: x.line, ...x.what !== void 0 ? { what: x.what } : {}, reason: x.reason.trim() };
1287
+ });
1151
1288
  }
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
1289
  function isSafeFix(h, s) {
1163
1290
  if (!s || s.level !== "exact") return false;
1164
1291
  switch (h.category) {
@@ -1183,49 +1310,338 @@ function isSafeFix(h, s) {
1183
1310
  return false;
1184
1311
  }
1185
1312
  }
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 } : {} });
1313
+ function auditCssText(text, lang, ds, opts) {
1314
+ const base = opts.offset ?? 0;
1315
+ const source = opts.source ?? "css";
1316
+ const at = (o) => opts.pos(base + o);
1317
+ const scan = scanCssText(text, lang);
1318
+ const findings = [];
1319
+ let tokenUsages = 0;
1320
+ let themeRefs = 0;
1321
+ for (const d of scan.decls) {
1322
+ for (const v of d.scan.vars) ds.isDsVar(v) ? tokenUsages++ : themeRefs++;
1323
+ if (d.scan.themeRef) themeRefs++;
1324
+ else if (lang !== "css" && !d.property.startsWith("$")) themeRefs += (d.value.match(/\$[\w-]+/g) ?? []).length;
1325
+ const property = d.property.toLowerCase();
1326
+ for (const h of d.scan.hits) {
1327
+ const suggestion = ds.suggest(h.category, h.text, property);
1328
+ const autofix = source === "css" && isSafeFix(h, suggestion);
1329
+ findings.push({ ...at(d.valueStart + h.index), category: h.category, property, value: h.text, source, ...suggestion ? { suggestion } : {}, ...autofix ? { autofix: true } : {} });
1330
+ }
1331
+ }
1332
+ for (const a of scan.applies) {
1333
+ for (const h of a.hits) {
1334
+ if (h.kind === "scale") themeRefs++;
1335
+ else if (h.kind !== "token" && h.value) {
1336
+ 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;
1337
+ const suggestion = ds.suggest(cat, h.value);
1338
+ findings.push({ ...at(a.start + h.index), category: cat, value: h.cls, source: "tailwind", ...suggestion ? { suggestion } : {} });
1339
+ } else if (h.kind === "token" && h.value) {
1340
+ const v = /var\(\s*(--[\w-]+)/.exec(h.value)?.[1];
1341
+ if (v && ds.isDsVar(v)) tokenUsages++;
1342
+ else themeRefs++;
1203
1343
  }
1204
1344
  }
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
- }
1345
+ }
1346
+ findings.sort((x, y) => x.line - y.line || x.column - y.column);
1347
+ return { findings, tokenUsages, themeRefs };
1348
+ }
1349
+ const MARKUP_EXTENSIONS = [".astro", ".vue", ".svelte"];
1350
+ function mask(text, ranges, keepInside) {
1351
+ const inside = new Uint8Array(text.length);
1352
+ for (const r of ranges) inside.fill(1, r.start, r.end);
1353
+ let out = "";
1354
+ for (let i = 0; i < text.length; i++) {
1355
+ const c = text[i];
1356
+ out += c === "\n" || Boolean(inside[i]) === keepInside ? c : " ";
1357
+ }
1358
+ return out;
1359
+ }
1360
+ const attrOf = (attrs, name) => new RegExp(`(?:^|\\s)${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s>]+))`, "i").exec(attrs)?.slice(1).find((v) => v !== void 0);
1361
+ const isCodeScript = (attrs) => {
1362
+ const type = attrOf(attrs, "type")?.toLowerCase();
1363
+ return !type || ["module", "text/javascript", "application/javascript", "text/typescript", "ts", "text/babel"].includes(type);
1364
+ };
1365
+ function markupBlocks(text, lang) {
1366
+ const nonTemplate = [];
1367
+ let frontmatter;
1368
+ let from = 0;
1369
+ if (lang === "astro") {
1370
+ const open = /^\s*---[^\S\n]*\n/.exec(text);
1371
+ if (open) {
1372
+ const close = /\n---[^\S\n]*(\n|$)/g;
1373
+ close.lastIndex = open[0].length - 1;
1374
+ const m = close.exec(text);
1375
+ if (m) {
1376
+ frontmatter = { start: open[0].length, end: m.index + 1 };
1377
+ from = m.index + m[0].length;
1378
+ nonTemplate.push({ start: 0, end: from });
1379
+ }
1380
+ }
1381
+ }
1382
+ const scripts = [];
1383
+ const styles = [];
1384
+ const re = /<(script|style)\b((?:[^>"']|"[^"]*"|'[^']*')*)>([\s\S]*?)<\/\1\s*>/gi;
1385
+ re.lastIndex = from;
1386
+ for (let m; m = re.exec(text); ) {
1387
+ const contentStart = m.index + m[0].indexOf(">", m[1].length + 1 + m[2].length) + 1;
1388
+ const block = { start: contentStart, end: contentStart + m[3].length, attrs: m[2] };
1389
+ (m[1].toLowerCase() === "script" ? scripts : styles).push(block);
1390
+ nonTemplate.push({ start: m.index, end: m.index + m[0].length });
1391
+ }
1392
+ for (const m of text.slice(from).matchAll(/<!--[\s\S]*?-->/g)) nonTemplate.push({ start: from + m.index, end: from + m.index + m[0].length });
1393
+ return { ...frontmatter ? { frontmatter } : {}, scripts, styles, nonTemplate };
1394
+ }
1395
+ function closeBrace(s, i) {
1396
+ let depth = 0;
1397
+ for (let k = i; k < s.length; k++) {
1398
+ const c = s[k];
1399
+ if (c === "{") depth++;
1400
+ else if (c === "}") {
1401
+ if (--depth === 0) return k + 1;
1402
+ } else if (c === '"' || c === "'") {
1403
+ const e = s.indexOf(c, k + 1);
1404
+ if (e < 0) return s.length;
1405
+ k = e;
1406
+ } else if (c === "`") {
1407
+ for (k++; k < s.length && s[k] !== "`"; k++) {
1408
+ if (s[k] === "\\") k++;
1409
+ else if (s[k] === "$" && s[k + 1] === "{") k = closeBrace(s, k + 1) - 1;
1410
+ }
1411
+ }
1412
+ }
1413
+ return s.length;
1414
+ }
1415
+ function readTag(s, i, lang) {
1416
+ const attrs = [];
1417
+ let k = i;
1418
+ while (k < s.length) {
1419
+ while (k < s.length && /\s/.test(s[k])) k++;
1420
+ const c = s[k];
1421
+ if (c === void 0) break;
1422
+ if (c === ">") return { attrs, end: k + 1, selfClosing: false };
1423
+ if (c === "/" && s[k + 1] === ">") return { attrs, end: k + 2, selfClosing: true };
1424
+ if (c === "{") {
1425
+ const e = closeBrace(s, k);
1426
+ const inner = s.slice(k + 1, e - 1).trim();
1427
+ attrs.push(inner.startsWith("...") ? { name: "", kind: "spread", value: inner, valueStart: k + 1 } : { name: inner, kind: "expr", value: inner, valueStart: k + 1 });
1428
+ k = e;
1429
+ continue;
1430
+ }
1431
+ const nm = /^[^\s=>/"'{]+/.exec(s.slice(k, k + 200));
1432
+ if (!nm) {
1433
+ k++;
1434
+ continue;
1435
+ }
1436
+ const name = nm[0];
1437
+ k += name.length;
1438
+ let j = k;
1439
+ while (j < s.length && /\s/.test(s[j])) j++;
1440
+ if (s[j] !== "=") {
1441
+ attrs.push({ name, kind: "bare", value: "", valueStart: k });
1442
+ continue;
1443
+ }
1444
+ j++;
1445
+ while (j < s.length && /\s/.test(s[j])) j++;
1446
+ const q = s[j];
1447
+ const bound = lang === "vue" && /^(:|v-bind:|v-on:|@|v-)/.test(name);
1448
+ if (q === '"' || q === "'") {
1449
+ const e = s.indexOf(q, j + 1);
1450
+ const end = e < 0 ? s.length : e;
1451
+ attrs.push({ name, kind: bound ? "expr" : "static", value: s.slice(j + 1, end), valueStart: j + 1 });
1452
+ k = end + 1;
1453
+ } else if (q === "{") {
1454
+ const e = closeBrace(s, j);
1455
+ attrs.push({ name, kind: "expr", value: s.slice(j + 1, e - 1), valueStart: j + 1 });
1456
+ k = e;
1457
+ } else {
1458
+ const m = /^[^\s>]+/.exec(s.slice(j));
1459
+ const v = m?.[0].replace(/\/$/, "") ?? "";
1460
+ attrs.push({ name, kind: "static", value: v, valueStart: j });
1461
+ k = j + v.length;
1462
+ }
1463
+ }
1464
+ return { attrs, end: s.length, selfClosing: false };
1465
+ }
1466
+ const attrName = (n) => n.replace(/^(v-bind:|:|bind:)/, "");
1467
+ const pascal = (kebab) => kebab.replace(/(^|-)([a-z0-9])/g, (_m, _d, c) => c.toUpperCase());
1468
+ const NO_COLOR_ATTR_TAGS = /* @__PURE__ */ new Set(["meta", "link"]);
1469
+ function scanMarkup(path, text, lang, ds) {
1470
+ const out = { findings: [], tokenUsages: 0, themeRefs: 0, ds: {}, components: [], definitions: [] };
1471
+ const pos = lineCol(text);
1472
+ const blocks = markupBlocks(text, lang);
1473
+ const imports = /* @__PURE__ */ new Map();
1474
+ const merge = (c) => {
1475
+ out.findings.push(...c.findings);
1476
+ out.tokenUsages += c.tokenUsages;
1477
+ out.themeRefs += c.themeRefs;
1478
+ for (const [n, k] of Object.entries(c.ds)) out.ds[n] = (out.ds[n] ?? 0) + k;
1479
+ out.components.push(...c.components);
1480
+ out.definitions.push(...c.definitions);
1481
+ };
1482
+ const codeRanges = [...blocks.frontmatter ? [{ ...blocks.frontmatter, attrs: "" }] : [], ...blocks.scripts.filter((b) => isCodeScript(b.attrs))];
1483
+ for (const r of codeRanges) {
1484
+ const jsx = /^(tsx|jsx)$/i.test(attrOf(r.attrs, "lang") ?? "");
1485
+ const code = mask(text, [r], true);
1486
+ const file = `${path}.${jsx ? "tsx" : "ts"}`;
1487
+ merge(scanCode(file, code, ds));
1488
+ for (const [k, v] of importsOf(file, code)) imports.set(k, v);
1489
+ }
1490
+ for (const b of blocks.styles) {
1491
+ const l = (attrOf(b.attrs, "lang") ?? "css").toLowerCase();
1492
+ const r = auditCssText(text.slice(b.start, b.end), l === "scss" || l === "sass" ? "scss" : l === "less" ? "less" : "css", ds, { pos, offset: b.start });
1493
+ out.findings.push(...r.findings);
1494
+ out.tokenUsages += r.tokenUsages;
1495
+ out.themeRefs += r.themeRefs;
1496
+ }
1497
+ const scanExpr = (prefix, expr, offset, suffix, keepLookalikes = false) => {
1498
+ const { line, column } = pos(offset);
1499
+ const src = line >= 2 ? `${"\n".repeat(line - 2)}${prefix}
1500
+ ${" ".repeat(column - 1)}${expr}${suffix}` : `${prefix}${expr}${suffix}`;
1501
+ const c = scanCode(`${path}.tsx`, src, ds);
1502
+ merge({ ...c, components: keepLookalikes ? c.components.filter((x) => x.kind === "recreated" && x.name.startsWith(".")) : [], definitions: [], ds: {} });
1503
+ };
1504
+ const styleAttr = (a) => {
1505
+ if (a.kind === "static") {
1506
+ const css = lang === "svelte" ? a.value.replace(/\{[^}]*\}/g, (m) => " ".repeat(m.length)) : a.value;
1507
+ const r = auditCssText(css, "css", ds, { pos, offset: a.valueStart, source: "style" });
1508
+ out.findings.push(...r.findings);
1509
+ out.tokenUsages += r.tokenUsages;
1510
+ out.themeRefs += r.themeRefs;
1511
+ return;
1512
+ }
1513
+ const str = /^(\s*)([`'"])([\s\S]*)\2\s*$/.exec(a.value);
1514
+ if (str) {
1515
+ const body = str[3].replace(/\$\{[^}]*\}/g, (m) => " ".repeat(m.length));
1516
+ const r = auditCssText(body, "css", ds, { pos, offset: a.valueStart + str[1].length + 1, source: "style" });
1517
+ out.findings.push(...r.findings);
1518
+ out.tokenUsages += r.tokenUsages;
1519
+ out.themeRefs += r.themeRefs;
1520
+ return;
1521
+ }
1522
+ scanExpr("const style: CSSProperties =", a.value, a.valueStart, ";");
1523
+ };
1524
+ const tpl = mask(text, blocks.nonTemplate, false);
1525
+ const pending = [];
1526
+ let noscript = 0;
1527
+ const TAG = /<(\/?)([A-Za-z][\w.:-]*)/g;
1528
+ for (let m; m = TAG.exec(tpl); ) {
1529
+ const raw = m[2];
1530
+ if (m[1]) {
1531
+ if (raw.toLowerCase() === "noscript") noscript = Math.max(0, noscript - 1);
1532
+ continue;
1533
+ }
1534
+ const tag = readTag(tpl, m.index + m[0].length, lang);
1535
+ TAG.lastIndex = tag.end;
1536
+ const lower = raw.toLowerCase();
1537
+ if (lower === "noscript" && !tag.selfClosing) {
1538
+ noscript++;
1539
+ continue;
1540
+ }
1541
+ const attrs = tag.attrs;
1542
+ const get = (n) => attrs.find((a) => attrName(a.name) === n);
1543
+ for (const a of attrs) {
1544
+ const n = attrName(a.name);
1545
+ if (n === "style") styleAttr(a);
1546
+ else if (lang === "svelte" && a.name.startsWith("style:") && a.kind === "static") {
1547
+ const prop = a.name.slice(6).split("|")[0];
1548
+ const r2 = auditCssText(`${prop}:${a.value}`, "css", ds, { pos, offset: a.valueStart - prop.length - 1, source: "style" });
1549
+ out.findings.push(...r2.findings);
1550
+ out.tokenUsages += r2.tokenUsages;
1551
+ out.themeRefs += r2.themeRefs;
1552
+ } else if (/^(class|className|class:list)$/.test(n)) {
1553
+ if (a.kind === "static") {
1554
+ 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 });
1555
+ const r2 = tailwindFindings(scanClassString(a.value), a.valueStart, pos, ds);
1556
+ out.findings.push(...r2.findings);
1557
+ out.themeRefs += r2.themeRefs;
1558
+ } else if (a.kind === "expr") scanExpr("cn(", a.value, a.valueStart, ")", true);
1559
+ } else if (LOOKALIKE_ATTRS[n] && ds.components.has(LOOKALIKE_ATTRS[n])) {
1560
+ out.components.push({ ...pos(a.valueStart - a.name.length - 1), name: `[${n}]`, kind: "recreated", library: "local", suggestion: LOOKALIKE_ATTRS[n] });
1561
+ } else if (a.kind === "static" && !NO_COLOR_ATTR_TAGS.has(lower) && isColorLiteral(a.value)) {
1562
+ const suggestion = ds.suggest("color", a.value.trim(), n);
1563
+ out.findings.push({ ...pos(a.valueStart), category: "color", property: n, value: a.value.trim(), source: "jsx-attr", ...suggestion ? { suggestion } : {} });
1217
1564
  }
1218
1565
  }
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: {} };
1566
+ let name = raw;
1567
+ if (raw.includes(":")) continue;
1568
+ if (raw.includes("-")) {
1569
+ if (!imports.has(pascal(raw))) continue;
1570
+ name = pascal(raw);
1571
+ }
1572
+ const isNative = /^[a-z]/.test(name) && !name.includes(".");
1573
+ if (isNative && noscript > 0) continue;
1574
+ const type = get("type");
1575
+ const r = classifyTag({ name, ...pos(m.index), ...type?.kind === "static" ? { type: type.value } : {}, hasHref: Boolean(get("href")) }, imports, ds);
1576
+ if (r.ds) out.ds[r.ds] = (out.ds[r.ds] ?? 0) + 1;
1577
+ if (r.component) out.components.push(r.component);
1578
+ if (r.localCandidate) pending.push(r.localCandidate);
1221
1579
  }
1222
- const c = scanCode(path, text, ds);
1580
+ const base = /([^/]+)\.(astro|vue|svelte)$/.exec(path)?.[1];
1581
+ if (base && /^[A-Z][A-Za-z0-9]*$/.test(base)) out.definitions.push({ name: base, line: 1 });
1582
+ const defined = new Set(out.definitions.map((d) => d.name));
1583
+ out.components.push(...pending.filter((f) => defined.has(f.suggestion)));
1584
+ out.findings.sort((a, b) => a.line - b.line || a.column - b.column);
1585
+ out.components.sort((a, b) => a.line - b.line || a.column - b.column);
1586
+ return out;
1587
+ }
1588
+ const AUDIT_EXTENSIONS = [".css", ".scss", ".sass", ".less", ".tsx", ".jsx", ".ts", ".js", ".mjs", ".cjs", ".mts", ".cts", ...MARKUP_EXTENSIONS];
1589
+ const AUDIT_IGNORE_DIRS = ["node_modules", "dist", "build", "out", "coverage", ".git", ".next", ".nuxt", ".astro", ".turbo", ".cache", ".vercel", "storybook-static", "vendor", "public"];
1590
+ 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$/;
1591
+ function isAuditable(path) {
1592
+ if (!AUDIT_EXTENSIONS.some((e) => path.endsWith(e))) return false;
1593
+ if (IGNORE_FILE.test(path)) return false;
1594
+ return !path.split("/").some((seg) => AUDIT_IGNORE_DIRS.includes(seg));
1595
+ }
1596
+ const langOf = (path) => {
1597
+ const ext = path.slice(path.lastIndexOf(".") + 1);
1598
+ if (ext === "css") return "css";
1599
+ if (ext === "scss" || ext === "sass") return "scss";
1600
+ if (ext === "less") return "less";
1601
+ if (ext === "tsx") return "tsx";
1602
+ if (ext === "jsx") return "jsx";
1603
+ if (/^[cm]?ts$/.test(ext)) return "ts";
1604
+ if (ext === "astro" || ext === "vue" || ext === "svelte") return ext;
1605
+ return "js";
1606
+ };
1607
+ function auditFile(path, text, ds) {
1608
+ const lang = langOf(path);
1609
+ if (lang === "css" || lang === "scss" || lang === "less") {
1610
+ const r = auditCssText(text, lang, ds, { pos: lineCol(text) });
1611
+ return withIgnores(text, { path, lang, tokenUsages: r.tokenUsages, themeRefs: r.themeRefs, hardcoded: 0, dsComponents: 0, otherComponents: 0, findings: r.findings, components: [], definitions: [], dsByName: {} });
1612
+ }
1613
+ const c = lang === "astro" || lang === "vue" || lang === "svelte" ? scanMarkup(path, text, lang, ds) : scanCode(path, text, ds);
1223
1614
  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 };
1615
+ 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 });
1616
+ }
1617
+ function withIgnores(text, f) {
1618
+ const r = applyIgnoreComments(text, f.findings, f.components);
1619
+ return {
1620
+ ...f,
1621
+ findings: r.findings,
1622
+ components: r.components,
1623
+ hardcoded: r.findings.length,
1624
+ otherComponents: r.components.length,
1625
+ ...r.ignored.length ? { ignored: r.ignored } : {},
1626
+ ...r.errors.length ? { ignoreErrors: r.errors } : {}
1627
+ };
1225
1628
  }
1226
1629
  const pct = (num2, den) => den > 0 ? Math.round(num2 / den * 1e3) / 10 : null;
1227
1630
  function buildAuditReport(input, ds) {
1228
- const reports = input.files.map((f) => auditFile(f.path, f.text, ds));
1631
+ const allow = input.allowlist ?? [];
1632
+ const usedAllow = /* @__PURE__ */ new Set();
1633
+ const reports = input.files.map((f) => {
1634
+ const r = auditFile(f.path, f.text, ds);
1635
+ const idx = allow.flatMap((e, i) => e.file === f.path ? [i] : []);
1636
+ if (!idx.length) return r;
1637
+ const a = applyAllowlist(idx.map((i) => allow[i]), r.findings, r.components);
1638
+ a.used.forEach((k) => usedAllow.add(idx[k]));
1639
+ if (!a.ignored.length) return r;
1640
+ const ignored = [...r.ignored ?? [], ...a.ignored].sort((x, y) => x.line - y.line || x.column - y.column);
1641
+ return { ...r, findings: a.findings, components: a.components, hardcoded: a.findings.length, otherComponents: a.components.length, ignored };
1642
+ });
1643
+ const ignoredItems = reports.flatMap((r) => (r.ignored ?? []).map((x) => ({ file: r.path, ...x })));
1644
+ const staleAllowlist = allow.filter((_e, i) => !usedAllow.has(i));
1229
1645
  const files = reports.map(({ definitions: _d, dsByName: _n, ...r }) => r);
1230
1646
  const byCategory = {};
1231
1647
  const fixes = /* @__PURE__ */ new Map();
@@ -1285,6 +1701,15 @@ function buildAuditReport(input, ds) {
1285
1701
  topFiles,
1286
1702
  topFixes,
1287
1703
  files: files.sort((a, b) => a.path.localeCompare(b.path)),
1704
+ ...ignoredItems.length || staleAllowlist.length ? {
1705
+ ignored: {
1706
+ total: ignoredItems.length,
1707
+ comment: ignoredItems.filter((x) => x.via === "comment").length,
1708
+ allowlist: ignoredItems.filter((x) => x.via === "allowlist").length,
1709
+ items: ignoredItems,
1710
+ staleAllowlist
1711
+ }
1712
+ } : {},
1288
1713
  history: []
1289
1714
  };
1290
1715
  }
@@ -1388,6 +1813,20 @@ function auditMarkdown(r, opts = {}) {
1388
1813
  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
1814
  lines.push("");
1390
1815
  }
1816
+ const ig = r.ignored;
1817
+ if (ig && (ig.total || ig.staleAllowlist.length)) {
1818
+ 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.`, "");
1819
+ const shown = ig.items.slice(0, opts.ignored ?? 50);
1820
+ if (shown.length) {
1821
+ lines.push("| Where | What | Reason |", "| --- | --- | --- |");
1822
+ for (const x of shown) lines.push(`| \`${esc(x.file)}:${x.line}\` | \`${esc(x.what)}\` | ${esc(x.reason)} (${x.via}) |`);
1823
+ if (ig.items.length > shown.length) lines.push(`| ... | ${ig.items.length - shown.length} more | |`);
1824
+ lines.push("");
1825
+ }
1826
+ if (ig.staleAllowlist.length) {
1827
+ 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(", ")}.`, "");
1828
+ }
1829
+ }
1391
1830
  if (b) {
1392
1831
  lines.push(`### Progress since ${b.label ?? b.generatedAt}`, "");
1393
1832
  lines.push(`- ${b.improved.length} files improved, ${b.regressed.length} regressed, ${b.newFiles.length} new, ${b.removedFiles.length} removed.`);
@@ -1415,40 +1854,56 @@ function planCssFixes(text, lang, ds) {
1415
1854
  }
1416
1855
  export {
1417
1856
  AUDIT_EXTENSIONS as A,
1418
- scanCssText as B,
1857
+ libraryLabel as B,
1419
1858
  COLOR_LITERAL_RE as C,
1420
1859
  DEFAULT_NATIVE_ELEMENTS as D,
1421
- scanCssValue as E,
1860
+ lineCol as E,
1422
1861
  FINDING_CATEGORIES as F,
1423
- scanJsStyle as G,
1424
- tailwindColor as H,
1425
- toOklab as I,
1862
+ lookalikesInClassList as G,
1863
+ markupBlocks as H,
1864
+ IGNORE_DIRECTIVE as I,
1865
+ nativeEquivalent as J,
1866
+ packageName as K,
1867
+ LOOKALIKE_ATTRS as L,
1868
+ MARKUP_EXTENSIONS as M,
1426
1869
  NAMED_COLORS as N,
1870
+ parseAllowlist as O,
1871
+ parseCssColor as P,
1872
+ planCssFixes as Q,
1873
+ scanClassString as R,
1874
+ scanCode as S,
1875
+ scanCssText as T,
1876
+ scanCssValue as U,
1877
+ scanJsStyle as V,
1878
+ scanMarkup as W,
1879
+ tailwindColor as X,
1880
+ tailwindFindings as Y,
1881
+ toOklab as Z,
1427
1882
  AUDIT_IGNORE_DIRS as a,
1428
1883
  AUDIT_SCHEMA_VERSION as b,
1429
1884
  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
1885
+ LOOKALIKE_CLASSES as d,
1886
+ applyAllowlist as e,
1887
+ applyIgnoreComments as f,
1888
+ auditCssText as g,
1889
+ auditFile as h,
1890
+ auditMarkdown as i,
1891
+ blankComments as j,
1892
+ buildAuditReport as k,
1893
+ classifyModule as l,
1894
+ classifyTag as m,
1895
+ compareWithBaseline as n,
1896
+ cssPropertyOf as o,
1897
+ deltaE as p,
1898
+ domRendererComponent as q,
1899
+ dsEquivalent as r,
1900
+ historyPoint as s,
1901
+ ignoreDirectives as t,
1902
+ importsOf as u,
1903
+ isAuditable as v,
1904
+ isColorLiteral as w,
1905
+ isNonVisual as x,
1906
+ isRouterLink as y,
1907
+ isSafeFix as z
1453
1908
  };
1454
- //# sourceMappingURL=report-Bf2HEr9t.js.map
1909
+ //# sourceMappingURL=report-4Z008HSf.js.map