@bridge-ds/core 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -5
- package/audit-report.schema.json +135 -1
- package/dist/audit/code.d.ts +50 -0
- 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-4Z008HSf.js} +598 -143
- package/dist/report-4Z008HSf.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,78 @@ 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
|
+
return { component: { ...at, name, kind: "native", library: "native", ...suggestion ? { suggestion } : {} } };
|
|
920
|
+
}
|
|
921
|
+
const root = name.split(".")[0];
|
|
922
|
+
const imp = imports.get(root);
|
|
923
|
+
const ns = imp?.imported === "*";
|
|
924
|
+
const shown = imp && !ns && imp.imported !== "default" ? [imp.imported, ...name.split(".").slice(1)].join(".") : name;
|
|
925
|
+
const leaf = ns && name.includes(".") ? name.split(".").slice(1).join(".") : shown;
|
|
926
|
+
if (imp) {
|
|
927
|
+
const kind = classifyModule(imp.module, ds.packages, ds.foreignLibraries);
|
|
928
|
+
if (isNonVisual(leaf.split(".").pop())) return {};
|
|
929
|
+
if (kind === "ds") return { ds: leaf };
|
|
930
|
+
if (kind === "charts") return { component: { ...at, name: shown, kind: "foreign", library: libraryLabel(imp.module, kind) } };
|
|
931
|
+
if (kind === "other" && isRouterLink(imp.module, imp.imported === "default" ? "default" : leaf)) {
|
|
932
|
+
if (t.inRenderProp) return {};
|
|
933
|
+
const suggestion = ds.components.has("Link") ? "Link" : void 0;
|
|
934
|
+
return { component: { ...at, name: shown, kind: "foreign", library: libraryLabel(imp.module, kind), ...suggestion ? { suggestion } : {} } };
|
|
935
|
+
}
|
|
936
|
+
if (kind === "foreign" || kind === "shadcn") {
|
|
937
|
+
const suggestion = (ns ? dsEquivalent(root, ds.components) : void 0) ?? dsEquivalent(leaf, ds.components);
|
|
938
|
+
return { component: { ...at, name: shown, kind: "foreign", library: libraryLabel(imp.module, kind), ...suggestion ? { suggestion } : {} } };
|
|
939
|
+
}
|
|
940
|
+
if (kind === "local" && ds.components.has(leaf.split(".")[0])) return { component: { ...at, name: leaf, kind: "recreated", library: "local", suggestion: leaf.split(".")[0] } };
|
|
941
|
+
return {};
|
|
942
|
+
}
|
|
943
|
+
if (ds.components.has(root)) return { localCandidate: { ...at, name, kind: "recreated", library: "local", suggestion: root } };
|
|
944
|
+
return {};
|
|
945
|
+
}
|
|
946
|
+
function tailwindFindings(hits, offset, pos, ds) {
|
|
947
|
+
const findings = [];
|
|
948
|
+
let themeRefs = 0;
|
|
949
|
+
for (const h of hits) {
|
|
950
|
+
if (h.kind === "scale") {
|
|
951
|
+
themeRefs++;
|
|
952
|
+
continue;
|
|
953
|
+
}
|
|
954
|
+
if (h.kind === "token") continue;
|
|
955
|
+
let cat = h.category;
|
|
956
|
+
const v = h.value;
|
|
957
|
+
const isColor = Boolean(parseCssColor(v));
|
|
958
|
+
if (cat === "color-or-size") cat = isColor ? "color" : "font-size";
|
|
959
|
+
else if (cat === "color-or-width") cat = isColor ? "color" : "border-width";
|
|
960
|
+
else if (cat === "color-or-shadow") cat = isColor ? "color" : "shadow";
|
|
961
|
+
else if (cat === "font") cat = /^\d{3}$/.test(v) ? "font-weight" : "font-family";
|
|
962
|
+
if (cat === "color" && !isColor) continue;
|
|
963
|
+
const suggestion = ds.suggest(cat, v);
|
|
964
|
+
const cap = h.kind === "palette" && suggestion && suggestion.confidence > 0.6 ? { ...suggestion, confidence: 0.6, level: "medium" } : suggestion;
|
|
965
|
+
findings.push({ ...pos(offset + h.index), category: cat, value: h.cls, source: "tailwind", ...cap ? { suggestion: cap } : {} });
|
|
966
|
+
}
|
|
967
|
+
return { findings, themeRefs };
|
|
968
|
+
}
|
|
873
969
|
function scanCode(path, text, ds) {
|
|
874
970
|
const sf = ts.createSourceFile(path, text, ts.ScriptTarget.Latest, true, kindFor(path));
|
|
875
971
|
const out = { findings: [], tokenUsages: 0, themeRefs: 0, ds: {}, components: [], definitions: [] };
|
|
@@ -884,24 +980,9 @@ function scanCode(path, text, ds) {
|
|
|
884
980
|
out.findings.push({ ...pos(offset), category: h.category, ...property ? { property } : {}, value: h.text, source, ...suggestion ? { suggestion } : {} });
|
|
885
981
|
};
|
|
886
982
|
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
|
-
}
|
|
983
|
+
const r = tailwindFindings(hits, offset, pos, ds);
|
|
984
|
+
out.themeRefs += r.themeRefs;
|
|
985
|
+
out.findings.push(...r.findings);
|
|
905
986
|
};
|
|
906
987
|
const styleValue = (key, node, ctx) => {
|
|
907
988
|
const source = ctx === "sx" ? "sx" : ctx === "style" ? "style" : ctx === "theme" ? "theme" : "styled";
|
|
@@ -962,13 +1043,18 @@ function scanCode(path, text, ds) {
|
|
|
962
1043
|
}
|
|
963
1044
|
for (const a of scan.applies) addTailwind(a.hits, start + a.start);
|
|
964
1045
|
};
|
|
1046
|
+
const lookalike = (list, offset) => {
|
|
1047
|
+
for (const l of lookalikesInClassList(list, ds.components)) out.components.push({ ...pos(offset), name: `.${l.token}`, kind: "recreated", library: "local", suggestion: l.component });
|
|
1048
|
+
};
|
|
965
1049
|
const classLiteral = (node) => {
|
|
966
1050
|
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
|
|
967
1051
|
handled.add(node);
|
|
1052
|
+
lookalike(node.text, node.getStart(sf) + 1);
|
|
968
1053
|
addTailwind(scanClassString(node.text), node.getStart(sf) + 1);
|
|
969
1054
|
} else if (ts.isTemplateExpression(node)) {
|
|
970
1055
|
for (const p of [node.head, ...node.templateSpans.map((s) => s.literal)]) {
|
|
971
1056
|
handled.add(p);
|
|
1057
|
+
lookalike(p.text, p.getStart(sf) + 1);
|
|
972
1058
|
addTailwind(scanClassString(p.text), p.getStart(sf) + 1);
|
|
973
1059
|
}
|
|
974
1060
|
node.templateSpans.forEach((s) => visit(s.expression, null, true));
|
|
@@ -977,49 +1063,13 @@ function scanCode(path, text, ds) {
|
|
|
977
1063
|
const jsxName = (n) => n.getText(sf);
|
|
978
1064
|
const element = (node) => {
|
|
979
1065
|
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 });
|
|
1066
|
+
const typeAttr = node.attributes.properties.find((p) => ts.isJsxAttribute(p) && p.name.getText(sf) === "type");
|
|
1067
|
+
const type = typeAttr?.initializer && ts.isStringLiteral(typeAttr.initializer) ? typeAttr.initializer.text : void 0;
|
|
1068
|
+
const hasHref = node.attributes.properties.some((p) => ts.isJsxAttribute(p) && p.name.getText(sf) === "href");
|
|
1069
|
+
const r = classifyTag({ name, ...pos(node.getStart(sf)), ...type !== void 0 ? { type } : {}, hasHref, inRenderProp: isRenderElement(node) }, imports, ds);
|
|
1070
|
+
if (r.ds) out.ds[r.ds] = (out.ds[r.ds] ?? 0) + 1;
|
|
1071
|
+
if (r.component) out.components.push(r.component);
|
|
1072
|
+
if (r.localCandidate) pendingLocal.push(r.localCandidate);
|
|
1023
1073
|
};
|
|
1024
1074
|
const pendingLocal = [];
|
|
1025
1075
|
function visit(node, ctx, classCtx) {
|
|
@@ -1079,6 +1129,17 @@ function scanCode(path, text, ds) {
|
|
|
1079
1129
|
cssTemplate(node.template);
|
|
1080
1130
|
return;
|
|
1081
1131
|
}
|
|
1132
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
|
|
1133
|
+
const imp = imports.get(node.expression.text);
|
|
1134
|
+
if (imp && imp.imported !== "*" && classifyModule(imp.module, ds.packages, ds.foreignLibraries) === "ds") {
|
|
1135
|
+
const comp = domRendererComponent(imp.imported === "default" ? node.expression.text : imp.imported, ds.components);
|
|
1136
|
+
if (comp) out.ds[comp] = (out.ds[comp] ?? 0) + 1;
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken && ts.isPropertyAccessExpression(node.left) && node.left.name.text === "className") {
|
|
1140
|
+
visit(node.left, ctx, classCtx);
|
|
1141
|
+
return classLiteral(node.right);
|
|
1142
|
+
}
|
|
1082
1143
|
if (ts.isCallExpression(node)) {
|
|
1083
1144
|
const callee = node.expression.getText(sf);
|
|
1084
1145
|
if (CLASS_FNS.test(callee)) {
|
|
@@ -1123,6 +1184,14 @@ function scanCode(path, text, ds) {
|
|
|
1123
1184
|
const literal = (node) => {
|
|
1124
1185
|
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node) || ts.isTemplateHead(node) || ts.isTemplateMiddle(node) || ts.isTemplateTail(node)) {
|
|
1125
1186
|
const t = node.text;
|
|
1187
|
+
if (!handled.has(node) && (t.includes("<") || ts.isTemplateMiddle(node) || ts.isTemplateTail(node))) {
|
|
1188
|
+
const base = node.getStart(sf) + 1;
|
|
1189
|
+
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 });
|
|
1190
|
+
for (const m of t.matchAll(HTML_ATTR_RE)) {
|
|
1191
|
+
const component = LOOKALIKE_ATTRS[m[1]];
|
|
1192
|
+
if (component && ds.components.has(component)) out.components.push({ ...pos(base + m.index + 1), name: `[${m[1]}]`, kind: "recreated", library: "local", suggestion: component });
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1126
1195
|
for (const m of t.matchAll(/var\(\s*(--[\w-]+)/g)) {
|
|
1127
1196
|
if (ds.isDsVar(m[1])) out.tokenUsages++;
|
|
1128
1197
|
else out.themeRefs++;
|
|
@@ -1141,24 +1210,82 @@ function scanCode(path, text, ds) {
|
|
|
1141
1210
|
out.components.sort((a, b) => a.line - b.line || a.column - b.column);
|
|
1142
1211
|
return out;
|
|
1143
1212
|
}
|
|
1144
|
-
const
|
|
1145
|
-
|
|
1146
|
-
const
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1213
|
+
const IGNORE_DIRECTIVE = "bridge-audit-ignore-next-line";
|
|
1214
|
+
function ignoreDirectives(text) {
|
|
1215
|
+
const at = lineCol(text);
|
|
1216
|
+
const out = [];
|
|
1217
|
+
for (const m of text.matchAll(new RegExp(`${IGNORE_DIRECTIVE}\\b`, "g"))) {
|
|
1218
|
+
const start = m.index;
|
|
1219
|
+
const opener = /(<!--|\/\*|\/\/)(\s*)$/.exec(text.slice(Math.max(0, start - 200), start));
|
|
1220
|
+
const open = opener?.[1];
|
|
1221
|
+
if (!open || open === "//" && opener[2].includes("\n")) continue;
|
|
1222
|
+
const from = start + m[0].length;
|
|
1223
|
+
let end;
|
|
1224
|
+
if (open === "//") {
|
|
1225
|
+
end = text.indexOf("\n", from);
|
|
1226
|
+
if (end < 0) end = text.length;
|
|
1227
|
+
} else {
|
|
1228
|
+
end = text.indexOf(open === "<!--" ? "-->" : "*/", from);
|
|
1229
|
+
if (end < 0) continue;
|
|
1230
|
+
}
|
|
1231
|
+
const reason = text.slice(from, end).replace(/^\s*:?/, "").replace(/\s+/g, " ").trim();
|
|
1232
|
+
out.push({ line: at(start).line, target: at(end).line + 1, reason });
|
|
1233
|
+
}
|
|
1234
|
+
return out;
|
|
1235
|
+
}
|
|
1236
|
+
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 };
|
|
1237
|
+
function applyIgnoreComments(text, findings, components) {
|
|
1238
|
+
const errors = [];
|
|
1239
|
+
const reasons = /* @__PURE__ */ new Map();
|
|
1240
|
+
for (const d of ignoreDirectives(text)) {
|
|
1241
|
+
if (!d.reason) {
|
|
1242
|
+
errors.push({ line: d.line, message: `${IGNORE_DIRECTIVE} needs a reason after a colon (${IGNORE_DIRECTIVE}: why this one stays); nothing was ignored` });
|
|
1243
|
+
continue;
|
|
1244
|
+
}
|
|
1245
|
+
if (!findings.some((f) => f.line === d.target) && !components.some((c) => c.line === d.target)) {
|
|
1246
|
+
errors.push({ line: d.line, message: `${IGNORE_DIRECTIVE} ignores nothing: line ${d.target} has no finding; remove the comment` });
|
|
1247
|
+
continue;
|
|
1248
|
+
}
|
|
1249
|
+
reasons.set(d.target, d.reason);
|
|
1250
|
+
}
|
|
1251
|
+
const ignored = [];
|
|
1252
|
+
const keep = (xs) => xs.filter((x) => {
|
|
1253
|
+
const r = reasons.get(x.line);
|
|
1254
|
+
if (r === void 0) return true;
|
|
1255
|
+
ignored.push(item(x, r, "comment"));
|
|
1256
|
+
return false;
|
|
1257
|
+
});
|
|
1258
|
+
return { findings: keep(findings), components: keep(components), ignored, errors };
|
|
1259
|
+
}
|
|
1260
|
+
function applyAllowlist(entries, findings, components) {
|
|
1261
|
+
const used = /* @__PURE__ */ new Set();
|
|
1262
|
+
const ignored = [];
|
|
1263
|
+
const keep = (xs) => xs.filter((x) => {
|
|
1264
|
+
const what = "category" in x ? x.value : x.name;
|
|
1265
|
+
const fits = (e) => e.line === x.line && (e.what === void 0 || e.what === what);
|
|
1266
|
+
let i = entries.findIndex((e, k) => fits(e) && !used.has(k));
|
|
1267
|
+
if (i < 0) i = entries.findIndex(fits);
|
|
1268
|
+
if (i < 0) return true;
|
|
1269
|
+
used.add(i);
|
|
1270
|
+
ignored.push(item(x, entries[i].reason, "allowlist"));
|
|
1271
|
+
return false;
|
|
1272
|
+
});
|
|
1273
|
+
return { findings: keep(findings), components: keep(components), ignored, used };
|
|
1274
|
+
}
|
|
1275
|
+
function parseAllowlist(json) {
|
|
1276
|
+
const fail = (msg) => {
|
|
1277
|
+
throw new Error(`allowlist: ${msg}`);
|
|
1278
|
+
};
|
|
1279
|
+
if (!json || typeof json !== "object" || !Array.isArray(json.entries)) fail('expected { "entries": [...] }');
|
|
1280
|
+
return json.entries.map((e, i) => {
|
|
1281
|
+
const x = e;
|
|
1282
|
+
if (typeof x.file !== "string" || !x.file) fail(`entry ${i} needs "file"`);
|
|
1283
|
+
if (typeof x.line !== "number" || !Number.isInteger(x.line) || x.line < 1) fail(`entry ${i} (${x.file}) needs a "line" number`);
|
|
1284
|
+
if (typeof x.reason !== "string" || !x.reason.trim()) fail(`entry ${i} (${x.file}:${x.line}) needs a "reason"`);
|
|
1285
|
+
if (x.what !== void 0 && typeof x.what !== "string") fail(`entry ${i} (${x.file}:${x.line}): "what" must be a string`);
|
|
1286
|
+
return { file: x.file, line: x.line, ...x.what !== void 0 ? { what: x.what } : {}, reason: x.reason.trim() };
|
|
1287
|
+
});
|
|
1151
1288
|
}
|
|
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
1289
|
function isSafeFix(h, s) {
|
|
1163
1290
|
if (!s || s.level !== "exact") return false;
|
|
1164
1291
|
switch (h.category) {
|
|
@@ -1183,49 +1310,338 @@ function isSafeFix(h, s) {
|
|
|
1183
1310
|
return false;
|
|
1184
1311
|
}
|
|
1185
1312
|
}
|
|
1186
|
-
function
|
|
1187
|
-
const
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1313
|
+
function auditCssText(text, lang, ds, opts) {
|
|
1314
|
+
const base = opts.offset ?? 0;
|
|
1315
|
+
const source = opts.source ?? "css";
|
|
1316
|
+
const at = (o) => opts.pos(base + o);
|
|
1317
|
+
const scan = scanCssText(text, lang);
|
|
1318
|
+
const findings = [];
|
|
1319
|
+
let tokenUsages = 0;
|
|
1320
|
+
let themeRefs = 0;
|
|
1321
|
+
for (const d of scan.decls) {
|
|
1322
|
+
for (const v of d.scan.vars) ds.isDsVar(v) ? tokenUsages++ : themeRefs++;
|
|
1323
|
+
if (d.scan.themeRef) themeRefs++;
|
|
1324
|
+
else if (lang !== "css" && !d.property.startsWith("$")) themeRefs += (d.value.match(/\$[\w-]+/g) ?? []).length;
|
|
1325
|
+
const property = d.property.toLowerCase();
|
|
1326
|
+
for (const h of d.scan.hits) {
|
|
1327
|
+
const suggestion = ds.suggest(h.category, h.text, property);
|
|
1328
|
+
const autofix = source === "css" && isSafeFix(h, suggestion);
|
|
1329
|
+
findings.push({ ...at(d.valueStart + h.index), category: h.category, property, value: h.text, source, ...suggestion ? { suggestion } : {}, ...autofix ? { autofix: true } : {} });
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
for (const a of scan.applies) {
|
|
1333
|
+
for (const h of a.hits) {
|
|
1334
|
+
if (h.kind === "scale") themeRefs++;
|
|
1335
|
+
else if (h.kind !== "token" && h.value) {
|
|
1336
|
+
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;
|
|
1337
|
+
const suggestion = ds.suggest(cat, h.value);
|
|
1338
|
+
findings.push({ ...at(a.start + h.index), category: cat, value: h.cls, source: "tailwind", ...suggestion ? { suggestion } : {} });
|
|
1339
|
+
} else if (h.kind === "token" && h.value) {
|
|
1340
|
+
const v = /var\(\s*(--[\w-]+)/.exec(h.value)?.[1];
|
|
1341
|
+
if (v && ds.isDsVar(v)) tokenUsages++;
|
|
1342
|
+
else themeRefs++;
|
|
1203
1343
|
}
|
|
1204
1344
|
}
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1345
|
+
}
|
|
1346
|
+
findings.sort((x, y) => x.line - y.line || x.column - y.column);
|
|
1347
|
+
return { findings, tokenUsages, themeRefs };
|
|
1348
|
+
}
|
|
1349
|
+
const MARKUP_EXTENSIONS = [".astro", ".vue", ".svelte"];
|
|
1350
|
+
function mask(text, ranges, keepInside) {
|
|
1351
|
+
const inside = new Uint8Array(text.length);
|
|
1352
|
+
for (const r of ranges) inside.fill(1, r.start, r.end);
|
|
1353
|
+
let out = "";
|
|
1354
|
+
for (let i = 0; i < text.length; i++) {
|
|
1355
|
+
const c = text[i];
|
|
1356
|
+
out += c === "\n" || Boolean(inside[i]) === keepInside ? c : " ";
|
|
1357
|
+
}
|
|
1358
|
+
return out;
|
|
1359
|
+
}
|
|
1360
|
+
const attrOf = (attrs, name) => new RegExp(`(?:^|\\s)${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s>]+))`, "i").exec(attrs)?.slice(1).find((v) => v !== void 0);
|
|
1361
|
+
const isCodeScript = (attrs) => {
|
|
1362
|
+
const type = attrOf(attrs, "type")?.toLowerCase();
|
|
1363
|
+
return !type || ["module", "text/javascript", "application/javascript", "text/typescript", "ts", "text/babel"].includes(type);
|
|
1364
|
+
};
|
|
1365
|
+
function markupBlocks(text, lang) {
|
|
1366
|
+
const nonTemplate = [];
|
|
1367
|
+
let frontmatter;
|
|
1368
|
+
let from = 0;
|
|
1369
|
+
if (lang === "astro") {
|
|
1370
|
+
const open = /^\s*---[^\S\n]*\n/.exec(text);
|
|
1371
|
+
if (open) {
|
|
1372
|
+
const close = /\n---[^\S\n]*(\n|$)/g;
|
|
1373
|
+
close.lastIndex = open[0].length - 1;
|
|
1374
|
+
const m = close.exec(text);
|
|
1375
|
+
if (m) {
|
|
1376
|
+
frontmatter = { start: open[0].length, end: m.index + 1 };
|
|
1377
|
+
from = m.index + m[0].length;
|
|
1378
|
+
nonTemplate.push({ start: 0, end: from });
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
const scripts = [];
|
|
1383
|
+
const styles = [];
|
|
1384
|
+
const re = /<(script|style)\b((?:[^>"']|"[^"]*"|'[^']*')*)>([\s\S]*?)<\/\1\s*>/gi;
|
|
1385
|
+
re.lastIndex = from;
|
|
1386
|
+
for (let m; m = re.exec(text); ) {
|
|
1387
|
+
const contentStart = m.index + m[0].indexOf(">", m[1].length + 1 + m[2].length) + 1;
|
|
1388
|
+
const block = { start: contentStart, end: contentStart + m[3].length, attrs: m[2] };
|
|
1389
|
+
(m[1].toLowerCase() === "script" ? scripts : styles).push(block);
|
|
1390
|
+
nonTemplate.push({ start: m.index, end: m.index + m[0].length });
|
|
1391
|
+
}
|
|
1392
|
+
for (const m of text.slice(from).matchAll(/<!--[\s\S]*?-->/g)) nonTemplate.push({ start: from + m.index, end: from + m.index + m[0].length });
|
|
1393
|
+
return { ...frontmatter ? { frontmatter } : {}, scripts, styles, nonTemplate };
|
|
1394
|
+
}
|
|
1395
|
+
function closeBrace(s, i) {
|
|
1396
|
+
let depth = 0;
|
|
1397
|
+
for (let k = i; k < s.length; k++) {
|
|
1398
|
+
const c = s[k];
|
|
1399
|
+
if (c === "{") depth++;
|
|
1400
|
+
else if (c === "}") {
|
|
1401
|
+
if (--depth === 0) return k + 1;
|
|
1402
|
+
} else if (c === '"' || c === "'") {
|
|
1403
|
+
const e = s.indexOf(c, k + 1);
|
|
1404
|
+
if (e < 0) return s.length;
|
|
1405
|
+
k = e;
|
|
1406
|
+
} else if (c === "`") {
|
|
1407
|
+
for (k++; k < s.length && s[k] !== "`"; k++) {
|
|
1408
|
+
if (s[k] === "\\") k++;
|
|
1409
|
+
else if (s[k] === "$" && s[k + 1] === "{") k = closeBrace(s, k + 1) - 1;
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
return s.length;
|
|
1414
|
+
}
|
|
1415
|
+
function readTag(s, i, lang) {
|
|
1416
|
+
const attrs = [];
|
|
1417
|
+
let k = i;
|
|
1418
|
+
while (k < s.length) {
|
|
1419
|
+
while (k < s.length && /\s/.test(s[k])) k++;
|
|
1420
|
+
const c = s[k];
|
|
1421
|
+
if (c === void 0) break;
|
|
1422
|
+
if (c === ">") return { attrs, end: k + 1, selfClosing: false };
|
|
1423
|
+
if (c === "/" && s[k + 1] === ">") return { attrs, end: k + 2, selfClosing: true };
|
|
1424
|
+
if (c === "{") {
|
|
1425
|
+
const e = closeBrace(s, k);
|
|
1426
|
+
const inner = s.slice(k + 1, e - 1).trim();
|
|
1427
|
+
attrs.push(inner.startsWith("...") ? { name: "", kind: "spread", value: inner, valueStart: k + 1 } : { name: inner, kind: "expr", value: inner, valueStart: k + 1 });
|
|
1428
|
+
k = e;
|
|
1429
|
+
continue;
|
|
1430
|
+
}
|
|
1431
|
+
const nm = /^[^\s=>/"'{]+/.exec(s.slice(k, k + 200));
|
|
1432
|
+
if (!nm) {
|
|
1433
|
+
k++;
|
|
1434
|
+
continue;
|
|
1435
|
+
}
|
|
1436
|
+
const name = nm[0];
|
|
1437
|
+
k += name.length;
|
|
1438
|
+
let j = k;
|
|
1439
|
+
while (j < s.length && /\s/.test(s[j])) j++;
|
|
1440
|
+
if (s[j] !== "=") {
|
|
1441
|
+
attrs.push({ name, kind: "bare", value: "", valueStart: k });
|
|
1442
|
+
continue;
|
|
1443
|
+
}
|
|
1444
|
+
j++;
|
|
1445
|
+
while (j < s.length && /\s/.test(s[j])) j++;
|
|
1446
|
+
const q = s[j];
|
|
1447
|
+
const bound = lang === "vue" && /^(:|v-bind:|v-on:|@|v-)/.test(name);
|
|
1448
|
+
if (q === '"' || q === "'") {
|
|
1449
|
+
const e = s.indexOf(q, j + 1);
|
|
1450
|
+
const end = e < 0 ? s.length : e;
|
|
1451
|
+
attrs.push({ name, kind: bound ? "expr" : "static", value: s.slice(j + 1, end), valueStart: j + 1 });
|
|
1452
|
+
k = end + 1;
|
|
1453
|
+
} else if (q === "{") {
|
|
1454
|
+
const e = closeBrace(s, j);
|
|
1455
|
+
attrs.push({ name, kind: "expr", value: s.slice(j + 1, e - 1), valueStart: j + 1 });
|
|
1456
|
+
k = e;
|
|
1457
|
+
} else {
|
|
1458
|
+
const m = /^[^\s>]+/.exec(s.slice(j));
|
|
1459
|
+
const v = m?.[0].replace(/\/$/, "") ?? "";
|
|
1460
|
+
attrs.push({ name, kind: "static", value: v, valueStart: j });
|
|
1461
|
+
k = j + v.length;
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
return { attrs, end: s.length, selfClosing: false };
|
|
1465
|
+
}
|
|
1466
|
+
const attrName = (n) => n.replace(/^(v-bind:|:|bind:)/, "");
|
|
1467
|
+
const pascal = (kebab) => kebab.replace(/(^|-)([a-z0-9])/g, (_m, _d, c) => c.toUpperCase());
|
|
1468
|
+
const NO_COLOR_ATTR_TAGS = /* @__PURE__ */ new Set(["meta", "link"]);
|
|
1469
|
+
function scanMarkup(path, text, lang, ds) {
|
|
1470
|
+
const out = { findings: [], tokenUsages: 0, themeRefs: 0, ds: {}, components: [], definitions: [] };
|
|
1471
|
+
const pos = lineCol(text);
|
|
1472
|
+
const blocks = markupBlocks(text, lang);
|
|
1473
|
+
const imports = /* @__PURE__ */ new Map();
|
|
1474
|
+
const merge = (c) => {
|
|
1475
|
+
out.findings.push(...c.findings);
|
|
1476
|
+
out.tokenUsages += c.tokenUsages;
|
|
1477
|
+
out.themeRefs += c.themeRefs;
|
|
1478
|
+
for (const [n, k] of Object.entries(c.ds)) out.ds[n] = (out.ds[n] ?? 0) + k;
|
|
1479
|
+
out.components.push(...c.components);
|
|
1480
|
+
out.definitions.push(...c.definitions);
|
|
1481
|
+
};
|
|
1482
|
+
const codeRanges = [...blocks.frontmatter ? [{ ...blocks.frontmatter, attrs: "" }] : [], ...blocks.scripts.filter((b) => isCodeScript(b.attrs))];
|
|
1483
|
+
for (const r of codeRanges) {
|
|
1484
|
+
const jsx = /^(tsx|jsx)$/i.test(attrOf(r.attrs, "lang") ?? "");
|
|
1485
|
+
const code = mask(text, [r], true);
|
|
1486
|
+
const file = `${path}.${jsx ? "tsx" : "ts"}`;
|
|
1487
|
+
merge(scanCode(file, code, ds));
|
|
1488
|
+
for (const [k, v] of importsOf(file, code)) imports.set(k, v);
|
|
1489
|
+
}
|
|
1490
|
+
for (const b of blocks.styles) {
|
|
1491
|
+
const l = (attrOf(b.attrs, "lang") ?? "css").toLowerCase();
|
|
1492
|
+
const r = auditCssText(text.slice(b.start, b.end), l === "scss" || l === "sass" ? "scss" : l === "less" ? "less" : "css", ds, { pos, offset: b.start });
|
|
1493
|
+
out.findings.push(...r.findings);
|
|
1494
|
+
out.tokenUsages += r.tokenUsages;
|
|
1495
|
+
out.themeRefs += r.themeRefs;
|
|
1496
|
+
}
|
|
1497
|
+
const scanExpr = (prefix, expr, offset, suffix, keepLookalikes = false) => {
|
|
1498
|
+
const { line, column } = pos(offset);
|
|
1499
|
+
const src = line >= 2 ? `${"\n".repeat(line - 2)}${prefix}
|
|
1500
|
+
${" ".repeat(column - 1)}${expr}${suffix}` : `${prefix}${expr}${suffix}`;
|
|
1501
|
+
const c = scanCode(`${path}.tsx`, src, ds);
|
|
1502
|
+
merge({ ...c, components: keepLookalikes ? c.components.filter((x) => x.kind === "recreated" && x.name.startsWith(".")) : [], definitions: [], ds: {} });
|
|
1503
|
+
};
|
|
1504
|
+
const styleAttr = (a) => {
|
|
1505
|
+
if (a.kind === "static") {
|
|
1506
|
+
const css = lang === "svelte" ? a.value.replace(/\{[^}]*\}/g, (m) => " ".repeat(m.length)) : a.value;
|
|
1507
|
+
const r = auditCssText(css, "css", ds, { pos, offset: a.valueStart, source: "style" });
|
|
1508
|
+
out.findings.push(...r.findings);
|
|
1509
|
+
out.tokenUsages += r.tokenUsages;
|
|
1510
|
+
out.themeRefs += r.themeRefs;
|
|
1511
|
+
return;
|
|
1512
|
+
}
|
|
1513
|
+
const str = /^(\s*)([`'"])([\s\S]*)\2\s*$/.exec(a.value);
|
|
1514
|
+
if (str) {
|
|
1515
|
+
const body = str[3].replace(/\$\{[^}]*\}/g, (m) => " ".repeat(m.length));
|
|
1516
|
+
const r = auditCssText(body, "css", ds, { pos, offset: a.valueStart + str[1].length + 1, source: "style" });
|
|
1517
|
+
out.findings.push(...r.findings);
|
|
1518
|
+
out.tokenUsages += r.tokenUsages;
|
|
1519
|
+
out.themeRefs += r.themeRefs;
|
|
1520
|
+
return;
|
|
1521
|
+
}
|
|
1522
|
+
scanExpr("const style: CSSProperties =", a.value, a.valueStart, ";");
|
|
1523
|
+
};
|
|
1524
|
+
const tpl = mask(text, blocks.nonTemplate, false);
|
|
1525
|
+
const pending = [];
|
|
1526
|
+
let noscript = 0;
|
|
1527
|
+
const TAG = /<(\/?)([A-Za-z][\w.:-]*)/g;
|
|
1528
|
+
for (let m; m = TAG.exec(tpl); ) {
|
|
1529
|
+
const raw = m[2];
|
|
1530
|
+
if (m[1]) {
|
|
1531
|
+
if (raw.toLowerCase() === "noscript") noscript = Math.max(0, noscript - 1);
|
|
1532
|
+
continue;
|
|
1533
|
+
}
|
|
1534
|
+
const tag = readTag(tpl, m.index + m[0].length, lang);
|
|
1535
|
+
TAG.lastIndex = tag.end;
|
|
1536
|
+
const lower = raw.toLowerCase();
|
|
1537
|
+
if (lower === "noscript" && !tag.selfClosing) {
|
|
1538
|
+
noscript++;
|
|
1539
|
+
continue;
|
|
1540
|
+
}
|
|
1541
|
+
const attrs = tag.attrs;
|
|
1542
|
+
const get = (n) => attrs.find((a) => attrName(a.name) === n);
|
|
1543
|
+
for (const a of attrs) {
|
|
1544
|
+
const n = attrName(a.name);
|
|
1545
|
+
if (n === "style") styleAttr(a);
|
|
1546
|
+
else if (lang === "svelte" && a.name.startsWith("style:") && a.kind === "static") {
|
|
1547
|
+
const prop = a.name.slice(6).split("|")[0];
|
|
1548
|
+
const r2 = auditCssText(`${prop}:${a.value}`, "css", ds, { pos, offset: a.valueStart - prop.length - 1, source: "style" });
|
|
1549
|
+
out.findings.push(...r2.findings);
|
|
1550
|
+
out.tokenUsages += r2.tokenUsages;
|
|
1551
|
+
out.themeRefs += r2.themeRefs;
|
|
1552
|
+
} else if (/^(class|className|class:list)$/.test(n)) {
|
|
1553
|
+
if (a.kind === "static") {
|
|
1554
|
+
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 });
|
|
1555
|
+
const r2 = tailwindFindings(scanClassString(a.value), a.valueStart, pos, ds);
|
|
1556
|
+
out.findings.push(...r2.findings);
|
|
1557
|
+
out.themeRefs += r2.themeRefs;
|
|
1558
|
+
} else if (a.kind === "expr") scanExpr("cn(", a.value, a.valueStart, ")", true);
|
|
1559
|
+
} else if (LOOKALIKE_ATTRS[n] && ds.components.has(LOOKALIKE_ATTRS[n])) {
|
|
1560
|
+
out.components.push({ ...pos(a.valueStart - a.name.length - 1), name: `[${n}]`, kind: "recreated", library: "local", suggestion: LOOKALIKE_ATTRS[n] });
|
|
1561
|
+
} else if (a.kind === "static" && !NO_COLOR_ATTR_TAGS.has(lower) && isColorLiteral(a.value)) {
|
|
1562
|
+
const suggestion = ds.suggest("color", a.value.trim(), n);
|
|
1563
|
+
out.findings.push({ ...pos(a.valueStart), category: "color", property: n, value: a.value.trim(), source: "jsx-attr", ...suggestion ? { suggestion } : {} });
|
|
1217
1564
|
}
|
|
1218
1565
|
}
|
|
1219
|
-
|
|
1220
|
-
|
|
1566
|
+
let name = raw;
|
|
1567
|
+
if (raw.includes(":")) continue;
|
|
1568
|
+
if (raw.includes("-")) {
|
|
1569
|
+
if (!imports.has(pascal(raw))) continue;
|
|
1570
|
+
name = pascal(raw);
|
|
1571
|
+
}
|
|
1572
|
+
const isNative = /^[a-z]/.test(name) && !name.includes(".");
|
|
1573
|
+
if (isNative && noscript > 0) continue;
|
|
1574
|
+
const type = get("type");
|
|
1575
|
+
const r = classifyTag({ name, ...pos(m.index), ...type?.kind === "static" ? { type: type.value } : {}, hasHref: Boolean(get("href")) }, imports, ds);
|
|
1576
|
+
if (r.ds) out.ds[r.ds] = (out.ds[r.ds] ?? 0) + 1;
|
|
1577
|
+
if (r.component) out.components.push(r.component);
|
|
1578
|
+
if (r.localCandidate) pending.push(r.localCandidate);
|
|
1221
1579
|
}
|
|
1222
|
-
const
|
|
1580
|
+
const base = /([^/]+)\.(astro|vue|svelte)$/.exec(path)?.[1];
|
|
1581
|
+
if (base && /^[A-Z][A-Za-z0-9]*$/.test(base)) out.definitions.push({ name: base, line: 1 });
|
|
1582
|
+
const defined = new Set(out.definitions.map((d) => d.name));
|
|
1583
|
+
out.components.push(...pending.filter((f) => defined.has(f.suggestion)));
|
|
1584
|
+
out.findings.sort((a, b) => a.line - b.line || a.column - b.column);
|
|
1585
|
+
out.components.sort((a, b) => a.line - b.line || a.column - b.column);
|
|
1586
|
+
return out;
|
|
1587
|
+
}
|
|
1588
|
+
const AUDIT_EXTENSIONS = [".css", ".scss", ".sass", ".less", ".tsx", ".jsx", ".ts", ".js", ".mjs", ".cjs", ".mts", ".cts", ...MARKUP_EXTENSIONS];
|
|
1589
|
+
const AUDIT_IGNORE_DIRS = ["node_modules", "dist", "build", "out", "coverage", ".git", ".next", ".nuxt", ".astro", ".turbo", ".cache", ".vercel", "storybook-static", "vendor", "public"];
|
|
1590
|
+
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$/;
|
|
1591
|
+
function isAuditable(path) {
|
|
1592
|
+
if (!AUDIT_EXTENSIONS.some((e) => path.endsWith(e))) return false;
|
|
1593
|
+
if (IGNORE_FILE.test(path)) return false;
|
|
1594
|
+
return !path.split("/").some((seg) => AUDIT_IGNORE_DIRS.includes(seg));
|
|
1595
|
+
}
|
|
1596
|
+
const langOf = (path) => {
|
|
1597
|
+
const ext = path.slice(path.lastIndexOf(".") + 1);
|
|
1598
|
+
if (ext === "css") return "css";
|
|
1599
|
+
if (ext === "scss" || ext === "sass") return "scss";
|
|
1600
|
+
if (ext === "less") return "less";
|
|
1601
|
+
if (ext === "tsx") return "tsx";
|
|
1602
|
+
if (ext === "jsx") return "jsx";
|
|
1603
|
+
if (/^[cm]?ts$/.test(ext)) return "ts";
|
|
1604
|
+
if (ext === "astro" || ext === "vue" || ext === "svelte") return ext;
|
|
1605
|
+
return "js";
|
|
1606
|
+
};
|
|
1607
|
+
function auditFile(path, text, ds) {
|
|
1608
|
+
const lang = langOf(path);
|
|
1609
|
+
if (lang === "css" || lang === "scss" || lang === "less") {
|
|
1610
|
+
const r = auditCssText(text, lang, ds, { pos: lineCol(text) });
|
|
1611
|
+
return withIgnores(text, { path, lang, tokenUsages: r.tokenUsages, themeRefs: r.themeRefs, hardcoded: 0, dsComponents: 0, otherComponents: 0, findings: r.findings, components: [], definitions: [], dsByName: {} });
|
|
1612
|
+
}
|
|
1613
|
+
const c = lang === "astro" || lang === "vue" || lang === "svelte" ? scanMarkup(path, text, lang, ds) : scanCode(path, text, ds);
|
|
1223
1614
|
const dsCount = Object.values(c.ds).reduce((a, b) => a + b, 0);
|
|
1224
|
-
return { path, lang, tokenUsages: c.tokenUsages, themeRefs: c.themeRefs, hardcoded:
|
|
1615
|
+
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 });
|
|
1616
|
+
}
|
|
1617
|
+
function withIgnores(text, f) {
|
|
1618
|
+
const r = applyIgnoreComments(text, f.findings, f.components);
|
|
1619
|
+
return {
|
|
1620
|
+
...f,
|
|
1621
|
+
findings: r.findings,
|
|
1622
|
+
components: r.components,
|
|
1623
|
+
hardcoded: r.findings.length,
|
|
1624
|
+
otherComponents: r.components.length,
|
|
1625
|
+
...r.ignored.length ? { ignored: r.ignored } : {},
|
|
1626
|
+
...r.errors.length ? { ignoreErrors: r.errors } : {}
|
|
1627
|
+
};
|
|
1225
1628
|
}
|
|
1226
1629
|
const pct = (num2, den) => den > 0 ? Math.round(num2 / den * 1e3) / 10 : null;
|
|
1227
1630
|
function buildAuditReport(input, ds) {
|
|
1228
|
-
const
|
|
1631
|
+
const allow = input.allowlist ?? [];
|
|
1632
|
+
const usedAllow = /* @__PURE__ */ new Set();
|
|
1633
|
+
const reports = input.files.map((f) => {
|
|
1634
|
+
const r = auditFile(f.path, f.text, ds);
|
|
1635
|
+
const idx = allow.flatMap((e, i) => e.file === f.path ? [i] : []);
|
|
1636
|
+
if (!idx.length) return r;
|
|
1637
|
+
const a = applyAllowlist(idx.map((i) => allow[i]), r.findings, r.components);
|
|
1638
|
+
a.used.forEach((k) => usedAllow.add(idx[k]));
|
|
1639
|
+
if (!a.ignored.length) return r;
|
|
1640
|
+
const ignored = [...r.ignored ?? [], ...a.ignored].sort((x, y) => x.line - y.line || x.column - y.column);
|
|
1641
|
+
return { ...r, findings: a.findings, components: a.components, hardcoded: a.findings.length, otherComponents: a.components.length, ignored };
|
|
1642
|
+
});
|
|
1643
|
+
const ignoredItems = reports.flatMap((r) => (r.ignored ?? []).map((x) => ({ file: r.path, ...x })));
|
|
1644
|
+
const staleAllowlist = allow.filter((_e, i) => !usedAllow.has(i));
|
|
1229
1645
|
const files = reports.map(({ definitions: _d, dsByName: _n, ...r }) => r);
|
|
1230
1646
|
const byCategory = {};
|
|
1231
1647
|
const fixes = /* @__PURE__ */ new Map();
|
|
@@ -1285,6 +1701,15 @@ function buildAuditReport(input, ds) {
|
|
|
1285
1701
|
topFiles,
|
|
1286
1702
|
topFixes,
|
|
1287
1703
|
files: files.sort((a, b) => a.path.localeCompare(b.path)),
|
|
1704
|
+
...ignoredItems.length || staleAllowlist.length ? {
|
|
1705
|
+
ignored: {
|
|
1706
|
+
total: ignoredItems.length,
|
|
1707
|
+
comment: ignoredItems.filter((x) => x.via === "comment").length,
|
|
1708
|
+
allowlist: ignoredItems.filter((x) => x.via === "allowlist").length,
|
|
1709
|
+
items: ignoredItems,
|
|
1710
|
+
staleAllowlist
|
|
1711
|
+
}
|
|
1712
|
+
} : {},
|
|
1288
1713
|
history: []
|
|
1289
1714
|
};
|
|
1290
1715
|
}
|
|
@@ -1388,6 +1813,20 @@ function auditMarkdown(r, opts = {}) {
|
|
|
1388
1813
|
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
1814
|
lines.push("");
|
|
1390
1815
|
}
|
|
1816
|
+
const ig = r.ignored;
|
|
1817
|
+
if (ig && (ig.total || ig.staleAllowlist.length)) {
|
|
1818
|
+
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.`, "");
|
|
1819
|
+
const shown = ig.items.slice(0, opts.ignored ?? 50);
|
|
1820
|
+
if (shown.length) {
|
|
1821
|
+
lines.push("| Where | What | Reason |", "| --- | --- | --- |");
|
|
1822
|
+
for (const x of shown) lines.push(`| \`${esc(x.file)}:${x.line}\` | \`${esc(x.what)}\` | ${esc(x.reason)} (${x.via}) |`);
|
|
1823
|
+
if (ig.items.length > shown.length) lines.push(`| ... | ${ig.items.length - shown.length} more | |`);
|
|
1824
|
+
lines.push("");
|
|
1825
|
+
}
|
|
1826
|
+
if (ig.staleAllowlist.length) {
|
|
1827
|
+
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(", ")}.`, "");
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1391
1830
|
if (b) {
|
|
1392
1831
|
lines.push(`### Progress since ${b.label ?? b.generatedAt}`, "");
|
|
1393
1832
|
lines.push(`- ${b.improved.length} files improved, ${b.regressed.length} regressed, ${b.newFiles.length} new, ${b.removedFiles.length} removed.`);
|
|
@@ -1415,40 +1854,56 @@ function planCssFixes(text, lang, ds) {
|
|
|
1415
1854
|
}
|
|
1416
1855
|
export {
|
|
1417
1856
|
AUDIT_EXTENSIONS as A,
|
|
1418
|
-
|
|
1857
|
+
libraryLabel as B,
|
|
1419
1858
|
COLOR_LITERAL_RE as C,
|
|
1420
1859
|
DEFAULT_NATIVE_ELEMENTS as D,
|
|
1421
|
-
|
|
1860
|
+
lineCol as E,
|
|
1422
1861
|
FINDING_CATEGORIES as F,
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1862
|
+
lookalikesInClassList as G,
|
|
1863
|
+
markupBlocks as H,
|
|
1864
|
+
IGNORE_DIRECTIVE as I,
|
|
1865
|
+
nativeEquivalent as J,
|
|
1866
|
+
packageName as K,
|
|
1867
|
+
LOOKALIKE_ATTRS as L,
|
|
1868
|
+
MARKUP_EXTENSIONS as M,
|
|
1426
1869
|
NAMED_COLORS as N,
|
|
1870
|
+
parseAllowlist as O,
|
|
1871
|
+
parseCssColor as P,
|
|
1872
|
+
planCssFixes as Q,
|
|
1873
|
+
scanClassString as R,
|
|
1874
|
+
scanCode as S,
|
|
1875
|
+
scanCssText as T,
|
|
1876
|
+
scanCssValue as U,
|
|
1877
|
+
scanJsStyle as V,
|
|
1878
|
+
scanMarkup as W,
|
|
1879
|
+
tailwindColor as X,
|
|
1880
|
+
tailwindFindings as Y,
|
|
1881
|
+
toOklab as Z,
|
|
1427
1882
|
AUDIT_IGNORE_DIRS as a,
|
|
1428
1883
|
AUDIT_SCHEMA_VERSION as b,
|
|
1429
1884
|
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
|
-
|
|
1885
|
+
LOOKALIKE_CLASSES as d,
|
|
1886
|
+
applyAllowlist as e,
|
|
1887
|
+
applyIgnoreComments as f,
|
|
1888
|
+
auditCssText as g,
|
|
1889
|
+
auditFile as h,
|
|
1890
|
+
auditMarkdown as i,
|
|
1891
|
+
blankComments as j,
|
|
1892
|
+
buildAuditReport as k,
|
|
1893
|
+
classifyModule as l,
|
|
1894
|
+
classifyTag as m,
|
|
1895
|
+
compareWithBaseline as n,
|
|
1896
|
+
cssPropertyOf as o,
|
|
1897
|
+
deltaE as p,
|
|
1898
|
+
domRendererComponent as q,
|
|
1899
|
+
dsEquivalent as r,
|
|
1900
|
+
historyPoint as s,
|
|
1901
|
+
ignoreDirectives as t,
|
|
1902
|
+
importsOf as u,
|
|
1903
|
+
isAuditable as v,
|
|
1904
|
+
isColorLiteral as w,
|
|
1905
|
+
isNonVisual as x,
|
|
1906
|
+
isRouterLink as y,
|
|
1907
|
+
isSafeFix as z
|
|
1453
1908
|
};
|
|
1454
|
-
//# sourceMappingURL=report-
|
|
1909
|
+
//# sourceMappingURL=report-4Z008HSf.js.map
|