@bridge-ds/cli 0.3.1 → 0.4.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.
Files changed (2) hide show
  1. package/dist/index.js +883 -144
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { existsSync as existsSync5 } from "node:fs";
5
- import { readFile as readFile5, writeFile as writeFile4 } from "node:fs/promises";
6
- import { isAbsolute, join as join5, relative as relative4, resolve as resolve3 } from "node:path";
4
+ import { existsSync as existsSync6 } from "node:fs";
5
+ import { readFile as readFile6, writeFile as writeFile5 } from "node:fs/promises";
6
+ import { isAbsolute, join as join6, relative as relative4, resolve as resolve3 } from "node:path";
7
7
 
8
8
  // ../core/src/audit/types.ts
9
9
  var AUDIT_SCHEMA_VERSION = 1;
@@ -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,79 @@ 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
+ if (!suggestion) return {};
1216
+ return { component: { ...at, name, kind: "native", library: "native", suggestion } };
1217
+ }
1218
+ const root = name.split(".")[0];
1219
+ const imp = imports.get(root);
1220
+ const ns = imp?.imported === "*";
1221
+ const shown = imp && !ns && imp.imported !== "default" ? [imp.imported, ...name.split(".").slice(1)].join(".") : name;
1222
+ const leaf = ns && name.includes(".") ? name.split(".").slice(1).join(".") : shown;
1223
+ if (imp) {
1224
+ const kind = classifyModule(imp.module, ds.packages, ds.foreignLibraries);
1225
+ if (isNonVisual(leaf.split(".").pop())) return {};
1226
+ if (kind === "ds") return { ds: leaf };
1227
+ if (kind === "charts") return { component: { ...at, name: shown, kind: "foreign", library: libraryLabel(imp.module, kind) } };
1228
+ if (kind === "other" && isRouterLink(imp.module, imp.imported === "default" ? "default" : leaf)) {
1229
+ if (t.inRenderProp) return {};
1230
+ const suggestion = ds.components.has("Link") ? "Link" : void 0;
1231
+ return { component: { ...at, name: shown, kind: "foreign", library: libraryLabel(imp.module, kind), ...suggestion ? { suggestion } : {} } };
1232
+ }
1233
+ if (kind === "foreign" || kind === "shadcn") {
1234
+ const suggestion = (ns ? dsEquivalent(root, ds.components) : void 0) ?? dsEquivalent(leaf, ds.components);
1235
+ return { component: { ...at, name: shown, kind: "foreign", library: libraryLabel(imp.module, kind), ...suggestion ? { suggestion } : {} } };
1236
+ }
1237
+ if (kind === "local" && ds.components.has(leaf.split(".")[0])) return { component: { ...at, name: leaf, kind: "recreated", library: "local", suggestion: leaf.split(".")[0] } };
1238
+ return {};
1239
+ }
1240
+ if (ds.components.has(root)) return { localCandidate: { ...at, name, kind: "recreated", library: "local", suggestion: root } };
1241
+ return {};
1242
+ }
1243
+ function tailwindFindings(hits, offset, pos, ds) {
1244
+ const findings = [];
1245
+ let themeRefs = 0;
1246
+ for (const h of hits) {
1247
+ if (h.kind === "scale") {
1248
+ themeRefs++;
1249
+ continue;
1250
+ }
1251
+ if (h.kind === "token") continue;
1252
+ let cat = h.category;
1253
+ const v = h.value;
1254
+ const isColor = Boolean(parseCssColor(v));
1255
+ if (cat === "color-or-size") cat = isColor ? "color" : "font-size";
1256
+ else if (cat === "color-or-width") cat = isColor ? "color" : "border-width";
1257
+ else if (cat === "color-or-shadow") cat = isColor ? "color" : "shadow";
1258
+ else if (cat === "font") cat = /^\d{3}$/.test(v) ? "font-weight" : "font-family";
1259
+ if (cat === "color" && !isColor) continue;
1260
+ const suggestion = ds.suggest(cat, v);
1261
+ const cap = h.kind === "palette" && suggestion && suggestion.confidence > 0.6 ? { ...suggestion, confidence: 0.6, level: "medium" } : suggestion;
1262
+ findings.push({ ...pos(offset + h.index), category: cat, value: h.cls, source: "tailwind", ...cap ? { suggestion: cap } : {} });
1263
+ }
1264
+ return { findings, themeRefs };
1265
+ }
1169
1266
  function scanCode(path, text, ds) {
1170
1267
  const sf = ts.createSourceFile(path, text, ts.ScriptTarget.Latest, true, kindFor(path));
1171
1268
  const out = { findings: [], tokenUsages: 0, themeRefs: 0, ds: {}, components: [], definitions: [] };
@@ -1180,24 +1277,9 @@ function scanCode(path, text, ds) {
1180
1277
  out.findings.push({ ...pos(offset), category: h.category, ...property ? { property } : {}, value: h.text, source, ...suggestion ? { suggestion } : {} });
1181
1278
  };
1182
1279
  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
- }
1280
+ const r = tailwindFindings(hits, offset, pos, ds);
1281
+ out.themeRefs += r.themeRefs;
1282
+ out.findings.push(...r.findings);
1201
1283
  };
1202
1284
  const styleValue = (key, node, ctx) => {
1203
1285
  const source = ctx === "sx" ? "sx" : ctx === "style" ? "style" : ctx === "theme" ? "theme" : "styled";
@@ -1258,13 +1340,18 @@ function scanCode(path, text, ds) {
1258
1340
  }
1259
1341
  for (const a of scan.applies) addTailwind(a.hits, start + a.start);
1260
1342
  };
1343
+ const lookalike = (list2, offset) => {
1344
+ for (const l of lookalikesInClassList(list2, ds.components)) out.components.push({ ...pos(offset), name: `.${l.token}`, kind: "recreated", library: "local", suggestion: l.component });
1345
+ };
1261
1346
  const classLiteral = (node) => {
1262
1347
  if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
1263
1348
  handled.add(node);
1349
+ lookalike(node.text, node.getStart(sf) + 1);
1264
1350
  addTailwind(scanClassString(node.text), node.getStart(sf) + 1);
1265
1351
  } else if (ts.isTemplateExpression(node)) {
1266
1352
  for (const p of [node.head, ...node.templateSpans.map((s) => s.literal)]) {
1267
1353
  handled.add(p);
1354
+ lookalike(p.text, p.getStart(sf) + 1);
1268
1355
  addTailwind(scanClassString(p.text), p.getStart(sf) + 1);
1269
1356
  }
1270
1357
  node.templateSpans.forEach((s) => visit(s.expression, null, true));
@@ -1273,49 +1360,13 @@ function scanCode(path, text, ds) {
1273
1360
  const jsxName = (n) => n.getText(sf);
1274
1361
  const element = (node) => {
1275
1362
  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 });
1363
+ const typeAttr = node.attributes.properties.find((p) => ts.isJsxAttribute(p) && p.name.getText(sf) === "type");
1364
+ const type = typeAttr?.initializer && ts.isStringLiteral(typeAttr.initializer) ? typeAttr.initializer.text : void 0;
1365
+ const hasHref = node.attributes.properties.some((p) => ts.isJsxAttribute(p) && p.name.getText(sf) === "href");
1366
+ const r = classifyTag({ name, ...pos(node.getStart(sf)), ...type !== void 0 ? { type } : {}, hasHref, inRenderProp: isRenderElement(node) }, imports, ds);
1367
+ if (r.ds) out.ds[r.ds] = (out.ds[r.ds] ?? 0) + 1;
1368
+ if (r.component) out.components.push(r.component);
1369
+ if (r.localCandidate) pendingLocal.push(r.localCandidate);
1319
1370
  };
1320
1371
  const pendingLocal = [];
1321
1372
  function visit(node, ctx, classCtx) {
@@ -1375,6 +1426,17 @@ function scanCode(path, text, ds) {
1375
1426
  cssTemplate(node.template);
1376
1427
  return;
1377
1428
  }
1429
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
1430
+ const imp = imports.get(node.expression.text);
1431
+ if (imp && imp.imported !== "*" && classifyModule(imp.module, ds.packages, ds.foreignLibraries) === "ds") {
1432
+ const comp = domRendererComponent(imp.imported === "default" ? node.expression.text : imp.imported, ds.components);
1433
+ if (comp) out.ds[comp] = (out.ds[comp] ?? 0) + 1;
1434
+ }
1435
+ }
1436
+ if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken && ts.isPropertyAccessExpression(node.left) && node.left.name.text === "className") {
1437
+ visit(node.left, ctx, classCtx);
1438
+ return classLiteral(node.right);
1439
+ }
1378
1440
  if (ts.isCallExpression(node)) {
1379
1441
  const callee = node.expression.getText(sf);
1380
1442
  if (CLASS_FNS.test(callee)) {
@@ -1419,6 +1481,14 @@ function scanCode(path, text, ds) {
1419
1481
  const literal = (node) => {
1420
1482
  if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node) || ts.isTemplateHead(node) || ts.isTemplateMiddle(node) || ts.isTemplateTail(node)) {
1421
1483
  const t = node.text;
1484
+ if (!handled.has(node) && (t.includes("<") || ts.isTemplateMiddle(node) || ts.isTemplateTail(node))) {
1485
+ const base = node.getStart(sf) + 1;
1486
+ 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 });
1487
+ for (const m of t.matchAll(HTML_ATTR_RE)) {
1488
+ const component = LOOKALIKE_ATTRS[m[1]];
1489
+ if (component && ds.components.has(component)) out.components.push({ ...pos(base + m.index + 1), name: `[${m[1]}]`, kind: "recreated", library: "local", suggestion: component });
1490
+ }
1491
+ }
1422
1492
  for (const m of t.matchAll(/var\(\s*(--[\w-]+)/g)) {
1423
1493
  if (ds.isDsVar(m[1])) out.tokenUsages++;
1424
1494
  else out.themeRefs++;
@@ -1438,25 +1508,85 @@ function scanCode(path, text, ds) {
1438
1508
  return out;
1439
1509
  }
1440
1510
 
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));
1511
+ // ../core/src/audit/ignore.ts
1512
+ var IGNORE_DIRECTIVE = "bridge-audit-ignore-next-line";
1513
+ function ignoreDirectives(text) {
1514
+ const at = lineCol(text);
1515
+ const out = [];
1516
+ for (const m of text.matchAll(new RegExp(`${IGNORE_DIRECTIVE}\\b`, "g"))) {
1517
+ const start = m.index;
1518
+ const opener = /(<!--|\/\*|\/\/)(\s*)$/.exec(text.slice(Math.max(0, start - 200), start));
1519
+ const open = opener?.[1];
1520
+ if (!open || open === "//" && opener[2].includes("\n")) continue;
1521
+ const from = start + m[0].length;
1522
+ let end;
1523
+ if (open === "//") {
1524
+ end = text.indexOf("\n", from);
1525
+ if (end < 0) end = text.length;
1526
+ } else {
1527
+ end = text.indexOf(open === "<!--" ? "-->" : "*/", from);
1528
+ if (end < 0) continue;
1529
+ }
1530
+ const reason = text.slice(from, end).replace(/^\s*:?/, "").replace(/\s+/g, " ").trim();
1531
+ out.push({ line: at(start).line, target: at(end).line + 1, reason });
1532
+ }
1533
+ return out;
1449
1534
  }
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
- };
1535
+ 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 };
1536
+ function applyIgnoreComments(text, findings, components) {
1537
+ const errors = [];
1538
+ const reasons = /* @__PURE__ */ new Map();
1539
+ for (const d of ignoreDirectives(text)) {
1540
+ if (!d.reason) {
1541
+ errors.push({ line: d.line, message: `${IGNORE_DIRECTIVE} needs a reason after a colon (${IGNORE_DIRECTIVE}: why this one stays); nothing was ignored` });
1542
+ continue;
1543
+ }
1544
+ if (!findings.some((f) => f.line === d.target) && !components.some((c) => c.line === d.target)) {
1545
+ errors.push({ line: d.line, message: `${IGNORE_DIRECTIVE} ignores nothing: line ${d.target} has no finding; remove the comment` });
1546
+ continue;
1547
+ }
1548
+ reasons.set(d.target, d.reason);
1549
+ }
1550
+ const ignored = [];
1551
+ const keep = (xs) => xs.filter((x) => {
1552
+ const r = reasons.get(x.line);
1553
+ if (r === void 0) return true;
1554
+ ignored.push(item(x, r, "comment"));
1555
+ return false;
1556
+ });
1557
+ return { findings: keep(findings), components: keep(components), ignored, errors };
1558
+ }
1559
+ function applyAllowlist(entries, findings, components) {
1560
+ const used = /* @__PURE__ */ new Set();
1561
+ const ignored = [];
1562
+ const keep = (xs) => xs.filter((x) => {
1563
+ const what = "category" in x ? x.value : x.name;
1564
+ const fits = (e) => e.line === x.line && (e.what === void 0 || e.what === what);
1565
+ let i = entries.findIndex((e, k) => fits(e) && !used.has(k));
1566
+ if (i < 0) i = entries.findIndex(fits);
1567
+ if (i < 0) return true;
1568
+ used.add(i);
1569
+ ignored.push(item(x, entries[i].reason, "allowlist"));
1570
+ return false;
1571
+ });
1572
+ return { findings: keep(findings), components: keep(components), ignored, used };
1573
+ }
1574
+ function parseAllowlist(json) {
1575
+ const fail = (msg) => {
1576
+ throw new Error(`allowlist: ${msg}`);
1577
+ };
1578
+ if (!json || typeof json !== "object" || !Array.isArray(json.entries)) fail('expected { "entries": [...] }');
1579
+ return json.entries.map((e, i) => {
1580
+ const x = e;
1581
+ if (typeof x.file !== "string" || !x.file) fail(`entry ${i} needs "file"`);
1582
+ if (typeof x.line !== "number" || !Number.isInteger(x.line) || x.line < 1) fail(`entry ${i} (${x.file}) needs a "line" number`);
1583
+ if (typeof x.reason !== "string" || !x.reason.trim()) fail(`entry ${i} (${x.file}:${x.line}) needs a "reason"`);
1584
+ if (x.what !== void 0 && typeof x.what !== "string") fail(`entry ${i} (${x.file}:${x.line}): "what" must be a string`);
1585
+ return { file: x.file, line: x.line, ...x.what !== void 0 ? { what: x.what } : {}, reason: x.reason.trim() };
1586
+ });
1587
+ }
1588
+
1589
+ // ../core/src/audit/style.ts
1460
1590
  function isSafeFix(h, s) {
1461
1591
  if (!s || s.level !== "exact") return false;
1462
1592
  switch (h.category) {
@@ -1481,49 +1611,342 @@ function isSafeFix(h, s) {
1481
1611
  return false;
1482
1612
  }
1483
1613
  }
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 } : {} });
1614
+ function auditCssText(text, lang, ds, opts) {
1615
+ const base = opts.offset ?? 0;
1616
+ const source = opts.source ?? "css";
1617
+ const at = (o) => opts.pos(base + o);
1618
+ const scan = scanCssText(text, lang);
1619
+ const findings = [];
1620
+ let tokenUsages = 0;
1621
+ let themeRefs = 0;
1622
+ for (const d of scan.decls) {
1623
+ for (const v of d.scan.vars) ds.isDsVar(v) ? tokenUsages++ : themeRefs++;
1624
+ if (d.scan.themeRef) themeRefs++;
1625
+ else if (lang !== "css" && !d.property.startsWith("$")) themeRefs += (d.value.match(/\$[\w-]+/g) ?? []).length;
1626
+ const property = d.property.toLowerCase();
1627
+ for (const h of d.scan.hits) {
1628
+ const suggestion = ds.suggest(h.category, h.text, property);
1629
+ const autofix = source === "css" && isSafeFix(h, suggestion);
1630
+ findings.push({ ...at(d.valueStart + h.index), category: h.category, property, value: h.text, source, ...suggestion ? { suggestion } : {}, ...autofix ? { autofix: true } : {} });
1631
+ }
1632
+ }
1633
+ for (const a of scan.applies) {
1634
+ for (const h of a.hits) {
1635
+ if (h.kind === "scale") themeRefs++;
1636
+ else if (h.kind !== "token" && h.value) {
1637
+ 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;
1638
+ const suggestion = ds.suggest(cat, h.value);
1639
+ findings.push({ ...at(a.start + h.index), category: cat, value: h.cls, source: "tailwind", ...suggestion ? { suggestion } : {} });
1640
+ } else if (h.kind === "token" && h.value) {
1641
+ const v = /var\(\s*(--[\w-]+)/.exec(h.value)?.[1];
1642
+ if (v && ds.isDsVar(v)) tokenUsages++;
1643
+ else themeRefs++;
1501
1644
  }
1502
1645
  }
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
- }
1646
+ }
1647
+ findings.sort((x, y) => x.line - y.line || x.column - y.column);
1648
+ return { findings, tokenUsages, themeRefs };
1649
+ }
1650
+
1651
+ // ../core/src/audit/markup.ts
1652
+ var MARKUP_EXTENSIONS = [".astro", ".vue", ".svelte"];
1653
+ function mask2(text, ranges, keepInside) {
1654
+ const inside = new Uint8Array(text.length);
1655
+ for (const r of ranges) inside.fill(1, r.start, r.end);
1656
+ let out = "";
1657
+ for (let i = 0; i < text.length; i++) {
1658
+ const c = text[i];
1659
+ out += c === "\n" || Boolean(inside[i]) === keepInside ? c : " ";
1660
+ }
1661
+ return out;
1662
+ }
1663
+ var attrOf = (attrs, name) => new RegExp(`(?:^|\\s)${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s>]+))`, "i").exec(attrs)?.slice(1).find((v) => v !== void 0);
1664
+ var isCodeScript = (attrs) => {
1665
+ const type = attrOf(attrs, "type")?.toLowerCase();
1666
+ return !type || ["module", "text/javascript", "application/javascript", "text/typescript", "ts", "text/babel"].includes(type);
1667
+ };
1668
+ function markupBlocks(text, lang) {
1669
+ const nonTemplate = [];
1670
+ let frontmatter;
1671
+ let from = 0;
1672
+ if (lang === "astro") {
1673
+ const open = /^\s*---[^\S\n]*\n/.exec(text);
1674
+ if (open) {
1675
+ const close = /\n---[^\S\n]*(\n|$)/g;
1676
+ close.lastIndex = open[0].length - 1;
1677
+ const m = close.exec(text);
1678
+ if (m) {
1679
+ frontmatter = { start: open[0].length, end: m.index + 1 };
1680
+ from = m.index + m[0].length;
1681
+ nonTemplate.push({ start: 0, end: from });
1682
+ }
1683
+ }
1684
+ }
1685
+ const scripts = [];
1686
+ const styles = [];
1687
+ const re = /<(script|style)\b((?:[^>"']|"[^"]*"|'[^']*')*)>([\s\S]*?)<\/\1\s*>/gi;
1688
+ re.lastIndex = from;
1689
+ for (let m; m = re.exec(text); ) {
1690
+ const contentStart = m.index + m[0].indexOf(">", m[1].length + 1 + m[2].length) + 1;
1691
+ const block = { start: contentStart, end: contentStart + m[3].length, attrs: m[2] };
1692
+ (m[1].toLowerCase() === "script" ? scripts : styles).push(block);
1693
+ nonTemplate.push({ start: m.index, end: m.index + m[0].length });
1694
+ }
1695
+ for (const m of text.slice(from).matchAll(/<!--[\s\S]*?-->/g)) nonTemplate.push({ start: from + m.index, end: from + m.index + m[0].length });
1696
+ return { ...frontmatter ? { frontmatter } : {}, scripts, styles, nonTemplate };
1697
+ }
1698
+ function closeBrace(s, i) {
1699
+ let depth = 0;
1700
+ for (let k = i; k < s.length; k++) {
1701
+ const c = s[k];
1702
+ if (c === "{") depth++;
1703
+ else if (c === "}") {
1704
+ if (--depth === 0) return k + 1;
1705
+ } else if (c === '"' || c === "'") {
1706
+ const e = s.indexOf(c, k + 1);
1707
+ if (e < 0) return s.length;
1708
+ k = e;
1709
+ } else if (c === "`") {
1710
+ for (k++; k < s.length && s[k] !== "`"; k++) {
1711
+ if (s[k] === "\\") k++;
1712
+ else if (s[k] === "$" && s[k + 1] === "{") k = closeBrace(s, k + 1) - 1;
1713
+ }
1714
+ }
1715
+ }
1716
+ return s.length;
1717
+ }
1718
+ function readTag(s, i, lang) {
1719
+ const attrs = [];
1720
+ let k = i;
1721
+ while (k < s.length) {
1722
+ while (k < s.length && /\s/.test(s[k])) k++;
1723
+ const c = s[k];
1724
+ if (c === void 0) break;
1725
+ if (c === ">") return { attrs, end: k + 1, selfClosing: false };
1726
+ if (c === "/" && s[k + 1] === ">") return { attrs, end: k + 2, selfClosing: true };
1727
+ if (c === "{") {
1728
+ const e = closeBrace(s, k);
1729
+ const inner = s.slice(k + 1, e - 1).trim();
1730
+ attrs.push(inner.startsWith("...") ? { name: "", kind: "spread", value: inner, valueStart: k + 1 } : { name: inner, kind: "expr", value: inner, valueStart: k + 1 });
1731
+ k = e;
1732
+ continue;
1733
+ }
1734
+ const nm = /^[^\s=>/"'{]+/.exec(s.slice(k, k + 200));
1735
+ if (!nm) {
1736
+ k++;
1737
+ continue;
1738
+ }
1739
+ const name = nm[0];
1740
+ k += name.length;
1741
+ let j = k;
1742
+ while (j < s.length && /\s/.test(s[j])) j++;
1743
+ if (s[j] !== "=") {
1744
+ attrs.push({ name, kind: "bare", value: "", valueStart: k });
1745
+ continue;
1746
+ }
1747
+ j++;
1748
+ while (j < s.length && /\s/.test(s[j])) j++;
1749
+ const q = s[j];
1750
+ const bound = lang === "vue" && /^(:|v-bind:|v-on:|@|v-)/.test(name);
1751
+ if (q === '"' || q === "'") {
1752
+ const e = s.indexOf(q, j + 1);
1753
+ const end = e < 0 ? s.length : e;
1754
+ attrs.push({ name, kind: bound ? "expr" : "static", value: s.slice(j + 1, end), valueStart: j + 1 });
1755
+ k = end + 1;
1756
+ } else if (q === "{") {
1757
+ const e = closeBrace(s, j);
1758
+ attrs.push({ name, kind: "expr", value: s.slice(j + 1, e - 1), valueStart: j + 1 });
1759
+ k = e;
1760
+ } else {
1761
+ const m = /^[^\s>]+/.exec(s.slice(j));
1762
+ const v = m?.[0].replace(/\/$/, "") ?? "";
1763
+ attrs.push({ name, kind: "static", value: v, valueStart: j });
1764
+ k = j + v.length;
1765
+ }
1766
+ }
1767
+ return { attrs, end: s.length, selfClosing: false };
1768
+ }
1769
+ var attrName = (n) => n.replace(/^(v-bind:|:|bind:)/, "");
1770
+ var pascal = (kebab) => kebab.replace(/(^|-)([a-z0-9])/g, (_m, _d, c) => c.toUpperCase());
1771
+ var NO_COLOR_ATTR_TAGS = /* @__PURE__ */ new Set(["meta", "link"]);
1772
+ function scanMarkup(path, text, lang, ds) {
1773
+ const out = { findings: [], tokenUsages: 0, themeRefs: 0, ds: {}, components: [], definitions: [] };
1774
+ const pos = lineCol(text);
1775
+ const blocks = markupBlocks(text, lang);
1776
+ const imports = /* @__PURE__ */ new Map();
1777
+ const merge = (c) => {
1778
+ out.findings.push(...c.findings);
1779
+ out.tokenUsages += c.tokenUsages;
1780
+ out.themeRefs += c.themeRefs;
1781
+ for (const [n, k] of Object.entries(c.ds)) out.ds[n] = (out.ds[n] ?? 0) + k;
1782
+ out.components.push(...c.components);
1783
+ out.definitions.push(...c.definitions);
1784
+ };
1785
+ const codeRanges = [...blocks.frontmatter ? [{ ...blocks.frontmatter, attrs: "" }] : [], ...blocks.scripts.filter((b) => isCodeScript(b.attrs))];
1786
+ for (const r of codeRanges) {
1787
+ const jsx = /^(tsx|jsx)$/i.test(attrOf(r.attrs, "lang") ?? "");
1788
+ const code = mask2(text, [r], true);
1789
+ const file = `${path}.${jsx ? "tsx" : "ts"}`;
1790
+ merge(scanCode(file, code, ds));
1791
+ for (const [k, v] of importsOf(file, code)) imports.set(k, v);
1792
+ }
1793
+ for (const b of blocks.styles) {
1794
+ const l = (attrOf(b.attrs, "lang") ?? "css").toLowerCase();
1795
+ const r = auditCssText(text.slice(b.start, b.end), l === "scss" || l === "sass" ? "scss" : l === "less" ? "less" : "css", ds, { pos, offset: b.start });
1796
+ out.findings.push(...r.findings);
1797
+ out.tokenUsages += r.tokenUsages;
1798
+ out.themeRefs += r.themeRefs;
1799
+ }
1800
+ const scanExpr = (prefix, expr, offset, suffix, keepLookalikes = false) => {
1801
+ const { line, column } = pos(offset);
1802
+ const src = line >= 2 ? `${"\n".repeat(line - 2)}${prefix}
1803
+ ${" ".repeat(column - 1)}${expr}${suffix}` : `${prefix}${expr}${suffix}`;
1804
+ const c = scanCode(`${path}.tsx`, src, ds);
1805
+ merge({ ...c, components: keepLookalikes ? c.components.filter((x) => x.kind === "recreated" && x.name.startsWith(".")) : [], definitions: [], ds: {} });
1806
+ };
1807
+ const styleAttr = (a) => {
1808
+ if (a.kind === "static") {
1809
+ const css = lang === "svelte" ? a.value.replace(/\{[^}]*\}/g, (m) => " ".repeat(m.length)) : a.value;
1810
+ const r = auditCssText(css, "css", ds, { pos, offset: a.valueStart, source: "style" });
1811
+ out.findings.push(...r.findings);
1812
+ out.tokenUsages += r.tokenUsages;
1813
+ out.themeRefs += r.themeRefs;
1814
+ return;
1815
+ }
1816
+ const str = /^(\s*)([`'"])([\s\S]*)\2\s*$/.exec(a.value);
1817
+ if (str) {
1818
+ const body = str[3].replace(/\$\{[^}]*\}/g, (m) => " ".repeat(m.length));
1819
+ const r = auditCssText(body, "css", ds, { pos, offset: a.valueStart + str[1].length + 1, source: "style" });
1820
+ out.findings.push(...r.findings);
1821
+ out.tokenUsages += r.tokenUsages;
1822
+ out.themeRefs += r.themeRefs;
1823
+ return;
1824
+ }
1825
+ scanExpr("const style: CSSProperties =", a.value, a.valueStart, ";");
1826
+ };
1827
+ const tpl = mask2(text, blocks.nonTemplate, false);
1828
+ const pending = [];
1829
+ let noscript = 0;
1830
+ const TAG = /<(\/?)([A-Za-z][\w.:-]*)/g;
1831
+ for (let m; m = TAG.exec(tpl); ) {
1832
+ const raw = m[2];
1833
+ if (m[1]) {
1834
+ if (raw.toLowerCase() === "noscript") noscript = Math.max(0, noscript - 1);
1835
+ continue;
1836
+ }
1837
+ const tag = readTag(tpl, m.index + m[0].length, lang);
1838
+ TAG.lastIndex = tag.end;
1839
+ const lower = raw.toLowerCase();
1840
+ if (lower === "noscript" && !tag.selfClosing) {
1841
+ noscript++;
1842
+ continue;
1843
+ }
1844
+ const attrs = tag.attrs;
1845
+ const get = (n) => attrs.find((a) => attrName(a.name) === n);
1846
+ for (const a of attrs) {
1847
+ const n = attrName(a.name);
1848
+ if (n === "style") styleAttr(a);
1849
+ else if (lang === "svelte" && a.name.startsWith("style:") && a.kind === "static") {
1850
+ const prop = a.name.slice(6).split("|")[0];
1851
+ const r2 = auditCssText(`${prop}:${a.value}`, "css", ds, { pos, offset: a.valueStart - prop.length - 1, source: "style" });
1852
+ out.findings.push(...r2.findings);
1853
+ out.tokenUsages += r2.tokenUsages;
1854
+ out.themeRefs += r2.themeRefs;
1855
+ } else if (/^(class|className|class:list)$/.test(n)) {
1856
+ if (a.kind === "static") {
1857
+ 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 });
1858
+ const r2 = tailwindFindings(scanClassString(a.value), a.valueStart, pos, ds);
1859
+ out.findings.push(...r2.findings);
1860
+ out.themeRefs += r2.themeRefs;
1861
+ } else if (a.kind === "expr") scanExpr("cn(", a.value, a.valueStart, ")", true);
1862
+ } else if (LOOKALIKE_ATTRS[n] && ds.components.has(LOOKALIKE_ATTRS[n])) {
1863
+ out.components.push({ ...pos(a.valueStart - a.name.length - 1), name: `[${n}]`, kind: "recreated", library: "local", suggestion: LOOKALIKE_ATTRS[n] });
1864
+ } else if (a.kind === "static" && !NO_COLOR_ATTR_TAGS.has(lower) && isColorLiteral(a.value)) {
1865
+ const suggestion = ds.suggest("color", a.value.trim(), n);
1866
+ out.findings.push({ ...pos(a.valueStart), category: "color", property: n, value: a.value.trim(), source: "jsx-attr", ...suggestion ? { suggestion } : {} });
1515
1867
  }
1516
1868
  }
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: {} };
1869
+ let name = raw;
1870
+ if (raw.includes(":")) continue;
1871
+ if (raw.includes("-")) {
1872
+ if (!imports.has(pascal(raw))) continue;
1873
+ name = pascal(raw);
1874
+ }
1875
+ const isNative = /^[a-z]/.test(name) && !name.includes(".");
1876
+ if (isNative && noscript > 0) continue;
1877
+ const type = get("type");
1878
+ const r = classifyTag({ name, ...pos(m.index), ...type?.kind === "static" ? { type: type.value } : {}, hasHref: Boolean(get("href")) }, imports, ds);
1879
+ if (r.ds) out.ds[r.ds] = (out.ds[r.ds] ?? 0) + 1;
1880
+ if (r.component) out.components.push(r.component);
1881
+ if (r.localCandidate) pending.push(r.localCandidate);
1882
+ }
1883
+ const base = /([^/]+)\.(astro|vue|svelte)$/.exec(path)?.[1];
1884
+ if (base && /^[A-Z][A-Za-z0-9]*$/.test(base)) out.definitions.push({ name: base, line: 1 });
1885
+ const defined = new Set(out.definitions.map((d) => d.name));
1886
+ out.components.push(...pending.filter((f) => defined.has(f.suggestion)));
1887
+ out.findings.sort((a, b) => a.line - b.line || a.column - b.column);
1888
+ out.components.sort((a, b) => a.line - b.line || a.column - b.column);
1889
+ return out;
1890
+ }
1891
+
1892
+ // ../core/src/audit/report.ts
1893
+ var AUDIT_EXTENSIONS = [".css", ".scss", ".sass", ".less", ".tsx", ".jsx", ".ts", ".js", ".mjs", ".cjs", ".mts", ".cts", ...MARKUP_EXTENSIONS];
1894
+ var AUDIT_IGNORE_DIRS = ["node_modules", "dist", "build", "out", "coverage", ".git", ".next", ".nuxt", ".astro", ".turbo", ".cache", ".vercel", "storybook-static", "vendor", "public"];
1895
+ 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$/;
1896
+ function isAuditable(path) {
1897
+ if (!AUDIT_EXTENSIONS.some((e) => path.endsWith(e))) return false;
1898
+ if (IGNORE_FILE.test(path)) return false;
1899
+ return !path.split("/").some((seg) => AUDIT_IGNORE_DIRS.includes(seg));
1900
+ }
1901
+ var langOf = (path) => {
1902
+ const ext = path.slice(path.lastIndexOf(".") + 1);
1903
+ if (ext === "css") return "css";
1904
+ if (ext === "scss" || ext === "sass") return "scss";
1905
+ if (ext === "less") return "less";
1906
+ if (ext === "tsx") return "tsx";
1907
+ if (ext === "jsx") return "jsx";
1908
+ if (/^[cm]?ts$/.test(ext)) return "ts";
1909
+ if (ext === "astro" || ext === "vue" || ext === "svelte") return ext;
1910
+ return "js";
1911
+ };
1912
+ function auditFile(path, text, ds) {
1913
+ const lang = langOf(path);
1914
+ if (lang === "css" || lang === "scss" || lang === "less") {
1915
+ const r = auditCssText(text, lang, ds, { pos: lineCol(text) });
1916
+ return withIgnores(text, { path, lang, tokenUsages: r.tokenUsages, themeRefs: r.themeRefs, hardcoded: 0, dsComponents: 0, otherComponents: 0, findings: r.findings, components: [], definitions: [], dsByName: {} });
1519
1917
  }
1520
- const c = scanCode(path, text, ds);
1918
+ const c = lang === "astro" || lang === "vue" || lang === "svelte" ? scanMarkup(path, text, lang, ds) : scanCode(path, text, ds);
1521
1919
  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 };
1920
+ 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 });
1921
+ }
1922
+ function withIgnores(text, f) {
1923
+ const r = applyIgnoreComments(text, f.findings, f.components);
1924
+ return {
1925
+ ...f,
1926
+ findings: r.findings,
1927
+ components: r.components,
1928
+ hardcoded: r.findings.length,
1929
+ otherComponents: r.components.length,
1930
+ ...r.ignored.length ? { ignored: r.ignored } : {},
1931
+ ...r.errors.length ? { ignoreErrors: r.errors } : {}
1932
+ };
1523
1933
  }
1524
1934
  var pct = (num2, den) => den > 0 ? Math.round(num2 / den * 1e3) / 10 : null;
1525
1935
  function buildAuditReport(input, ds) {
1526
- const reports = input.files.map((f) => auditFile(f.path, f.text, ds));
1936
+ const allow = input.allowlist ?? [];
1937
+ const usedAllow = /* @__PURE__ */ new Set();
1938
+ const reports = input.files.map((f) => {
1939
+ const r = auditFile(f.path, f.text, ds);
1940
+ const idx = allow.flatMap((e, i) => e.file === f.path ? [i] : []);
1941
+ if (!idx.length) return r;
1942
+ const a = applyAllowlist(idx.map((i) => allow[i]), r.findings, r.components);
1943
+ a.used.forEach((k) => usedAllow.add(idx[k]));
1944
+ if (!a.ignored.length) return r;
1945
+ const ignored = [...r.ignored ?? [], ...a.ignored].sort((x, y) => x.line - y.line || x.column - y.column);
1946
+ return { ...r, findings: a.findings, components: a.components, hardcoded: a.findings.length, otherComponents: a.components.length, ignored };
1947
+ });
1948
+ const ignoredItems = reports.flatMap((r) => (r.ignored ?? []).map((x) => ({ file: r.path, ...x })));
1949
+ const staleAllowlist = allow.filter((_e, i) => !usedAllow.has(i));
1527
1950
  const files = reports.map(({ definitions: _d, dsByName: _n, ...r }) => r);
1528
1951
  const byCategory = {};
1529
1952
  const fixes = /* @__PURE__ */ new Map();
@@ -1583,6 +2006,15 @@ function buildAuditReport(input, ds) {
1583
2006
  topFiles,
1584
2007
  topFixes,
1585
2008
  files: files.sort((a, b) => a.path.localeCompare(b.path)),
2009
+ ...ignoredItems.length || staleAllowlist.length ? {
2010
+ ignored: {
2011
+ total: ignoredItems.length,
2012
+ comment: ignoredItems.filter((x) => x.via === "comment").length,
2013
+ allowlist: ignoredItems.filter((x) => x.via === "allowlist").length,
2014
+ items: ignoredItems,
2015
+ staleAllowlist
2016
+ }
2017
+ } : {},
1586
2018
  history: []
1587
2019
  };
1588
2020
  }
@@ -1686,6 +2118,20 @@ function auditMarkdown(r, opts = {}) {
1686
2118
  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
2119
  lines.push("");
1688
2120
  }
2121
+ const ig = r.ignored;
2122
+ if (ig && (ig.total || ig.staleAllowlist.length)) {
2123
+ 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.`, "");
2124
+ const shown = ig.items.slice(0, opts.ignored ?? 50);
2125
+ if (shown.length) {
2126
+ lines.push("| Where | What | Reason |", "| --- | --- | --- |");
2127
+ for (const x of shown) lines.push(`| \`${esc(x.file)}:${x.line}\` | \`${esc(x.what)}\` | ${esc(x.reason)} (${x.via}) |`);
2128
+ if (ig.items.length > shown.length) lines.push(`| ... | ${ig.items.length - shown.length} more | |`);
2129
+ lines.push("");
2130
+ }
2131
+ if (ig.staleAllowlist.length) {
2132
+ 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(", ")}.`, "");
2133
+ }
2134
+ }
1689
2135
  if (b) {
1690
2136
  lines.push(`### Progress since ${b.label ?? b.generatedAt}`, "");
1691
2137
  lines.push(`- ${b.improved.length} files improved, ${b.regressed.length} regressed, ${b.newFiles.length} new, ${b.removedFiles.length} removed.`);
@@ -2425,13 +2871,16 @@ function collectAuditFiles(target, exclude = []) {
2425
2871
  }
2426
2872
  return { root: abs, files, skipped };
2427
2873
  }
2874
+ function loadAllowlist(path) {
2875
+ return parseAllowlist(JSON.parse(readFileSync(path, "utf8")));
2876
+ }
2428
2877
  function runAudit(target, opts = {}) {
2429
2878
  const dsRoot = opts.ds ?? findDesignSystemRoot(process.cwd());
2430
2879
  if (!dsRoot) throw new Error("No design system found: run from a repo with bridge.config.json or pass --ds <dir>.");
2431
2880
  const ds = loadDesignSystem(dsRoot);
2432
2881
  const { files, skipped } = collectAuditFiles(target, [...ds.config.audit?.exclude ?? [], ...opts.exclude ?? []]);
2433
2882
  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 } : {} },
2883
+ { 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
2884
  ds.index
2436
2885
  );
2437
2886
  return opts.baseline ? compareWithBaseline(report, opts.baseline) : report;
@@ -2450,6 +2899,22 @@ function fixCss(target, ds, write) {
2450
2899
  return out;
2451
2900
  }
2452
2901
 
2902
+ // ../core/src/component-spec.ts
2903
+ var COMPONENT_SPECS_VERSION = 1;
2904
+ var variantKeyOf = (props) => Object.keys(props).sort().map((k) => `${k}=${props[k]}`).join(", ") || "Default";
2905
+ function parseComponentSpecs(text) {
2906
+ let data;
2907
+ try {
2908
+ data = JSON.parse(text);
2909
+ } catch {
2910
+ throw new Error("component-specs.json is not valid JSON.");
2911
+ }
2912
+ const f = data;
2913
+ if (!f || typeof f !== "object" || !Array.isArray(f.components)) throw new Error('component-specs.json has no "components" list.');
2914
+ if (f.version !== COMPONENT_SPECS_VERSION) throw new Error(`component-specs.json is version ${String(f.version)}; this tool reads version ${COMPONENT_SPECS_VERSION}.`);
2915
+ return f;
2916
+ }
2917
+
2453
2918
  // src/index.ts
2454
2919
  import { fileURLToPath } from "node:url";
2455
2920
 
@@ -2600,6 +3065,12 @@ function classifyAuth(status, detail, opts = {}) {
2600
3065
  }
2601
3066
  return {};
2602
3067
  }
3068
+ function rateLimitMessage(x) {
3069
+ const when = x.resetsAt ? ` It resets at ${x.resetsAt.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}.` : "";
3070
+ if (x.withToken) return `GitHub's hourly request limit for this token is used up.${when} Try again after that.`;
3071
+ const hint = x.tokenHint ?? "add a GitHub token";
3072
+ 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.`;
3073
+ }
2603
3074
  var DEFAULT_RETRY = { attempts: 3, baseDelayMs: 600, sleep: (ms) => new Promise((r) => setTimeout(r, ms)) };
2604
3075
  var isTransient = (status, detail) => status === 0 || status >= 500 || status === 429 || status === 403 && /secondary rate limit/i.test(detail);
2605
3076
  var API = "https://api.github.com";
@@ -2703,12 +3174,8 @@ var GitHubClient = class _GitHubClient {
2703
3174
  const remaining = res.headers?.get("x-ratelimit-remaining");
2704
3175
  const reset = res.headers?.get("x-ratelimit-reset");
2705
3176
  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
- );
3177
+ const resetsAt = reset ? new Date(Number(reset) * 1e3) : void 0;
3178
+ throw new GitHubError(429, rateLimitMessage({ withToken: Boolean(this.token), resetsAt, tokenHint: this.options.tokenHint }), detail);
2712
3179
  }
2713
3180
  const info = classifyAuth(res.status, detail, {
2714
3181
  path,
@@ -3472,6 +3939,132 @@ function jiraIssueFields(r, jira, issueUrl) {
3472
3939
  };
3473
3940
  }
3474
3941
 
3942
+ // ../core/src/fidelity-references.ts
3943
+ var FIGMA_REFERENCES_DIR = "figma-references";
3944
+ var FIGMA_REFERENCES_MANIFEST = `${FIGMA_REFERENCES_DIR}/manifest.json`;
3945
+ var FIGMA_REFERENCE_STORY = "internal-figma-reference--variant";
3946
+ var FIGMA_FILL_WIDTH_TOKEN = "size.80";
3947
+ var STATE_PSEUDO = {
3948
+ default: "rest",
3949
+ rest: "rest",
3950
+ enabled: "rest",
3951
+ hover: "hover",
3952
+ hovered: "hover",
3953
+ pressed: "active",
3954
+ active: "active",
3955
+ focus: "focus-visible",
3956
+ focused: "focus-visible",
3957
+ "focus visible": "focus-visible",
3958
+ disabled: "disabled"
3959
+ };
3960
+ var iconName = (component) => component.replace(/^Icon\//, "");
3961
+ var exportName = (name) => name.replace(/[^A-Za-z0-9]+/g, "");
3962
+ function planReference(spec, variant, file) {
3963
+ if (spec.view === "popup") return { ok: false, reason: "drawn open in Figma; the code image shows it closed" };
3964
+ const props = { ...spec.fixed ?? {} };
3965
+ let pseudo;
3966
+ for (const axis of spec.axes) {
3967
+ const value = variant.props[axis.name];
3968
+ if (value === void 0) return { ok: false, reason: `the variant has no ${axis.name} value` };
3969
+ if (axis.prop) {
3970
+ props[axis.prop] = value === "true" ? true : value === "false" ? false : value;
3971
+ continue;
3972
+ }
3973
+ if (axis.name === "Checked") {
3974
+ props.checked = value === "true";
3975
+ if (value === "mixed") props.indeterminate = true;
3976
+ continue;
3977
+ }
3978
+ if (axis.name.toLowerCase() !== "state") return { ok: false, reason: `${axis.name} has no matching option in code` };
3979
+ const s = STATE_PSEUDO[value.toLowerCase()];
3980
+ if (!s) return { ok: false, reason: `the ${value} state has no code equivalent to render` };
3981
+ if (s === "disabled") props.disabled = true;
3982
+ else if (s !== "rest") pseudo = s;
3983
+ }
3984
+ const root = spec.parts.find((p) => !p.parent);
3985
+ if (!root) return { ok: false, reason: "the spec has no root layer" };
3986
+ const children = (id) => spec.parts.filter((p) => p.parent === id);
3987
+ const textOf = (part) => {
3988
+ const swapTo = variant.slots?.[part.id] ?? part.swap?.default;
3989
+ if (part.swap && swapTo) {
3990
+ const slot = file?.slots?.find((s) => s.name === swapTo);
3991
+ if (!slot) return { reason: `${swapTo} is not in the specs` };
3992
+ if (slot.kind !== "text" || !slot.text) return { reason: `Figma shows a placeholder (${swapTo}) that code has no example of` };
3993
+ return slot.text.default;
3994
+ }
3995
+ const prop = spec.properties.find((p) => p.name === part.text && p.type === "TEXT");
3996
+ return typeof prop?.default === "string" ? prop.default : part.name;
3997
+ };
3998
+ let text;
3999
+ const icons = {};
4000
+ for (const prop of spec.properties) {
4001
+ if (prop.type === "TEXT") {
4002
+ const hidden = spec.properties.some((b) => b.type === "BOOLEAN" && b.prop === prop.prop && b.default === false);
4003
+ if (hidden && prop.prop) continue;
4004
+ if (prop.prop === "children") text = String(prop.default);
4005
+ else if (prop.prop) props[prop.prop] = String(prop.default);
4006
+ continue;
4007
+ }
4008
+ const iconPart = spec.parts.find((p) => p.kind === "icon" && p.visible === prop.name);
4009
+ if (prop.default === true && !iconPart && prop.prop && props[prop.prop] === void 0 && spec.example?.p?.[prop.prop] === void 0) props[prop.prop] = true;
4010
+ }
4011
+ for (const part of spec.parts) {
4012
+ if (part.kind !== "icon" || !part.swap) continue;
4013
+ const shown = part.visible ? spec.properties.find((p) => p.name === part.visible)?.default === true : true;
4014
+ if (!shown) continue;
4015
+ if (!part.swap.prop) return { ok: false, reason: `the ${part.name} icon has no matching option in code` };
4016
+ icons[part.swap.prop] = iconName(variant.slots?.[part.id] ?? part.swap.default);
4017
+ }
4018
+ let parts;
4019
+ const loose = spec.properties.filter((p) => p.type === "TEXT" && !p.prop);
4020
+ const frames = children(root.id).filter((p) => p.kind === "frame");
4021
+ const compound = text === void 0 && frames.length > 0 && frames.every((f) => spec.parts.some((p) => p.parent === f.id && p.kind === "text" && (p.swap || p.text === f.name)));
4022
+ if (compound) {
4023
+ parts = [];
4024
+ for (const f of frames) {
4025
+ const t = spec.parts.find((p) => p.parent === f.id && p.kind === "text");
4026
+ const value = textOf(t);
4027
+ if (typeof value !== "string") return { ok: false, reason: value.reason };
4028
+ parts.push({ component: `${exportName(spec.name)}${exportName(f.name)}`, text: value });
4029
+ }
4030
+ }
4031
+ for (const p of loose) {
4032
+ if (p.name === "Value") continue;
4033
+ const part = spec.parts.find((x) => x.text === p.name);
4034
+ if (compound && part && part.parent !== root.id) continue;
4035
+ if (part && /placeholder$/.test(part.id)) {
4036
+ props.placeholder = String(p.default);
4037
+ continue;
4038
+ }
4039
+ return { ok: false, reason: `the ${p.name} text has no matching option in code` };
4040
+ }
4041
+ const example = spec.example;
4042
+ const exampleChildren = example?.children?.length ? example.children : void 0;
4043
+ return {
4044
+ ok: true,
4045
+ render: {
4046
+ component: exportName(spec.view || spec.fixed || example ? example?.c ?? spec.component ?? spec.name : spec.name),
4047
+ props: { ...example?.p ?? {}, ...props },
4048
+ ...exampleChildren ? { children: exampleChildren } : {},
4049
+ ...text !== void 0 && !exampleChildren ? { text } : {},
4050
+ ...parts && !exampleChildren ? { parts } : {},
4051
+ ...Object.keys(icons).length ? { icons } : {},
4052
+ ...pseudo ? { pseudo } : {},
4053
+ ...root.fill ? { width: FIGMA_FILL_WIDTH_TOKEN } : {}
4054
+ }
4055
+ };
4056
+ }
4057
+ function referenceImagePath(component, key) {
4058
+ const slug = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
4059
+ const file = key.split(",").map((kv) => slug(kv)).filter(Boolean).join("__");
4060
+ return `${slug(component)}/${file || "default"}.png`;
4061
+ }
4062
+ function planReferences(file) {
4063
+ return file.components.flatMap(
4064
+ (spec) => spec.variants.map((v) => ({ component: spec.name, specHash: spec.hash, key: variantKeyOf(v.props), plan: planReference(spec, v, file) }))
4065
+ );
4066
+ }
4067
+
3475
4068
  // ../visual/src/capture.ts
3476
4069
  import { mkdir, writeFile } from "node:fs/promises";
3477
4070
  import { dirname as dirname2 } from "node:path";
@@ -3495,8 +4088,8 @@ async function launch(o = {}) {
3495
4088
  args: ["--lang=en-US", "--disable-gpu", "--font-render-hinting=none", "--disable-lcd-text", "--force-color-profile=srgb", "--hide-scrollbars", "--disable-skia-runtime-opts", "--disable-renderer-backgrounding"]
3496
4089
  });
3497
4090
  }
3498
- async function newContext(browser, viewport) {
3499
- const ctx = await browser.newContext({ viewport, deviceScaleFactor: 1, reducedMotion: "reduce", colorScheme: "light", locale: "en-US", timezoneId: "UTC", serviceWorkers: "block" });
4091
+ async function newContext(browser, viewport, deviceScaleFactor = 1) {
4092
+ const ctx = await browser.newContext({ viewport, deviceScaleFactor, reducedMotion: "reduce", colorScheme: "light", locale: "en-US", timezoneId: "UTC", serviceWorkers: "block" });
3500
4093
  await ctx.addInitScript(INIT_SCRIPT);
3501
4094
  await ctx.route(
3502
4095
  /^https?:\/\/(?!127\.0\.0\.1[:/]|localhost[:/])/,
@@ -3929,9 +4522,9 @@ function selectImpact(input) {
3929
4522
  app
3930
4523
  };
3931
4524
  }
3932
- function storyId(title, exportName) {
4525
+ function storyId(title, exportName2) {
3933
4526
  const sanitize = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-+/, "").replace(/-+$/, "");
3934
- const startCase = exportName.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z])([A-Z][a-z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
4527
+ const startCase = exportName2.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z])([A-Z][a-z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
3935
4528
  return `${sanitize(title)}--${sanitize(startCase)}`;
3936
4529
  }
3937
4530
  function storiesFromSource(files) {
@@ -4446,9 +5039,105 @@ async function runImpact(o) {
4446
5039
  return result;
4447
5040
  }
4448
5041
 
5042
+ // ../visual/src/references.ts
5043
+ import { existsSync as existsSync5 } from "node:fs";
5044
+ import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from "node:fs/promises";
5045
+ import { dirname as dirname4, join as join5 } from "node:path";
5046
+ async function captureFigmaReferences(o) {
5047
+ const log = o.log ?? (() => void 0);
5048
+ const scale = o.scale ?? 2;
5049
+ const brand2 = o.brand ?? "bridge";
5050
+ const theme = o.theme ?? "light";
5051
+ if (!existsSync5(join5(o.storybook, "index.json"))) throw new Error(`No built Storybook in ${o.storybook}: run npm run build-storybook first.`);
5052
+ const index = JSON.parse(await readFile4(join5(o.storybook, "index.json"), "utf8"));
5053
+ if (!index.entries[FIGMA_REFERENCE_STORY]) throw new Error(`The Storybook build has no ${FIGMA_REFERENCE_STORY} story (packages/react/src/stories/FigmaReference.stories.tsx).`);
5054
+ const specs = parseComponentSpecs(await readFile4(o.specs, "utf8"));
5055
+ const jobs = planReferences(specs).filter((j) => !o.only?.length || o.only.includes(j.component));
5056
+ const manifest = {
5057
+ version: 1,
5058
+ tool: "@bridge-ds/visual",
5059
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
5060
+ sha: o.sha,
5061
+ scale,
5062
+ mode: { brand: brand2, theme, density: "comfortable" },
5063
+ storyId: FIGMA_REFERENCE_STORY,
5064
+ components: {}
5065
+ };
5066
+ for (const j of jobs) manifest.components[j.component] ??= { specHash: j.specHash, variants: {} };
5067
+ let captured = 0;
5068
+ let skipped = 0;
5069
+ const served = await serveDir(o.storybook);
5070
+ const browser = await launch();
5071
+ try {
5072
+ const ctx = await newContext(browser, { width: 1200, height: 800 }, scale);
5073
+ const page = await newPage(ctx);
5074
+ await page.goto(storyUrl(served.url, FIGMA_REFERENCE_STORY, brand2, theme), { waitUntil: "load", timeout: 3e4 });
5075
+ await page.waitForSelector("[data-fidelity-root], [data-fidelity-error]", { state: "attached", timeout: 2e4 });
5076
+ await settle(page, { busy: false });
5077
+ log(`Capturing ${jobs.length} variants at ${scale}x (${brand2} ${theme})`);
5078
+ for (const j of jobs) {
5079
+ const variants = manifest.components[j.component].variants;
5080
+ if (!j.plan.ok) {
5081
+ variants[j.key] = { skipped: j.plan.reason };
5082
+ skipped++;
5083
+ continue;
5084
+ }
5085
+ const plan = JSON.stringify(j.plan.render);
5086
+ await page.evaluate(
5087
+ ({ id, plan: plan2 }) => new Promise((resolve4) => {
5088
+ const ch = window.__STORYBOOK_ADDONS_CHANNEL__;
5089
+ const done = () => {
5090
+ clearTimeout(timer);
5091
+ ch.off("storyRendered", done);
5092
+ resolve4();
5093
+ };
5094
+ const timer = setTimeout(done, 1e4);
5095
+ ch.on("storyRendered", done);
5096
+ ch.emit("updateStoryArgs", { storyId: id, updatedArgs: { plan: plan2 } });
5097
+ }),
5098
+ { id: FIGMA_REFERENCE_STORY, plan }
5099
+ );
5100
+ await settle(page, { network: false, quiet: 2, interval: 40, busy: false });
5101
+ const error = await page.locator("[data-fidelity-error]").first().textContent({ timeout: 200 }).catch(() => null);
5102
+ let target = page.locator("[data-fidelity-root] > *").first();
5103
+ if (!error && !await target.count()) {
5104
+ const marked = page.locator("[data-fidelity-target]").first();
5105
+ await marked.waitFor({ state: "visible", timeout: 3e3 }).catch(() => void 0);
5106
+ await settle(page, { network: false, quiet: 2, interval: 40, busy: false });
5107
+ target = marked;
5108
+ }
5109
+ if (error || !await target.count()) {
5110
+ variants[j.key] = { skipped: error ?? "the code example did not render" };
5111
+ skipped++;
5112
+ continue;
5113
+ }
5114
+ const file = referenceImagePath(j.component, j.key);
5115
+ const path = join5(o.out, file);
5116
+ await mkdir4(dirname4(path), { recursive: true });
5117
+ await target.screenshot({ path, omitBackground: true, animations: "disabled", caret: "hide", scale: "device" });
5118
+ const box = await target.boundingBox();
5119
+ variants[j.key] = { file, width: Math.round(box?.width ?? 0), height: Math.round(box?.height ?? 0) };
5120
+ captured++;
5121
+ }
5122
+ await ctx.close();
5123
+ } finally {
5124
+ await browser.close().catch(() => void 0);
5125
+ await served.close();
5126
+ }
5127
+ await mkdir4(o.out, { recursive: true });
5128
+ const manifestPath = join5(o.out, "manifest.json");
5129
+ await writeFile4(manifestPath, `${JSON.stringify(manifest, null, 2)}
5130
+ `);
5131
+ log(`${captured} images, ${skipped} variants skipped. ${manifestPath}`);
5132
+ return { manifest, manifestPath, captured, skipped };
5133
+ }
5134
+
5135
+ // src/index.ts
5136
+ import { execFileSync as execFileSync2 } from "node:child_process";
5137
+
4449
5138
  // src/request.ts
4450
5139
  import { execFileSync, spawnSync } from "node:child_process";
4451
- import { readFile as readFile4 } from "node:fs/promises";
5140
+ import { readFile as readFile5 } from "node:fs/promises";
4452
5141
  import { BridgeContext as BridgeContext2, requests as requests2 } from "@bridge-ds/mcp";
4453
5142
 
4454
5143
  // src/jira-sync.ts
@@ -4722,7 +5411,7 @@ ${r.issue.body}`);
4722
5411
  let event = {};
4723
5412
  if (eventPath) {
4724
5413
  try {
4725
- event = JSON.parse(await readFile4(eventPath, "utf8"));
5414
+ event = JSON.parse(await readFile5(eventPath, "utf8"));
4726
5415
  } catch {
4727
5416
  throw new RequestUsageError(`Could not read the GitHub event at ${eventPath}.`);
4728
5417
  }
@@ -4914,15 +5603,26 @@ Usage:
4914
5603
  visual impact: build Storybook and the app at both refs, screenshot
4915
5604
  what the change touches, pixel-diff, write manifest.json + images.
4916
5605
  Exit 0 even with diffs (2 on setup errors). Needs Chrome installed.
5606
+ bridge figma-references [--storybook storybook-static] [--specs packages/react/component-specs.json]
5607
+ [--out figma-references] [--sha <sha>] [--only Button,Card] [--json]
5608
+ code images for the Figma fidelity check: one PNG per generated
5609
+ Figma variant from a built Storybook, plus manifest.json.
5610
+ Needs Chrome installed
4917
5611
  bridge history [token] [--json] [--limit n] [--author a] [--tier n] [--version v]
4918
5612
  token history: versions, merged changes, who, when, why
4919
5613
  bridge revert <pr|commit|id> [--dry-run] [--restore-conflicts] [--use-before path@file] [--json]
4920
5614
  undo a merged token change against HEAD: conflicts shown,
4921
5615
  new local bridge/* branch (never pushes, never main)
4922
5616
  bridge audit <path> [--json [out.json]] [--md [out.md]] [--fail-under 80] [--baseline prev.json]
4923
- [--label "..."] [--ds dir] [--no-regress]
5617
+ [--label "..."] [--ds dir] [--no-regress] [--allowlist known.json]
4924
5618
  design system adoption in a product folder: token and component
4925
5619
  adoption, hardcoded values with the closest token, top files.
5620
+ Reads CSS, SCSS, Less, TS/JS, TSX/JSX, and Astro, Vue and Svelte
5621
+ (template markup, frontmatter and <script>, <style> blocks).
5622
+ Leave one exception out with a comment on the line above:
5623
+ <!-- bridge-audit-ignore-next-line: reason --> (or // or /* */);
5624
+ --allowlist lists known file:line exceptions, each with a reason.
5625
+ Ignored items stay listed in the report.
4926
5626
  Exit 0 ok, 1 under --fail-under or regressed, 2 setup error
4927
5627
  bridge audit <path> --fix [--write] safe autofix: CSS values equal to a token become var(--token).
4928
5628
  Dry run unless --write
@@ -4974,9 +5674,9 @@ var print = (s) => process.stdout.write(`${s}
4974
5674
  `);
4975
5675
  var modeArg = (s) => s ? Object.fromEntries(s.split(",").map((kv) => kv.split("="))) : void 0;
4976
5676
  async function init(a, root) {
4977
- const cfgPath = join5(root, CONFIG_FILE2);
5677
+ const cfgPath = join6(root, CONFIG_FILE2);
4978
5678
  let config;
4979
- if (existsSync5(cfgPath) && !has3(a, "force")) {
5679
+ if (existsSync6(cfgPath) && !has3(a, "force")) {
4980
5680
  print(`${CONFIG_FILE2} exists, keeping it (--force to regenerate).`);
4981
5681
  config = (await (await BridgeContext4.create({ root })).config()).config;
4982
5682
  } else {
@@ -4984,7 +5684,7 @@ async function init(a, root) {
4984
5684
  const files = await src.list("");
4985
5685
  const repo = await originRepo(root);
4986
5686
  config = await detectConfig(files, (p) => src.read(p), repo ? { repo } : {});
4987
- await writeFile4(cfgPath, `${JSON.stringify(toConfigJson(config, "https://raw.githubusercontent.com/peterbono/ai-design-system-demo/main/packages/core/bridge.config.schema.json"), null, 2)}
5687
+ await writeFile5(cfgPath, `${JSON.stringify(toConfigJson(config, "https://raw.githubusercontent.com/peterbono/ai-design-system-demo/main/packages/core/bridge.config.schema.json"), null, 2)}
4988
5688
  `);
4989
5689
  print(`Wrote ${CONFIG_FILE2}: tokens in ${config.tokens.root}, ${config.tokens.layers.length} layers, modes ${Object.keys(config.axes).join(", ") || "none"}.`);
4990
5690
  print("Check the layers order (lowest first) and the tiers; detection is a starting point.");
@@ -4993,10 +5693,10 @@ async function init(a, root) {
4993
5693
  const relServer = relative4(root, serverJs);
4994
5694
  const inRepo = !relServer.startsWith("..") && !isAbsolute(relServer);
4995
5695
  const server = (dir) => inRepo ? { command: "node", args: [relServer, "--root", dir] } : { command: "npx", args: ["-y", MCP_PACKAGE, "--root", dir] };
4996
- const mcpPath = join5(root, ".mcp.json");
4997
- const existing = existsSync5(mcpPath) ? JSON.parse(await readFile5(mcpPath, "utf8")) : {};
5696
+ const mcpPath = join6(root, ".mcp.json");
5697
+ const existing = existsSync6(mcpPath) ? JSON.parse(await readFile6(mcpPath, "utf8")) : {};
4998
5698
  existing.mcpServers = { ...existing.mcpServers ?? {}, bridge: server(".") };
4999
- await writeFile4(mcpPath, `${JSON.stringify(existing, null, 2)}
5699
+ await writeFile5(mcpPath, `${JSON.stringify(existing, null, 2)}
5000
5700
  `);
5001
5701
  print(`Wrote .mcp.json: Claude Code picks up the "bridge" server next time it starts in ${root}.`);
5002
5702
  const desktop = inRepo ? { command: "node", args: [serverJs, "--root", root] } : server(root);
@@ -5119,7 +5819,7 @@ async function propose(a, root) {
5119
5819
  edits.push({ op: "set", path: s.slice(0, i), value: s.slice(i + 1), ...file ? { file } : {}, ...mode2 ? { mode: mode2 } : {} });
5120
5820
  }
5121
5821
  const editsFile = flag3(a, "edits");
5122
- if (editsFile) edits.push(...JSON.parse(await readFile5(resolve3(editsFile), "utf8")));
5822
+ if (editsFile) edits.push(...JSON.parse(await readFile6(resolve3(editsFile), "utf8")));
5123
5823
  if (!edits.length) throw new UsageError("Give at least one --set path=value or --edits file.json");
5124
5824
  const ctx = await BridgeContext4.create({ root });
5125
5825
  const summary = flag3(a, "summary");
@@ -5185,6 +5885,27 @@ async function brandCmd(a, root) {
5185
5885
  if (r.hint) print(String(r.hint));
5186
5886
  return r.status === "blocked" ? 1 : 0;
5187
5887
  }
5888
+ async function figmaReferences(a, root) {
5889
+ let sha = flag3(a, "sha");
5890
+ if (!sha) {
5891
+ try {
5892
+ sha = execFileSync2("git", ["rev-parse", "HEAD"], { cwd: root, encoding: "utf8" }).trim();
5893
+ } catch {
5894
+ sha = "unknown";
5895
+ }
5896
+ }
5897
+ const only = (flag3(a, "only") ?? "").split(",").filter(Boolean);
5898
+ const r = await captureFigmaReferences({
5899
+ storybook: resolve3(root, flag3(a, "storybook") ?? "storybook-static"),
5900
+ specs: resolve3(root, flag3(a, "specs") ?? "packages/react/component-specs.json"),
5901
+ out: resolve3(root, flag3(a, "out") ?? "figma-references"),
5902
+ sha,
5903
+ ...only.length ? { only } : {},
5904
+ log: has3(a, "json") ? void 0 : (line) => print(line)
5905
+ });
5906
+ if (has3(a, "json")) print(JSON.stringify({ manifest: r.manifestPath, captured: r.captured, skipped: r.skipped }, null, 2));
5907
+ return 0;
5908
+ }
5188
5909
  async function impact(a, root) {
5189
5910
  const only = (flag3(a, "only") ?? "").split(",").filter(Boolean);
5190
5911
  for (const x of only) if (x !== "stories" && x !== "app") throw new UsageError(`--only takes stories or app, got "${x}"`);
@@ -5294,8 +6015,8 @@ ${r.prBody}`);
5294
6015
  async function audit(a, root) {
5295
6016
  const target = a._[1];
5296
6017
  if (!target) throw new UsageError("bridge audit <path> (a product folder or file that uses the design system)");
5297
- if (!existsSync5(target)) throw new UsageError(`No such file or folder: ${target}`);
5298
- const dsRoot = flag3(a, "ds") ? resolve3(flag3(a, "ds")) : existsSync5(join5(root, CONFIG_FILE2)) ? root : findDesignSystemRoot(root);
6018
+ if (!existsSync6(target)) throw new UsageError(`No such file or folder: ${target}`);
6019
+ const dsRoot = flag3(a, "ds") ? resolve3(flag3(a, "ds")) : existsSync6(join6(root, CONFIG_FILE2)) ? root : findDesignSystemRoot(root);
5299
6020
  if (!dsRoot) throw new UsageError(`No ${CONFIG_FILE2} found: run bridge audit from the design system repo or pass --ds <dir>.`);
5300
6021
  const ds = loadDesignSystem(dsRoot);
5301
6022
  if (has3(a, "fix")) {
@@ -5309,17 +6030,25 @@ async function audit(a, root) {
5309
6030
  const baselinePath = flag3(a, "baseline");
5310
6031
  let baseline;
5311
6032
  if (baselinePath) {
5312
- if (!existsSync5(baselinePath)) throw new UsageError(`Baseline not found: ${baselinePath}`);
5313
- baseline = JSON.parse(await readFile5(baselinePath, "utf8"));
6033
+ if (!existsSync6(baselinePath)) throw new UsageError(`Baseline not found: ${baselinePath}`);
6034
+ baseline = JSON.parse(await readFile6(baselinePath, "utf8"));
5314
6035
  }
5315
6036
  const label = flag3(a, "label");
5316
- const report = runAudit(target, { ds: dsRoot, version: VERSION, ...label ? { label } : {}, ...baseline ? { baseline } : {} });
6037
+ const allowlistPath = flag3(a, "allowlist");
6038
+ if (allowlistPath && !existsSync6(allowlistPath)) throw new UsageError(`Allowlist not found: ${allowlistPath}`);
6039
+ let allowlist;
6040
+ try {
6041
+ allowlist = allowlistPath ? loadAllowlist(allowlistPath) : void 0;
6042
+ } catch (e) {
6043
+ throw new UsageError(`${allowlistPath}: ${e.message}`);
6044
+ }
6045
+ const report = runAudit(target, { ds: dsRoot, version: VERSION, ...label ? { label } : {}, ...baseline ? { baseline } : {}, ...allowlist ? { allowlist } : {} });
5317
6046
  const md = auditMarkdown(report);
5318
6047
  const jsonOut = flag3(a, "json");
5319
6048
  const mdOut = flag3(a, "md");
5320
- if (jsonOut && jsonOut !== "true") await writeFile4(jsonOut, `${JSON.stringify(report, null, 2)}
6049
+ if (jsonOut && jsonOut !== "true") await writeFile5(jsonOut, `${JSON.stringify(report, null, 2)}
5321
6050
  `);
5322
- if (mdOut && mdOut !== "true") await writeFile4(mdOut, `${md}
6051
+ if (mdOut && mdOut !== "true") await writeFile5(mdOut, `${md}
5323
6052
  `);
5324
6053
  if (jsonOut === "true") print(JSON.stringify(report, null, 2));
5325
6054
  else if (mdOut === "true") print(md);
@@ -5344,9 +6073,17 @@ async function audit(a, root) {
5344
6073
  print(" top fixes:");
5345
6074
  for (const f of fixes) print(` ${f.value} -> var(${f.suggestion.cssVar}) ${f.suggestion.level}, ${f.count}x`);
5346
6075
  }
6076
+ const ig = report.ignored;
6077
+ if (ig?.total) print(` ignored on purpose ${ig.total} (${ig.comment} by comment, ${ig.allowlist} by the allowlist); listed in --json and --md`);
6078
+ if (ig?.staleAllowlist.length) {
6079
+ print(` allowlist entries that match nothing any more (remove them):`);
6080
+ for (const e of ig.staleAllowlist) print(` ${e.file}:${e.line}${e.what ? ` ${e.what}` : ""}`);
6081
+ }
5347
6082
  if (jsonOut) print(`Wrote ${jsonOut}`);
5348
6083
  if (mdOut) print(`Wrote ${mdOut}`);
5349
6084
  }
6085
+ for (const f of report.files) for (const e of f.ignoreErrors ?? []) process.stderr.write(`${report.target.path}/${f.path}:${e.line}: ${e.message}
6086
+ `);
5350
6087
  let code = 0;
5351
6088
  const failUnder = flag3(a, "fail-under");
5352
6089
  if (failUnder !== void 0) {
@@ -5429,6 +6166,8 @@ async function main() {
5429
6166
  return brandCmd(a, root);
5430
6167
  case "impact":
5431
6168
  return impact(a, root);
6169
+ case "figma-references":
6170
+ return figmaReferences(a, root);
5432
6171
  case "history":
5433
6172
  return historyCmd(a, root);
5434
6173
  case "revert":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bridge-ds/cli",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
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.3",
47
+ "@bridge-ds/visual": "^0.3.0",
48
48
  "typescript": "5.9.3"
49
49
  },
50
50
  "devDependencies": {