@bridge-ds/core 0.4.0 → 0.5.1
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/README.md +16 -5
- package/audit-report.schema.json +135 -1
- package/dist/audit/code.d.ts +50 -0
- package/dist/audit/components.d.ts +1 -1
- package/dist/audit/ignore.d.ts +34 -0
- package/dist/audit/index.d.ts +3 -0
- package/dist/audit/index.js +45 -29
- package/dist/audit/markup.d.ts +21 -0
- package/dist/audit/report.d.ts +5 -7
- package/dist/audit/style.d.ts +25 -0
- package/dist/audit/types.d.ts +39 -1
- package/dist/github-client.d.ts +14 -0
- package/dist/index.js +9 -6
- package/dist/index.js.map +1 -1
- package/dist/node/audit.d.ts +5 -1
- package/dist/node/index.js +6 -2
- package/dist/node/index.js.map +1 -1
- package/dist/{report-Bf2HEr9t.js → report-iPSnsjYi.js} +599 -143
- package/dist/report-iPSnsjYi.js.map +1 -0
- package/package.json +1 -1
- package/dist/report-Bf2HEr9t.js.map +0 -1
|
@@ -306,7 +306,7 @@ const looseKey = (p) => p.startsWith("$") || p.startsWith("--") || !/^-?[a-z]+(-
|
|
|
306
306
|
const KNOWN_PROPS = new Set(
|
|
307
307
|
"color background background-color background-image border border-top border-right border-bottom border-left border-color border-width border-style border-radius border-top-left-radius border-top-right-radius border-bottom-left-radius border-bottom-right-radius border-top-color border-bottom-color border-left-color border-right-color border-top-width border-bottom-width border-left-width border-right-width border-block border-inline border-block-start border-block-end border-inline-start border-inline-end outline outline-color outline-width outline-offset fill stroke text-decoration text-decoration-color caret-color accent-color box-shadow text-shadow padding padding-top padding-right padding-bottom padding-left padding-block padding-inline padding-x padding-y padding-inline-start padding-inline-end padding-block-start padding-block-end margin margin-top margin-right margin-bottom margin-left margin-block margin-inline margin-x margin-y margin-inline-start margin-inline-end margin-block-start margin-block-end gap row-gap column-gap grid-gap inset top right bottom left width height min-width min-height max-width max-height flex-basis font font-family font-size font-weight line-height letter-spacing transition transition-duration transition-delay transition-timing-function animation animation-duration animation-delay animation-timing-function z-index opacity display position flex flex-direction flex-wrap flex-grow flex-shrink align-items justify-content overflow text-align white-space cursor content transform".split(" ")
|
|
308
308
|
);
|
|
309
|
-
function mask(value) {
|
|
309
|
+
function mask$1(value) {
|
|
310
310
|
let out = value;
|
|
311
311
|
for (const fn of ["var(", "url(", "env("]) {
|
|
312
312
|
let i = out.indexOf(fn);
|
|
@@ -352,7 +352,7 @@ function scanCssValue(property, value) {
|
|
|
352
352
|
out.themeRef = true;
|
|
353
353
|
return out;
|
|
354
354
|
}
|
|
355
|
-
const masked = mask(value);
|
|
355
|
+
const masked = mask$1(value);
|
|
356
356
|
if (prop === "font-family") {
|
|
357
357
|
if (masked.trim() && !/^(inherit|initial|unset)$/i.test(v)) hits.push({ category: "font-family", text: v, index: value.indexOf(v) });
|
|
358
358
|
return out;
|
|
@@ -572,6 +572,10 @@ const EQUIVALENTS = [
|
|
|
572
572
|
[/^Badge$/, ["Badge"]],
|
|
573
573
|
[/^(Chip|Tag|ChipField)$/, ["Tag", "Badge"]],
|
|
574
574
|
[/^(Alert|AlertTitle|Callout)$/, ["Alert"]],
|
|
575
|
+
[/^(Banner|AnnouncementBar|TopBanner)$/, ["Banner", "Alert"]],
|
|
576
|
+
[/^(CodeBlock|Code|CodeSnippet|Snippet|Prism|SyntaxHighlighter|CopyBlock)$/, ["CodeBlock"]],
|
|
577
|
+
[/^(RadioCard|RadioCards|RadioTile|ChoiceCard|ChoiceCardGroup)$/, ["ChoiceCard", "RadioGroup"]],
|
|
578
|
+
[/^(Checklist|ChecklistItem|StepList)$/, ["Checklist"]],
|
|
575
579
|
[/^(Snackbar|Toast|Toaster|Notification)$/, ["Toast"]],
|
|
576
580
|
[/^(CircularProgress|Spinner|Loading|LoadingIndicator)$/, ["Spinner", "Progress"]],
|
|
577
581
|
[/^(LinearProgress|Progress)$/, ["Progress"]],
|
|
@@ -851,6 +855,26 @@ const THEME_FNS = /^(createTheme|extendTheme|createMuiTheme|responsiveFontSizes|
|
|
|
851
855
|
const CSS_TAGS = /^(styled(\.\w+|\(.*\)(\.attrs\(.*\))?)|css|keyframes|createGlobalStyle|injectGlobal|tw\.\w+)$/s;
|
|
852
856
|
const MUI_SPACING_ATTR = /^([mp][trblxy]?|(margin|padding)(Top|Bottom|Left|Right|X|Y)?|gap|rowGap|columnGap|spacing)$/;
|
|
853
857
|
const THEME_REF_RE = /\btheme\.(palette|spacing|shape|typography|shadows|zIndex|transitions|vars)\b/g;
|
|
858
|
+
const DOM_RENDERER_RE = /^([a-z][A-Za-z0-9]*?)(Html|HTML|HtmlAttributes|Attributes|ClassName)$/;
|
|
859
|
+
function domRendererComponent(fn, components) {
|
|
860
|
+
const m = DOM_RENDERER_RE.exec(fn);
|
|
861
|
+
if (!m) return void 0;
|
|
862
|
+
const pascal2 = m[1][0].toUpperCase() + m[1].slice(1);
|
|
863
|
+
if (components.has(pascal2)) return pascal2;
|
|
864
|
+
return [...components].filter((c) => pascal2.startsWith(c)).sort((a, b) => b.length - a.length)[0];
|
|
865
|
+
}
|
|
866
|
+
const LOOKALIKE_CLASSES = { pill: "Badge", badge: "Badge", chip: "Tag", tooltip: "Tooltip", tip: "Tooltip", "has-tip": "Tooltip" };
|
|
867
|
+
const LOOKALIKE_ATTRS = { "data-tip": "Tooltip" };
|
|
868
|
+
const HTML_CLASS_RE = /\bclass(?:Name)?\s*=\s*["']([^"'$]*)/g;
|
|
869
|
+
const HTML_ATTR_RE = /\s(data-tip)\s*=/g;
|
|
870
|
+
function lookalikesInClassList(list, components) {
|
|
871
|
+
const out = [];
|
|
872
|
+
for (const token of list.split(/\s+/)) {
|
|
873
|
+
const component = LOOKALIKE_CLASSES[token];
|
|
874
|
+
if (component && components.has(component) && !out.some((o) => o.component === component)) out.push({ token, component });
|
|
875
|
+
}
|
|
876
|
+
return out;
|
|
877
|
+
}
|
|
854
878
|
function kindFor(path) {
|
|
855
879
|
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;
|
|
856
880
|
}
|
|
@@ -870,6 +894,79 @@ function contextForDeclaration(name, typeText) {
|
|
|
870
894
|
if (/^(styles?|sx|classes)$|Styles?$|Sx$/.test(name)) return /sx$/i.test(name) ? "sx" : "styled";
|
|
871
895
|
return null;
|
|
872
896
|
}
|
|
897
|
+
function importsOf(path, text) {
|
|
898
|
+
const sf = ts.createSourceFile(path, text, ts.ScriptTarget.Latest, false, kindFor(path));
|
|
899
|
+
const imports = /* @__PURE__ */ new Map();
|
|
900
|
+
for (const node of sf.statements) {
|
|
901
|
+
if (!ts.isImportDeclaration(node) || !ts.isStringLiteral(node.moduleSpecifier)) continue;
|
|
902
|
+
const mod = node.moduleSpecifier.text;
|
|
903
|
+
const c = node.importClause;
|
|
904
|
+
if (c?.name) imports.set(c.name.text, { module: mod, imported: "default" });
|
|
905
|
+
if (c?.namedBindings && ts.isNamespaceImport(c.namedBindings)) imports.set(c.namedBindings.name.text, { module: mod, imported: "*" });
|
|
906
|
+
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 });
|
|
907
|
+
}
|
|
908
|
+
return imports;
|
|
909
|
+
}
|
|
910
|
+
function classifyTag(t, imports, ds) {
|
|
911
|
+
const { name } = t;
|
|
912
|
+
const at = { line: t.line, column: t.column };
|
|
913
|
+
if (/^[a-z][\w-]*$/.test(name)) {
|
|
914
|
+
if (!ds.nativeElements.includes(name)) return {};
|
|
915
|
+
if (name === "input" && t.type === "hidden") return {};
|
|
916
|
+
if (name === "a" && !t.hasHref) return {};
|
|
917
|
+
if (t.inRenderProp) return {};
|
|
918
|
+
const suggestion = nativeEquivalent(name, t.type, ds.components);
|
|
919
|
+
if (!suggestion) return {};
|
|
920
|
+
return { component: { ...at, name, kind: "native", library: "native", suggestion } };
|
|
921
|
+
}
|
|
922
|
+
const root = name.split(".")[0];
|
|
923
|
+
const imp = imports.get(root);
|
|
924
|
+
const ns = imp?.imported === "*";
|
|
925
|
+
const shown = imp && !ns && imp.imported !== "default" ? [imp.imported, ...name.split(".").slice(1)].join(".") : name;
|
|
926
|
+
const leaf = ns && name.includes(".") ? name.split(".").slice(1).join(".") : shown;
|
|
927
|
+
if (imp) {
|
|
928
|
+
const kind = classifyModule(imp.module, ds.packages, ds.foreignLibraries);
|
|
929
|
+
if (isNonVisual(leaf.split(".").pop())) return {};
|
|
930
|
+
if (kind === "ds") return { ds: leaf };
|
|
931
|
+
if (kind === "charts") return { component: { ...at, name: shown, kind: "foreign", library: libraryLabel(imp.module, kind) } };
|
|
932
|
+
if (kind === "other" && isRouterLink(imp.module, imp.imported === "default" ? "default" : leaf)) {
|
|
933
|
+
if (t.inRenderProp) return {};
|
|
934
|
+
const suggestion = ds.components.has("Link") ? "Link" : void 0;
|
|
935
|
+
return { component: { ...at, name: shown, kind: "foreign", library: libraryLabel(imp.module, kind), ...suggestion ? { suggestion } : {} } };
|
|
936
|
+
}
|
|
937
|
+
if (kind === "foreign" || kind === "shadcn") {
|
|
938
|
+
const suggestion = (ns ? dsEquivalent(root, ds.components) : void 0) ?? dsEquivalent(leaf, ds.components);
|
|
939
|
+
return { component: { ...at, name: shown, kind: "foreign", library: libraryLabel(imp.module, kind), ...suggestion ? { suggestion } : {} } };
|
|
940
|
+
}
|
|
941
|
+
if (kind === "local" && ds.components.has(leaf.split(".")[0])) return { component: { ...at, name: leaf, kind: "recreated", library: "local", suggestion: leaf.split(".")[0] } };
|
|
942
|
+
return {};
|
|
943
|
+
}
|
|
944
|
+
if (ds.components.has(root)) return { localCandidate: { ...at, name, kind: "recreated", library: "local", suggestion: root } };
|
|
945
|
+
return {};
|
|
946
|
+
}
|
|
947
|
+
function tailwindFindings(hits, offset, pos, ds) {
|
|
948
|
+
const findings = [];
|
|
949
|
+
let themeRefs = 0;
|
|
950
|
+
for (const h of hits) {
|
|
951
|
+
if (h.kind === "scale") {
|
|
952
|
+
themeRefs++;
|
|
953
|
+
continue;
|
|
954
|
+
}
|
|
955
|
+
if (h.kind === "token") continue;
|
|
956
|
+
let cat = h.category;
|
|
957
|
+
const v = h.value;
|
|
958
|
+
const isColor = Boolean(parseCssColor(v));
|
|
959
|
+
if (cat === "color-or-size") cat = isColor ? "color" : "font-size";
|
|
960
|
+
else if (cat === "color-or-width") cat = isColor ? "color" : "border-width";
|
|
961
|
+
else if (cat === "color-or-shadow") cat = isColor ? "color" : "shadow";
|
|
962
|
+
else if (cat === "font") cat = /^\d{3}$/.test(v) ? "font-weight" : "font-family";
|
|
963
|
+
if (cat === "color" && !isColor) continue;
|
|
964
|
+
const suggestion = ds.suggest(cat, v);
|
|
965
|
+
const cap = h.kind === "palette" && suggestion && suggestion.confidence > 0.6 ? { ...suggestion, confidence: 0.6, level: "medium" } : suggestion;
|
|
966
|
+
findings.push({ ...pos(offset + h.index), category: cat, value: h.cls, source: "tailwind", ...cap ? { suggestion: cap } : {} });
|
|
967
|
+
}
|
|
968
|
+
return { findings, themeRefs };
|
|
969
|
+
}
|
|
873
970
|
function scanCode(path, text, ds) {
|
|
874
971
|
const sf = ts.createSourceFile(path, text, ts.ScriptTarget.Latest, true, kindFor(path));
|
|
875
972
|
const out = { findings: [], tokenUsages: 0, themeRefs: 0, ds: {}, components: [], definitions: [] };
|
|
@@ -884,24 +981,9 @@ function scanCode(path, text, ds) {
|
|
|
884
981
|
out.findings.push({ ...pos(offset), category: h.category, ...property ? { property } : {}, value: h.text, source, ...suggestion ? { suggestion } : {} });
|
|
885
982
|
};
|
|
886
983
|
const addTailwind = (hits, offset) => {
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
continue;
|
|
891
|
-
}
|
|
892
|
-
if (h.kind === "token") continue;
|
|
893
|
-
let cat = h.category;
|
|
894
|
-
const v = h.value;
|
|
895
|
-
const isColor = Boolean(parseCssColor(v));
|
|
896
|
-
if (cat === "color-or-size") cat = isColor ? "color" : "font-size";
|
|
897
|
-
else if (cat === "color-or-width") cat = isColor ? "color" : "border-width";
|
|
898
|
-
else if (cat === "color-or-shadow") cat = isColor ? "color" : "shadow";
|
|
899
|
-
else if (cat === "font") cat = /^\d{3}$/.test(v) ? "font-weight" : "font-family";
|
|
900
|
-
if (cat === "color" && !isColor) continue;
|
|
901
|
-
const suggestion = ds.suggest(cat, v);
|
|
902
|
-
const cap = h.kind === "palette" && suggestion && suggestion.confidence > 0.6 ? { ...suggestion, confidence: 0.6, level: "medium" } : suggestion;
|
|
903
|
-
out.findings.push({ ...pos(offset + h.index), category: cat, value: h.cls, source: "tailwind", ...cap ? { suggestion: cap } : {} });
|
|
904
|
-
}
|
|
984
|
+
const r = tailwindFindings(hits, offset, pos, ds);
|
|
985
|
+
out.themeRefs += r.themeRefs;
|
|
986
|
+
out.findings.push(...r.findings);
|
|
905
987
|
};
|
|
906
988
|
const styleValue = (key, node, ctx) => {
|
|
907
989
|
const source = ctx === "sx" ? "sx" : ctx === "style" ? "style" : ctx === "theme" ? "theme" : "styled";
|
|
@@ -962,13 +1044,18 @@ function scanCode(path, text, ds) {
|
|
|
962
1044
|
}
|
|
963
1045
|
for (const a of scan.applies) addTailwind(a.hits, start + a.start);
|
|
964
1046
|
};
|
|
1047
|
+
const lookalike = (list, offset) => {
|
|
1048
|
+
for (const l of lookalikesInClassList(list, ds.components)) out.components.push({ ...pos(offset), name: `.${l.token}`, kind: "recreated", library: "local", suggestion: l.component });
|
|
1049
|
+
};
|
|
965
1050
|
const classLiteral = (node) => {
|
|
966
1051
|
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
|
|
967
1052
|
handled.add(node);
|
|
1053
|
+
lookalike(node.text, node.getStart(sf) + 1);
|
|
968
1054
|
addTailwind(scanClassString(node.text), node.getStart(sf) + 1);
|
|
969
1055
|
} else if (ts.isTemplateExpression(node)) {
|
|
970
1056
|
for (const p of [node.head, ...node.templateSpans.map((s) => s.literal)]) {
|
|
971
1057
|
handled.add(p);
|
|
1058
|
+
lookalike(p.text, p.getStart(sf) + 1);
|
|
972
1059
|
addTailwind(scanClassString(p.text), p.getStart(sf) + 1);
|
|
973
1060
|
}
|
|
974
1061
|
node.templateSpans.forEach((s) => visit(s.expression, null, true));
|
|
@@ -977,49 +1064,13 @@ function scanCode(path, text, ds) {
|
|
|
977
1064
|
const jsxName = (n) => n.getText(sf);
|
|
978
1065
|
const element = (node) => {
|
|
979
1066
|
const name = jsxName(node.tagName);
|
|
980
|
-
const
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
if (isRenderElement(node)) return;
|
|
988
|
-
const suggestion = nativeEquivalent(name, type, ds.components);
|
|
989
|
-
out.components.push({ ...at, name, kind: "native", library: "native", ...suggestion ? { suggestion } : {} });
|
|
990
|
-
return;
|
|
991
|
-
}
|
|
992
|
-
const root = name.split(".")[0];
|
|
993
|
-
const imp = imports.get(root);
|
|
994
|
-
const ns = imp?.imported === "*";
|
|
995
|
-
const shown = imp && !ns && imp.imported !== "default" ? [imp.imported, ...name.split(".").slice(1)].join(".") : name;
|
|
996
|
-
const leaf = ns && name.includes(".") ? name.split(".").slice(1).join(".") : shown;
|
|
997
|
-
if (imp) {
|
|
998
|
-
const kind = classifyModule(imp.module, ds.packages, ds.foreignLibraries);
|
|
999
|
-
if (isNonVisual(leaf.split(".").pop())) return;
|
|
1000
|
-
if (kind === "ds") {
|
|
1001
|
-
out.ds[leaf] = (out.ds[leaf] ?? 0) + 1;
|
|
1002
|
-
return;
|
|
1003
|
-
}
|
|
1004
|
-
if (kind === "charts") {
|
|
1005
|
-
out.components.push({ ...at, name: shown, kind: "foreign", library: libraryLabel(imp.module, kind) });
|
|
1006
|
-
return;
|
|
1007
|
-
}
|
|
1008
|
-
if (kind === "other" && isRouterLink(imp.module, imp.imported === "default" ? "default" : leaf)) {
|
|
1009
|
-
if (isRenderElement(node)) return;
|
|
1010
|
-
const suggestion = ds.components.has("Link") ? "Link" : void 0;
|
|
1011
|
-
out.components.push({ ...at, name: shown, kind: "foreign", library: libraryLabel(imp.module, kind), ...suggestion ? { suggestion } : {} });
|
|
1012
|
-
return;
|
|
1013
|
-
}
|
|
1014
|
-
if (kind === "foreign" || kind === "shadcn") {
|
|
1015
|
-
const suggestion = (ns ? dsEquivalent(root, ds.components) : void 0) ?? dsEquivalent(leaf, ds.components);
|
|
1016
|
-
out.components.push({ ...at, name: shown, kind: "foreign", library: libraryLabel(imp.module, kind), ...suggestion ? { suggestion } : {} });
|
|
1017
|
-
return;
|
|
1018
|
-
}
|
|
1019
|
-
if (kind === "local" && ds.components.has(leaf.split(".")[0])) out.components.push({ ...at, name: leaf, kind: "recreated", library: "local", suggestion: leaf.split(".")[0] });
|
|
1020
|
-
return;
|
|
1021
|
-
}
|
|
1022
|
-
if (ds.components.has(root)) pendingLocal.push({ ...at, name, kind: "recreated", library: "local", suggestion: root });
|
|
1067
|
+
const typeAttr = node.attributes.properties.find((p) => ts.isJsxAttribute(p) && p.name.getText(sf) === "type");
|
|
1068
|
+
const type = typeAttr?.initializer && ts.isStringLiteral(typeAttr.initializer) ? typeAttr.initializer.text : void 0;
|
|
1069
|
+
const hasHref = node.attributes.properties.some((p) => ts.isJsxAttribute(p) && p.name.getText(sf) === "href");
|
|
1070
|
+
const r = classifyTag({ name, ...pos(node.getStart(sf)), ...type !== void 0 ? { type } : {}, hasHref, inRenderProp: isRenderElement(node) }, imports, ds);
|
|
1071
|
+
if (r.ds) out.ds[r.ds] = (out.ds[r.ds] ?? 0) + 1;
|
|
1072
|
+
if (r.component) out.components.push(r.component);
|
|
1073
|
+
if (r.localCandidate) pendingLocal.push(r.localCandidate);
|
|
1023
1074
|
};
|
|
1024
1075
|
const pendingLocal = [];
|
|
1025
1076
|
function visit(node, ctx, classCtx) {
|
|
@@ -1079,6 +1130,17 @@ function scanCode(path, text, ds) {
|
|
|
1079
1130
|
cssTemplate(node.template);
|
|
1080
1131
|
return;
|
|
1081
1132
|
}
|
|
1133
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
|
|
1134
|
+
const imp = imports.get(node.expression.text);
|
|
1135
|
+
if (imp && imp.imported !== "*" && classifyModule(imp.module, ds.packages, ds.foreignLibraries) === "ds") {
|
|
1136
|
+
const comp = domRendererComponent(imp.imported === "default" ? node.expression.text : imp.imported, ds.components);
|
|
1137
|
+
if (comp) out.ds[comp] = (out.ds[comp] ?? 0) + 1;
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken && ts.isPropertyAccessExpression(node.left) && node.left.name.text === "className") {
|
|
1141
|
+
visit(node.left, ctx, classCtx);
|
|
1142
|
+
return classLiteral(node.right);
|
|
1143
|
+
}
|
|
1082
1144
|
if (ts.isCallExpression(node)) {
|
|
1083
1145
|
const callee = node.expression.getText(sf);
|
|
1084
1146
|
if (CLASS_FNS.test(callee)) {
|
|
@@ -1123,6 +1185,14 @@ function scanCode(path, text, ds) {
|
|
|
1123
1185
|
const literal = (node) => {
|
|
1124
1186
|
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node) || ts.isTemplateHead(node) || ts.isTemplateMiddle(node) || ts.isTemplateTail(node)) {
|
|
1125
1187
|
const t = node.text;
|
|
1188
|
+
if (!handled.has(node) && (t.includes("<") || ts.isTemplateMiddle(node) || ts.isTemplateTail(node))) {
|
|
1189
|
+
const base = node.getStart(sf) + 1;
|
|
1190
|
+
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 });
|
|
1191
|
+
for (const m of t.matchAll(HTML_ATTR_RE)) {
|
|
1192
|
+
const component = LOOKALIKE_ATTRS[m[1]];
|
|
1193
|
+
if (component && ds.components.has(component)) out.components.push({ ...pos(base + m.index + 1), name: `[${m[1]}]`, kind: "recreated", library: "local", suggestion: component });
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1126
1196
|
for (const m of t.matchAll(/var\(\s*(--[\w-]+)/g)) {
|
|
1127
1197
|
if (ds.isDsVar(m[1])) out.tokenUsages++;
|
|
1128
1198
|
else out.themeRefs++;
|
|
@@ -1141,24 +1211,82 @@ function scanCode(path, text, ds) {
|
|
|
1141
1211
|
out.components.sort((a, b) => a.line - b.line || a.column - b.column);
|
|
1142
1212
|
return out;
|
|
1143
1213
|
}
|
|
1144
|
-
const
|
|
1145
|
-
|
|
1146
|
-
const
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1214
|
+
const IGNORE_DIRECTIVE = "bridge-audit-ignore-next-line";
|
|
1215
|
+
function ignoreDirectives(text) {
|
|
1216
|
+
const at = lineCol(text);
|
|
1217
|
+
const out = [];
|
|
1218
|
+
for (const m of text.matchAll(new RegExp(`${IGNORE_DIRECTIVE}\\b`, "g"))) {
|
|
1219
|
+
const start = m.index;
|
|
1220
|
+
const opener = /(<!--|\/\*|\/\/)(\s*)$/.exec(text.slice(Math.max(0, start - 200), start));
|
|
1221
|
+
const open = opener?.[1];
|
|
1222
|
+
if (!open || open === "//" && opener[2].includes("\n")) continue;
|
|
1223
|
+
const from = start + m[0].length;
|
|
1224
|
+
let end;
|
|
1225
|
+
if (open === "//") {
|
|
1226
|
+
end = text.indexOf("\n", from);
|
|
1227
|
+
if (end < 0) end = text.length;
|
|
1228
|
+
} else {
|
|
1229
|
+
end = text.indexOf(open === "<!--" ? "-->" : "*/", from);
|
|
1230
|
+
if (end < 0) continue;
|
|
1231
|
+
}
|
|
1232
|
+
const reason = text.slice(from, end).replace(/^\s*:?/, "").replace(/\s+/g, " ").trim();
|
|
1233
|
+
out.push({ line: at(start).line, target: at(end).line + 1, reason });
|
|
1234
|
+
}
|
|
1235
|
+
return out;
|
|
1236
|
+
}
|
|
1237
|
+
const 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 };
|
|
1238
|
+
function applyIgnoreComments(text, findings, components) {
|
|
1239
|
+
const errors = [];
|
|
1240
|
+
const reasons = /* @__PURE__ */ new Map();
|
|
1241
|
+
for (const d of ignoreDirectives(text)) {
|
|
1242
|
+
if (!d.reason) {
|
|
1243
|
+
errors.push({ line: d.line, message: `${IGNORE_DIRECTIVE} needs a reason after a colon (${IGNORE_DIRECTIVE}: why this one stays); nothing was ignored` });
|
|
1244
|
+
continue;
|
|
1245
|
+
}
|
|
1246
|
+
if (!findings.some((f) => f.line === d.target) && !components.some((c) => c.line === d.target)) {
|
|
1247
|
+
errors.push({ line: d.line, message: `${IGNORE_DIRECTIVE} ignores nothing: line ${d.target} has no finding; remove the comment` });
|
|
1248
|
+
continue;
|
|
1249
|
+
}
|
|
1250
|
+
reasons.set(d.target, d.reason);
|
|
1251
|
+
}
|
|
1252
|
+
const ignored = [];
|
|
1253
|
+
const keep = (xs) => xs.filter((x) => {
|
|
1254
|
+
const r = reasons.get(x.line);
|
|
1255
|
+
if (r === void 0) return true;
|
|
1256
|
+
ignored.push(item(x, r, "comment"));
|
|
1257
|
+
return false;
|
|
1258
|
+
});
|
|
1259
|
+
return { findings: keep(findings), components: keep(components), ignored, errors };
|
|
1260
|
+
}
|
|
1261
|
+
function applyAllowlist(entries, findings, components) {
|
|
1262
|
+
const used = /* @__PURE__ */ new Set();
|
|
1263
|
+
const ignored = [];
|
|
1264
|
+
const keep = (xs) => xs.filter((x) => {
|
|
1265
|
+
const what = "category" in x ? x.value : x.name;
|
|
1266
|
+
const fits = (e) => e.line === x.line && (e.what === void 0 || e.what === what);
|
|
1267
|
+
let i = entries.findIndex((e, k) => fits(e) && !used.has(k));
|
|
1268
|
+
if (i < 0) i = entries.findIndex(fits);
|
|
1269
|
+
if (i < 0) return true;
|
|
1270
|
+
used.add(i);
|
|
1271
|
+
ignored.push(item(x, entries[i].reason, "allowlist"));
|
|
1272
|
+
return false;
|
|
1273
|
+
});
|
|
1274
|
+
return { findings: keep(findings), components: keep(components), ignored, used };
|
|
1275
|
+
}
|
|
1276
|
+
function parseAllowlist(json) {
|
|
1277
|
+
const fail = (msg) => {
|
|
1278
|
+
throw new Error(`allowlist: ${msg}`);
|
|
1279
|
+
};
|
|
1280
|
+
if (!json || typeof json !== "object" || !Array.isArray(json.entries)) fail('expected { "entries": [...] }');
|
|
1281
|
+
return json.entries.map((e, i) => {
|
|
1282
|
+
const x = e;
|
|
1283
|
+
if (typeof x.file !== "string" || !x.file) fail(`entry ${i} needs "file"`);
|
|
1284
|
+
if (typeof x.line !== "number" || !Number.isInteger(x.line) || x.line < 1) fail(`entry ${i} (${x.file}) needs a "line" number`);
|
|
1285
|
+
if (typeof x.reason !== "string" || !x.reason.trim()) fail(`entry ${i} (${x.file}:${x.line}) needs a "reason"`);
|
|
1286
|
+
if (x.what !== void 0 && typeof x.what !== "string") fail(`entry ${i} (${x.file}:${x.line}): "what" must be a string`);
|
|
1287
|
+
return { file: x.file, line: x.line, ...x.what !== void 0 ? { what: x.what } : {}, reason: x.reason.trim() };
|
|
1288
|
+
});
|
|
1151
1289
|
}
|
|
1152
|
-
const langOf = (path) => {
|
|
1153
|
-
const ext = path.slice(path.lastIndexOf(".") + 1);
|
|
1154
|
-
if (ext === "css") return "css";
|
|
1155
|
-
if (ext === "scss" || ext === "sass") return "scss";
|
|
1156
|
-
if (ext === "less") return "less";
|
|
1157
|
-
if (ext === "tsx") return "tsx";
|
|
1158
|
-
if (ext === "jsx") return "jsx";
|
|
1159
|
-
if (/^[cm]?ts$/.test(ext)) return "ts";
|
|
1160
|
-
return "js";
|
|
1161
|
-
};
|
|
1162
1290
|
function isSafeFix(h, s) {
|
|
1163
1291
|
if (!s || s.level !== "exact") return false;
|
|
1164
1292
|
switch (h.category) {
|
|
@@ -1183,49 +1311,338 @@ function isSafeFix(h, s) {
|
|
|
1183
1311
|
return false;
|
|
1184
1312
|
}
|
|
1185
1313
|
}
|
|
1186
|
-
function
|
|
1187
|
-
const
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1314
|
+
function auditCssText(text, lang, ds, opts) {
|
|
1315
|
+
const base = opts.offset ?? 0;
|
|
1316
|
+
const source = opts.source ?? "css";
|
|
1317
|
+
const at = (o) => opts.pos(base + o);
|
|
1318
|
+
const scan = scanCssText(text, lang);
|
|
1319
|
+
const findings = [];
|
|
1320
|
+
let tokenUsages = 0;
|
|
1321
|
+
let themeRefs = 0;
|
|
1322
|
+
for (const d of scan.decls) {
|
|
1323
|
+
for (const v of d.scan.vars) ds.isDsVar(v) ? tokenUsages++ : themeRefs++;
|
|
1324
|
+
if (d.scan.themeRef) themeRefs++;
|
|
1325
|
+
else if (lang !== "css" && !d.property.startsWith("$")) themeRefs += (d.value.match(/\$[\w-]+/g) ?? []).length;
|
|
1326
|
+
const property = d.property.toLowerCase();
|
|
1327
|
+
for (const h of d.scan.hits) {
|
|
1328
|
+
const suggestion = ds.suggest(h.category, h.text, property);
|
|
1329
|
+
const autofix = source === "css" && isSafeFix(h, suggestion);
|
|
1330
|
+
findings.push({ ...at(d.valueStart + h.index), category: h.category, property, value: h.text, source, ...suggestion ? { suggestion } : {}, ...autofix ? { autofix: true } : {} });
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
for (const a of scan.applies) {
|
|
1334
|
+
for (const h of a.hits) {
|
|
1335
|
+
if (h.kind === "scale") themeRefs++;
|
|
1336
|
+
else if (h.kind !== "token" && h.value) {
|
|
1337
|
+
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;
|
|
1338
|
+
const suggestion = ds.suggest(cat, h.value);
|
|
1339
|
+
findings.push({ ...at(a.start + h.index), category: cat, value: h.cls, source: "tailwind", ...suggestion ? { suggestion } : {} });
|
|
1340
|
+
} else if (h.kind === "token" && h.value) {
|
|
1341
|
+
const v = /var\(\s*(--[\w-]+)/.exec(h.value)?.[1];
|
|
1342
|
+
if (v && ds.isDsVar(v)) tokenUsages++;
|
|
1343
|
+
else themeRefs++;
|
|
1203
1344
|
}
|
|
1204
1345
|
}
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1346
|
+
}
|
|
1347
|
+
findings.sort((x, y) => x.line - y.line || x.column - y.column);
|
|
1348
|
+
return { findings, tokenUsages, themeRefs };
|
|
1349
|
+
}
|
|
1350
|
+
const MARKUP_EXTENSIONS = [".astro", ".vue", ".svelte"];
|
|
1351
|
+
function mask(text, ranges, keepInside) {
|
|
1352
|
+
const inside = new Uint8Array(text.length);
|
|
1353
|
+
for (const r of ranges) inside.fill(1, r.start, r.end);
|
|
1354
|
+
let out = "";
|
|
1355
|
+
for (let i = 0; i < text.length; i++) {
|
|
1356
|
+
const c = text[i];
|
|
1357
|
+
out += c === "\n" || Boolean(inside[i]) === keepInside ? c : " ";
|
|
1358
|
+
}
|
|
1359
|
+
return out;
|
|
1360
|
+
}
|
|
1361
|
+
const attrOf = (attrs, name) => new RegExp(`(?:^|\\s)${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s>]+))`, "i").exec(attrs)?.slice(1).find((v) => v !== void 0);
|
|
1362
|
+
const isCodeScript = (attrs) => {
|
|
1363
|
+
const type = attrOf(attrs, "type")?.toLowerCase();
|
|
1364
|
+
return !type || ["module", "text/javascript", "application/javascript", "text/typescript", "ts", "text/babel"].includes(type);
|
|
1365
|
+
};
|
|
1366
|
+
function markupBlocks(text, lang) {
|
|
1367
|
+
const nonTemplate = [];
|
|
1368
|
+
let frontmatter;
|
|
1369
|
+
let from = 0;
|
|
1370
|
+
if (lang === "astro") {
|
|
1371
|
+
const open = /^\s*---[^\S\n]*\n/.exec(text);
|
|
1372
|
+
if (open) {
|
|
1373
|
+
const close = /\n---[^\S\n]*(\n|$)/g;
|
|
1374
|
+
close.lastIndex = open[0].length - 1;
|
|
1375
|
+
const m = close.exec(text);
|
|
1376
|
+
if (m) {
|
|
1377
|
+
frontmatter = { start: open[0].length, end: m.index + 1 };
|
|
1378
|
+
from = m.index + m[0].length;
|
|
1379
|
+
nonTemplate.push({ start: 0, end: from });
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
const scripts = [];
|
|
1384
|
+
const styles = [];
|
|
1385
|
+
const re = /<(script|style)\b((?:[^>"']|"[^"]*"|'[^']*')*)>([\s\S]*?)<\/\1\s*>/gi;
|
|
1386
|
+
re.lastIndex = from;
|
|
1387
|
+
for (let m; m = re.exec(text); ) {
|
|
1388
|
+
const contentStart = m.index + m[0].indexOf(">", m[1].length + 1 + m[2].length) + 1;
|
|
1389
|
+
const block = { start: contentStart, end: contentStart + m[3].length, attrs: m[2] };
|
|
1390
|
+
(m[1].toLowerCase() === "script" ? scripts : styles).push(block);
|
|
1391
|
+
nonTemplate.push({ start: m.index, end: m.index + m[0].length });
|
|
1392
|
+
}
|
|
1393
|
+
for (const m of text.slice(from).matchAll(/<!--[\s\S]*?-->/g)) nonTemplate.push({ start: from + m.index, end: from + m.index + m[0].length });
|
|
1394
|
+
return { ...frontmatter ? { frontmatter } : {}, scripts, styles, nonTemplate };
|
|
1395
|
+
}
|
|
1396
|
+
function closeBrace(s, i) {
|
|
1397
|
+
let depth = 0;
|
|
1398
|
+
for (let k = i; k < s.length; k++) {
|
|
1399
|
+
const c = s[k];
|
|
1400
|
+
if (c === "{") depth++;
|
|
1401
|
+
else if (c === "}") {
|
|
1402
|
+
if (--depth === 0) return k + 1;
|
|
1403
|
+
} else if (c === '"' || c === "'") {
|
|
1404
|
+
const e = s.indexOf(c, k + 1);
|
|
1405
|
+
if (e < 0) return s.length;
|
|
1406
|
+
k = e;
|
|
1407
|
+
} else if (c === "`") {
|
|
1408
|
+
for (k++; k < s.length && s[k] !== "`"; k++) {
|
|
1409
|
+
if (s[k] === "\\") k++;
|
|
1410
|
+
else if (s[k] === "$" && s[k + 1] === "{") k = closeBrace(s, k + 1) - 1;
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
return s.length;
|
|
1415
|
+
}
|
|
1416
|
+
function readTag(s, i, lang) {
|
|
1417
|
+
const attrs = [];
|
|
1418
|
+
let k = i;
|
|
1419
|
+
while (k < s.length) {
|
|
1420
|
+
while (k < s.length && /\s/.test(s[k])) k++;
|
|
1421
|
+
const c = s[k];
|
|
1422
|
+
if (c === void 0) break;
|
|
1423
|
+
if (c === ">") return { attrs, end: k + 1, selfClosing: false };
|
|
1424
|
+
if (c === "/" && s[k + 1] === ">") return { attrs, end: k + 2, selfClosing: true };
|
|
1425
|
+
if (c === "{") {
|
|
1426
|
+
const e = closeBrace(s, k);
|
|
1427
|
+
const inner = s.slice(k + 1, e - 1).trim();
|
|
1428
|
+
attrs.push(inner.startsWith("...") ? { name: "", kind: "spread", value: inner, valueStart: k + 1 } : { name: inner, kind: "expr", value: inner, valueStart: k + 1 });
|
|
1429
|
+
k = e;
|
|
1430
|
+
continue;
|
|
1431
|
+
}
|
|
1432
|
+
const nm = /^[^\s=>/"'{]+/.exec(s.slice(k, k + 200));
|
|
1433
|
+
if (!nm) {
|
|
1434
|
+
k++;
|
|
1435
|
+
continue;
|
|
1436
|
+
}
|
|
1437
|
+
const name = nm[0];
|
|
1438
|
+
k += name.length;
|
|
1439
|
+
let j = k;
|
|
1440
|
+
while (j < s.length && /\s/.test(s[j])) j++;
|
|
1441
|
+
if (s[j] !== "=") {
|
|
1442
|
+
attrs.push({ name, kind: "bare", value: "", valueStart: k });
|
|
1443
|
+
continue;
|
|
1444
|
+
}
|
|
1445
|
+
j++;
|
|
1446
|
+
while (j < s.length && /\s/.test(s[j])) j++;
|
|
1447
|
+
const q = s[j];
|
|
1448
|
+
const bound = lang === "vue" && /^(:|v-bind:|v-on:|@|v-)/.test(name);
|
|
1449
|
+
if (q === '"' || q === "'") {
|
|
1450
|
+
const e = s.indexOf(q, j + 1);
|
|
1451
|
+
const end = e < 0 ? s.length : e;
|
|
1452
|
+
attrs.push({ name, kind: bound ? "expr" : "static", value: s.slice(j + 1, end), valueStart: j + 1 });
|
|
1453
|
+
k = end + 1;
|
|
1454
|
+
} else if (q === "{") {
|
|
1455
|
+
const e = closeBrace(s, j);
|
|
1456
|
+
attrs.push({ name, kind: "expr", value: s.slice(j + 1, e - 1), valueStart: j + 1 });
|
|
1457
|
+
k = e;
|
|
1458
|
+
} else {
|
|
1459
|
+
const m = /^[^\s>]+/.exec(s.slice(j));
|
|
1460
|
+
const v = m?.[0].replace(/\/$/, "") ?? "";
|
|
1461
|
+
attrs.push({ name, kind: "static", value: v, valueStart: j });
|
|
1462
|
+
k = j + v.length;
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
return { attrs, end: s.length, selfClosing: false };
|
|
1466
|
+
}
|
|
1467
|
+
const attrName = (n) => n.replace(/^(v-bind:|:|bind:)/, "");
|
|
1468
|
+
const pascal = (kebab) => kebab.replace(/(^|-)([a-z0-9])/g, (_m, _d, c) => c.toUpperCase());
|
|
1469
|
+
const NO_COLOR_ATTR_TAGS = /* @__PURE__ */ new Set(["meta", "link"]);
|
|
1470
|
+
function scanMarkup(path, text, lang, ds) {
|
|
1471
|
+
const out = { findings: [], tokenUsages: 0, themeRefs: 0, ds: {}, components: [], definitions: [] };
|
|
1472
|
+
const pos = lineCol(text);
|
|
1473
|
+
const blocks = markupBlocks(text, lang);
|
|
1474
|
+
const imports = /* @__PURE__ */ new Map();
|
|
1475
|
+
const merge = (c) => {
|
|
1476
|
+
out.findings.push(...c.findings);
|
|
1477
|
+
out.tokenUsages += c.tokenUsages;
|
|
1478
|
+
out.themeRefs += c.themeRefs;
|
|
1479
|
+
for (const [n, k] of Object.entries(c.ds)) out.ds[n] = (out.ds[n] ?? 0) + k;
|
|
1480
|
+
out.components.push(...c.components);
|
|
1481
|
+
out.definitions.push(...c.definitions);
|
|
1482
|
+
};
|
|
1483
|
+
const codeRanges = [...blocks.frontmatter ? [{ ...blocks.frontmatter, attrs: "" }] : [], ...blocks.scripts.filter((b) => isCodeScript(b.attrs))];
|
|
1484
|
+
for (const r of codeRanges) {
|
|
1485
|
+
const jsx = /^(tsx|jsx)$/i.test(attrOf(r.attrs, "lang") ?? "");
|
|
1486
|
+
const code = mask(text, [r], true);
|
|
1487
|
+
const file = `${path}.${jsx ? "tsx" : "ts"}`;
|
|
1488
|
+
merge(scanCode(file, code, ds));
|
|
1489
|
+
for (const [k, v] of importsOf(file, code)) imports.set(k, v);
|
|
1490
|
+
}
|
|
1491
|
+
for (const b of blocks.styles) {
|
|
1492
|
+
const l = (attrOf(b.attrs, "lang") ?? "css").toLowerCase();
|
|
1493
|
+
const r = auditCssText(text.slice(b.start, b.end), l === "scss" || l === "sass" ? "scss" : l === "less" ? "less" : "css", ds, { pos, offset: b.start });
|
|
1494
|
+
out.findings.push(...r.findings);
|
|
1495
|
+
out.tokenUsages += r.tokenUsages;
|
|
1496
|
+
out.themeRefs += r.themeRefs;
|
|
1497
|
+
}
|
|
1498
|
+
const scanExpr = (prefix, expr, offset, suffix, keepLookalikes = false) => {
|
|
1499
|
+
const { line, column } = pos(offset);
|
|
1500
|
+
const src = line >= 2 ? `${"\n".repeat(line - 2)}${prefix}
|
|
1501
|
+
${" ".repeat(column - 1)}${expr}${suffix}` : `${prefix}${expr}${suffix}`;
|
|
1502
|
+
const c = scanCode(`${path}.tsx`, src, ds);
|
|
1503
|
+
merge({ ...c, components: keepLookalikes ? c.components.filter((x) => x.kind === "recreated" && x.name.startsWith(".")) : [], definitions: [], ds: {} });
|
|
1504
|
+
};
|
|
1505
|
+
const styleAttr = (a) => {
|
|
1506
|
+
if (a.kind === "static") {
|
|
1507
|
+
const css = lang === "svelte" ? a.value.replace(/\{[^}]*\}/g, (m) => " ".repeat(m.length)) : a.value;
|
|
1508
|
+
const r = auditCssText(css, "css", ds, { pos, offset: a.valueStart, source: "style" });
|
|
1509
|
+
out.findings.push(...r.findings);
|
|
1510
|
+
out.tokenUsages += r.tokenUsages;
|
|
1511
|
+
out.themeRefs += r.themeRefs;
|
|
1512
|
+
return;
|
|
1513
|
+
}
|
|
1514
|
+
const str = /^(\s*)([`'"])([\s\S]*)\2\s*$/.exec(a.value);
|
|
1515
|
+
if (str) {
|
|
1516
|
+
const body = str[3].replace(/\$\{[^}]*\}/g, (m) => " ".repeat(m.length));
|
|
1517
|
+
const r = auditCssText(body, "css", ds, { pos, offset: a.valueStart + str[1].length + 1, source: "style" });
|
|
1518
|
+
out.findings.push(...r.findings);
|
|
1519
|
+
out.tokenUsages += r.tokenUsages;
|
|
1520
|
+
out.themeRefs += r.themeRefs;
|
|
1521
|
+
return;
|
|
1522
|
+
}
|
|
1523
|
+
scanExpr("const style: CSSProperties =", a.value, a.valueStart, ";");
|
|
1524
|
+
};
|
|
1525
|
+
const tpl = mask(text, blocks.nonTemplate, false);
|
|
1526
|
+
const pending = [];
|
|
1527
|
+
let noscript = 0;
|
|
1528
|
+
const TAG = /<(\/?)([A-Za-z][\w.:-]*)/g;
|
|
1529
|
+
for (let m; m = TAG.exec(tpl); ) {
|
|
1530
|
+
const raw = m[2];
|
|
1531
|
+
if (m[1]) {
|
|
1532
|
+
if (raw.toLowerCase() === "noscript") noscript = Math.max(0, noscript - 1);
|
|
1533
|
+
continue;
|
|
1534
|
+
}
|
|
1535
|
+
const tag = readTag(tpl, m.index + m[0].length, lang);
|
|
1536
|
+
TAG.lastIndex = tag.end;
|
|
1537
|
+
const lower = raw.toLowerCase();
|
|
1538
|
+
if (lower === "noscript" && !tag.selfClosing) {
|
|
1539
|
+
noscript++;
|
|
1540
|
+
continue;
|
|
1541
|
+
}
|
|
1542
|
+
const attrs = tag.attrs;
|
|
1543
|
+
const get = (n) => attrs.find((a) => attrName(a.name) === n);
|
|
1544
|
+
for (const a of attrs) {
|
|
1545
|
+
const n = attrName(a.name);
|
|
1546
|
+
if (n === "style") styleAttr(a);
|
|
1547
|
+
else if (lang === "svelte" && a.name.startsWith("style:") && a.kind === "static") {
|
|
1548
|
+
const prop = a.name.slice(6).split("|")[0];
|
|
1549
|
+
const r2 = auditCssText(`${prop}:${a.value}`, "css", ds, { pos, offset: a.valueStart - prop.length - 1, source: "style" });
|
|
1550
|
+
out.findings.push(...r2.findings);
|
|
1551
|
+
out.tokenUsages += r2.tokenUsages;
|
|
1552
|
+
out.themeRefs += r2.themeRefs;
|
|
1553
|
+
} else if (/^(class|className|class:list)$/.test(n)) {
|
|
1554
|
+
if (a.kind === "static") {
|
|
1555
|
+
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 });
|
|
1556
|
+
const r2 = tailwindFindings(scanClassString(a.value), a.valueStart, pos, ds);
|
|
1557
|
+
out.findings.push(...r2.findings);
|
|
1558
|
+
out.themeRefs += r2.themeRefs;
|
|
1559
|
+
} else if (a.kind === "expr") scanExpr("cn(", a.value, a.valueStart, ")", true);
|
|
1560
|
+
} else if (LOOKALIKE_ATTRS[n] && ds.components.has(LOOKALIKE_ATTRS[n])) {
|
|
1561
|
+
out.components.push({ ...pos(a.valueStart - a.name.length - 1), name: `[${n}]`, kind: "recreated", library: "local", suggestion: LOOKALIKE_ATTRS[n] });
|
|
1562
|
+
} else if (a.kind === "static" && !NO_COLOR_ATTR_TAGS.has(lower) && isColorLiteral(a.value)) {
|
|
1563
|
+
const suggestion = ds.suggest("color", a.value.trim(), n);
|
|
1564
|
+
out.findings.push({ ...pos(a.valueStart), category: "color", property: n, value: a.value.trim(), source: "jsx-attr", ...suggestion ? { suggestion } : {} });
|
|
1217
1565
|
}
|
|
1218
1566
|
}
|
|
1219
|
-
|
|
1220
|
-
|
|
1567
|
+
let name = raw;
|
|
1568
|
+
if (raw.includes(":")) continue;
|
|
1569
|
+
if (raw.includes("-")) {
|
|
1570
|
+
if (!imports.has(pascal(raw))) continue;
|
|
1571
|
+
name = pascal(raw);
|
|
1572
|
+
}
|
|
1573
|
+
const isNative = /^[a-z]/.test(name) && !name.includes(".");
|
|
1574
|
+
if (isNative && noscript > 0) continue;
|
|
1575
|
+
const type = get("type");
|
|
1576
|
+
const r = classifyTag({ name, ...pos(m.index), ...type?.kind === "static" ? { type: type.value } : {}, hasHref: Boolean(get("href")) }, imports, ds);
|
|
1577
|
+
if (r.ds) out.ds[r.ds] = (out.ds[r.ds] ?? 0) + 1;
|
|
1578
|
+
if (r.component) out.components.push(r.component);
|
|
1579
|
+
if (r.localCandidate) pending.push(r.localCandidate);
|
|
1221
1580
|
}
|
|
1222
|
-
const
|
|
1581
|
+
const base = /([^/]+)\.(astro|vue|svelte)$/.exec(path)?.[1];
|
|
1582
|
+
if (base && /^[A-Z][A-Za-z0-9]*$/.test(base)) out.definitions.push({ name: base, line: 1 });
|
|
1583
|
+
const defined = new Set(out.definitions.map((d) => d.name));
|
|
1584
|
+
out.components.push(...pending.filter((f) => defined.has(f.suggestion)));
|
|
1585
|
+
out.findings.sort((a, b) => a.line - b.line || a.column - b.column);
|
|
1586
|
+
out.components.sort((a, b) => a.line - b.line || a.column - b.column);
|
|
1587
|
+
return out;
|
|
1588
|
+
}
|
|
1589
|
+
const AUDIT_EXTENSIONS = [".css", ".scss", ".sass", ".less", ".tsx", ".jsx", ".ts", ".js", ".mjs", ".cjs", ".mts", ".cts", ...MARKUP_EXTENSIONS];
|
|
1590
|
+
const AUDIT_IGNORE_DIRS = ["node_modules", "dist", "build", "out", "coverage", ".git", ".next", ".nuxt", ".astro", ".turbo", ".cache", ".vercel", "storybook-static", "vendor", "public"];
|
|
1591
|
+
const 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$/;
|
|
1592
|
+
function isAuditable(path) {
|
|
1593
|
+
if (!AUDIT_EXTENSIONS.some((e) => path.endsWith(e))) return false;
|
|
1594
|
+
if (IGNORE_FILE.test(path)) return false;
|
|
1595
|
+
return !path.split("/").some((seg) => AUDIT_IGNORE_DIRS.includes(seg));
|
|
1596
|
+
}
|
|
1597
|
+
const langOf = (path) => {
|
|
1598
|
+
const ext = path.slice(path.lastIndexOf(".") + 1);
|
|
1599
|
+
if (ext === "css") return "css";
|
|
1600
|
+
if (ext === "scss" || ext === "sass") return "scss";
|
|
1601
|
+
if (ext === "less") return "less";
|
|
1602
|
+
if (ext === "tsx") return "tsx";
|
|
1603
|
+
if (ext === "jsx") return "jsx";
|
|
1604
|
+
if (/^[cm]?ts$/.test(ext)) return "ts";
|
|
1605
|
+
if (ext === "astro" || ext === "vue" || ext === "svelte") return ext;
|
|
1606
|
+
return "js";
|
|
1607
|
+
};
|
|
1608
|
+
function auditFile(path, text, ds) {
|
|
1609
|
+
const lang = langOf(path);
|
|
1610
|
+
if (lang === "css" || lang === "scss" || lang === "less") {
|
|
1611
|
+
const r = auditCssText(text, lang, ds, { pos: lineCol(text) });
|
|
1612
|
+
return withIgnores(text, { path, lang, tokenUsages: r.tokenUsages, themeRefs: r.themeRefs, hardcoded: 0, dsComponents: 0, otherComponents: 0, findings: r.findings, components: [], definitions: [], dsByName: {} });
|
|
1613
|
+
}
|
|
1614
|
+
const c = lang === "astro" || lang === "vue" || lang === "svelte" ? scanMarkup(path, text, lang, ds) : scanCode(path, text, ds);
|
|
1223
1615
|
const dsCount = Object.values(c.ds).reduce((a, b) => a + b, 0);
|
|
1224
|
-
return { path, lang, tokenUsages: c.tokenUsages, themeRefs: c.themeRefs, hardcoded:
|
|
1616
|
+
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 });
|
|
1617
|
+
}
|
|
1618
|
+
function withIgnores(text, f) {
|
|
1619
|
+
const r = applyIgnoreComments(text, f.findings, f.components);
|
|
1620
|
+
return {
|
|
1621
|
+
...f,
|
|
1622
|
+
findings: r.findings,
|
|
1623
|
+
components: r.components,
|
|
1624
|
+
hardcoded: r.findings.length,
|
|
1625
|
+
otherComponents: r.components.length,
|
|
1626
|
+
...r.ignored.length ? { ignored: r.ignored } : {},
|
|
1627
|
+
...r.errors.length ? { ignoreErrors: r.errors } : {}
|
|
1628
|
+
};
|
|
1225
1629
|
}
|
|
1226
1630
|
const pct = (num2, den) => den > 0 ? Math.round(num2 / den * 1e3) / 10 : null;
|
|
1227
1631
|
function buildAuditReport(input, ds) {
|
|
1228
|
-
const
|
|
1632
|
+
const allow = input.allowlist ?? [];
|
|
1633
|
+
const usedAllow = /* @__PURE__ */ new Set();
|
|
1634
|
+
const reports = input.files.map((f) => {
|
|
1635
|
+
const r = auditFile(f.path, f.text, ds);
|
|
1636
|
+
const idx = allow.flatMap((e, i) => e.file === f.path ? [i] : []);
|
|
1637
|
+
if (!idx.length) return r;
|
|
1638
|
+
const a = applyAllowlist(idx.map((i) => allow[i]), r.findings, r.components);
|
|
1639
|
+
a.used.forEach((k) => usedAllow.add(idx[k]));
|
|
1640
|
+
if (!a.ignored.length) return r;
|
|
1641
|
+
const ignored = [...r.ignored ?? [], ...a.ignored].sort((x, y) => x.line - y.line || x.column - y.column);
|
|
1642
|
+
return { ...r, findings: a.findings, components: a.components, hardcoded: a.findings.length, otherComponents: a.components.length, ignored };
|
|
1643
|
+
});
|
|
1644
|
+
const ignoredItems = reports.flatMap((r) => (r.ignored ?? []).map((x) => ({ file: r.path, ...x })));
|
|
1645
|
+
const staleAllowlist = allow.filter((_e, i) => !usedAllow.has(i));
|
|
1229
1646
|
const files = reports.map(({ definitions: _d, dsByName: _n, ...r }) => r);
|
|
1230
1647
|
const byCategory = {};
|
|
1231
1648
|
const fixes = /* @__PURE__ */ new Map();
|
|
@@ -1285,6 +1702,15 @@ function buildAuditReport(input, ds) {
|
|
|
1285
1702
|
topFiles,
|
|
1286
1703
|
topFixes,
|
|
1287
1704
|
files: files.sort((a, b) => a.path.localeCompare(b.path)),
|
|
1705
|
+
...ignoredItems.length || staleAllowlist.length ? {
|
|
1706
|
+
ignored: {
|
|
1707
|
+
total: ignoredItems.length,
|
|
1708
|
+
comment: ignoredItems.filter((x) => x.via === "comment").length,
|
|
1709
|
+
allowlist: ignoredItems.filter((x) => x.via === "allowlist").length,
|
|
1710
|
+
items: ignoredItems,
|
|
1711
|
+
staleAllowlist
|
|
1712
|
+
}
|
|
1713
|
+
} : {},
|
|
1288
1714
|
history: []
|
|
1289
1715
|
};
|
|
1290
1716
|
}
|
|
@@ -1388,6 +1814,20 @@ function auditMarkdown(r, opts = {}) {
|
|
|
1388
1814
|
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} |`);
|
|
1389
1815
|
lines.push("");
|
|
1390
1816
|
}
|
|
1817
|
+
const ig = r.ignored;
|
|
1818
|
+
if (ig && (ig.total || ig.staleAllowlist.length)) {
|
|
1819
|
+
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.`, "");
|
|
1820
|
+
const shown = ig.items.slice(0, opts.ignored ?? 50);
|
|
1821
|
+
if (shown.length) {
|
|
1822
|
+
lines.push("| Where | What | Reason |", "| --- | --- | --- |");
|
|
1823
|
+
for (const x of shown) lines.push(`| \`${esc(x.file)}:${x.line}\` | \`${esc(x.what)}\` | ${esc(x.reason)} (${x.via}) |`);
|
|
1824
|
+
if (ig.items.length > shown.length) lines.push(`| ... | ${ig.items.length - shown.length} more | |`);
|
|
1825
|
+
lines.push("");
|
|
1826
|
+
}
|
|
1827
|
+
if (ig.staleAllowlist.length) {
|
|
1828
|
+
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(", ")}.`, "");
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1391
1831
|
if (b) {
|
|
1392
1832
|
lines.push(`### Progress since ${b.label ?? b.generatedAt}`, "");
|
|
1393
1833
|
lines.push(`- ${b.improved.length} files improved, ${b.regressed.length} regressed, ${b.newFiles.length} new, ${b.removedFiles.length} removed.`);
|
|
@@ -1415,40 +1855,56 @@ function planCssFixes(text, lang, ds) {
|
|
|
1415
1855
|
}
|
|
1416
1856
|
export {
|
|
1417
1857
|
AUDIT_EXTENSIONS as A,
|
|
1418
|
-
|
|
1858
|
+
libraryLabel as B,
|
|
1419
1859
|
COLOR_LITERAL_RE as C,
|
|
1420
1860
|
DEFAULT_NATIVE_ELEMENTS as D,
|
|
1421
|
-
|
|
1861
|
+
lineCol as E,
|
|
1422
1862
|
FINDING_CATEGORIES as F,
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1863
|
+
lookalikesInClassList as G,
|
|
1864
|
+
markupBlocks as H,
|
|
1865
|
+
IGNORE_DIRECTIVE as I,
|
|
1866
|
+
nativeEquivalent as J,
|
|
1867
|
+
packageName as K,
|
|
1868
|
+
LOOKALIKE_ATTRS as L,
|
|
1869
|
+
MARKUP_EXTENSIONS as M,
|
|
1426
1870
|
NAMED_COLORS as N,
|
|
1871
|
+
parseAllowlist as O,
|
|
1872
|
+
parseCssColor as P,
|
|
1873
|
+
planCssFixes as Q,
|
|
1874
|
+
scanClassString as R,
|
|
1875
|
+
scanCode as S,
|
|
1876
|
+
scanCssText as T,
|
|
1877
|
+
scanCssValue as U,
|
|
1878
|
+
scanJsStyle as V,
|
|
1879
|
+
scanMarkup as W,
|
|
1880
|
+
tailwindColor as X,
|
|
1881
|
+
tailwindFindings as Y,
|
|
1882
|
+
toOklab as Z,
|
|
1427
1883
|
AUDIT_IGNORE_DIRS as a,
|
|
1428
1884
|
AUDIT_SCHEMA_VERSION as b,
|
|
1429
1885
|
DesignSystemIndex as c,
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1886
|
+
LOOKALIKE_CLASSES as d,
|
|
1887
|
+
applyAllowlist as e,
|
|
1888
|
+
applyIgnoreComments as f,
|
|
1889
|
+
auditCssText as g,
|
|
1890
|
+
auditFile as h,
|
|
1891
|
+
auditMarkdown as i,
|
|
1892
|
+
blankComments as j,
|
|
1893
|
+
buildAuditReport as k,
|
|
1894
|
+
classifyModule as l,
|
|
1895
|
+
classifyTag as m,
|
|
1896
|
+
compareWithBaseline as n,
|
|
1897
|
+
cssPropertyOf as o,
|
|
1898
|
+
deltaE as p,
|
|
1899
|
+
domRendererComponent as q,
|
|
1900
|
+
dsEquivalent as r,
|
|
1901
|
+
historyPoint as s,
|
|
1902
|
+
ignoreDirectives as t,
|
|
1903
|
+
importsOf as u,
|
|
1904
|
+
isAuditable as v,
|
|
1905
|
+
isColorLiteral as w,
|
|
1906
|
+
isNonVisual as x,
|
|
1907
|
+
isRouterLink as y,
|
|
1908
|
+
isSafeFix as z
|
|
1453
1909
|
};
|
|
1454
|
-
//# sourceMappingURL=report-
|
|
1910
|
+
//# sourceMappingURL=report-iPSnsjYi.js.map
|