@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.
- package/dist/index.js +883 -144
- 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
|
|
5
|
-
import { readFile as
|
|
6
|
-
import { isAbsolute, join as
|
|
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
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
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
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
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/
|
|
1442
|
-
var
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
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
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
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
|
|
1485
|
-
const
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
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
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
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
|
-
|
|
1518
|
-
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
4525
|
+
function storyId(title, exportName2) {
|
|
3933
4526
|
const sanitize = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-+/, "").replace(/-+$/, "");
|
|
3934
|
-
const startCase =
|
|
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
|
|
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
|
|
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 =
|
|
5677
|
+
const cfgPath = join6(root, CONFIG_FILE2);
|
|
4978
5678
|
let config;
|
|
4979
|
-
if (
|
|
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
|
|
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 =
|
|
4997
|
-
const existing =
|
|
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
|
|
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
|
|
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 (!
|
|
5298
|
-
const dsRoot = flag3(a, "ds") ? resolve3(flag3(a, "ds")) :
|
|
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 (!
|
|
5313
|
-
baseline = JSON.parse(await
|
|
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
|
|
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
|
|
6049
|
+
if (jsonOut && jsonOut !== "true") await writeFile5(jsonOut, `${JSON.stringify(report, null, 2)}
|
|
5321
6050
|
`);
|
|
5322
|
-
if (mdOut && mdOut !== "true") await
|
|
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
|
+
"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.
|
|
47
|
-
"@bridge-ds/visual": "^0.
|
|
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": {
|