@bridge-ds/cli 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +594 -122
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -602,6 +602,10 @@ var EQUIVALENTS = [
|
|
|
602
602
|
[/^Badge$/, ["Badge"]],
|
|
603
603
|
[/^(Chip|Tag|ChipField)$/, ["Tag", "Badge"]],
|
|
604
604
|
[/^(Alert|AlertTitle|Callout)$/, ["Alert"]],
|
|
605
|
+
[/^(Banner|AnnouncementBar|TopBanner)$/, ["Banner", "Alert"]],
|
|
606
|
+
[/^(CodeBlock|Code|CodeSnippet|Snippet|Prism|SyntaxHighlighter|CopyBlock)$/, ["CodeBlock"]],
|
|
607
|
+
[/^(RadioCard|RadioCards|RadioTile|ChoiceCard|ChoiceCardGroup)$/, ["ChoiceCard", "RadioGroup"]],
|
|
608
|
+
[/^(Checklist|ChecklistItem|StepList)$/, ["Checklist"]],
|
|
605
609
|
[/^(Snackbar|Toast|Toaster|Notification)$/, ["Toast"]],
|
|
606
610
|
[/^(CircularProgress|Spinner|Loading|LoadingIndicator)$/, ["Spinner", "Progress"]],
|
|
607
611
|
[/^(LinearProgress|Progress)$/, ["Progress"]],
|
|
@@ -1147,6 +1151,26 @@ var THEME_FNS = /^(createTheme|extendTheme|createMuiTheme|responsiveFontSizes|ex
|
|
|
1147
1151
|
var CSS_TAGS = /^(styled(\.\w+|\(.*\)(\.attrs\(.*\))?)|css|keyframes|createGlobalStyle|injectGlobal|tw\.\w+)$/s;
|
|
1148
1152
|
var MUI_SPACING_ATTR = /^([mp][trblxy]?|(margin|padding)(Top|Bottom|Left|Right|X|Y)?|gap|rowGap|columnGap|spacing)$/;
|
|
1149
1153
|
var THEME_REF_RE = /\btheme\.(palette|spacing|shape|typography|shadows|zIndex|transitions|vars)\b/g;
|
|
1154
|
+
var DOM_RENDERER_RE = /^([a-z][A-Za-z0-9]*?)(Html|HTML|HtmlAttributes|Attributes|ClassName)$/;
|
|
1155
|
+
function domRendererComponent(fn, components) {
|
|
1156
|
+
const m = DOM_RENDERER_RE.exec(fn);
|
|
1157
|
+
if (!m) return void 0;
|
|
1158
|
+
const pascal2 = m[1][0].toUpperCase() + m[1].slice(1);
|
|
1159
|
+
if (components.has(pascal2)) return pascal2;
|
|
1160
|
+
return [...components].filter((c) => pascal2.startsWith(c)).sort((a, b) => b.length - a.length)[0];
|
|
1161
|
+
}
|
|
1162
|
+
var LOOKALIKE_CLASSES = { pill: "Badge", badge: "Badge", chip: "Tag", tooltip: "Tooltip", tip: "Tooltip", "has-tip": "Tooltip" };
|
|
1163
|
+
var LOOKALIKE_ATTRS = { "data-tip": "Tooltip" };
|
|
1164
|
+
var HTML_CLASS_RE = /\bclass(?:Name)?\s*=\s*["']([^"'$]*)/g;
|
|
1165
|
+
var HTML_ATTR_RE = /\s(data-tip)\s*=/g;
|
|
1166
|
+
function lookalikesInClassList(list2, components) {
|
|
1167
|
+
const out = [];
|
|
1168
|
+
for (const token of list2.split(/\s+/)) {
|
|
1169
|
+
const component = LOOKALIKE_CLASSES[token];
|
|
1170
|
+
if (component && components.has(component) && !out.some((o) => o.component === component)) out.push({ token, component });
|
|
1171
|
+
}
|
|
1172
|
+
return out;
|
|
1173
|
+
}
|
|
1150
1174
|
function kindFor(path) {
|
|
1151
1175
|
return path.endsWith(".tsx") ? ts.ScriptKind.TSX : path.endsWith(".jsx") ? ts.ScriptKind.JSX : path.endsWith(".ts") || path.endsWith(".mts") || path.endsWith(".cts") ? ts.ScriptKind.TS : ts.ScriptKind.JSX;
|
|
1152
1176
|
}
|
|
@@ -1166,6 +1190,78 @@ function contextForDeclaration(name, typeText) {
|
|
|
1166
1190
|
if (/^(styles?|sx|classes)$|Styles?$|Sx$/.test(name)) return /sx$/i.test(name) ? "sx" : "styled";
|
|
1167
1191
|
return null;
|
|
1168
1192
|
}
|
|
1193
|
+
function importsOf(path, text) {
|
|
1194
|
+
const sf = ts.createSourceFile(path, text, ts.ScriptTarget.Latest, false, kindFor(path));
|
|
1195
|
+
const imports = /* @__PURE__ */ new Map();
|
|
1196
|
+
for (const node of sf.statements) {
|
|
1197
|
+
if (!ts.isImportDeclaration(node) || !ts.isStringLiteral(node.moduleSpecifier)) continue;
|
|
1198
|
+
const mod = node.moduleSpecifier.text;
|
|
1199
|
+
const c = node.importClause;
|
|
1200
|
+
if (c?.name) imports.set(c.name.text, { module: mod, imported: "default" });
|
|
1201
|
+
if (c?.namedBindings && ts.isNamespaceImport(c.namedBindings)) imports.set(c.namedBindings.name.text, { module: mod, imported: "*" });
|
|
1202
|
+
if (c?.namedBindings && ts.isNamedImports(c.namedBindings)) for (const el of c.namedBindings.elements) imports.set(el.name.text, { module: mod, imported: (el.propertyName ?? el.name).text });
|
|
1203
|
+
}
|
|
1204
|
+
return imports;
|
|
1205
|
+
}
|
|
1206
|
+
function classifyTag(t, imports, ds) {
|
|
1207
|
+
const { name } = t;
|
|
1208
|
+
const at = { line: t.line, column: t.column };
|
|
1209
|
+
if (/^[a-z][\w-]*$/.test(name)) {
|
|
1210
|
+
if (!ds.nativeElements.includes(name)) return {};
|
|
1211
|
+
if (name === "input" && t.type === "hidden") return {};
|
|
1212
|
+
if (name === "a" && !t.hasHref) return {};
|
|
1213
|
+
if (t.inRenderProp) return {};
|
|
1214
|
+
const suggestion = nativeEquivalent(name, t.type, ds.components);
|
|
1215
|
+
return { component: { ...at, name, kind: "native", library: "native", ...suggestion ? { suggestion } : {} } };
|
|
1216
|
+
}
|
|
1217
|
+
const root = name.split(".")[0];
|
|
1218
|
+
const imp = imports.get(root);
|
|
1219
|
+
const ns = imp?.imported === "*";
|
|
1220
|
+
const shown = imp && !ns && imp.imported !== "default" ? [imp.imported, ...name.split(".").slice(1)].join(".") : name;
|
|
1221
|
+
const leaf = ns && name.includes(".") ? name.split(".").slice(1).join(".") : shown;
|
|
1222
|
+
if (imp) {
|
|
1223
|
+
const kind = classifyModule(imp.module, ds.packages, ds.foreignLibraries);
|
|
1224
|
+
if (isNonVisual(leaf.split(".").pop())) return {};
|
|
1225
|
+
if (kind === "ds") return { ds: leaf };
|
|
1226
|
+
if (kind === "charts") return { component: { ...at, name: shown, kind: "foreign", library: libraryLabel(imp.module, kind) } };
|
|
1227
|
+
if (kind === "other" && isRouterLink(imp.module, imp.imported === "default" ? "default" : leaf)) {
|
|
1228
|
+
if (t.inRenderProp) return {};
|
|
1229
|
+
const suggestion = ds.components.has("Link") ? "Link" : void 0;
|
|
1230
|
+
return { component: { ...at, name: shown, kind: "foreign", library: libraryLabel(imp.module, kind), ...suggestion ? { suggestion } : {} } };
|
|
1231
|
+
}
|
|
1232
|
+
if (kind === "foreign" || kind === "shadcn") {
|
|
1233
|
+
const suggestion = (ns ? dsEquivalent(root, ds.components) : void 0) ?? dsEquivalent(leaf, ds.components);
|
|
1234
|
+
return { component: { ...at, name: shown, kind: "foreign", library: libraryLabel(imp.module, kind), ...suggestion ? { suggestion } : {} } };
|
|
1235
|
+
}
|
|
1236
|
+
if (kind === "local" && ds.components.has(leaf.split(".")[0])) return { component: { ...at, name: leaf, kind: "recreated", library: "local", suggestion: leaf.split(".")[0] } };
|
|
1237
|
+
return {};
|
|
1238
|
+
}
|
|
1239
|
+
if (ds.components.has(root)) return { localCandidate: { ...at, name, kind: "recreated", library: "local", suggestion: root } };
|
|
1240
|
+
return {};
|
|
1241
|
+
}
|
|
1242
|
+
function tailwindFindings(hits, offset, pos, ds) {
|
|
1243
|
+
const findings = [];
|
|
1244
|
+
let themeRefs = 0;
|
|
1245
|
+
for (const h of hits) {
|
|
1246
|
+
if (h.kind === "scale") {
|
|
1247
|
+
themeRefs++;
|
|
1248
|
+
continue;
|
|
1249
|
+
}
|
|
1250
|
+
if (h.kind === "token") continue;
|
|
1251
|
+
let cat = h.category;
|
|
1252
|
+
const v = h.value;
|
|
1253
|
+
const isColor = Boolean(parseCssColor(v));
|
|
1254
|
+
if (cat === "color-or-size") cat = isColor ? "color" : "font-size";
|
|
1255
|
+
else if (cat === "color-or-width") cat = isColor ? "color" : "border-width";
|
|
1256
|
+
else if (cat === "color-or-shadow") cat = isColor ? "color" : "shadow";
|
|
1257
|
+
else if (cat === "font") cat = /^\d{3}$/.test(v) ? "font-weight" : "font-family";
|
|
1258
|
+
if (cat === "color" && !isColor) continue;
|
|
1259
|
+
const suggestion = ds.suggest(cat, v);
|
|
1260
|
+
const cap = h.kind === "palette" && suggestion && suggestion.confidence > 0.6 ? { ...suggestion, confidence: 0.6, level: "medium" } : suggestion;
|
|
1261
|
+
findings.push({ ...pos(offset + h.index), category: cat, value: h.cls, source: "tailwind", ...cap ? { suggestion: cap } : {} });
|
|
1262
|
+
}
|
|
1263
|
+
return { findings, themeRefs };
|
|
1264
|
+
}
|
|
1169
1265
|
function scanCode(path, text, ds) {
|
|
1170
1266
|
const sf = ts.createSourceFile(path, text, ts.ScriptTarget.Latest, true, kindFor(path));
|
|
1171
1267
|
const out = { findings: [], tokenUsages: 0, themeRefs: 0, ds: {}, components: [], definitions: [] };
|
|
@@ -1180,24 +1276,9 @@ function scanCode(path, text, ds) {
|
|
|
1180
1276
|
out.findings.push({ ...pos(offset), category: h.category, ...property ? { property } : {}, value: h.text, source, ...suggestion ? { suggestion } : {} });
|
|
1181
1277
|
};
|
|
1182
1278
|
const addTailwind = (hits, offset) => {
|
|
1183
|
-
|
|
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,
|
|
@@ -4920,9 +5370,15 @@ Usage:
|
|
|
4920
5370
|
undo a merged token change against HEAD: conflicts shown,
|
|
4921
5371
|
new local bridge/* branch (never pushes, never main)
|
|
4922
5372
|
bridge audit <path> [--json [out.json]] [--md [out.md]] [--fail-under 80] [--baseline prev.json]
|
|
4923
|
-
[--label "..."] [--ds dir] [--no-regress]
|
|
5373
|
+
[--label "..."] [--ds dir] [--no-regress] [--allowlist known.json]
|
|
4924
5374
|
design system adoption in a product folder: token and component
|
|
4925
5375
|
adoption, hardcoded values with the closest token, top files.
|
|
5376
|
+
Reads CSS, SCSS, Less, TS/JS, TSX/JSX, and Astro, Vue and Svelte
|
|
5377
|
+
(template markup, frontmatter and <script>, <style> blocks).
|
|
5378
|
+
Leave one exception out with a comment on the line above:
|
|
5379
|
+
<!-- bridge-audit-ignore-next-line: reason --> (or // or /* */);
|
|
5380
|
+
--allowlist lists known file:line exceptions, each with a reason.
|
|
5381
|
+
Ignored items stay listed in the report.
|
|
4926
5382
|
Exit 0 ok, 1 under --fail-under or regressed, 2 setup error
|
|
4927
5383
|
bridge audit <path> --fix [--write] safe autofix: CSS values equal to a token become var(--token).
|
|
4928
5384
|
Dry run unless --write
|
|
@@ -5313,7 +5769,15 @@ async function audit(a, root) {
|
|
|
5313
5769
|
baseline = JSON.parse(await readFile5(baselinePath, "utf8"));
|
|
5314
5770
|
}
|
|
5315
5771
|
const label = flag3(a, "label");
|
|
5316
|
-
const
|
|
5772
|
+
const allowlistPath = flag3(a, "allowlist");
|
|
5773
|
+
if (allowlistPath && !existsSync5(allowlistPath)) throw new UsageError(`Allowlist not found: ${allowlistPath}`);
|
|
5774
|
+
let allowlist;
|
|
5775
|
+
try {
|
|
5776
|
+
allowlist = allowlistPath ? loadAllowlist(allowlistPath) : void 0;
|
|
5777
|
+
} catch (e) {
|
|
5778
|
+
throw new UsageError(`${allowlistPath}: ${e.message}`);
|
|
5779
|
+
}
|
|
5780
|
+
const report = runAudit(target, { ds: dsRoot, version: VERSION, ...label ? { label } : {}, ...baseline ? { baseline } : {}, ...allowlist ? { allowlist } : {} });
|
|
5317
5781
|
const md = auditMarkdown(report);
|
|
5318
5782
|
const jsonOut = flag3(a, "json");
|
|
5319
5783
|
const mdOut = flag3(a, "md");
|
|
@@ -5344,9 +5808,17 @@ async function audit(a, root) {
|
|
|
5344
5808
|
print(" top fixes:");
|
|
5345
5809
|
for (const f of fixes) print(` ${f.value} -> var(${f.suggestion.cssVar}) ${f.suggestion.level}, ${f.count}x`);
|
|
5346
5810
|
}
|
|
5811
|
+
const ig = report.ignored;
|
|
5812
|
+
if (ig?.total) print(` ignored on purpose ${ig.total} (${ig.comment} by comment, ${ig.allowlist} by the allowlist); listed in --json and --md`);
|
|
5813
|
+
if (ig?.staleAllowlist.length) {
|
|
5814
|
+
print(` allowlist entries that match nothing any more (remove them):`);
|
|
5815
|
+
for (const e of ig.staleAllowlist) print(` ${e.file}:${e.line}${e.what ? ` ${e.what}` : ""}`);
|
|
5816
|
+
}
|
|
5347
5817
|
if (jsonOut) print(`Wrote ${jsonOut}`);
|
|
5348
5818
|
if (mdOut) print(`Wrote ${mdOut}`);
|
|
5349
5819
|
}
|
|
5820
|
+
for (const f of report.files) for (const e of f.ignoreErrors ?? []) process.stderr.write(`${report.target.path}/${f.path}:${e.line}: ${e.message}
|
|
5821
|
+
`);
|
|
5350
5822
|
let code = 0;
|
|
5351
5823
|
const failUnder = flag3(a, "fail-under");
|
|
5352
5824
|
if (failUnder !== void 0) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bridge-ds/cli",
|
|
3
|
-
"version": "0.3.
|
|
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": {
|