@bridge-ds/cli 0.3.1 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +594 -122
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -602,6 +602,10 @@ var EQUIVALENTS = [
602
602
  [/^Badge$/, ["Badge"]],
603
603
  [/^(Chip|Tag|ChipField)$/, ["Tag", "Badge"]],
604
604
  [/^(Alert|AlertTitle|Callout)$/, ["Alert"]],
605
+ [/^(Banner|AnnouncementBar|TopBanner)$/, ["Banner", "Alert"]],
606
+ [/^(CodeBlock|Code|CodeSnippet|Snippet|Prism|SyntaxHighlighter|CopyBlock)$/, ["CodeBlock"]],
607
+ [/^(RadioCard|RadioCards|RadioTile|ChoiceCard|ChoiceCardGroup)$/, ["ChoiceCard", "RadioGroup"]],
608
+ [/^(Checklist|ChecklistItem|StepList)$/, ["Checklist"]],
605
609
  [/^(Snackbar|Toast|Toaster|Notification)$/, ["Toast"]],
606
610
  [/^(CircularProgress|Spinner|Loading|LoadingIndicator)$/, ["Spinner", "Progress"]],
607
611
  [/^(LinearProgress|Progress)$/, ["Progress"]],
@@ -1147,6 +1151,26 @@ var THEME_FNS = /^(createTheme|extendTheme|createMuiTheme|responsiveFontSizes|ex
1147
1151
  var CSS_TAGS = /^(styled(\.\w+|\(.*\)(\.attrs\(.*\))?)|css|keyframes|createGlobalStyle|injectGlobal|tw\.\w+)$/s;
1148
1152
  var MUI_SPACING_ATTR = /^([mp][trblxy]?|(margin|padding)(Top|Bottom|Left|Right|X|Y)?|gap|rowGap|columnGap|spacing)$/;
1149
1153
  var THEME_REF_RE = /\btheme\.(palette|spacing|shape|typography|shadows|zIndex|transitions|vars)\b/g;
1154
+ var DOM_RENDERER_RE = /^([a-z][A-Za-z0-9]*?)(Html|HTML|HtmlAttributes|Attributes|ClassName)$/;
1155
+ function domRendererComponent(fn, components) {
1156
+ const m = DOM_RENDERER_RE.exec(fn);
1157
+ if (!m) return void 0;
1158
+ const pascal2 = m[1][0].toUpperCase() + m[1].slice(1);
1159
+ if (components.has(pascal2)) return pascal2;
1160
+ return [...components].filter((c) => pascal2.startsWith(c)).sort((a, b) => b.length - a.length)[0];
1161
+ }
1162
+ var LOOKALIKE_CLASSES = { pill: "Badge", badge: "Badge", chip: "Tag", tooltip: "Tooltip", tip: "Tooltip", "has-tip": "Tooltip" };
1163
+ var LOOKALIKE_ATTRS = { "data-tip": "Tooltip" };
1164
+ var HTML_CLASS_RE = /\bclass(?:Name)?\s*=\s*["']([^"'$]*)/g;
1165
+ var HTML_ATTR_RE = /\s(data-tip)\s*=/g;
1166
+ function lookalikesInClassList(list2, components) {
1167
+ const out = [];
1168
+ for (const token of list2.split(/\s+/)) {
1169
+ const component = LOOKALIKE_CLASSES[token];
1170
+ if (component && components.has(component) && !out.some((o) => o.component === component)) out.push({ token, component });
1171
+ }
1172
+ return out;
1173
+ }
1150
1174
  function kindFor(path) {
1151
1175
  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;
1152
1176
  }
@@ -1166,6 +1190,78 @@ function contextForDeclaration(name, typeText) {
1166
1190
  if (/^(styles?|sx|classes)$|Styles?$|Sx$/.test(name)) return /sx$/i.test(name) ? "sx" : "styled";
1167
1191
  return null;
1168
1192
  }
1193
+ function importsOf(path, text) {
1194
+ const sf = ts.createSourceFile(path, text, ts.ScriptTarget.Latest, false, kindFor(path));
1195
+ const imports = /* @__PURE__ */ new Map();
1196
+ for (const node of sf.statements) {
1197
+ if (!ts.isImportDeclaration(node) || !ts.isStringLiteral(node.moduleSpecifier)) continue;
1198
+ const mod = node.moduleSpecifier.text;
1199
+ const c = node.importClause;
1200
+ if (c?.name) imports.set(c.name.text, { module: mod, imported: "default" });
1201
+ if (c?.namedBindings && ts.isNamespaceImport(c.namedBindings)) imports.set(c.namedBindings.name.text, { module: mod, imported: "*" });
1202
+ 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 });
1203
+ }
1204
+ return imports;
1205
+ }
1206
+ function classifyTag(t, imports, ds) {
1207
+ const { name } = t;
1208
+ const at = { line: t.line, column: t.column };
1209
+ if (/^[a-z][\w-]*$/.test(name)) {
1210
+ if (!ds.nativeElements.includes(name)) return {};
1211
+ if (name === "input" && t.type === "hidden") return {};
1212
+ if (name === "a" && !t.hasHref) return {};
1213
+ if (t.inRenderProp) return {};
1214
+ const suggestion = nativeEquivalent(name, t.type, ds.components);
1215
+ return { component: { ...at, name, kind: "native", library: "native", ...suggestion ? { suggestion } : {} } };
1216
+ }
1217
+ const root = name.split(".")[0];
1218
+ const imp = imports.get(root);
1219
+ const ns = imp?.imported === "*";
1220
+ const shown = imp && !ns && imp.imported !== "default" ? [imp.imported, ...name.split(".").slice(1)].join(".") : name;
1221
+ const leaf = ns && name.includes(".") ? name.split(".").slice(1).join(".") : shown;
1222
+ if (imp) {
1223
+ const kind = classifyModule(imp.module, ds.packages, ds.foreignLibraries);
1224
+ if (isNonVisual(leaf.split(".").pop())) return {};
1225
+ if (kind === "ds") return { ds: leaf };
1226
+ if (kind === "charts") return { component: { ...at, name: shown, kind: "foreign", library: libraryLabel(imp.module, kind) } };
1227
+ if (kind === "other" && isRouterLink(imp.module, imp.imported === "default" ? "default" : leaf)) {
1228
+ if (t.inRenderProp) return {};
1229
+ const suggestion = ds.components.has("Link") ? "Link" : void 0;
1230
+ return { component: { ...at, name: shown, kind: "foreign", library: libraryLabel(imp.module, kind), ...suggestion ? { suggestion } : {} } };
1231
+ }
1232
+ if (kind === "foreign" || kind === "shadcn") {
1233
+ const suggestion = (ns ? dsEquivalent(root, ds.components) : void 0) ?? dsEquivalent(leaf, ds.components);
1234
+ return { component: { ...at, name: shown, kind: "foreign", library: libraryLabel(imp.module, kind), ...suggestion ? { suggestion } : {} } };
1235
+ }
1236
+ if (kind === "local" && ds.components.has(leaf.split(".")[0])) return { component: { ...at, name: leaf, kind: "recreated", library: "local", suggestion: leaf.split(".")[0] } };
1237
+ return {};
1238
+ }
1239
+ if (ds.components.has(root)) return { localCandidate: { ...at, name, kind: "recreated", library: "local", suggestion: root } };
1240
+ return {};
1241
+ }
1242
+ function tailwindFindings(hits, offset, pos, ds) {
1243
+ const findings = [];
1244
+ let themeRefs = 0;
1245
+ for (const h of hits) {
1246
+ if (h.kind === "scale") {
1247
+ themeRefs++;
1248
+ continue;
1249
+ }
1250
+ if (h.kind === "token") continue;
1251
+ let cat = h.category;
1252
+ const v = h.value;
1253
+ const isColor = Boolean(parseCssColor(v));
1254
+ if (cat === "color-or-size") cat = isColor ? "color" : "font-size";
1255
+ else if (cat === "color-or-width") cat = isColor ? "color" : "border-width";
1256
+ else if (cat === "color-or-shadow") cat = isColor ? "color" : "shadow";
1257
+ else if (cat === "font") cat = /^\d{3}$/.test(v) ? "font-weight" : "font-family";
1258
+ if (cat === "color" && !isColor) continue;
1259
+ const suggestion = ds.suggest(cat, v);
1260
+ const cap = h.kind === "palette" && suggestion && suggestion.confidence > 0.6 ? { ...suggestion, confidence: 0.6, level: "medium" } : suggestion;
1261
+ findings.push({ ...pos(offset + h.index), category: cat, value: h.cls, source: "tailwind", ...cap ? { suggestion: cap } : {} });
1262
+ }
1263
+ return { findings, themeRefs };
1264
+ }
1169
1265
  function scanCode(path, text, ds) {
1170
1266
  const sf = ts.createSourceFile(path, text, ts.ScriptTarget.Latest, true, kindFor(path));
1171
1267
  const out = { findings: [], tokenUsages: 0, themeRefs: 0, ds: {}, components: [], definitions: [] };
@@ -1180,24 +1276,9 @@ function scanCode(path, text, ds) {
1180
1276
  out.findings.push({ ...pos(offset), category: h.category, ...property ? { property } : {}, value: h.text, source, ...suggestion ? { suggestion } : {} });
1181
1277
  };
1182
1278
  const addTailwind = (hits, offset) => {
1183
- for (const h of hits) {
1184
- if (h.kind === "scale") {
1185
- out.themeRefs++;
1186
- continue;
1187
- }
1188
- if (h.kind === "token") continue;
1189
- let cat = h.category;
1190
- const v = h.value;
1191
- const isColor = Boolean(parseCssColor(v));
1192
- if (cat === "color-or-size") cat = isColor ? "color" : "font-size";
1193
- else if (cat === "color-or-width") cat = isColor ? "color" : "border-width";
1194
- else if (cat === "color-or-shadow") cat = isColor ? "color" : "shadow";
1195
- else if (cat === "font") cat = /^\d{3}$/.test(v) ? "font-weight" : "font-family";
1196
- if (cat === "color" && !isColor) continue;
1197
- const suggestion = ds.suggest(cat, v);
1198
- const cap = h.kind === "palette" && suggestion && suggestion.confidence > 0.6 ? { ...suggestion, confidence: 0.6, level: "medium" } : suggestion;
1199
- out.findings.push({ ...pos(offset + h.index), category: cat, value: h.cls, source: "tailwind", ...cap ? { suggestion: cap } : {} });
1200
- }
1279
+ const r = tailwindFindings(hits, offset, pos, ds);
1280
+ out.themeRefs += r.themeRefs;
1281
+ out.findings.push(...r.findings);
1201
1282
  };
1202
1283
  const styleValue = (key, node, ctx) => {
1203
1284
  const source = ctx === "sx" ? "sx" : ctx === "style" ? "style" : ctx === "theme" ? "theme" : "styled";
@@ -1258,13 +1339,18 @@ function scanCode(path, text, ds) {
1258
1339
  }
1259
1340
  for (const a of scan.applies) addTailwind(a.hits, start + a.start);
1260
1341
  };
1342
+ const lookalike = (list2, offset) => {
1343
+ for (const l of lookalikesInClassList(list2, ds.components)) out.components.push({ ...pos(offset), name: `.${l.token}`, kind: "recreated", library: "local", suggestion: l.component });
1344
+ };
1261
1345
  const classLiteral = (node) => {
1262
1346
  if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
1263
1347
  handled.add(node);
1348
+ lookalike(node.text, node.getStart(sf) + 1);
1264
1349
  addTailwind(scanClassString(node.text), node.getStart(sf) + 1);
1265
1350
  } else if (ts.isTemplateExpression(node)) {
1266
1351
  for (const p of [node.head, ...node.templateSpans.map((s) => s.literal)]) {
1267
1352
  handled.add(p);
1353
+ lookalike(p.text, p.getStart(sf) + 1);
1268
1354
  addTailwind(scanClassString(p.text), p.getStart(sf) + 1);
1269
1355
  }
1270
1356
  node.templateSpans.forEach((s) => visit(s.expression, null, true));
@@ -1273,49 +1359,13 @@ function scanCode(path, text, ds) {
1273
1359
  const jsxName = (n) => n.getText(sf);
1274
1360
  const element = (node) => {
1275
1361
  const name = jsxName(node.tagName);
1276
- const at = pos(node.getStart(sf));
1277
- if (/^[a-z][\w-]*$/.test(name)) {
1278
- if (!ds.nativeElements.includes(name)) return;
1279
- const typeAttr = node.attributes.properties.find((p) => ts.isJsxAttribute(p) && p.name.getText(sf) === "type");
1280
- const type = typeAttr?.initializer && ts.isStringLiteral(typeAttr.initializer) ? typeAttr.initializer.text : void 0;
1281
- if (name === "input" && type === "hidden") return;
1282
- if (name === "a" && !node.attributes.properties.some((p) => ts.isJsxAttribute(p) && p.name.getText(sf) === "href")) return;
1283
- if (isRenderElement(node)) return;
1284
- const suggestion = nativeEquivalent(name, type, ds.components);
1285
- out.components.push({ ...at, name, kind: "native", library: "native", ...suggestion ? { suggestion } : {} });
1286
- return;
1287
- }
1288
- const root = name.split(".")[0];
1289
- const imp = imports.get(root);
1290
- const ns = imp?.imported === "*";
1291
- const shown = imp && !ns && imp.imported !== "default" ? [imp.imported, ...name.split(".").slice(1)].join(".") : name;
1292
- const leaf = ns && name.includes(".") ? name.split(".").slice(1).join(".") : shown;
1293
- if (imp) {
1294
- const kind = classifyModule(imp.module, ds.packages, ds.foreignLibraries);
1295
- if (isNonVisual(leaf.split(".").pop())) return;
1296
- if (kind === "ds") {
1297
- out.ds[leaf] = (out.ds[leaf] ?? 0) + 1;
1298
- return;
1299
- }
1300
- if (kind === "charts") {
1301
- out.components.push({ ...at, name: shown, kind: "foreign", library: libraryLabel(imp.module, kind) });
1302
- return;
1303
- }
1304
- if (kind === "other" && isRouterLink(imp.module, imp.imported === "default" ? "default" : leaf)) {
1305
- if (isRenderElement(node)) return;
1306
- const suggestion = ds.components.has("Link") ? "Link" : void 0;
1307
- out.components.push({ ...at, name: shown, kind: "foreign", library: libraryLabel(imp.module, kind), ...suggestion ? { suggestion } : {} });
1308
- return;
1309
- }
1310
- if (kind === "foreign" || kind === "shadcn") {
1311
- const suggestion = (ns ? dsEquivalent(root, ds.components) : void 0) ?? dsEquivalent(leaf, ds.components);
1312
- out.components.push({ ...at, name: shown, kind: "foreign", library: libraryLabel(imp.module, kind), ...suggestion ? { suggestion } : {} });
1313
- return;
1314
- }
1315
- if (kind === "local" && ds.components.has(leaf.split(".")[0])) out.components.push({ ...at, name: leaf, kind: "recreated", library: "local", suggestion: leaf.split(".")[0] });
1316
- return;
1317
- }
1318
- if (ds.components.has(root)) pendingLocal.push({ ...at, name, kind: "recreated", library: "local", suggestion: root });
1362
+ const typeAttr = node.attributes.properties.find((p) => ts.isJsxAttribute(p) && p.name.getText(sf) === "type");
1363
+ const type = typeAttr?.initializer && ts.isStringLiteral(typeAttr.initializer) ? typeAttr.initializer.text : void 0;
1364
+ const hasHref = node.attributes.properties.some((p) => ts.isJsxAttribute(p) && p.name.getText(sf) === "href");
1365
+ const r = classifyTag({ name, ...pos(node.getStart(sf)), ...type !== void 0 ? { type } : {}, hasHref, inRenderProp: isRenderElement(node) }, imports, ds);
1366
+ if (r.ds) out.ds[r.ds] = (out.ds[r.ds] ?? 0) + 1;
1367
+ if (r.component) out.components.push(r.component);
1368
+ if (r.localCandidate) pendingLocal.push(r.localCandidate);
1319
1369
  };
1320
1370
  const pendingLocal = [];
1321
1371
  function visit(node, ctx, classCtx) {
@@ -1375,6 +1425,17 @@ function scanCode(path, text, ds) {
1375
1425
  cssTemplate(node.template);
1376
1426
  return;
1377
1427
  }
1428
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
1429
+ const imp = imports.get(node.expression.text);
1430
+ if (imp && imp.imported !== "*" && classifyModule(imp.module, ds.packages, ds.foreignLibraries) === "ds") {
1431
+ const comp = domRendererComponent(imp.imported === "default" ? node.expression.text : imp.imported, ds.components);
1432
+ if (comp) out.ds[comp] = (out.ds[comp] ?? 0) + 1;
1433
+ }
1434
+ }
1435
+ if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken && ts.isPropertyAccessExpression(node.left) && node.left.name.text === "className") {
1436
+ visit(node.left, ctx, classCtx);
1437
+ return classLiteral(node.right);
1438
+ }
1378
1439
  if (ts.isCallExpression(node)) {
1379
1440
  const callee = node.expression.getText(sf);
1380
1441
  if (CLASS_FNS.test(callee)) {
@@ -1419,6 +1480,14 @@ function scanCode(path, text, ds) {
1419
1480
  const literal = (node) => {
1420
1481
  if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node) || ts.isTemplateHead(node) || ts.isTemplateMiddle(node) || ts.isTemplateTail(node)) {
1421
1482
  const t = node.text;
1483
+ if (!handled.has(node) && (t.includes("<") || ts.isTemplateMiddle(node) || ts.isTemplateTail(node))) {
1484
+ const base = node.getStart(sf) + 1;
1485
+ 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 });
1486
+ for (const m of t.matchAll(HTML_ATTR_RE)) {
1487
+ const component = LOOKALIKE_ATTRS[m[1]];
1488
+ if (component && ds.components.has(component)) out.components.push({ ...pos(base + m.index + 1), name: `[${m[1]}]`, kind: "recreated", library: "local", suggestion: component });
1489
+ }
1490
+ }
1422
1491
  for (const m of t.matchAll(/var\(\s*(--[\w-]+)/g)) {
1423
1492
  if (ds.isDsVar(m[1])) out.tokenUsages++;
1424
1493
  else out.themeRefs++;
@@ -1438,25 +1507,85 @@ function scanCode(path, text, ds) {
1438
1507
  return out;
1439
1508
  }
1440
1509
 
1441
- // ../core/src/audit/report.ts
1442
- var AUDIT_EXTENSIONS = [".css", ".scss", ".sass", ".less", ".tsx", ".jsx", ".ts", ".js", ".mjs", ".cjs", ".mts", ".cts"];
1443
- var AUDIT_IGNORE_DIRS = ["node_modules", "dist", "build", "out", "coverage", ".git", ".next", ".nuxt", ".astro", ".turbo", ".cache", ".vercel", "storybook-static", "vendor", "public"];
1444
- var 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$/;
1445
- function isAuditable(path) {
1446
- if (!AUDIT_EXTENSIONS.some((e) => path.endsWith(e))) return false;
1447
- if (IGNORE_FILE.test(path)) return false;
1448
- return !path.split("/").some((seg) => AUDIT_IGNORE_DIRS.includes(seg));
1510
+ // ../core/src/audit/ignore.ts
1511
+ var IGNORE_DIRECTIVE = "bridge-audit-ignore-next-line";
1512
+ function ignoreDirectives(text) {
1513
+ const at = lineCol(text);
1514
+ const out = [];
1515
+ for (const m of text.matchAll(new RegExp(`${IGNORE_DIRECTIVE}\\b`, "g"))) {
1516
+ const start = m.index;
1517
+ const opener = /(<!--|\/\*|\/\/)(\s*)$/.exec(text.slice(Math.max(0, start - 200), start));
1518
+ const open = opener?.[1];
1519
+ if (!open || open === "//" && opener[2].includes("\n")) continue;
1520
+ const from = start + m[0].length;
1521
+ let end;
1522
+ if (open === "//") {
1523
+ end = text.indexOf("\n", from);
1524
+ if (end < 0) end = text.length;
1525
+ } else {
1526
+ end = text.indexOf(open === "<!--" ? "-->" : "*/", from);
1527
+ if (end < 0) continue;
1528
+ }
1529
+ const reason = text.slice(from, end).replace(/^\s*:?/, "").replace(/\s+/g, " ").trim();
1530
+ out.push({ line: at(start).line, target: at(end).line + 1, reason });
1531
+ }
1532
+ return out;
1449
1533
  }
1450
- var langOf = (path) => {
1451
- const ext = path.slice(path.lastIndexOf(".") + 1);
1452
- if (ext === "css") return "css";
1453
- if (ext === "scss" || ext === "sass") return "scss";
1454
- if (ext === "less") return "less";
1455
- if (ext === "tsx") return "tsx";
1456
- if (ext === "jsx") return "jsx";
1457
- if (/^[cm]?ts$/.test(ext)) return "ts";
1458
- return "js";
1459
- };
1534
+ var 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 };
1535
+ function applyIgnoreComments(text, findings, components) {
1536
+ const errors = [];
1537
+ const reasons = /* @__PURE__ */ new Map();
1538
+ for (const d of ignoreDirectives(text)) {
1539
+ if (!d.reason) {
1540
+ errors.push({ line: d.line, message: `${IGNORE_DIRECTIVE} needs a reason after a colon (${IGNORE_DIRECTIVE}: why this one stays); nothing was ignored` });
1541
+ continue;
1542
+ }
1543
+ if (!findings.some((f) => f.line === d.target) && !components.some((c) => c.line === d.target)) {
1544
+ errors.push({ line: d.line, message: `${IGNORE_DIRECTIVE} ignores nothing: line ${d.target} has no finding; remove the comment` });
1545
+ continue;
1546
+ }
1547
+ reasons.set(d.target, d.reason);
1548
+ }
1549
+ const ignored = [];
1550
+ const keep = (xs) => xs.filter((x) => {
1551
+ const r = reasons.get(x.line);
1552
+ if (r === void 0) return true;
1553
+ ignored.push(item(x, r, "comment"));
1554
+ return false;
1555
+ });
1556
+ return { findings: keep(findings), components: keep(components), ignored, errors };
1557
+ }
1558
+ function applyAllowlist(entries, findings, components) {
1559
+ const used = /* @__PURE__ */ new Set();
1560
+ const ignored = [];
1561
+ const keep = (xs) => xs.filter((x) => {
1562
+ const what = "category" in x ? x.value : x.name;
1563
+ const fits = (e) => e.line === x.line && (e.what === void 0 || e.what === what);
1564
+ let i = entries.findIndex((e, k) => fits(e) && !used.has(k));
1565
+ if (i < 0) i = entries.findIndex(fits);
1566
+ if (i < 0) return true;
1567
+ used.add(i);
1568
+ ignored.push(item(x, entries[i].reason, "allowlist"));
1569
+ return false;
1570
+ });
1571
+ return { findings: keep(findings), components: keep(components), ignored, used };
1572
+ }
1573
+ function parseAllowlist(json) {
1574
+ const fail = (msg) => {
1575
+ throw new Error(`allowlist: ${msg}`);
1576
+ };
1577
+ if (!json || typeof json !== "object" || !Array.isArray(json.entries)) fail('expected { "entries": [...] }');
1578
+ return json.entries.map((e, i) => {
1579
+ const x = e;
1580
+ if (typeof x.file !== "string" || !x.file) fail(`entry ${i} needs "file"`);
1581
+ if (typeof x.line !== "number" || !Number.isInteger(x.line) || x.line < 1) fail(`entry ${i} (${x.file}) needs a "line" number`);
1582
+ if (typeof x.reason !== "string" || !x.reason.trim()) fail(`entry ${i} (${x.file}:${x.line}) needs a "reason"`);
1583
+ if (x.what !== void 0 && typeof x.what !== "string") fail(`entry ${i} (${x.file}:${x.line}): "what" must be a string`);
1584
+ return { file: x.file, line: x.line, ...x.what !== void 0 ? { what: x.what } : {}, reason: x.reason.trim() };
1585
+ });
1586
+ }
1587
+
1588
+ // ../core/src/audit/style.ts
1460
1589
  function isSafeFix(h, s) {
1461
1590
  if (!s || s.level !== "exact") return false;
1462
1591
  switch (h.category) {
@@ -1481,49 +1610,342 @@ function isSafeFix(h, s) {
1481
1610
  return false;
1482
1611
  }
1483
1612
  }
1484
- function auditFile(path, text, ds) {
1485
- const lang = langOf(path);
1486
- if (lang === "css" || lang === "scss" || lang === "less") {
1487
- const at = lineCol(text);
1488
- const scan = scanCssText(text, lang);
1489
- const findings = [];
1490
- let tokenUsages = 0;
1491
- let themeRefs = 0;
1492
- for (const d of scan.decls) {
1493
- for (const v of d.scan.vars) ds.isDsVar(v) ? tokenUsages++ : themeRefs++;
1494
- if (d.scan.themeRef) themeRefs++;
1495
- else if (lang !== "css" && !d.property.startsWith("$")) themeRefs += (d.value.match(/\$[\w-]+/g) ?? []).length;
1496
- const property = d.property.toLowerCase();
1497
- for (const h of d.scan.hits) {
1498
- const suggestion = ds.suggest(h.category, h.text, property);
1499
- const p = at(d.valueStart + h.index);
1500
- findings.push({ ...p, category: h.category, property, value: h.text, source: "css", ...suggestion ? { suggestion } : {}, ...isSafeFix(h, suggestion) ? { autofix: true } : {} });
1613
+ function auditCssText(text, lang, ds, opts) {
1614
+ const base = opts.offset ?? 0;
1615
+ const source = opts.source ?? "css";
1616
+ const at = (o) => opts.pos(base + o);
1617
+ const scan = scanCssText(text, lang);
1618
+ const findings = [];
1619
+ let tokenUsages = 0;
1620
+ let themeRefs = 0;
1621
+ for (const d of scan.decls) {
1622
+ for (const v of d.scan.vars) ds.isDsVar(v) ? tokenUsages++ : themeRefs++;
1623
+ if (d.scan.themeRef) themeRefs++;
1624
+ else if (lang !== "css" && !d.property.startsWith("$")) themeRefs += (d.value.match(/\$[\w-]+/g) ?? []).length;
1625
+ const property = d.property.toLowerCase();
1626
+ for (const h of d.scan.hits) {
1627
+ const suggestion = ds.suggest(h.category, h.text, property);
1628
+ const autofix = source === "css" && isSafeFix(h, suggestion);
1629
+ findings.push({ ...at(d.valueStart + h.index), category: h.category, property, value: h.text, source, ...suggestion ? { suggestion } : {}, ...autofix ? { autofix: true } : {} });
1630
+ }
1631
+ }
1632
+ for (const a of scan.applies) {
1633
+ for (const h of a.hits) {
1634
+ if (h.kind === "scale") themeRefs++;
1635
+ else if (h.kind !== "token" && h.value) {
1636
+ 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;
1637
+ const suggestion = ds.suggest(cat, h.value);
1638
+ findings.push({ ...at(a.start + h.index), category: cat, value: h.cls, source: "tailwind", ...suggestion ? { suggestion } : {} });
1639
+ } else if (h.kind === "token" && h.value) {
1640
+ const v = /var\(\s*(--[\w-]+)/.exec(h.value)?.[1];
1641
+ if (v && ds.isDsVar(v)) tokenUsages++;
1642
+ else themeRefs++;
1501
1643
  }
1502
1644
  }
1503
- for (const a of scan.applies) {
1504
- for (const h of a.hits) {
1505
- if (h.kind === "scale") themeRefs++;
1506
- else if (h.kind !== "token" && h.value) {
1507
- 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;
1508
- const suggestion = ds.suggest(cat, h.value);
1509
- findings.push({ ...at(a.start + h.index), category: cat, value: h.cls, source: "tailwind", ...suggestion ? { suggestion } : {} });
1510
- } else if (h.kind === "token" && h.value) {
1511
- const v = /var\(\s*(--[\w-]+)/.exec(h.value)?.[1];
1512
- if (v && ds.isDsVar(v)) tokenUsages++;
1513
- else themeRefs++;
1514
- }
1645
+ }
1646
+ findings.sort((x, y) => x.line - y.line || x.column - y.column);
1647
+ return { findings, tokenUsages, themeRefs };
1648
+ }
1649
+
1650
+ // ../core/src/audit/markup.ts
1651
+ var MARKUP_EXTENSIONS = [".astro", ".vue", ".svelte"];
1652
+ function mask2(text, ranges, keepInside) {
1653
+ const inside = new Uint8Array(text.length);
1654
+ for (const r of ranges) inside.fill(1, r.start, r.end);
1655
+ let out = "";
1656
+ for (let i = 0; i < text.length; i++) {
1657
+ const c = text[i];
1658
+ out += c === "\n" || Boolean(inside[i]) === keepInside ? c : " ";
1659
+ }
1660
+ return out;
1661
+ }
1662
+ var attrOf = (attrs, name) => new RegExp(`(?:^|\\s)${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s>]+))`, "i").exec(attrs)?.slice(1).find((v) => v !== void 0);
1663
+ var isCodeScript = (attrs) => {
1664
+ const type = attrOf(attrs, "type")?.toLowerCase();
1665
+ return !type || ["module", "text/javascript", "application/javascript", "text/typescript", "ts", "text/babel"].includes(type);
1666
+ };
1667
+ function markupBlocks(text, lang) {
1668
+ const nonTemplate = [];
1669
+ let frontmatter;
1670
+ let from = 0;
1671
+ if (lang === "astro") {
1672
+ const open = /^\s*---[^\S\n]*\n/.exec(text);
1673
+ if (open) {
1674
+ const close = /\n---[^\S\n]*(\n|$)/g;
1675
+ close.lastIndex = open[0].length - 1;
1676
+ const m = close.exec(text);
1677
+ if (m) {
1678
+ frontmatter = { start: open[0].length, end: m.index + 1 };
1679
+ from = m.index + m[0].length;
1680
+ nonTemplate.push({ start: 0, end: from });
1515
1681
  }
1516
1682
  }
1517
- findings.sort((x, y) => x.line - y.line || x.column - y.column);
1518
- return { path, lang, tokenUsages, themeRefs, hardcoded: findings.length, dsComponents: 0, otherComponents: 0, findings, components: [], definitions: [], dsByName: {} };
1519
1683
  }
1520
- const c = scanCode(path, text, ds);
1684
+ const scripts = [];
1685
+ const styles = [];
1686
+ const re = /<(script|style)\b((?:[^>"']|"[^"]*"|'[^']*')*)>([\s\S]*?)<\/\1\s*>/gi;
1687
+ re.lastIndex = from;
1688
+ for (let m; m = re.exec(text); ) {
1689
+ const contentStart = m.index + m[0].indexOf(">", m[1].length + 1 + m[2].length) + 1;
1690
+ const block = { start: contentStart, end: contentStart + m[3].length, attrs: m[2] };
1691
+ (m[1].toLowerCase() === "script" ? scripts : styles).push(block);
1692
+ nonTemplate.push({ start: m.index, end: m.index + m[0].length });
1693
+ }
1694
+ for (const m of text.slice(from).matchAll(/<!--[\s\S]*?-->/g)) nonTemplate.push({ start: from + m.index, end: from + m.index + m[0].length });
1695
+ return { ...frontmatter ? { frontmatter } : {}, scripts, styles, nonTemplate };
1696
+ }
1697
+ function closeBrace(s, i) {
1698
+ let depth = 0;
1699
+ for (let k = i; k < s.length; k++) {
1700
+ const c = s[k];
1701
+ if (c === "{") depth++;
1702
+ else if (c === "}") {
1703
+ if (--depth === 0) return k + 1;
1704
+ } else if (c === '"' || c === "'") {
1705
+ const e = s.indexOf(c, k + 1);
1706
+ if (e < 0) return s.length;
1707
+ k = e;
1708
+ } else if (c === "`") {
1709
+ for (k++; k < s.length && s[k] !== "`"; k++) {
1710
+ if (s[k] === "\\") k++;
1711
+ else if (s[k] === "$" && s[k + 1] === "{") k = closeBrace(s, k + 1) - 1;
1712
+ }
1713
+ }
1714
+ }
1715
+ return s.length;
1716
+ }
1717
+ function readTag(s, i, lang) {
1718
+ const attrs = [];
1719
+ let k = i;
1720
+ while (k < s.length) {
1721
+ while (k < s.length && /\s/.test(s[k])) k++;
1722
+ const c = s[k];
1723
+ if (c === void 0) break;
1724
+ if (c === ">") return { attrs, end: k + 1, selfClosing: false };
1725
+ if (c === "/" && s[k + 1] === ">") return { attrs, end: k + 2, selfClosing: true };
1726
+ if (c === "{") {
1727
+ const e = closeBrace(s, k);
1728
+ const inner = s.slice(k + 1, e - 1).trim();
1729
+ attrs.push(inner.startsWith("...") ? { name: "", kind: "spread", value: inner, valueStart: k + 1 } : { name: inner, kind: "expr", value: inner, valueStart: k + 1 });
1730
+ k = e;
1731
+ continue;
1732
+ }
1733
+ const nm = /^[^\s=>/"'{]+/.exec(s.slice(k, k + 200));
1734
+ if (!nm) {
1735
+ k++;
1736
+ continue;
1737
+ }
1738
+ const name = nm[0];
1739
+ k += name.length;
1740
+ let j = k;
1741
+ while (j < s.length && /\s/.test(s[j])) j++;
1742
+ if (s[j] !== "=") {
1743
+ attrs.push({ name, kind: "bare", value: "", valueStart: k });
1744
+ continue;
1745
+ }
1746
+ j++;
1747
+ while (j < s.length && /\s/.test(s[j])) j++;
1748
+ const q = s[j];
1749
+ const bound = lang === "vue" && /^(:|v-bind:|v-on:|@|v-)/.test(name);
1750
+ if (q === '"' || q === "'") {
1751
+ const e = s.indexOf(q, j + 1);
1752
+ const end = e < 0 ? s.length : e;
1753
+ attrs.push({ name, kind: bound ? "expr" : "static", value: s.slice(j + 1, end), valueStart: j + 1 });
1754
+ k = end + 1;
1755
+ } else if (q === "{") {
1756
+ const e = closeBrace(s, j);
1757
+ attrs.push({ name, kind: "expr", value: s.slice(j + 1, e - 1), valueStart: j + 1 });
1758
+ k = e;
1759
+ } else {
1760
+ const m = /^[^\s>]+/.exec(s.slice(j));
1761
+ const v = m?.[0].replace(/\/$/, "") ?? "";
1762
+ attrs.push({ name, kind: "static", value: v, valueStart: j });
1763
+ k = j + v.length;
1764
+ }
1765
+ }
1766
+ return { attrs, end: s.length, selfClosing: false };
1767
+ }
1768
+ var attrName = (n) => n.replace(/^(v-bind:|:|bind:)/, "");
1769
+ var pascal = (kebab) => kebab.replace(/(^|-)([a-z0-9])/g, (_m, _d, c) => c.toUpperCase());
1770
+ var NO_COLOR_ATTR_TAGS = /* @__PURE__ */ new Set(["meta", "link"]);
1771
+ function scanMarkup(path, text, lang, ds) {
1772
+ const out = { findings: [], tokenUsages: 0, themeRefs: 0, ds: {}, components: [], definitions: [] };
1773
+ const pos = lineCol(text);
1774
+ const blocks = markupBlocks(text, lang);
1775
+ const imports = /* @__PURE__ */ new Map();
1776
+ const merge = (c) => {
1777
+ out.findings.push(...c.findings);
1778
+ out.tokenUsages += c.tokenUsages;
1779
+ out.themeRefs += c.themeRefs;
1780
+ for (const [n, k] of Object.entries(c.ds)) out.ds[n] = (out.ds[n] ?? 0) + k;
1781
+ out.components.push(...c.components);
1782
+ out.definitions.push(...c.definitions);
1783
+ };
1784
+ const codeRanges = [...blocks.frontmatter ? [{ ...blocks.frontmatter, attrs: "" }] : [], ...blocks.scripts.filter((b) => isCodeScript(b.attrs))];
1785
+ for (const r of codeRanges) {
1786
+ const jsx = /^(tsx|jsx)$/i.test(attrOf(r.attrs, "lang") ?? "");
1787
+ const code = mask2(text, [r], true);
1788
+ const file = `${path}.${jsx ? "tsx" : "ts"}`;
1789
+ merge(scanCode(file, code, ds));
1790
+ for (const [k, v] of importsOf(file, code)) imports.set(k, v);
1791
+ }
1792
+ for (const b of blocks.styles) {
1793
+ const l = (attrOf(b.attrs, "lang") ?? "css").toLowerCase();
1794
+ const r = auditCssText(text.slice(b.start, b.end), l === "scss" || l === "sass" ? "scss" : l === "less" ? "less" : "css", ds, { pos, offset: b.start });
1795
+ out.findings.push(...r.findings);
1796
+ out.tokenUsages += r.tokenUsages;
1797
+ out.themeRefs += r.themeRefs;
1798
+ }
1799
+ const scanExpr = (prefix, expr, offset, suffix, keepLookalikes = false) => {
1800
+ const { line, column } = pos(offset);
1801
+ const src = line >= 2 ? `${"\n".repeat(line - 2)}${prefix}
1802
+ ${" ".repeat(column - 1)}${expr}${suffix}` : `${prefix}${expr}${suffix}`;
1803
+ const c = scanCode(`${path}.tsx`, src, ds);
1804
+ merge({ ...c, components: keepLookalikes ? c.components.filter((x) => x.kind === "recreated" && x.name.startsWith(".")) : [], definitions: [], ds: {} });
1805
+ };
1806
+ const styleAttr = (a) => {
1807
+ if (a.kind === "static") {
1808
+ const css = lang === "svelte" ? a.value.replace(/\{[^}]*\}/g, (m) => " ".repeat(m.length)) : a.value;
1809
+ const r = auditCssText(css, "css", ds, { pos, offset: a.valueStart, source: "style" });
1810
+ out.findings.push(...r.findings);
1811
+ out.tokenUsages += r.tokenUsages;
1812
+ out.themeRefs += r.themeRefs;
1813
+ return;
1814
+ }
1815
+ const str = /^(\s*)([`'"])([\s\S]*)\2\s*$/.exec(a.value);
1816
+ if (str) {
1817
+ const body = str[3].replace(/\$\{[^}]*\}/g, (m) => " ".repeat(m.length));
1818
+ const r = auditCssText(body, "css", ds, { pos, offset: a.valueStart + str[1].length + 1, source: "style" });
1819
+ out.findings.push(...r.findings);
1820
+ out.tokenUsages += r.tokenUsages;
1821
+ out.themeRefs += r.themeRefs;
1822
+ return;
1823
+ }
1824
+ scanExpr("const style: CSSProperties =", a.value, a.valueStart, ";");
1825
+ };
1826
+ const tpl = mask2(text, blocks.nonTemplate, false);
1827
+ const pending = [];
1828
+ let noscript = 0;
1829
+ const TAG = /<(\/?)([A-Za-z][\w.:-]*)/g;
1830
+ for (let m; m = TAG.exec(tpl); ) {
1831
+ const raw = m[2];
1832
+ if (m[1]) {
1833
+ if (raw.toLowerCase() === "noscript") noscript = Math.max(0, noscript - 1);
1834
+ continue;
1835
+ }
1836
+ const tag = readTag(tpl, m.index + m[0].length, lang);
1837
+ TAG.lastIndex = tag.end;
1838
+ const lower = raw.toLowerCase();
1839
+ if (lower === "noscript" && !tag.selfClosing) {
1840
+ noscript++;
1841
+ continue;
1842
+ }
1843
+ const attrs = tag.attrs;
1844
+ const get = (n) => attrs.find((a) => attrName(a.name) === n);
1845
+ for (const a of attrs) {
1846
+ const n = attrName(a.name);
1847
+ if (n === "style") styleAttr(a);
1848
+ else if (lang === "svelte" && a.name.startsWith("style:") && a.kind === "static") {
1849
+ const prop = a.name.slice(6).split("|")[0];
1850
+ const r2 = auditCssText(`${prop}:${a.value}`, "css", ds, { pos, offset: a.valueStart - prop.length - 1, source: "style" });
1851
+ out.findings.push(...r2.findings);
1852
+ out.tokenUsages += r2.tokenUsages;
1853
+ out.themeRefs += r2.themeRefs;
1854
+ } else if (/^(class|className|class:list)$/.test(n)) {
1855
+ if (a.kind === "static") {
1856
+ 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 });
1857
+ const r2 = tailwindFindings(scanClassString(a.value), a.valueStart, pos, ds);
1858
+ out.findings.push(...r2.findings);
1859
+ out.themeRefs += r2.themeRefs;
1860
+ } else if (a.kind === "expr") scanExpr("cn(", a.value, a.valueStart, ")", true);
1861
+ } else if (LOOKALIKE_ATTRS[n] && ds.components.has(LOOKALIKE_ATTRS[n])) {
1862
+ out.components.push({ ...pos(a.valueStart - a.name.length - 1), name: `[${n}]`, kind: "recreated", library: "local", suggestion: LOOKALIKE_ATTRS[n] });
1863
+ } else if (a.kind === "static" && !NO_COLOR_ATTR_TAGS.has(lower) && isColorLiteral(a.value)) {
1864
+ const suggestion = ds.suggest("color", a.value.trim(), n);
1865
+ out.findings.push({ ...pos(a.valueStart), category: "color", property: n, value: a.value.trim(), source: "jsx-attr", ...suggestion ? { suggestion } : {} });
1866
+ }
1867
+ }
1868
+ let name = raw;
1869
+ if (raw.includes(":")) continue;
1870
+ if (raw.includes("-")) {
1871
+ if (!imports.has(pascal(raw))) continue;
1872
+ name = pascal(raw);
1873
+ }
1874
+ const isNative = /^[a-z]/.test(name) && !name.includes(".");
1875
+ if (isNative && noscript > 0) continue;
1876
+ const type = get("type");
1877
+ const r = classifyTag({ name, ...pos(m.index), ...type?.kind === "static" ? { type: type.value } : {}, hasHref: Boolean(get("href")) }, imports, ds);
1878
+ if (r.ds) out.ds[r.ds] = (out.ds[r.ds] ?? 0) + 1;
1879
+ if (r.component) out.components.push(r.component);
1880
+ if (r.localCandidate) pending.push(r.localCandidate);
1881
+ }
1882
+ const base = /([^/]+)\.(astro|vue|svelte)$/.exec(path)?.[1];
1883
+ if (base && /^[A-Z][A-Za-z0-9]*$/.test(base)) out.definitions.push({ name: base, line: 1 });
1884
+ const defined = new Set(out.definitions.map((d) => d.name));
1885
+ out.components.push(...pending.filter((f) => defined.has(f.suggestion)));
1886
+ out.findings.sort((a, b) => a.line - b.line || a.column - b.column);
1887
+ out.components.sort((a, b) => a.line - b.line || a.column - b.column);
1888
+ return out;
1889
+ }
1890
+
1891
+ // ../core/src/audit/report.ts
1892
+ var AUDIT_EXTENSIONS = [".css", ".scss", ".sass", ".less", ".tsx", ".jsx", ".ts", ".js", ".mjs", ".cjs", ".mts", ".cts", ...MARKUP_EXTENSIONS];
1893
+ var AUDIT_IGNORE_DIRS = ["node_modules", "dist", "build", "out", "coverage", ".git", ".next", ".nuxt", ".astro", ".turbo", ".cache", ".vercel", "storybook-static", "vendor", "public"];
1894
+ var 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$/;
1895
+ function isAuditable(path) {
1896
+ if (!AUDIT_EXTENSIONS.some((e) => path.endsWith(e))) return false;
1897
+ if (IGNORE_FILE.test(path)) return false;
1898
+ return !path.split("/").some((seg) => AUDIT_IGNORE_DIRS.includes(seg));
1899
+ }
1900
+ var langOf = (path) => {
1901
+ const ext = path.slice(path.lastIndexOf(".") + 1);
1902
+ if (ext === "css") return "css";
1903
+ if (ext === "scss" || ext === "sass") return "scss";
1904
+ if (ext === "less") return "less";
1905
+ if (ext === "tsx") return "tsx";
1906
+ if (ext === "jsx") return "jsx";
1907
+ if (/^[cm]?ts$/.test(ext)) return "ts";
1908
+ if (ext === "astro" || ext === "vue" || ext === "svelte") return ext;
1909
+ return "js";
1910
+ };
1911
+ function auditFile(path, text, ds) {
1912
+ const lang = langOf(path);
1913
+ if (lang === "css" || lang === "scss" || lang === "less") {
1914
+ const r = auditCssText(text, lang, ds, { pos: lineCol(text) });
1915
+ return withIgnores(text, { path, lang, tokenUsages: r.tokenUsages, themeRefs: r.themeRefs, hardcoded: 0, dsComponents: 0, otherComponents: 0, findings: r.findings, components: [], definitions: [], dsByName: {} });
1916
+ }
1917
+ const c = lang === "astro" || lang === "vue" || lang === "svelte" ? scanMarkup(path, text, lang, ds) : scanCode(path, text, ds);
1521
1918
  const dsCount = Object.values(c.ds).reduce((a, b) => a + b, 0);
1522
- 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 };
1919
+ 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 });
1920
+ }
1921
+ function withIgnores(text, f) {
1922
+ const r = applyIgnoreComments(text, f.findings, f.components);
1923
+ return {
1924
+ ...f,
1925
+ findings: r.findings,
1926
+ components: r.components,
1927
+ hardcoded: r.findings.length,
1928
+ otherComponents: r.components.length,
1929
+ ...r.ignored.length ? { ignored: r.ignored } : {},
1930
+ ...r.errors.length ? { ignoreErrors: r.errors } : {}
1931
+ };
1523
1932
  }
1524
1933
  var pct = (num2, den) => den > 0 ? Math.round(num2 / den * 1e3) / 10 : null;
1525
1934
  function buildAuditReport(input, ds) {
1526
- const reports = input.files.map((f) => auditFile(f.path, f.text, ds));
1935
+ const allow = input.allowlist ?? [];
1936
+ const usedAllow = /* @__PURE__ */ new Set();
1937
+ const reports = input.files.map((f) => {
1938
+ const r = auditFile(f.path, f.text, ds);
1939
+ const idx = allow.flatMap((e, i) => e.file === f.path ? [i] : []);
1940
+ if (!idx.length) return r;
1941
+ const a = applyAllowlist(idx.map((i) => allow[i]), r.findings, r.components);
1942
+ a.used.forEach((k) => usedAllow.add(idx[k]));
1943
+ if (!a.ignored.length) return r;
1944
+ const ignored = [...r.ignored ?? [], ...a.ignored].sort((x, y) => x.line - y.line || x.column - y.column);
1945
+ return { ...r, findings: a.findings, components: a.components, hardcoded: a.findings.length, otherComponents: a.components.length, ignored };
1946
+ });
1947
+ const ignoredItems = reports.flatMap((r) => (r.ignored ?? []).map((x) => ({ file: r.path, ...x })));
1948
+ const staleAllowlist = allow.filter((_e, i) => !usedAllow.has(i));
1527
1949
  const files = reports.map(({ definitions: _d, dsByName: _n, ...r }) => r);
1528
1950
  const byCategory = {};
1529
1951
  const fixes = /* @__PURE__ */ new Map();
@@ -1583,6 +2005,15 @@ function buildAuditReport(input, ds) {
1583
2005
  topFiles,
1584
2006
  topFixes,
1585
2007
  files: files.sort((a, b) => a.path.localeCompare(b.path)),
2008
+ ...ignoredItems.length || staleAllowlist.length ? {
2009
+ ignored: {
2010
+ total: ignoredItems.length,
2011
+ comment: ignoredItems.filter((x) => x.via === "comment").length,
2012
+ allowlist: ignoredItems.filter((x) => x.via === "allowlist").length,
2013
+ items: ignoredItems,
2014
+ staleAllowlist
2015
+ }
2016
+ } : {},
1586
2017
  history: []
1587
2018
  };
1588
2019
  }
@@ -1686,6 +2117,20 @@ function auditMarkdown(r, opts = {}) {
1686
2117
  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} |`);
1687
2118
  lines.push("");
1688
2119
  }
2120
+ const ig = r.ignored;
2121
+ if (ig && (ig.total || ig.staleAllowlist.length)) {
2122
+ 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.`, "");
2123
+ const shown = ig.items.slice(0, opts.ignored ?? 50);
2124
+ if (shown.length) {
2125
+ lines.push("| Where | What | Reason |", "| --- | --- | --- |");
2126
+ for (const x of shown) lines.push(`| \`${esc(x.file)}:${x.line}\` | \`${esc(x.what)}\` | ${esc(x.reason)} (${x.via}) |`);
2127
+ if (ig.items.length > shown.length) lines.push(`| ... | ${ig.items.length - shown.length} more | |`);
2128
+ lines.push("");
2129
+ }
2130
+ if (ig.staleAllowlist.length) {
2131
+ 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(", ")}.`, "");
2132
+ }
2133
+ }
1689
2134
  if (b) {
1690
2135
  lines.push(`### Progress since ${b.label ?? b.generatedAt}`, "");
1691
2136
  lines.push(`- ${b.improved.length} files improved, ${b.regressed.length} regressed, ${b.newFiles.length} new, ${b.removedFiles.length} removed.`);
@@ -2425,13 +2870,16 @@ function collectAuditFiles(target, exclude = []) {
2425
2870
  }
2426
2871
  return { root: abs, files, skipped };
2427
2872
  }
2873
+ function loadAllowlist(path) {
2874
+ return parseAllowlist(JSON.parse(readFileSync(path, "utf8")));
2875
+ }
2428
2876
  function runAudit(target, opts = {}) {
2429
2877
  const dsRoot = opts.ds ?? findDesignSystemRoot(process.cwd());
2430
2878
  if (!dsRoot) throw new Error("No design system found: run from a repo with bridge.config.json or pass --ds <dir>.");
2431
2879
  const ds = loadDesignSystem(dsRoot);
2432
2880
  const { files, skipped } = collectAuditFiles(target, [...ds.config.audit?.exclude ?? [], ...opts.exclude ?? []]);
2433
2881
  const report = buildAuditReport(
2434
- { files, skipped, target: opts.displayPath ?? (toPosix(relative(process.cwd(), resolve(target))) || "."), ...opts.label ? { label: opts.label } : {}, ...opts.version ? { version: opts.version } : {} },
2882
+ { files, skipped, target: opts.displayPath ?? (toPosix(relative(process.cwd(), resolve(target))) || "."), ...opts.label ? { label: opts.label } : {}, ...opts.version ? { version: opts.version } : {}, ...opts.allowlist ? { allowlist: opts.allowlist } : {} },
2435
2883
  ds.index
2436
2884
  );
2437
2885
  return opts.baseline ? compareWithBaseline(report, opts.baseline) : report;
@@ -2600,6 +3048,12 @@ function classifyAuth(status, detail, opts = {}) {
2600
3048
  }
2601
3049
  return {};
2602
3050
  }
3051
+ function rateLimitMessage(x) {
3052
+ const when = x.resetsAt ? ` It resets at ${x.resetsAt.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}.` : "";
3053
+ if (x.withToken) return `GitHub's hourly request limit for this token is used up.${when} Try again after that.`;
3054
+ const hint = x.tokenHint ?? "add a GitHub token";
3055
+ return `GitHub's hourly limit for requests without a token (60 an hour) is used up.${when} To keep going now, ${hint}: a token gets 5,000 requests an hour.`;
3056
+ }
2603
3057
  var DEFAULT_RETRY = { attempts: 3, baseDelayMs: 600, sleep: (ms) => new Promise((r) => setTimeout(r, ms)) };
2604
3058
  var isTransient = (status, detail) => status === 0 || status >= 500 || status === 429 || status === 403 && /secondary rate limit/i.test(detail);
2605
3059
  var API = "https://api.github.com";
@@ -2703,12 +3157,8 @@ var GitHubClient = class _GitHubClient {
2703
3157
  const remaining = res.headers?.get("x-ratelimit-remaining");
2704
3158
  const reset = res.headers?.get("x-ratelimit-reset");
2705
3159
  if ((res.status === 403 || res.status === 429) && remaining === "0") {
2706
- const when = reset ? new Date(Number(reset) * 1e3) : void 0;
2707
- throw new GitHubError(
2708
- 429,
2709
- `GitHub's hourly request limit for this token is used up.${when ? ` It resets at ${when.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}.` : ""} Try again after that.`,
2710
- detail
2711
- );
3160
+ const resetsAt = reset ? new Date(Number(reset) * 1e3) : void 0;
3161
+ throw new GitHubError(429, rateLimitMessage({ withToken: Boolean(this.token), resetsAt, tokenHint: this.options.tokenHint }), detail);
2712
3162
  }
2713
3163
  const info = classifyAuth(res.status, detail, {
2714
3164
  path,
@@ -4920,9 +5370,15 @@ Usage:
4920
5370
  undo a merged token change against HEAD: conflicts shown,
4921
5371
  new local bridge/* branch (never pushes, never main)
4922
5372
  bridge audit <path> [--json [out.json]] [--md [out.md]] [--fail-under 80] [--baseline prev.json]
4923
- [--label "..."] [--ds dir] [--no-regress]
5373
+ [--label "..."] [--ds dir] [--no-regress] [--allowlist known.json]
4924
5374
  design system adoption in a product folder: token and component
4925
5375
  adoption, hardcoded values with the closest token, top files.
5376
+ Reads CSS, SCSS, Less, TS/JS, TSX/JSX, and Astro, Vue and Svelte
5377
+ (template markup, frontmatter and <script>, <style> blocks).
5378
+ Leave one exception out with a comment on the line above:
5379
+ <!-- bridge-audit-ignore-next-line: reason --> (or // or /* */);
5380
+ --allowlist lists known file:line exceptions, each with a reason.
5381
+ Ignored items stay listed in the report.
4926
5382
  Exit 0 ok, 1 under --fail-under or regressed, 2 setup error
4927
5383
  bridge audit <path> --fix [--write] safe autofix: CSS values equal to a token become var(--token).
4928
5384
  Dry run unless --write
@@ -5313,7 +5769,15 @@ async function audit(a, root) {
5313
5769
  baseline = JSON.parse(await readFile5(baselinePath, "utf8"));
5314
5770
  }
5315
5771
  const label = flag3(a, "label");
5316
- const report = runAudit(target, { ds: dsRoot, version: VERSION, ...label ? { label } : {}, ...baseline ? { baseline } : {} });
5772
+ const allowlistPath = flag3(a, "allowlist");
5773
+ if (allowlistPath && !existsSync5(allowlistPath)) throw new UsageError(`Allowlist not found: ${allowlistPath}`);
5774
+ let allowlist;
5775
+ try {
5776
+ allowlist = allowlistPath ? loadAllowlist(allowlistPath) : void 0;
5777
+ } catch (e) {
5778
+ throw new UsageError(`${allowlistPath}: ${e.message}`);
5779
+ }
5780
+ const report = runAudit(target, { ds: dsRoot, version: VERSION, ...label ? { label } : {}, ...baseline ? { baseline } : {}, ...allowlist ? { allowlist } : {} });
5317
5781
  const md = auditMarkdown(report);
5318
5782
  const jsonOut = flag3(a, "json");
5319
5783
  const mdOut = flag3(a, "md");
@@ -5344,9 +5808,17 @@ async function audit(a, root) {
5344
5808
  print(" top fixes:");
5345
5809
  for (const f of fixes) print(` ${f.value} -> var(${f.suggestion.cssVar}) ${f.suggestion.level}, ${f.count}x`);
5346
5810
  }
5811
+ const ig = report.ignored;
5812
+ if (ig?.total) print(` ignored on purpose ${ig.total} (${ig.comment} by comment, ${ig.allowlist} by the allowlist); listed in --json and --md`);
5813
+ if (ig?.staleAllowlist.length) {
5814
+ print(` allowlist entries that match nothing any more (remove them):`);
5815
+ for (const e of ig.staleAllowlist) print(` ${e.file}:${e.line}${e.what ? ` ${e.what}` : ""}`);
5816
+ }
5347
5817
  if (jsonOut) print(`Wrote ${jsonOut}`);
5348
5818
  if (mdOut) print(`Wrote ${mdOut}`);
5349
5819
  }
5820
+ for (const f of report.files) for (const e of f.ignoreErrors ?? []) process.stderr.write(`${report.target.path}/${f.path}:${e.line}: ${e.message}
5821
+ `);
5350
5822
  let code = 0;
5351
5823
  const failUnder = flag3(a, "fail-under");
5352
5824
  if (failUnder !== void 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bridge-ds/cli",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "type": "module",
5
5
  "description": "bridge CLI: set up Bridge in a design system repo, check tokens in CI, search and diff tokens, propose changes, start the MCP server.",
6
6
  "license": "MIT",
@@ -43,8 +43,8 @@
43
43
  "build:package": "node ../../scripts/build-packages.mjs cli"
44
44
  },
45
45
  "dependencies": {
46
- "@bridge-ds/mcp": "^0.3.1",
47
- "@bridge-ds/visual": "^0.2.2",
46
+ "@bridge-ds/mcp": "^0.3.2",
47
+ "@bridge-ds/visual": "^0.2.3",
48
48
  "typescript": "5.9.3"
49
49
  },
50
50
  "devDependencies": {