@bridge-ds/cli 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +624 -130
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { existsSync as existsSync5 } from "node:fs";
|
|
5
5
|
import { readFile as readFile5, writeFile as writeFile4 } from "node:fs/promises";
|
|
6
|
-
import { join as join5, relative as relative4, resolve as resolve3 } from "node:path";
|
|
6
|
+
import { isAbsolute, join as join5, 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,78 @@ function contextForDeclaration(name, typeText) {
|
|
|
1166
1190
|
if (/^(styles?|sx|classes)$|Styles?$|Sx$/.test(name)) return /sx$/i.test(name) ? "sx" : "styled";
|
|
1167
1191
|
return null;
|
|
1168
1192
|
}
|
|
1193
|
+
function importsOf(path, text) {
|
|
1194
|
+
const sf = ts.createSourceFile(path, text, ts.ScriptTarget.Latest, false, kindFor(path));
|
|
1195
|
+
const imports = /* @__PURE__ */ new Map();
|
|
1196
|
+
for (const node of sf.statements) {
|
|
1197
|
+
if (!ts.isImportDeclaration(node) || !ts.isStringLiteral(node.moduleSpecifier)) continue;
|
|
1198
|
+
const mod = node.moduleSpecifier.text;
|
|
1199
|
+
const c = node.importClause;
|
|
1200
|
+
if (c?.name) imports.set(c.name.text, { module: mod, imported: "default" });
|
|
1201
|
+
if (c?.namedBindings && ts.isNamespaceImport(c.namedBindings)) imports.set(c.namedBindings.name.text, { module: mod, imported: "*" });
|
|
1202
|
+
if (c?.namedBindings && ts.isNamedImports(c.namedBindings)) for (const el of c.namedBindings.elements) imports.set(el.name.text, { module: mod, imported: (el.propertyName ?? el.name).text });
|
|
1203
|
+
}
|
|
1204
|
+
return imports;
|
|
1205
|
+
}
|
|
1206
|
+
function classifyTag(t, imports, ds) {
|
|
1207
|
+
const { name } = t;
|
|
1208
|
+
const at = { line: t.line, column: t.column };
|
|
1209
|
+
if (/^[a-z][\w-]*$/.test(name)) {
|
|
1210
|
+
if (!ds.nativeElements.includes(name)) return {};
|
|
1211
|
+
if (name === "input" && t.type === "hidden") return {};
|
|
1212
|
+
if (name === "a" && !t.hasHref) return {};
|
|
1213
|
+
if (t.inRenderProp) return {};
|
|
1214
|
+
const suggestion = nativeEquivalent(name, t.type, ds.components);
|
|
1215
|
+
return { component: { ...at, name, kind: "native", library: "native", ...suggestion ? { suggestion } : {} } };
|
|
1216
|
+
}
|
|
1217
|
+
const root = name.split(".")[0];
|
|
1218
|
+
const imp = imports.get(root);
|
|
1219
|
+
const ns = imp?.imported === "*";
|
|
1220
|
+
const shown = imp && !ns && imp.imported !== "default" ? [imp.imported, ...name.split(".").slice(1)].join(".") : name;
|
|
1221
|
+
const leaf = ns && name.includes(".") ? name.split(".").slice(1).join(".") : shown;
|
|
1222
|
+
if (imp) {
|
|
1223
|
+
const kind = classifyModule(imp.module, ds.packages, ds.foreignLibraries);
|
|
1224
|
+
if (isNonVisual(leaf.split(".").pop())) return {};
|
|
1225
|
+
if (kind === "ds") return { ds: leaf };
|
|
1226
|
+
if (kind === "charts") return { component: { ...at, name: shown, kind: "foreign", library: libraryLabel(imp.module, kind) } };
|
|
1227
|
+
if (kind === "other" && isRouterLink(imp.module, imp.imported === "default" ? "default" : leaf)) {
|
|
1228
|
+
if (t.inRenderProp) return {};
|
|
1229
|
+
const suggestion = ds.components.has("Link") ? "Link" : void 0;
|
|
1230
|
+
return { component: { ...at, name: shown, kind: "foreign", library: libraryLabel(imp.module, kind), ...suggestion ? { suggestion } : {} } };
|
|
1231
|
+
}
|
|
1232
|
+
if (kind === "foreign" || kind === "shadcn") {
|
|
1233
|
+
const suggestion = (ns ? dsEquivalent(root, ds.components) : void 0) ?? dsEquivalent(leaf, ds.components);
|
|
1234
|
+
return { component: { ...at, name: shown, kind: "foreign", library: libraryLabel(imp.module, kind), ...suggestion ? { suggestion } : {} } };
|
|
1235
|
+
}
|
|
1236
|
+
if (kind === "local" && ds.components.has(leaf.split(".")[0])) return { component: { ...at, name: leaf, kind: "recreated", library: "local", suggestion: leaf.split(".")[0] } };
|
|
1237
|
+
return {};
|
|
1238
|
+
}
|
|
1239
|
+
if (ds.components.has(root)) return { localCandidate: { ...at, name, kind: "recreated", library: "local", suggestion: root } };
|
|
1240
|
+
return {};
|
|
1241
|
+
}
|
|
1242
|
+
function tailwindFindings(hits, offset, pos, ds) {
|
|
1243
|
+
const findings = [];
|
|
1244
|
+
let themeRefs = 0;
|
|
1245
|
+
for (const h of hits) {
|
|
1246
|
+
if (h.kind === "scale") {
|
|
1247
|
+
themeRefs++;
|
|
1248
|
+
continue;
|
|
1249
|
+
}
|
|
1250
|
+
if (h.kind === "token") continue;
|
|
1251
|
+
let cat = h.category;
|
|
1252
|
+
const v = h.value;
|
|
1253
|
+
const isColor = Boolean(parseCssColor(v));
|
|
1254
|
+
if (cat === "color-or-size") cat = isColor ? "color" : "font-size";
|
|
1255
|
+
else if (cat === "color-or-width") cat = isColor ? "color" : "border-width";
|
|
1256
|
+
else if (cat === "color-or-shadow") cat = isColor ? "color" : "shadow";
|
|
1257
|
+
else if (cat === "font") cat = /^\d{3}$/.test(v) ? "font-weight" : "font-family";
|
|
1258
|
+
if (cat === "color" && !isColor) continue;
|
|
1259
|
+
const suggestion = ds.suggest(cat, v);
|
|
1260
|
+
const cap = h.kind === "palette" && suggestion && suggestion.confidence > 0.6 ? { ...suggestion, confidence: 0.6, level: "medium" } : suggestion;
|
|
1261
|
+
findings.push({ ...pos(offset + h.index), category: cat, value: h.cls, source: "tailwind", ...cap ? { suggestion: cap } : {} });
|
|
1262
|
+
}
|
|
1263
|
+
return { findings, themeRefs };
|
|
1264
|
+
}
|
|
1169
1265
|
function scanCode(path, text, ds) {
|
|
1170
1266
|
const sf = ts.createSourceFile(path, text, ts.ScriptTarget.Latest, true, kindFor(path));
|
|
1171
1267
|
const out = { findings: [], tokenUsages: 0, themeRefs: 0, ds: {}, components: [], definitions: [] };
|
|
@@ -1180,24 +1276,9 @@ function scanCode(path, text, ds) {
|
|
|
1180
1276
|
out.findings.push({ ...pos(offset), category: h.category, ...property ? { property } : {}, value: h.text, source, ...suggestion ? { suggestion } : {} });
|
|
1181
1277
|
};
|
|
1182
1278
|
const addTailwind = (hits, offset) => {
|
|
1183
|
-
|
|
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
|
-
}
|
|
1279
|
+
const r = tailwindFindings(hits, offset, pos, ds);
|
|
1280
|
+
out.themeRefs += r.themeRefs;
|
|
1281
|
+
out.findings.push(...r.findings);
|
|
1201
1282
|
};
|
|
1202
1283
|
const styleValue = (key, node, ctx) => {
|
|
1203
1284
|
const source = ctx === "sx" ? "sx" : ctx === "style" ? "style" : ctx === "theme" ? "theme" : "styled";
|
|
@@ -1258,13 +1339,18 @@ function scanCode(path, text, ds) {
|
|
|
1258
1339
|
}
|
|
1259
1340
|
for (const a of scan.applies) addTailwind(a.hits, start + a.start);
|
|
1260
1341
|
};
|
|
1342
|
+
const lookalike = (list2, offset) => {
|
|
1343
|
+
for (const l of lookalikesInClassList(list2, ds.components)) out.components.push({ ...pos(offset), name: `.${l.token}`, kind: "recreated", library: "local", suggestion: l.component });
|
|
1344
|
+
};
|
|
1261
1345
|
const classLiteral = (node) => {
|
|
1262
1346
|
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
|
|
1263
1347
|
handled.add(node);
|
|
1348
|
+
lookalike(node.text, node.getStart(sf) + 1);
|
|
1264
1349
|
addTailwind(scanClassString(node.text), node.getStart(sf) + 1);
|
|
1265
1350
|
} else if (ts.isTemplateExpression(node)) {
|
|
1266
1351
|
for (const p of [node.head, ...node.templateSpans.map((s) => s.literal)]) {
|
|
1267
1352
|
handled.add(p);
|
|
1353
|
+
lookalike(p.text, p.getStart(sf) + 1);
|
|
1268
1354
|
addTailwind(scanClassString(p.text), p.getStart(sf) + 1);
|
|
1269
1355
|
}
|
|
1270
1356
|
node.templateSpans.forEach((s) => visit(s.expression, null, true));
|
|
@@ -1273,49 +1359,13 @@ function scanCode(path, text, ds) {
|
|
|
1273
1359
|
const jsxName = (n) => n.getText(sf);
|
|
1274
1360
|
const element = (node) => {
|
|
1275
1361
|
const name = jsxName(node.tagName);
|
|
1276
|
-
const
|
|
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 });
|
|
1362
|
+
const typeAttr = node.attributes.properties.find((p) => ts.isJsxAttribute(p) && p.name.getText(sf) === "type");
|
|
1363
|
+
const type = typeAttr?.initializer && ts.isStringLiteral(typeAttr.initializer) ? typeAttr.initializer.text : void 0;
|
|
1364
|
+
const hasHref = node.attributes.properties.some((p) => ts.isJsxAttribute(p) && p.name.getText(sf) === "href");
|
|
1365
|
+
const r = classifyTag({ name, ...pos(node.getStart(sf)), ...type !== void 0 ? { type } : {}, hasHref, inRenderProp: isRenderElement(node) }, imports, ds);
|
|
1366
|
+
if (r.ds) out.ds[r.ds] = (out.ds[r.ds] ?? 0) + 1;
|
|
1367
|
+
if (r.component) out.components.push(r.component);
|
|
1368
|
+
if (r.localCandidate) pendingLocal.push(r.localCandidate);
|
|
1319
1369
|
};
|
|
1320
1370
|
const pendingLocal = [];
|
|
1321
1371
|
function visit(node, ctx, classCtx) {
|
|
@@ -1375,6 +1425,17 @@ function scanCode(path, text, ds) {
|
|
|
1375
1425
|
cssTemplate(node.template);
|
|
1376
1426
|
return;
|
|
1377
1427
|
}
|
|
1428
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
|
|
1429
|
+
const imp = imports.get(node.expression.text);
|
|
1430
|
+
if (imp && imp.imported !== "*" && classifyModule(imp.module, ds.packages, ds.foreignLibraries) === "ds") {
|
|
1431
|
+
const comp = domRendererComponent(imp.imported === "default" ? node.expression.text : imp.imported, ds.components);
|
|
1432
|
+
if (comp) out.ds[comp] = (out.ds[comp] ?? 0) + 1;
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken && ts.isPropertyAccessExpression(node.left) && node.left.name.text === "className") {
|
|
1436
|
+
visit(node.left, ctx, classCtx);
|
|
1437
|
+
return classLiteral(node.right);
|
|
1438
|
+
}
|
|
1378
1439
|
if (ts.isCallExpression(node)) {
|
|
1379
1440
|
const callee = node.expression.getText(sf);
|
|
1380
1441
|
if (CLASS_FNS.test(callee)) {
|
|
@@ -1419,6 +1480,14 @@ function scanCode(path, text, ds) {
|
|
|
1419
1480
|
const literal = (node) => {
|
|
1420
1481
|
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node) || ts.isTemplateHead(node) || ts.isTemplateMiddle(node) || ts.isTemplateTail(node)) {
|
|
1421
1482
|
const t = node.text;
|
|
1483
|
+
if (!handled.has(node) && (t.includes("<") || ts.isTemplateMiddle(node) || ts.isTemplateTail(node))) {
|
|
1484
|
+
const base = node.getStart(sf) + 1;
|
|
1485
|
+
for (const m of t.matchAll(HTML_CLASS_RE)) for (const l of lookalikesInClassList(m[1], ds.components)) out.components.push({ ...pos(base + m.index), name: `.${l.token}`, kind: "recreated", library: "local", suggestion: l.component });
|
|
1486
|
+
for (const m of t.matchAll(HTML_ATTR_RE)) {
|
|
1487
|
+
const component = LOOKALIKE_ATTRS[m[1]];
|
|
1488
|
+
if (component && ds.components.has(component)) out.components.push({ ...pos(base + m.index + 1), name: `[${m[1]}]`, kind: "recreated", library: "local", suggestion: component });
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1422
1491
|
for (const m of t.matchAll(/var\(\s*(--[\w-]+)/g)) {
|
|
1423
1492
|
if (ds.isDsVar(m[1])) out.tokenUsages++;
|
|
1424
1493
|
else out.themeRefs++;
|
|
@@ -1438,25 +1507,85 @@ function scanCode(path, text, ds) {
|
|
|
1438
1507
|
return out;
|
|
1439
1508
|
}
|
|
1440
1509
|
|
|
1441
|
-
// ../core/src/audit/
|
|
1442
|
-
var
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1510
|
+
// ../core/src/audit/ignore.ts
|
|
1511
|
+
var IGNORE_DIRECTIVE = "bridge-audit-ignore-next-line";
|
|
1512
|
+
function ignoreDirectives(text) {
|
|
1513
|
+
const at = lineCol(text);
|
|
1514
|
+
const out = [];
|
|
1515
|
+
for (const m of text.matchAll(new RegExp(`${IGNORE_DIRECTIVE}\\b`, "g"))) {
|
|
1516
|
+
const start = m.index;
|
|
1517
|
+
const opener = /(<!--|\/\*|\/\/)(\s*)$/.exec(text.slice(Math.max(0, start - 200), start));
|
|
1518
|
+
const open = opener?.[1];
|
|
1519
|
+
if (!open || open === "//" && opener[2].includes("\n")) continue;
|
|
1520
|
+
const from = start + m[0].length;
|
|
1521
|
+
let end;
|
|
1522
|
+
if (open === "//") {
|
|
1523
|
+
end = text.indexOf("\n", from);
|
|
1524
|
+
if (end < 0) end = text.length;
|
|
1525
|
+
} else {
|
|
1526
|
+
end = text.indexOf(open === "<!--" ? "-->" : "*/", from);
|
|
1527
|
+
if (end < 0) continue;
|
|
1528
|
+
}
|
|
1529
|
+
const reason = text.slice(from, end).replace(/^\s*:?/, "").replace(/\s+/g, " ").trim();
|
|
1530
|
+
out.push({ line: at(start).line, target: at(end).line + 1, reason });
|
|
1531
|
+
}
|
|
1532
|
+
return out;
|
|
1449
1533
|
}
|
|
1450
|
-
var
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1534
|
+
var item = (x, reason, via) => "category" in x ? { line: x.line, column: x.column, kind: "value", what: x.value, reason, via } : { line: x.line, column: x.column, kind: "component", what: x.name, reason, via };
|
|
1535
|
+
function applyIgnoreComments(text, findings, components) {
|
|
1536
|
+
const errors = [];
|
|
1537
|
+
const reasons = /* @__PURE__ */ new Map();
|
|
1538
|
+
for (const d of ignoreDirectives(text)) {
|
|
1539
|
+
if (!d.reason) {
|
|
1540
|
+
errors.push({ line: d.line, message: `${IGNORE_DIRECTIVE} needs a reason after a colon (${IGNORE_DIRECTIVE}: why this one stays); nothing was ignored` });
|
|
1541
|
+
continue;
|
|
1542
|
+
}
|
|
1543
|
+
if (!findings.some((f) => f.line === d.target) && !components.some((c) => c.line === d.target)) {
|
|
1544
|
+
errors.push({ line: d.line, message: `${IGNORE_DIRECTIVE} ignores nothing: line ${d.target} has no finding; remove the comment` });
|
|
1545
|
+
continue;
|
|
1546
|
+
}
|
|
1547
|
+
reasons.set(d.target, d.reason);
|
|
1548
|
+
}
|
|
1549
|
+
const ignored = [];
|
|
1550
|
+
const keep = (xs) => xs.filter((x) => {
|
|
1551
|
+
const r = reasons.get(x.line);
|
|
1552
|
+
if (r === void 0) return true;
|
|
1553
|
+
ignored.push(item(x, r, "comment"));
|
|
1554
|
+
return false;
|
|
1555
|
+
});
|
|
1556
|
+
return { findings: keep(findings), components: keep(components), ignored, errors };
|
|
1557
|
+
}
|
|
1558
|
+
function applyAllowlist(entries, findings, components) {
|
|
1559
|
+
const used = /* @__PURE__ */ new Set();
|
|
1560
|
+
const ignored = [];
|
|
1561
|
+
const keep = (xs) => xs.filter((x) => {
|
|
1562
|
+
const what = "category" in x ? x.value : x.name;
|
|
1563
|
+
const fits = (e) => e.line === x.line && (e.what === void 0 || e.what === what);
|
|
1564
|
+
let i = entries.findIndex((e, k) => fits(e) && !used.has(k));
|
|
1565
|
+
if (i < 0) i = entries.findIndex(fits);
|
|
1566
|
+
if (i < 0) return true;
|
|
1567
|
+
used.add(i);
|
|
1568
|
+
ignored.push(item(x, entries[i].reason, "allowlist"));
|
|
1569
|
+
return false;
|
|
1570
|
+
});
|
|
1571
|
+
return { findings: keep(findings), components: keep(components), ignored, used };
|
|
1572
|
+
}
|
|
1573
|
+
function parseAllowlist(json) {
|
|
1574
|
+
const fail = (msg) => {
|
|
1575
|
+
throw new Error(`allowlist: ${msg}`);
|
|
1576
|
+
};
|
|
1577
|
+
if (!json || typeof json !== "object" || !Array.isArray(json.entries)) fail('expected { "entries": [...] }');
|
|
1578
|
+
return json.entries.map((e, i) => {
|
|
1579
|
+
const x = e;
|
|
1580
|
+
if (typeof x.file !== "string" || !x.file) fail(`entry ${i} needs "file"`);
|
|
1581
|
+
if (typeof x.line !== "number" || !Number.isInteger(x.line) || x.line < 1) fail(`entry ${i} (${x.file}) needs a "line" number`);
|
|
1582
|
+
if (typeof x.reason !== "string" || !x.reason.trim()) fail(`entry ${i} (${x.file}:${x.line}) needs a "reason"`);
|
|
1583
|
+
if (x.what !== void 0 && typeof x.what !== "string") fail(`entry ${i} (${x.file}:${x.line}): "what" must be a string`);
|
|
1584
|
+
return { file: x.file, line: x.line, ...x.what !== void 0 ? { what: x.what } : {}, reason: x.reason.trim() };
|
|
1585
|
+
});
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
// ../core/src/audit/style.ts
|
|
1460
1589
|
function isSafeFix(h, s) {
|
|
1461
1590
|
if (!s || s.level !== "exact") return false;
|
|
1462
1591
|
switch (h.category) {
|
|
@@ -1481,49 +1610,342 @@ function isSafeFix(h, s) {
|
|
|
1481
1610
|
return false;
|
|
1482
1611
|
}
|
|
1483
1612
|
}
|
|
1484
|
-
function
|
|
1485
|
-
const
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1613
|
+
function auditCssText(text, lang, ds, opts) {
|
|
1614
|
+
const base = opts.offset ?? 0;
|
|
1615
|
+
const source = opts.source ?? "css";
|
|
1616
|
+
const at = (o) => opts.pos(base + o);
|
|
1617
|
+
const scan = scanCssText(text, lang);
|
|
1618
|
+
const findings = [];
|
|
1619
|
+
let tokenUsages = 0;
|
|
1620
|
+
let themeRefs = 0;
|
|
1621
|
+
for (const d of scan.decls) {
|
|
1622
|
+
for (const v of d.scan.vars) ds.isDsVar(v) ? tokenUsages++ : themeRefs++;
|
|
1623
|
+
if (d.scan.themeRef) themeRefs++;
|
|
1624
|
+
else if (lang !== "css" && !d.property.startsWith("$")) themeRefs += (d.value.match(/\$[\w-]+/g) ?? []).length;
|
|
1625
|
+
const property = d.property.toLowerCase();
|
|
1626
|
+
for (const h of d.scan.hits) {
|
|
1627
|
+
const suggestion = ds.suggest(h.category, h.text, property);
|
|
1628
|
+
const autofix = source === "css" && isSafeFix(h, suggestion);
|
|
1629
|
+
findings.push({ ...at(d.valueStart + h.index), category: h.category, property, value: h.text, source, ...suggestion ? { suggestion } : {}, ...autofix ? { autofix: true } : {} });
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
for (const a of scan.applies) {
|
|
1633
|
+
for (const h of a.hits) {
|
|
1634
|
+
if (h.kind === "scale") themeRefs++;
|
|
1635
|
+
else if (h.kind !== "token" && h.value) {
|
|
1636
|
+
const cat = parseCssColor(h.value) ? "color" : h.category === "color-or-size" ? "font-size" : h.category === "color-or-width" ? "border-width" : h.category === "color-or-shadow" ? "shadow" : h.category === "font" ? "font-family" : h.category;
|
|
1637
|
+
const suggestion = ds.suggest(cat, h.value);
|
|
1638
|
+
findings.push({ ...at(a.start + h.index), category: cat, value: h.cls, source: "tailwind", ...suggestion ? { suggestion } : {} });
|
|
1639
|
+
} else if (h.kind === "token" && h.value) {
|
|
1640
|
+
const v = /var\(\s*(--[\w-]+)/.exec(h.value)?.[1];
|
|
1641
|
+
if (v && ds.isDsVar(v)) tokenUsages++;
|
|
1642
|
+
else themeRefs++;
|
|
1501
1643
|
}
|
|
1502
1644
|
}
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1645
|
+
}
|
|
1646
|
+
findings.sort((x, y) => x.line - y.line || x.column - y.column);
|
|
1647
|
+
return { findings, tokenUsages, themeRefs };
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
// ../core/src/audit/markup.ts
|
|
1651
|
+
var MARKUP_EXTENSIONS = [".astro", ".vue", ".svelte"];
|
|
1652
|
+
function mask2(text, ranges, keepInside) {
|
|
1653
|
+
const inside = new Uint8Array(text.length);
|
|
1654
|
+
for (const r of ranges) inside.fill(1, r.start, r.end);
|
|
1655
|
+
let out = "";
|
|
1656
|
+
for (let i = 0; i < text.length; i++) {
|
|
1657
|
+
const c = text[i];
|
|
1658
|
+
out += c === "\n" || Boolean(inside[i]) === keepInside ? c : " ";
|
|
1659
|
+
}
|
|
1660
|
+
return out;
|
|
1661
|
+
}
|
|
1662
|
+
var attrOf = (attrs, name) => new RegExp(`(?:^|\\s)${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s>]+))`, "i").exec(attrs)?.slice(1).find((v) => v !== void 0);
|
|
1663
|
+
var isCodeScript = (attrs) => {
|
|
1664
|
+
const type = attrOf(attrs, "type")?.toLowerCase();
|
|
1665
|
+
return !type || ["module", "text/javascript", "application/javascript", "text/typescript", "ts", "text/babel"].includes(type);
|
|
1666
|
+
};
|
|
1667
|
+
function markupBlocks(text, lang) {
|
|
1668
|
+
const nonTemplate = [];
|
|
1669
|
+
let frontmatter;
|
|
1670
|
+
let from = 0;
|
|
1671
|
+
if (lang === "astro") {
|
|
1672
|
+
const open = /^\s*---[^\S\n]*\n/.exec(text);
|
|
1673
|
+
if (open) {
|
|
1674
|
+
const close = /\n---[^\S\n]*(\n|$)/g;
|
|
1675
|
+
close.lastIndex = open[0].length - 1;
|
|
1676
|
+
const m = close.exec(text);
|
|
1677
|
+
if (m) {
|
|
1678
|
+
frontmatter = { start: open[0].length, end: m.index + 1 };
|
|
1679
|
+
from = m.index + m[0].length;
|
|
1680
|
+
nonTemplate.push({ start: 0, end: from });
|
|
1515
1681
|
}
|
|
1516
1682
|
}
|
|
1517
|
-
findings.sort((x, y) => x.line - y.line || x.column - y.column);
|
|
1518
|
-
return { path, lang, tokenUsages, themeRefs, hardcoded: findings.length, dsComponents: 0, otherComponents: 0, findings, components: [], definitions: [], dsByName: {} };
|
|
1519
1683
|
}
|
|
1520
|
-
const
|
|
1684
|
+
const scripts = [];
|
|
1685
|
+
const styles = [];
|
|
1686
|
+
const re = /<(script|style)\b((?:[^>"']|"[^"]*"|'[^']*')*)>([\s\S]*?)<\/\1\s*>/gi;
|
|
1687
|
+
re.lastIndex = from;
|
|
1688
|
+
for (let m; m = re.exec(text); ) {
|
|
1689
|
+
const contentStart = m.index + m[0].indexOf(">", m[1].length + 1 + m[2].length) + 1;
|
|
1690
|
+
const block = { start: contentStart, end: contentStart + m[3].length, attrs: m[2] };
|
|
1691
|
+
(m[1].toLowerCase() === "script" ? scripts : styles).push(block);
|
|
1692
|
+
nonTemplate.push({ start: m.index, end: m.index + m[0].length });
|
|
1693
|
+
}
|
|
1694
|
+
for (const m of text.slice(from).matchAll(/<!--[\s\S]*?-->/g)) nonTemplate.push({ start: from + m.index, end: from + m.index + m[0].length });
|
|
1695
|
+
return { ...frontmatter ? { frontmatter } : {}, scripts, styles, nonTemplate };
|
|
1696
|
+
}
|
|
1697
|
+
function closeBrace(s, i) {
|
|
1698
|
+
let depth = 0;
|
|
1699
|
+
for (let k = i; k < s.length; k++) {
|
|
1700
|
+
const c = s[k];
|
|
1701
|
+
if (c === "{") depth++;
|
|
1702
|
+
else if (c === "}") {
|
|
1703
|
+
if (--depth === 0) return k + 1;
|
|
1704
|
+
} else if (c === '"' || c === "'") {
|
|
1705
|
+
const e = s.indexOf(c, k + 1);
|
|
1706
|
+
if (e < 0) return s.length;
|
|
1707
|
+
k = e;
|
|
1708
|
+
} else if (c === "`") {
|
|
1709
|
+
for (k++; k < s.length && s[k] !== "`"; k++) {
|
|
1710
|
+
if (s[k] === "\\") k++;
|
|
1711
|
+
else if (s[k] === "$" && s[k + 1] === "{") k = closeBrace(s, k + 1) - 1;
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
return s.length;
|
|
1716
|
+
}
|
|
1717
|
+
function readTag(s, i, lang) {
|
|
1718
|
+
const attrs = [];
|
|
1719
|
+
let k = i;
|
|
1720
|
+
while (k < s.length) {
|
|
1721
|
+
while (k < s.length && /\s/.test(s[k])) k++;
|
|
1722
|
+
const c = s[k];
|
|
1723
|
+
if (c === void 0) break;
|
|
1724
|
+
if (c === ">") return { attrs, end: k + 1, selfClosing: false };
|
|
1725
|
+
if (c === "/" && s[k + 1] === ">") return { attrs, end: k + 2, selfClosing: true };
|
|
1726
|
+
if (c === "{") {
|
|
1727
|
+
const e = closeBrace(s, k);
|
|
1728
|
+
const inner = s.slice(k + 1, e - 1).trim();
|
|
1729
|
+
attrs.push(inner.startsWith("...") ? { name: "", kind: "spread", value: inner, valueStart: k + 1 } : { name: inner, kind: "expr", value: inner, valueStart: k + 1 });
|
|
1730
|
+
k = e;
|
|
1731
|
+
continue;
|
|
1732
|
+
}
|
|
1733
|
+
const nm = /^[^\s=>/"'{]+/.exec(s.slice(k, k + 200));
|
|
1734
|
+
if (!nm) {
|
|
1735
|
+
k++;
|
|
1736
|
+
continue;
|
|
1737
|
+
}
|
|
1738
|
+
const name = nm[0];
|
|
1739
|
+
k += name.length;
|
|
1740
|
+
let j = k;
|
|
1741
|
+
while (j < s.length && /\s/.test(s[j])) j++;
|
|
1742
|
+
if (s[j] !== "=") {
|
|
1743
|
+
attrs.push({ name, kind: "bare", value: "", valueStart: k });
|
|
1744
|
+
continue;
|
|
1745
|
+
}
|
|
1746
|
+
j++;
|
|
1747
|
+
while (j < s.length && /\s/.test(s[j])) j++;
|
|
1748
|
+
const q = s[j];
|
|
1749
|
+
const bound = lang === "vue" && /^(:|v-bind:|v-on:|@|v-)/.test(name);
|
|
1750
|
+
if (q === '"' || q === "'") {
|
|
1751
|
+
const e = s.indexOf(q, j + 1);
|
|
1752
|
+
const end = e < 0 ? s.length : e;
|
|
1753
|
+
attrs.push({ name, kind: bound ? "expr" : "static", value: s.slice(j + 1, end), valueStart: j + 1 });
|
|
1754
|
+
k = end + 1;
|
|
1755
|
+
} else if (q === "{") {
|
|
1756
|
+
const e = closeBrace(s, j);
|
|
1757
|
+
attrs.push({ name, kind: "expr", value: s.slice(j + 1, e - 1), valueStart: j + 1 });
|
|
1758
|
+
k = e;
|
|
1759
|
+
} else {
|
|
1760
|
+
const m = /^[^\s>]+/.exec(s.slice(j));
|
|
1761
|
+
const v = m?.[0].replace(/\/$/, "") ?? "";
|
|
1762
|
+
attrs.push({ name, kind: "static", value: v, valueStart: j });
|
|
1763
|
+
k = j + v.length;
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
return { attrs, end: s.length, selfClosing: false };
|
|
1767
|
+
}
|
|
1768
|
+
var attrName = (n) => n.replace(/^(v-bind:|:|bind:)/, "");
|
|
1769
|
+
var pascal = (kebab) => kebab.replace(/(^|-)([a-z0-9])/g, (_m, _d, c) => c.toUpperCase());
|
|
1770
|
+
var NO_COLOR_ATTR_TAGS = /* @__PURE__ */ new Set(["meta", "link"]);
|
|
1771
|
+
function scanMarkup(path, text, lang, ds) {
|
|
1772
|
+
const out = { findings: [], tokenUsages: 0, themeRefs: 0, ds: {}, components: [], definitions: [] };
|
|
1773
|
+
const pos = lineCol(text);
|
|
1774
|
+
const blocks = markupBlocks(text, lang);
|
|
1775
|
+
const imports = /* @__PURE__ */ new Map();
|
|
1776
|
+
const merge = (c) => {
|
|
1777
|
+
out.findings.push(...c.findings);
|
|
1778
|
+
out.tokenUsages += c.tokenUsages;
|
|
1779
|
+
out.themeRefs += c.themeRefs;
|
|
1780
|
+
for (const [n, k] of Object.entries(c.ds)) out.ds[n] = (out.ds[n] ?? 0) + k;
|
|
1781
|
+
out.components.push(...c.components);
|
|
1782
|
+
out.definitions.push(...c.definitions);
|
|
1783
|
+
};
|
|
1784
|
+
const codeRanges = [...blocks.frontmatter ? [{ ...blocks.frontmatter, attrs: "" }] : [], ...blocks.scripts.filter((b) => isCodeScript(b.attrs))];
|
|
1785
|
+
for (const r of codeRanges) {
|
|
1786
|
+
const jsx = /^(tsx|jsx)$/i.test(attrOf(r.attrs, "lang") ?? "");
|
|
1787
|
+
const code = mask2(text, [r], true);
|
|
1788
|
+
const file = `${path}.${jsx ? "tsx" : "ts"}`;
|
|
1789
|
+
merge(scanCode(file, code, ds));
|
|
1790
|
+
for (const [k, v] of importsOf(file, code)) imports.set(k, v);
|
|
1791
|
+
}
|
|
1792
|
+
for (const b of blocks.styles) {
|
|
1793
|
+
const l = (attrOf(b.attrs, "lang") ?? "css").toLowerCase();
|
|
1794
|
+
const r = auditCssText(text.slice(b.start, b.end), l === "scss" || l === "sass" ? "scss" : l === "less" ? "less" : "css", ds, { pos, offset: b.start });
|
|
1795
|
+
out.findings.push(...r.findings);
|
|
1796
|
+
out.tokenUsages += r.tokenUsages;
|
|
1797
|
+
out.themeRefs += r.themeRefs;
|
|
1798
|
+
}
|
|
1799
|
+
const scanExpr = (prefix, expr, offset, suffix, keepLookalikes = false) => {
|
|
1800
|
+
const { line, column } = pos(offset);
|
|
1801
|
+
const src = line >= 2 ? `${"\n".repeat(line - 2)}${prefix}
|
|
1802
|
+
${" ".repeat(column - 1)}${expr}${suffix}` : `${prefix}${expr}${suffix}`;
|
|
1803
|
+
const c = scanCode(`${path}.tsx`, src, ds);
|
|
1804
|
+
merge({ ...c, components: keepLookalikes ? c.components.filter((x) => x.kind === "recreated" && x.name.startsWith(".")) : [], definitions: [], ds: {} });
|
|
1805
|
+
};
|
|
1806
|
+
const styleAttr = (a) => {
|
|
1807
|
+
if (a.kind === "static") {
|
|
1808
|
+
const css = lang === "svelte" ? a.value.replace(/\{[^}]*\}/g, (m) => " ".repeat(m.length)) : a.value;
|
|
1809
|
+
const r = auditCssText(css, "css", ds, { pos, offset: a.valueStart, source: "style" });
|
|
1810
|
+
out.findings.push(...r.findings);
|
|
1811
|
+
out.tokenUsages += r.tokenUsages;
|
|
1812
|
+
out.themeRefs += r.themeRefs;
|
|
1813
|
+
return;
|
|
1814
|
+
}
|
|
1815
|
+
const str = /^(\s*)([`'"])([\s\S]*)\2\s*$/.exec(a.value);
|
|
1816
|
+
if (str) {
|
|
1817
|
+
const body = str[3].replace(/\$\{[^}]*\}/g, (m) => " ".repeat(m.length));
|
|
1818
|
+
const r = auditCssText(body, "css", ds, { pos, offset: a.valueStart + str[1].length + 1, source: "style" });
|
|
1819
|
+
out.findings.push(...r.findings);
|
|
1820
|
+
out.tokenUsages += r.tokenUsages;
|
|
1821
|
+
out.themeRefs += r.themeRefs;
|
|
1822
|
+
return;
|
|
1823
|
+
}
|
|
1824
|
+
scanExpr("const style: CSSProperties =", a.value, a.valueStart, ";");
|
|
1825
|
+
};
|
|
1826
|
+
const tpl = mask2(text, blocks.nonTemplate, false);
|
|
1827
|
+
const pending = [];
|
|
1828
|
+
let noscript = 0;
|
|
1829
|
+
const TAG = /<(\/?)([A-Za-z][\w.:-]*)/g;
|
|
1830
|
+
for (let m; m = TAG.exec(tpl); ) {
|
|
1831
|
+
const raw = m[2];
|
|
1832
|
+
if (m[1]) {
|
|
1833
|
+
if (raw.toLowerCase() === "noscript") noscript = Math.max(0, noscript - 1);
|
|
1834
|
+
continue;
|
|
1835
|
+
}
|
|
1836
|
+
const tag = readTag(tpl, m.index + m[0].length, lang);
|
|
1837
|
+
TAG.lastIndex = tag.end;
|
|
1838
|
+
const lower = raw.toLowerCase();
|
|
1839
|
+
if (lower === "noscript" && !tag.selfClosing) {
|
|
1840
|
+
noscript++;
|
|
1841
|
+
continue;
|
|
1842
|
+
}
|
|
1843
|
+
const attrs = tag.attrs;
|
|
1844
|
+
const get = (n) => attrs.find((a) => attrName(a.name) === n);
|
|
1845
|
+
for (const a of attrs) {
|
|
1846
|
+
const n = attrName(a.name);
|
|
1847
|
+
if (n === "style") styleAttr(a);
|
|
1848
|
+
else if (lang === "svelte" && a.name.startsWith("style:") && a.kind === "static") {
|
|
1849
|
+
const prop = a.name.slice(6).split("|")[0];
|
|
1850
|
+
const r2 = auditCssText(`${prop}:${a.value}`, "css", ds, { pos, offset: a.valueStart - prop.length - 1, source: "style" });
|
|
1851
|
+
out.findings.push(...r2.findings);
|
|
1852
|
+
out.tokenUsages += r2.tokenUsages;
|
|
1853
|
+
out.themeRefs += r2.themeRefs;
|
|
1854
|
+
} else if (/^(class|className|class:list)$/.test(n)) {
|
|
1855
|
+
if (a.kind === "static") {
|
|
1856
|
+
for (const l of lookalikesInClassList(a.value, ds.components)) out.components.push({ ...pos(a.valueStart), name: `.${l.token}`, kind: "recreated", library: "local", suggestion: l.component });
|
|
1857
|
+
const r2 = tailwindFindings(scanClassString(a.value), a.valueStart, pos, ds);
|
|
1858
|
+
out.findings.push(...r2.findings);
|
|
1859
|
+
out.themeRefs += r2.themeRefs;
|
|
1860
|
+
} else if (a.kind === "expr") scanExpr("cn(", a.value, a.valueStart, ")", true);
|
|
1861
|
+
} else if (LOOKALIKE_ATTRS[n] && ds.components.has(LOOKALIKE_ATTRS[n])) {
|
|
1862
|
+
out.components.push({ ...pos(a.valueStart - a.name.length - 1), name: `[${n}]`, kind: "recreated", library: "local", suggestion: LOOKALIKE_ATTRS[n] });
|
|
1863
|
+
} else if (a.kind === "static" && !NO_COLOR_ATTR_TAGS.has(lower) && isColorLiteral(a.value)) {
|
|
1864
|
+
const suggestion = ds.suggest("color", a.value.trim(), n);
|
|
1865
|
+
out.findings.push({ ...pos(a.valueStart), category: "color", property: n, value: a.value.trim(), source: "jsx-attr", ...suggestion ? { suggestion } : {} });
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
let name = raw;
|
|
1869
|
+
if (raw.includes(":")) continue;
|
|
1870
|
+
if (raw.includes("-")) {
|
|
1871
|
+
if (!imports.has(pascal(raw))) continue;
|
|
1872
|
+
name = pascal(raw);
|
|
1873
|
+
}
|
|
1874
|
+
const isNative = /^[a-z]/.test(name) && !name.includes(".");
|
|
1875
|
+
if (isNative && noscript > 0) continue;
|
|
1876
|
+
const type = get("type");
|
|
1877
|
+
const r = classifyTag({ name, ...pos(m.index), ...type?.kind === "static" ? { type: type.value } : {}, hasHref: Boolean(get("href")) }, imports, ds);
|
|
1878
|
+
if (r.ds) out.ds[r.ds] = (out.ds[r.ds] ?? 0) + 1;
|
|
1879
|
+
if (r.component) out.components.push(r.component);
|
|
1880
|
+
if (r.localCandidate) pending.push(r.localCandidate);
|
|
1881
|
+
}
|
|
1882
|
+
const base = /([^/]+)\.(astro|vue|svelte)$/.exec(path)?.[1];
|
|
1883
|
+
if (base && /^[A-Z][A-Za-z0-9]*$/.test(base)) out.definitions.push({ name: base, line: 1 });
|
|
1884
|
+
const defined = new Set(out.definitions.map((d) => d.name));
|
|
1885
|
+
out.components.push(...pending.filter((f) => defined.has(f.suggestion)));
|
|
1886
|
+
out.findings.sort((a, b) => a.line - b.line || a.column - b.column);
|
|
1887
|
+
out.components.sort((a, b) => a.line - b.line || a.column - b.column);
|
|
1888
|
+
return out;
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
// ../core/src/audit/report.ts
|
|
1892
|
+
var AUDIT_EXTENSIONS = [".css", ".scss", ".sass", ".less", ".tsx", ".jsx", ".ts", ".js", ".mjs", ".cjs", ".mts", ".cts", ...MARKUP_EXTENSIONS];
|
|
1893
|
+
var AUDIT_IGNORE_DIRS = ["node_modules", "dist", "build", "out", "coverage", ".git", ".next", ".nuxt", ".astro", ".turbo", ".cache", ".vercel", "storybook-static", "vendor", "public"];
|
|
1894
|
+
var IGNORE_FILE = /\.d\.[cm]?ts$|\.(test|spec|stories|story)\.[cm]?[jt]sx?$|\.min\.(css|js)$|(^|\/)(vite|vitest|jest|webpack|rollup|eslint|postcss|tailwind|babel|next|astro|prettier|stylelint|tsup)\.config\.[cm]?[jt]s$/;
|
|
1895
|
+
function isAuditable(path) {
|
|
1896
|
+
if (!AUDIT_EXTENSIONS.some((e) => path.endsWith(e))) return false;
|
|
1897
|
+
if (IGNORE_FILE.test(path)) return false;
|
|
1898
|
+
return !path.split("/").some((seg) => AUDIT_IGNORE_DIRS.includes(seg));
|
|
1899
|
+
}
|
|
1900
|
+
var langOf = (path) => {
|
|
1901
|
+
const ext = path.slice(path.lastIndexOf(".") + 1);
|
|
1902
|
+
if (ext === "css") return "css";
|
|
1903
|
+
if (ext === "scss" || ext === "sass") return "scss";
|
|
1904
|
+
if (ext === "less") return "less";
|
|
1905
|
+
if (ext === "tsx") return "tsx";
|
|
1906
|
+
if (ext === "jsx") return "jsx";
|
|
1907
|
+
if (/^[cm]?ts$/.test(ext)) return "ts";
|
|
1908
|
+
if (ext === "astro" || ext === "vue" || ext === "svelte") return ext;
|
|
1909
|
+
return "js";
|
|
1910
|
+
};
|
|
1911
|
+
function auditFile(path, text, ds) {
|
|
1912
|
+
const lang = langOf(path);
|
|
1913
|
+
if (lang === "css" || lang === "scss" || lang === "less") {
|
|
1914
|
+
const r = auditCssText(text, lang, ds, { pos: lineCol(text) });
|
|
1915
|
+
return withIgnores(text, { path, lang, tokenUsages: r.tokenUsages, themeRefs: r.themeRefs, hardcoded: 0, dsComponents: 0, otherComponents: 0, findings: r.findings, components: [], definitions: [], dsByName: {} });
|
|
1916
|
+
}
|
|
1917
|
+
const c = lang === "astro" || lang === "vue" || lang === "svelte" ? scanMarkup(path, text, lang, ds) : scanCode(path, text, ds);
|
|
1521
1918
|
const dsCount = Object.values(c.ds).reduce((a, b) => a + b, 0);
|
|
1522
|
-
return { path, lang, tokenUsages: c.tokenUsages, themeRefs: c.themeRefs, hardcoded:
|
|
1919
|
+
return withIgnores(text, { path, lang, tokenUsages: c.tokenUsages, themeRefs: c.themeRefs, hardcoded: 0, dsComponents: dsCount, otherComponents: 0, findings: c.findings, components: c.components, definitions: c.definitions, dsByName: c.ds });
|
|
1920
|
+
}
|
|
1921
|
+
function withIgnores(text, f) {
|
|
1922
|
+
const r = applyIgnoreComments(text, f.findings, f.components);
|
|
1923
|
+
return {
|
|
1924
|
+
...f,
|
|
1925
|
+
findings: r.findings,
|
|
1926
|
+
components: r.components,
|
|
1927
|
+
hardcoded: r.findings.length,
|
|
1928
|
+
otherComponents: r.components.length,
|
|
1929
|
+
...r.ignored.length ? { ignored: r.ignored } : {},
|
|
1930
|
+
...r.errors.length ? { ignoreErrors: r.errors } : {}
|
|
1931
|
+
};
|
|
1523
1932
|
}
|
|
1524
1933
|
var pct = (num2, den) => den > 0 ? Math.round(num2 / den * 1e3) / 10 : null;
|
|
1525
1934
|
function buildAuditReport(input, ds) {
|
|
1526
|
-
const
|
|
1935
|
+
const allow = input.allowlist ?? [];
|
|
1936
|
+
const usedAllow = /* @__PURE__ */ new Set();
|
|
1937
|
+
const reports = input.files.map((f) => {
|
|
1938
|
+
const r = auditFile(f.path, f.text, ds);
|
|
1939
|
+
const idx = allow.flatMap((e, i) => e.file === f.path ? [i] : []);
|
|
1940
|
+
if (!idx.length) return r;
|
|
1941
|
+
const a = applyAllowlist(idx.map((i) => allow[i]), r.findings, r.components);
|
|
1942
|
+
a.used.forEach((k) => usedAllow.add(idx[k]));
|
|
1943
|
+
if (!a.ignored.length) return r;
|
|
1944
|
+
const ignored = [...r.ignored ?? [], ...a.ignored].sort((x, y) => x.line - y.line || x.column - y.column);
|
|
1945
|
+
return { ...r, findings: a.findings, components: a.components, hardcoded: a.findings.length, otherComponents: a.components.length, ignored };
|
|
1946
|
+
});
|
|
1947
|
+
const ignoredItems = reports.flatMap((r) => (r.ignored ?? []).map((x) => ({ file: r.path, ...x })));
|
|
1948
|
+
const staleAllowlist = allow.filter((_e, i) => !usedAllow.has(i));
|
|
1527
1949
|
const files = reports.map(({ definitions: _d, dsByName: _n, ...r }) => r);
|
|
1528
1950
|
const byCategory = {};
|
|
1529
1951
|
const fixes = /* @__PURE__ */ new Map();
|
|
@@ -1583,6 +2005,15 @@ function buildAuditReport(input, ds) {
|
|
|
1583
2005
|
topFiles,
|
|
1584
2006
|
topFixes,
|
|
1585
2007
|
files: files.sort((a, b) => a.path.localeCompare(b.path)),
|
|
2008
|
+
...ignoredItems.length || staleAllowlist.length ? {
|
|
2009
|
+
ignored: {
|
|
2010
|
+
total: ignoredItems.length,
|
|
2011
|
+
comment: ignoredItems.filter((x) => x.via === "comment").length,
|
|
2012
|
+
allowlist: ignoredItems.filter((x) => x.via === "allowlist").length,
|
|
2013
|
+
items: ignoredItems,
|
|
2014
|
+
staleAllowlist
|
|
2015
|
+
}
|
|
2016
|
+
} : {},
|
|
1586
2017
|
history: []
|
|
1587
2018
|
};
|
|
1588
2019
|
}
|
|
@@ -1686,6 +2117,20 @@ function auditMarkdown(r, opts = {}) {
|
|
|
1686
2117
|
for (const f of fixes) lines.push(`| \`${esc(f.value)}\` (${f.category}) | \`var(${f.suggestion.cssVar})\` ${esc(f.suggestion.value)}${f.suggestion.mode ? ` [${f.suggestion.mode}]` : ""} | ${f.suggestion.level} | ${f.count} |`);
|
|
1687
2118
|
lines.push("");
|
|
1688
2119
|
}
|
|
2120
|
+
const ig = r.ignored;
|
|
2121
|
+
if (ig && (ig.total || ig.staleAllowlist.length)) {
|
|
2122
|
+
lines.push("### Ignored on purpose", "", `${ig.total} item(s) left out of the counts: ${ig.comment} by \`bridge-audit-ignore-next-line\` comments, ${ig.allowlist} by the allowlist.`, "");
|
|
2123
|
+
const shown = ig.items.slice(0, opts.ignored ?? 50);
|
|
2124
|
+
if (shown.length) {
|
|
2125
|
+
lines.push("| Where | What | Reason |", "| --- | --- | --- |");
|
|
2126
|
+
for (const x of shown) lines.push(`| \`${esc(x.file)}:${x.line}\` | \`${esc(x.what)}\` | ${esc(x.reason)} (${x.via}) |`);
|
|
2127
|
+
if (ig.items.length > shown.length) lines.push(`| ... | ${ig.items.length - shown.length} more | |`);
|
|
2128
|
+
lines.push("");
|
|
2129
|
+
}
|
|
2130
|
+
if (ig.staleAllowlist.length) {
|
|
2131
|
+
lines.push(`Allowlist entries that match nothing any more (remove them): ${ig.staleAllowlist.map((e) => `\`${esc(e.file)}:${e.line}${e.what ? ` ${esc(e.what)}` : ""}\``).join(", ")}.`, "");
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
1689
2134
|
if (b) {
|
|
1690
2135
|
lines.push(`### Progress since ${b.label ?? b.generatedAt}`, "");
|
|
1691
2136
|
lines.push(`- ${b.improved.length} files improved, ${b.regressed.length} regressed, ${b.newFiles.length} new, ${b.removedFiles.length} removed.`);
|
|
@@ -2425,13 +2870,16 @@ function collectAuditFiles(target, exclude = []) {
|
|
|
2425
2870
|
}
|
|
2426
2871
|
return { root: abs, files, skipped };
|
|
2427
2872
|
}
|
|
2873
|
+
function loadAllowlist(path) {
|
|
2874
|
+
return parseAllowlist(JSON.parse(readFileSync(path, "utf8")));
|
|
2875
|
+
}
|
|
2428
2876
|
function runAudit(target, opts = {}) {
|
|
2429
2877
|
const dsRoot = opts.ds ?? findDesignSystemRoot(process.cwd());
|
|
2430
2878
|
if (!dsRoot) throw new Error("No design system found: run from a repo with bridge.config.json or pass --ds <dir>.");
|
|
2431
2879
|
const ds = loadDesignSystem(dsRoot);
|
|
2432
2880
|
const { files, skipped } = collectAuditFiles(target, [...ds.config.audit?.exclude ?? [], ...opts.exclude ?? []]);
|
|
2433
2881
|
const report = buildAuditReport(
|
|
2434
|
-
{ files, skipped, target: opts.displayPath ?? (toPosix(relative(process.cwd(), resolve(target))) || "."), ...opts.label ? { label: opts.label } : {}, ...opts.version ? { version: opts.version } : {} },
|
|
2882
|
+
{ files, skipped, target: opts.displayPath ?? (toPosix(relative(process.cwd(), resolve(target))) || "."), ...opts.label ? { label: opts.label } : {}, ...opts.version ? { version: opts.version } : {}, ...opts.allowlist ? { allowlist: opts.allowlist } : {} },
|
|
2435
2883
|
ds.index
|
|
2436
2884
|
);
|
|
2437
2885
|
return opts.baseline ? compareWithBaseline(report, opts.baseline) : report;
|
|
@@ -2600,6 +3048,12 @@ function classifyAuth(status, detail, opts = {}) {
|
|
|
2600
3048
|
}
|
|
2601
3049
|
return {};
|
|
2602
3050
|
}
|
|
3051
|
+
function rateLimitMessage(x) {
|
|
3052
|
+
const when = x.resetsAt ? ` It resets at ${x.resetsAt.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}.` : "";
|
|
3053
|
+
if (x.withToken) return `GitHub's hourly request limit for this token is used up.${when} Try again after that.`;
|
|
3054
|
+
const hint = x.tokenHint ?? "add a GitHub token";
|
|
3055
|
+
return `GitHub's hourly limit for requests without a token (60 an hour) is used up.${when} To keep going now, ${hint}: a token gets 5,000 requests an hour.`;
|
|
3056
|
+
}
|
|
2603
3057
|
var DEFAULT_RETRY = { attempts: 3, baseDelayMs: 600, sleep: (ms) => new Promise((r) => setTimeout(r, ms)) };
|
|
2604
3058
|
var isTransient = (status, detail) => status === 0 || status >= 500 || status === 429 || status === 403 && /secondary rate limit/i.test(detail);
|
|
2605
3059
|
var API = "https://api.github.com";
|
|
@@ -2703,12 +3157,8 @@ var GitHubClient = class _GitHubClient {
|
|
|
2703
3157
|
const remaining = res.headers?.get("x-ratelimit-remaining");
|
|
2704
3158
|
const reset = res.headers?.get("x-ratelimit-reset");
|
|
2705
3159
|
if ((res.status === 403 || res.status === 429) && remaining === "0") {
|
|
2706
|
-
const
|
|
2707
|
-
throw new GitHubError(
|
|
2708
|
-
429,
|
|
2709
|
-
`GitHub's hourly request limit for this token is used up.${when ? ` It resets at ${when.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}.` : ""} Try again after that.`,
|
|
2710
|
-
detail
|
|
2711
|
-
);
|
|
3160
|
+
const resetsAt = reset ? new Date(Number(reset) * 1e3) : void 0;
|
|
3161
|
+
throw new GitHubError(429, rateLimitMessage({ withToken: Boolean(this.token), resetsAt, tokenHint: this.options.tokenHint }), detail);
|
|
2712
3162
|
}
|
|
2713
3163
|
const info = classifyAuth(res.status, detail, {
|
|
2714
3164
|
path,
|
|
@@ -2865,6 +3315,25 @@ var GitHubClient = class _GitHubClient {
|
|
|
2865
3315
|
throw new GitHubError(422, `GitHub refused to mark #${n} ready for review${detail ? `: ${detail}` : "."} Use "Ready for review" on GitHub instead.`, detail);
|
|
2866
3316
|
}
|
|
2867
3317
|
}
|
|
3318
|
+
/**
|
|
3319
|
+
* Turns an open pull request back into a draft: the GraphQL mutation convertPullRequestToDraft (REST cannot set
|
|
3320
|
+
* `draft` either), same token. No-op when it is already a draft.
|
|
3321
|
+
*/
|
|
3322
|
+
async convertToDraft(n) {
|
|
3323
|
+
this.assertToken("Turning a pull request back into a draft");
|
|
3324
|
+
const pr = await this.request("GET", `${this.repoPath}/pulls/${n}`);
|
|
3325
|
+
if (pr.draft === true) return;
|
|
3326
|
+
if (pr.state && pr.state !== "open") throw new GitHubError(422, `Pull request #${n} is ${pr.state}: only an open one can go back to draft.`);
|
|
3327
|
+
if (!pr.node_id) throw new GitHubError(422, `Pull request #${n} has no node id: convert it to draft on GitHub.`);
|
|
3328
|
+
const r = await this.request("POST", "/graphql", {
|
|
3329
|
+
query: "mutation($id: ID!) { convertPullRequestToDraft(input: { pullRequestId: $id }) { pullRequest { number isDraft } } }",
|
|
3330
|
+
variables: { id: pr.node_id }
|
|
3331
|
+
});
|
|
3332
|
+
if (r?.errors?.length) {
|
|
3333
|
+
const detail = r.errors.map((e) => e.message).filter(Boolean).join(" ");
|
|
3334
|
+
throw new GitHubError(422, `GitHub refused to turn #${n} back into a draft${detail ? `: ${detail}` : "."} Use "Convert to draft" on GitHub instead.`, detail);
|
|
3335
|
+
}
|
|
3336
|
+
}
|
|
2868
3337
|
async createPullRequest(input) {
|
|
2869
3338
|
this.assertWritableBranch(input.head, input.base);
|
|
2870
3339
|
const r = await this.request(
|
|
@@ -4747,6 +5216,7 @@ async function context2(root, io) {
|
|
|
4747
5216
|
const token = io.env.BRIDGE_GITHUB_TOKEN || io.env.GITHUB_TOKEN || io.ghToken();
|
|
4748
5217
|
return BridgeContext3.create({ root, ...token ? { token } : {}, ...io.fetch ? { fetch: io.fetch } : {} });
|
|
4749
5218
|
}
|
|
5219
|
+
var exploreSource = (env) => ({ source: "cli", ...env.CLAUDECODE ? { agent: "claude" } : {} });
|
|
4750
5220
|
var date = (iso) => iso ? iso.slice(0, 10) : "";
|
|
4751
5221
|
async function runExplore(a, root, io = defaultIO()) {
|
|
4752
5222
|
const sub = a._[1];
|
|
@@ -4762,7 +5232,7 @@ async function runExplore(a, root, io = defaultIO()) {
|
|
|
4762
5232
|
if (!name || !purpose) throw new RequestUsageError('Usage: bridge explore start <name> --purpose "What you want to learn"');
|
|
4763
5233
|
const useCase = flag2(a, "use-case");
|
|
4764
5234
|
const ctx = await context2(root, io);
|
|
4765
|
-
const r = await explorations.startExploration(ctx, { name, purpose, ...useCase ? { useCase } : {}, ...a.flags.experimental ? { experimental: a.flags.experimental } : {}, dryRun: has2(a, "dry-run") });
|
|
5235
|
+
const r = await explorations.startExploration(ctx, { name, purpose, ...exploreSource(io.env), ...useCase ? { useCase } : {}, ...a.flags.experimental ? { experimental: a.flags.experimental } : {}, dryRun: has2(a, "dry-run") });
|
|
4766
5236
|
if (json) return io.print(JSON.stringify(r, null, 2)), 0;
|
|
4767
5237
|
if (r.status === "dry-run") {
|
|
4768
5238
|
io.print(`Dry run, nothing written.
|
|
@@ -4865,6 +5335,7 @@ async function rolesReport(opts) {
|
|
|
4865
5335
|
|
|
4866
5336
|
// src/index.ts
|
|
4867
5337
|
import { brand, BridgeContext as BridgeContext4, CONFIG_FILE as CONFIG_FILE2, core, detectConfig, GitRefSource, history as hist, LocalSource, ops, optionsFromArgv, serveStdio, toConfigJson, VERSION } from "@bridge-ds/mcp";
|
|
5338
|
+
var MCP_PACKAGE = "@bridge-ds/mcp";
|
|
4868
5339
|
var HELP = `bridge ${VERSION}: design system as code, for you and your Claude
|
|
4869
5340
|
|
|
4870
5341
|
Usage:
|
|
@@ -4899,9 +5370,15 @@ Usage:
|
|
|
4899
5370
|
undo a merged token change against HEAD: conflicts shown,
|
|
4900
5371
|
new local bridge/* branch (never pushes, never main)
|
|
4901
5372
|
bridge audit <path> [--json [out.json]] [--md [out.md]] [--fail-under 80] [--baseline prev.json]
|
|
4902
|
-
[--label "..."] [--ds dir] [--no-regress]
|
|
5373
|
+
[--label "..."] [--ds dir] [--no-regress] [--allowlist known.json]
|
|
4903
5374
|
design system adoption in a product folder: token and component
|
|
4904
5375
|
adoption, hardcoded values with the closest token, top files.
|
|
5376
|
+
Reads CSS, SCSS, Less, TS/JS, TSX/JSX, and Astro, Vue and Svelte
|
|
5377
|
+
(template markup, frontmatter and <script>, <style> blocks).
|
|
5378
|
+
Leave one exception out with a comment on the line above:
|
|
5379
|
+
<!-- bridge-audit-ignore-next-line: reason --> (or // or /* */);
|
|
5380
|
+
--allowlist lists known file:line exceptions, each with a reason.
|
|
5381
|
+
Ignored items stay listed in the report.
|
|
4905
5382
|
Exit 0 ok, 1 under --fail-under or regressed, 2 setup error
|
|
4906
5383
|
bridge audit <path> --fix [--write] safe autofix: CSS values equal to a token become var(--token).
|
|
4907
5384
|
Dry run unless --write
|
|
@@ -4970,18 +5447,19 @@ async function init(a, root) {
|
|
|
4970
5447
|
}
|
|
4971
5448
|
const serverJs = fileURLToPath(new URL("./index.js", import.meta.resolve("@bridge-ds/mcp")));
|
|
4972
5449
|
const relServer = relative4(root, serverJs);
|
|
4973
|
-
const inRepo = !relServer.startsWith("..");
|
|
5450
|
+
const inRepo = !relServer.startsWith("..") && !isAbsolute(relServer);
|
|
5451
|
+
const server = (dir) => inRepo ? { command: "node", args: [relServer, "--root", dir] } : { command: "npx", args: ["-y", MCP_PACKAGE, "--root", dir] };
|
|
4974
5452
|
const mcpPath = join5(root, ".mcp.json");
|
|
4975
5453
|
const existing = existsSync5(mcpPath) ? JSON.parse(await readFile5(mcpPath, "utf8")) : {};
|
|
4976
|
-
existing.mcpServers = { ...existing.mcpServers ?? {}, bridge:
|
|
5454
|
+
existing.mcpServers = { ...existing.mcpServers ?? {}, bridge: server(".") };
|
|
4977
5455
|
await writeFile4(mcpPath, `${JSON.stringify(existing, null, 2)}
|
|
4978
5456
|
`);
|
|
4979
5457
|
print(`Wrote .mcp.json: Claude Code picks up the "bridge" server next time it starts in ${root}.`);
|
|
5458
|
+
const desktop = inRepo ? { command: "node", args: [serverJs, "--root", root] } : server(root);
|
|
4980
5459
|
print("\nClaude Desktop: add this to claude_desktop_config.json (Settings > Developer > Edit config):");
|
|
4981
|
-
print(JSON.stringify({ mcpServers: { bridge:
|
|
5460
|
+
print(JSON.stringify({ mcpServers: { bridge: desktop } }, null, 2));
|
|
4982
5461
|
print("\nOr with the Claude Code CLI:");
|
|
4983
|
-
print(` claude mcp add bridge --
|
|
4984
|
-
print("(Once @bridge-ds/mcp is published: claude mcp add bridge -- npx -y @bridge-ds/mcp)");
|
|
5462
|
+
print(` claude mcp add bridge -- ${desktop.command} ${desktop.args.join(" ")}`);
|
|
4985
5463
|
}
|
|
4986
5464
|
async function check(a, root) {
|
|
4987
5465
|
const ctx = await BridgeContext4.create({ root });
|
|
@@ -5291,7 +5769,15 @@ async function audit(a, root) {
|
|
|
5291
5769
|
baseline = JSON.parse(await readFile5(baselinePath, "utf8"));
|
|
5292
5770
|
}
|
|
5293
5771
|
const label = flag3(a, "label");
|
|
5294
|
-
const
|
|
5772
|
+
const allowlistPath = flag3(a, "allowlist");
|
|
5773
|
+
if (allowlistPath && !existsSync5(allowlistPath)) throw new UsageError(`Allowlist not found: ${allowlistPath}`);
|
|
5774
|
+
let allowlist;
|
|
5775
|
+
try {
|
|
5776
|
+
allowlist = allowlistPath ? loadAllowlist(allowlistPath) : void 0;
|
|
5777
|
+
} catch (e) {
|
|
5778
|
+
throw new UsageError(`${allowlistPath}: ${e.message}`);
|
|
5779
|
+
}
|
|
5780
|
+
const report = runAudit(target, { ds: dsRoot, version: VERSION, ...label ? { label } : {}, ...baseline ? { baseline } : {}, ...allowlist ? { allowlist } : {} });
|
|
5295
5781
|
const md = auditMarkdown(report);
|
|
5296
5782
|
const jsonOut = flag3(a, "json");
|
|
5297
5783
|
const mdOut = flag3(a, "md");
|
|
@@ -5322,9 +5808,17 @@ async function audit(a, root) {
|
|
|
5322
5808
|
print(" top fixes:");
|
|
5323
5809
|
for (const f of fixes) print(` ${f.value} -> var(${f.suggestion.cssVar}) ${f.suggestion.level}, ${f.count}x`);
|
|
5324
5810
|
}
|
|
5811
|
+
const ig = report.ignored;
|
|
5812
|
+
if (ig?.total) print(` ignored on purpose ${ig.total} (${ig.comment} by comment, ${ig.allowlist} by the allowlist); listed in --json and --md`);
|
|
5813
|
+
if (ig?.staleAllowlist.length) {
|
|
5814
|
+
print(` allowlist entries that match nothing any more (remove them):`);
|
|
5815
|
+
for (const e of ig.staleAllowlist) print(` ${e.file}:${e.line}${e.what ? ` ${e.what}` : ""}`);
|
|
5816
|
+
}
|
|
5325
5817
|
if (jsonOut) print(`Wrote ${jsonOut}`);
|
|
5326
5818
|
if (mdOut) print(`Wrote ${mdOut}`);
|
|
5327
5819
|
}
|
|
5820
|
+
for (const f of report.files) for (const e of f.ignoreErrors ?? []) process.stderr.write(`${report.target.path}/${f.path}:${e.line}: ${e.message}
|
|
5821
|
+
`);
|
|
5328
5822
|
let code = 0;
|
|
5329
5823
|
const failUnder = flag3(a, "fail-under");
|
|
5330
5824
|
if (failUnder !== void 0) {
|
|
@@ -5387,7 +5881,7 @@ async function main() {
|
|
|
5387
5881
|
if (cmd === "explore") return runExplore(a, root);
|
|
5388
5882
|
if (!cmd || has3(a, "help") || cmd === "help") {
|
|
5389
5883
|
print(HELP);
|
|
5390
|
-
return cmd ? 0 : 2;
|
|
5884
|
+
return cmd || has3(a, "help") ? 0 : 2;
|
|
5391
5885
|
}
|
|
5392
5886
|
if (cmd === "--version" || cmd === "version") return print(VERSION), 0;
|
|
5393
5887
|
switch (cmd) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bridge-ds/cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "bridge CLI: set up Bridge in a design system repo, check tokens in CI, search and diff tokens, propose changes, start the MCP server.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
"build:package": "node ../../scripts/build-packages.mjs cli"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@bridge-ds/mcp": "^0.3.
|
|
47
|
-
"@bridge-ds/visual": "^0.2.
|
|
46
|
+
"@bridge-ds/mcp": "^0.3.2",
|
|
47
|
+
"@bridge-ds/visual": "^0.2.3",
|
|
48
48
|
"typescript": "5.9.3"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|