uilint-eslint 0.1.16 → 0.1.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -267,36 +267,129 @@ var COLOR_PREFIXES = [
267
267
  "to-"
268
268
  ];
269
269
  var EXEMPT_SUFFIXES = ["transparent", "inherit", "current", "auto", "none"];
270
- var COLOR_NAMES = /* @__PURE__ */ new Set([
271
- // Grayscale
272
- "slate",
273
- "gray",
274
- "zinc",
275
- "neutral",
276
- "stone",
277
- // Colors
278
- "red",
279
- "orange",
280
- "amber",
281
- "yellow",
282
- "lime",
283
- "green",
284
- "emerald",
285
- "teal",
286
- "cyan",
287
- "sky",
288
- "blue",
289
- "indigo",
290
- "violet",
291
- "purple",
292
- "fuchsia",
293
- "pink",
294
- "rose",
295
- // Special
296
- "black",
297
- "white"
270
+ var NON_COLOR_UTILITIES = /* @__PURE__ */ new Set([
271
+ // Exempt values (colorless or inherited) - don't need dark variants
272
+ "transparent",
273
+ "inherit",
274
+ "current",
275
+ "auto",
276
+ "none",
277
+ // text- utilities that aren't colors
278
+ "xs",
279
+ "sm",
280
+ "base",
281
+ "lg",
282
+ "xl",
283
+ "2xl",
284
+ "3xl",
285
+ "4xl",
286
+ "5xl",
287
+ "6xl",
288
+ "7xl",
289
+ "8xl",
290
+ "9xl",
291
+ "left",
292
+ "center",
293
+ "right",
294
+ "justify",
295
+ "start",
296
+ "end",
297
+ "wrap",
298
+ "nowrap",
299
+ "balance",
300
+ "pretty",
301
+ "ellipsis",
302
+ "clip",
303
+ // border- utilities that aren't colors
304
+ "0",
305
+ "2",
306
+ "4",
307
+ "8",
308
+ "solid",
309
+ "dashed",
310
+ "dotted",
311
+ "double",
312
+ "hidden",
313
+ "collapse",
314
+ "separate",
315
+ // shadow- utilities that aren't colors
316
+ // Note: "sm", "lg", "xl", "2xl" already included above
317
+ "md",
318
+ "inner",
319
+ // ring- utilities that aren't colors
320
+ // Note: "0", "2", "4", "8" already included above
321
+ "1",
322
+ "inset",
323
+ // outline- utilities that aren't colors
324
+ // Note: numeric values already included
325
+ "offset-0",
326
+ "offset-1",
327
+ "offset-2",
328
+ "offset-4",
329
+ "offset-8",
330
+ // decoration- utilities that aren't colors
331
+ // Note: "solid", "double", "dotted", "dashed" already included
332
+ "wavy",
333
+ "from-font",
334
+ "clone",
335
+ "slice",
336
+ // divide- utilities that aren't colors
337
+ "x",
338
+ "y",
339
+ "x-0",
340
+ "x-2",
341
+ "x-4",
342
+ "x-8",
343
+ "y-0",
344
+ "y-2",
345
+ "y-4",
346
+ "y-8",
347
+ "x-reverse",
348
+ "y-reverse",
349
+ // gradient direction utilities (from-, via-, to- prefixes)
350
+ "t",
351
+ "tr",
352
+ "r",
353
+ "br",
354
+ "b",
355
+ "bl",
356
+ "l",
357
+ "tl"
298
358
  ]);
299
- var COLOR_VALUE_PATTERN = /^(?:(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-\d{1,3}(?:\/\d+)?|black|white|inherit|current|transparent|\[.+\])$/;
359
+ var SEMANTIC_COLOR_NAMES = /* @__PURE__ */ new Set([
360
+ // Core shadcn colors
361
+ "background",
362
+ "foreground",
363
+ // Component colors
364
+ "card",
365
+ "card-foreground",
366
+ "popover",
367
+ "popover-foreground",
368
+ "primary",
369
+ "primary-foreground",
370
+ "secondary",
371
+ "secondary-foreground",
372
+ "muted",
373
+ "muted-foreground",
374
+ "accent",
375
+ "accent-foreground",
376
+ "destructive",
377
+ "destructive-foreground",
378
+ // Form/UI colors
379
+ "border",
380
+ "input",
381
+ "ring",
382
+ // Sidebar colors (shadcn sidebar component)
383
+ "sidebar",
384
+ "sidebar-foreground",
385
+ "sidebar-border",
386
+ "sidebar-primary",
387
+ "sidebar-primary-foreground",
388
+ "sidebar-accent",
389
+ "sidebar-accent-foreground",
390
+ "sidebar-ring"
391
+ ]);
392
+ var CHART_COLOR_PATTERN = /^chart-\d+$/;
300
393
  function hasDarkVariant(className) {
301
394
  const parts = className.split(":");
302
395
  const variants = parts.slice(0, -1);
@@ -312,20 +405,28 @@ function getColorPrefix(baseClass) {
312
405
  );
313
406
  return sortedPrefixes.find((p) => baseClass.startsWith(p)) || null;
314
407
  }
315
- function isColorValue(baseClass, prefix) {
316
- const value = baseClass.slice(prefix.length);
317
- if (COLOR_VALUE_PATTERN.test(value)) {
408
+ function isSemanticColor(value) {
409
+ if (SEMANTIC_COLOR_NAMES.has(value)) {
318
410
  return true;
319
411
  }
320
- const firstPart = value.split("-")[0];
321
- if (COLOR_NAMES.has(firstPart)) {
322
- return true;
323
- }
324
- if (value.startsWith("[") && value.endsWith("]")) {
412
+ if (CHART_COLOR_PATTERN.test(value)) {
325
413
  return true;
326
414
  }
327
415
  return false;
328
416
  }
417
+ function isColorValue(baseClass, prefix) {
418
+ const value = baseClass.slice(prefix.length);
419
+ if (!value) {
420
+ return false;
421
+ }
422
+ if (isSemanticColor(value)) {
423
+ return false;
424
+ }
425
+ if (NON_COLOR_UTILITIES.has(value)) {
426
+ return false;
427
+ }
428
+ return true;
429
+ }
329
430
  function isExempt(baseClass) {
330
431
  return EXEMPT_SUFFIXES.some((suffix) => baseClass.endsWith(suffix));
331
432
  }
@@ -715,78 +816,602 @@ var prefer_zustand_state_management_default = createRule({
715
816
  }
716
817
  });
717
818
 
718
- // src/rules/no-mixed-component-libraries.ts
819
+ // src/utils/export-resolver.ts
820
+ import { ResolverFactory } from "oxc-resolver";
821
+ import { parse } from "@typescript-eslint/typescript-estree";
822
+ import { readFileSync, existsSync } from "fs";
823
+ import { dirname, join } from "path";
824
+ var resolverFactory = null;
825
+ var exportCache = /* @__PURE__ */ new Map();
826
+ var astCache = /* @__PURE__ */ new Map();
827
+ var resolvedPathCache = /* @__PURE__ */ new Map();
828
+ function getResolverFactory() {
829
+ if (!resolverFactory) {
830
+ resolverFactory = new ResolverFactory({
831
+ extensions: [".tsx", ".ts", ".jsx", ".js"],
832
+ mainFields: ["module", "main"],
833
+ conditionNames: ["import", "require", "node", "default"],
834
+ // Enable TypeScript path resolution
835
+ tsconfig: {
836
+ configFile: "tsconfig.json",
837
+ references: "auto"
838
+ }
839
+ });
840
+ }
841
+ return resolverFactory;
842
+ }
843
+ function resolveImportPath(importSource, fromFile) {
844
+ const cacheKey = `${fromFile}::${importSource}`;
845
+ if (resolvedPathCache.has(cacheKey)) {
846
+ return resolvedPathCache.get(cacheKey) ?? null;
847
+ }
848
+ if (importSource.startsWith("react") || importSource.startsWith("next") || !importSource.startsWith(".") && !importSource.startsWith("@/") && !importSource.startsWith("~/")) {
849
+ if (importSource.includes("@mui/") || importSource.includes("@chakra-ui/") || importSource.includes("antd") || importSource.includes("@radix-ui/")) {
850
+ resolvedPathCache.set(cacheKey, null);
851
+ return null;
852
+ }
853
+ resolvedPathCache.set(cacheKey, null);
854
+ return null;
855
+ }
856
+ try {
857
+ const factory = getResolverFactory();
858
+ const fromDir = dirname(fromFile);
859
+ const result = factory.sync(fromDir, importSource);
860
+ if (result.path) {
861
+ resolvedPathCache.set(cacheKey, result.path);
862
+ return result.path;
863
+ }
864
+ } catch {
865
+ const resolved = manualResolve(importSource, fromFile);
866
+ resolvedPathCache.set(cacheKey, resolved);
867
+ return resolved;
868
+ }
869
+ resolvedPathCache.set(cacheKey, null);
870
+ return null;
871
+ }
872
+ function manualResolve(importSource, fromFile) {
873
+ const fromDir = dirname(fromFile);
874
+ const extensions = [".tsx", ".ts", ".jsx", ".js"];
875
+ if (importSource.startsWith("@/")) {
876
+ const projectRoot = findProjectRoot(fromFile);
877
+ if (projectRoot) {
878
+ const relativePath = importSource.slice(2);
879
+ for (const ext of extensions) {
880
+ const candidate = join(projectRoot, relativePath + ext);
881
+ if (existsSync(candidate)) {
882
+ return candidate;
883
+ }
884
+ const indexCandidate = join(projectRoot, relativePath, `index${ext}`);
885
+ if (existsSync(indexCandidate)) {
886
+ return indexCandidate;
887
+ }
888
+ }
889
+ }
890
+ }
891
+ if (importSource.startsWith(".")) {
892
+ for (const ext of extensions) {
893
+ const candidate = join(fromDir, importSource + ext);
894
+ if (existsSync(candidate)) {
895
+ return candidate;
896
+ }
897
+ const indexCandidate = join(fromDir, importSource, `index${ext}`);
898
+ if (existsSync(indexCandidate)) {
899
+ return indexCandidate;
900
+ }
901
+ }
902
+ }
903
+ return null;
904
+ }
905
+ function findProjectRoot(fromFile) {
906
+ let dir = dirname(fromFile);
907
+ const root = "/";
908
+ while (dir !== root) {
909
+ if (existsSync(join(dir, "tsconfig.json"))) {
910
+ return dir;
911
+ }
912
+ if (existsSync(join(dir, "package.json"))) {
913
+ return dir;
914
+ }
915
+ dir = dirname(dir);
916
+ }
917
+ return null;
918
+ }
919
+ function parseFile(filePath) {
920
+ if (astCache.has(filePath)) {
921
+ return astCache.get(filePath);
922
+ }
923
+ try {
924
+ const content = readFileSync(filePath, "utf-8");
925
+ const ast = parse(content, {
926
+ jsx: true,
927
+ loc: true,
928
+ range: true
929
+ });
930
+ astCache.set(filePath, ast);
931
+ return ast;
932
+ } catch {
933
+ return null;
934
+ }
935
+ }
936
+ function extractExports(filePath) {
937
+ if (exportCache.has(filePath)) {
938
+ return exportCache.get(filePath);
939
+ }
940
+ const exports = /* @__PURE__ */ new Map();
941
+ const ast = parseFile(filePath);
942
+ if (!ast) {
943
+ exportCache.set(filePath, exports);
944
+ return exports;
945
+ }
946
+ for (const node of ast.body) {
947
+ if (node.type === "ExportNamedDeclaration" && node.declaration?.type === "FunctionDeclaration" && node.declaration.id) {
948
+ exports.set(node.declaration.id.name, {
949
+ localName: node.declaration.id.name
950
+ });
951
+ }
952
+ if (node.type === "ExportNamedDeclaration" && node.declaration?.type === "VariableDeclaration") {
953
+ for (const decl of node.declaration.declarations) {
954
+ if (decl.id.type === "Identifier") {
955
+ exports.set(decl.id.name, { localName: decl.id.name });
956
+ }
957
+ }
958
+ }
959
+ if (node.type === "ExportNamedDeclaration" && node.specifiers.length > 0) {
960
+ const source = node.source?.value;
961
+ for (const spec of node.specifiers) {
962
+ if (spec.type === "ExportSpecifier") {
963
+ const exportedName = spec.exported.type === "Identifier" ? spec.exported.name : spec.exported.value;
964
+ const localName = spec.local.type === "Identifier" ? spec.local.name : spec.local.value;
965
+ exports.set(exportedName, {
966
+ localName,
967
+ reexportSource: source
968
+ });
969
+ }
970
+ }
971
+ }
972
+ if (node.type === "ExportDefaultDeclaration" && node.declaration.type === "FunctionDeclaration" && node.declaration.id) {
973
+ exports.set("default", { localName: node.declaration.id.name });
974
+ }
975
+ if (node.type === "ExportDefaultDeclaration" && node.declaration.type === "Identifier") {
976
+ exports.set("default", { localName: node.declaration.name });
977
+ }
978
+ }
979
+ exportCache.set(filePath, exports);
980
+ return exports;
981
+ }
982
+ function resolveExport(exportName, filePath, visited = /* @__PURE__ */ new Set()) {
983
+ const key = `${filePath}::${exportName}`;
984
+ if (visited.has(key)) {
985
+ return null;
986
+ }
987
+ visited.add(key);
988
+ const exports = extractExports(filePath);
989
+ const exportInfo = exports.get(exportName);
990
+ if (!exportInfo) {
991
+ return null;
992
+ }
993
+ if (exportInfo.reexportSource) {
994
+ const resolvedPath = resolveImportPath(exportInfo.reexportSource, filePath);
995
+ if (resolvedPath) {
996
+ return resolveExport(exportInfo.localName, resolvedPath, visited);
997
+ }
998
+ return null;
999
+ }
1000
+ return {
1001
+ name: exportName,
1002
+ filePath,
1003
+ localName: exportInfo.localName,
1004
+ isReexport: false
1005
+ };
1006
+ }
1007
+
1008
+ // src/utils/component-parser.ts
719
1009
  var LIBRARY_PATTERNS = {
720
1010
  shadcn: ["@/components/ui", "@radix-ui/", "components/ui/"],
721
- mui: ["@mui/material", "@mui/icons-material", "@emotion/"]
1011
+ mui: ["@mui/material", "@mui/icons-material", "@emotion/"],
1012
+ chakra: ["@chakra-ui/"],
1013
+ antd: ["antd", "@ant-design/"]
722
1014
  };
1015
+ function extractImports(ast) {
1016
+ const imports = /* @__PURE__ */ new Map();
1017
+ for (const node of ast.body) {
1018
+ if (node.type === "ImportDeclaration") {
1019
+ const source = node.source.value;
1020
+ for (const spec of node.specifiers) {
1021
+ if (spec.type === "ImportSpecifier") {
1022
+ imports.set(spec.local.name, source);
1023
+ } else if (spec.type === "ImportDefaultSpecifier") {
1024
+ imports.set(spec.local.name, source);
1025
+ } else if (spec.type === "ImportNamespaceSpecifier") {
1026
+ imports.set(spec.local.name, source);
1027
+ }
1028
+ }
1029
+ }
1030
+ }
1031
+ return imports;
1032
+ }
1033
+ function detectLibraryFromSource(importSource) {
1034
+ for (const [library, patterns] of Object.entries(LIBRARY_PATTERNS)) {
1035
+ if (patterns.some((p) => importSource.includes(p))) {
1036
+ return library;
1037
+ }
1038
+ }
1039
+ return null;
1040
+ }
1041
+ function findComponentDefinition(ast, componentName) {
1042
+ for (const node of ast.body) {
1043
+ if (node.type === "ExportNamedDeclaration" && node.declaration?.type === "FunctionDeclaration" && node.declaration.id?.name === componentName) {
1044
+ return node.declaration;
1045
+ }
1046
+ if (node.type === "ExportNamedDeclaration" && node.declaration?.type === "VariableDeclaration") {
1047
+ for (const decl of node.declaration.declarations) {
1048
+ if (decl.id.type === "Identifier" && decl.id.name === componentName && decl.init?.type === "ArrowFunctionExpression") {
1049
+ return decl.init;
1050
+ }
1051
+ }
1052
+ }
1053
+ if (node.type === "FunctionDeclaration" && node.id?.name === componentName) {
1054
+ return node;
1055
+ }
1056
+ if (node.type === "VariableDeclaration") {
1057
+ for (const decl of node.declarations) {
1058
+ if (decl.id.type === "Identifier" && decl.id.name === componentName && decl.init?.type === "ArrowFunctionExpression") {
1059
+ return decl.init;
1060
+ }
1061
+ }
1062
+ }
1063
+ if (node.type === "ExportDefaultDeclaration" && node.declaration.type === "FunctionDeclaration" && node.declaration.id?.name === componentName) {
1064
+ return node.declaration;
1065
+ }
1066
+ }
1067
+ return null;
1068
+ }
1069
+ function extractTailwindClasses(classString) {
1070
+ return classString.split(/\s+/).filter(Boolean);
1071
+ }
1072
+ function traverseForStyling(node, imports, result) {
1073
+ if (!node || typeof node !== "object") return;
1074
+ if (node.type === "JSXElement" && node.openingElement) {
1075
+ const opening = node.openingElement;
1076
+ if (opening.name.type === "JSXIdentifier" && /^[A-Z]/.test(opening.name.name)) {
1077
+ const componentName = opening.name.name;
1078
+ const importSource = imports.get(componentName);
1079
+ if (importSource) {
1080
+ result.usedComponents.push({
1081
+ name: componentName,
1082
+ importSource,
1083
+ line: opening.loc.start.line,
1084
+ column: opening.loc.start.column
1085
+ });
1086
+ const library = detectLibraryFromSource(importSource);
1087
+ if (library && !result.directLibrary) {
1088
+ result.directLibrary = library;
1089
+ }
1090
+ }
1091
+ }
1092
+ if (opening.name.type === "JSXMemberExpression") {
1093
+ let objectName = null;
1094
+ let current = opening.name.object;
1095
+ while (current.type === "JSXMemberExpression") {
1096
+ current = current.object;
1097
+ }
1098
+ if (current.type === "JSXIdentifier") {
1099
+ objectName = current.name;
1100
+ }
1101
+ if (objectName) {
1102
+ const importSource = imports.get(objectName);
1103
+ if (importSource) {
1104
+ const library = detectLibraryFromSource(importSource);
1105
+ if (library && !result.directLibrary) {
1106
+ result.directLibrary = library;
1107
+ }
1108
+ }
1109
+ }
1110
+ }
1111
+ for (const attr of opening.attributes) {
1112
+ if (attr.type === "JSXAttribute" && attr.name.type === "JSXIdentifier" && (attr.name.name === "className" || attr.name.name === "class")) {
1113
+ if (attr.value?.type === "Literal" && typeof attr.value.value === "string") {
1114
+ result.tailwindClasses.push(
1115
+ ...extractTailwindClasses(attr.value.value)
1116
+ );
1117
+ }
1118
+ if (attr.value?.type === "JSXExpressionContainer") {
1119
+ const expr = attr.value.expression;
1120
+ if (expr.type === "Literal" && typeof expr.value === "string") {
1121
+ result.tailwindClasses.push(...extractTailwindClasses(expr.value));
1122
+ }
1123
+ if (expr.type === "TemplateLiteral") {
1124
+ for (const quasi of expr.quasis) {
1125
+ result.tailwindClasses.push(
1126
+ ...extractTailwindClasses(quasi.value.raw)
1127
+ );
1128
+ }
1129
+ }
1130
+ }
1131
+ }
1132
+ if (attr.type === "JSXAttribute" && attr.name.type === "JSXIdentifier" && attr.name.name === "style") {
1133
+ if (attr.value?.type === "JSXExpressionContainer") {
1134
+ result.inlineStyles.push("[inline style]");
1135
+ }
1136
+ }
1137
+ }
1138
+ }
1139
+ if (node.type === "CallExpression" && node.callee.type === "Identifier" && ["cn", "clsx", "classnames", "twMerge"].includes(node.callee.name)) {
1140
+ for (const arg of node.arguments) {
1141
+ if (arg.type === "Literal" && typeof arg.value === "string") {
1142
+ result.tailwindClasses.push(...extractTailwindClasses(arg.value));
1143
+ }
1144
+ if (arg.type === "TemplateLiteral") {
1145
+ for (const quasi of arg.quasis) {
1146
+ result.tailwindClasses.push(
1147
+ ...extractTailwindClasses(quasi.value.raw)
1148
+ );
1149
+ }
1150
+ }
1151
+ }
1152
+ }
1153
+ for (const key of Object.keys(node)) {
1154
+ if (key === "parent" || key === "loc" || key === "range") continue;
1155
+ const child = node[key];
1156
+ if (Array.isArray(child)) {
1157
+ for (const item of child) {
1158
+ if (item && typeof item === "object") {
1159
+ traverseForStyling(item, imports, result);
1160
+ }
1161
+ }
1162
+ } else if (child && typeof child === "object") {
1163
+ traverseForStyling(child, imports, result);
1164
+ }
1165
+ }
1166
+ }
1167
+ function parseComponentBody(filePath, componentName) {
1168
+ const ast = parseFile(filePath);
1169
+ if (!ast) return null;
1170
+ const imports = extractImports(ast);
1171
+ const componentDef = findComponentDefinition(ast, componentName);
1172
+ if (!componentDef) {
1173
+ return null;
1174
+ }
1175
+ const result = {
1176
+ tailwindClasses: [],
1177
+ inlineStyles: [],
1178
+ usedComponents: [],
1179
+ directLibrary: null
1180
+ };
1181
+ if (componentDef.body) {
1182
+ traverseForStyling(componentDef.body, imports, result);
1183
+ }
1184
+ result.tailwindClasses = [...new Set(result.tailwindClasses)];
1185
+ return result;
1186
+ }
1187
+
1188
+ // src/utils/import-graph.ts
1189
+ var componentLibraryCache = /* @__PURE__ */ new Map();
1190
+ function getComponentLibrary(contextFilePath, componentName, importSource) {
1191
+ const directLibrary = detectLibraryFromSource(importSource);
1192
+ if (directLibrary) {
1193
+ return {
1194
+ library: directLibrary,
1195
+ internalLibraries: /* @__PURE__ */ new Set(),
1196
+ libraryEvidence: [],
1197
+ isLocalComponent: false
1198
+ };
1199
+ }
1200
+ const resolvedPath = resolveImportPath(importSource, contextFilePath);
1201
+ if (!resolvedPath) {
1202
+ return {
1203
+ library: null,
1204
+ internalLibraries: /* @__PURE__ */ new Set(),
1205
+ libraryEvidence: [],
1206
+ isLocalComponent: false
1207
+ };
1208
+ }
1209
+ const cacheKey = `${resolvedPath}::${componentName}`;
1210
+ if (componentLibraryCache.has(cacheKey)) {
1211
+ return componentLibraryCache.get(cacheKey);
1212
+ }
1213
+ const resolvedExport = resolveExport(componentName, resolvedPath);
1214
+ const actualFilePath = resolvedExport?.filePath ?? resolvedPath;
1215
+ const actualComponentName = resolvedExport?.localName ?? componentName;
1216
+ const result = analyzeComponentLibraries(
1217
+ actualFilePath,
1218
+ actualComponentName,
1219
+ /* @__PURE__ */ new Set([cacheKey])
1220
+ // Track visited to prevent cycles
1221
+ );
1222
+ componentLibraryCache.set(cacheKey, result);
1223
+ return result;
1224
+ }
1225
+ function analyzeComponentLibraries(filePath, componentName, visited) {
1226
+ const styleInfo = parseComponentBody(filePath, componentName);
1227
+ if (!styleInfo) {
1228
+ return {
1229
+ library: null,
1230
+ internalLibraries: /* @__PURE__ */ new Set(),
1231
+ libraryEvidence: [],
1232
+ isLocalComponent: true
1233
+ };
1234
+ }
1235
+ const internalLibraries = /* @__PURE__ */ new Set();
1236
+ const libraryEvidence = [];
1237
+ if (styleInfo.directLibrary) {
1238
+ internalLibraries.add(styleInfo.directLibrary);
1239
+ }
1240
+ for (const usedComponent of styleInfo.usedComponents) {
1241
+ const usedLibrary = detectLibraryFromSource(usedComponent.importSource);
1242
+ if (usedLibrary) {
1243
+ internalLibraries.add(usedLibrary);
1244
+ libraryEvidence.push({
1245
+ componentName: usedComponent.name,
1246
+ library: usedLibrary
1247
+ });
1248
+ } else {
1249
+ const resolvedPath = resolveImportPath(usedComponent.importSource, filePath);
1250
+ if (resolvedPath) {
1251
+ const cacheKey = `${resolvedPath}::${usedComponent.name}`;
1252
+ if (!visited.has(cacheKey)) {
1253
+ visited.add(cacheKey);
1254
+ let nestedInfo;
1255
+ if (componentLibraryCache.has(cacheKey)) {
1256
+ nestedInfo = componentLibraryCache.get(cacheKey);
1257
+ } else {
1258
+ const resolvedExport = resolveExport(usedComponent.name, resolvedPath);
1259
+ const actualFilePath = resolvedExport?.filePath ?? resolvedPath;
1260
+ const actualComponentName = resolvedExport?.localName ?? usedComponent.name;
1261
+ nestedInfo = analyzeComponentLibraries(
1262
+ actualFilePath,
1263
+ actualComponentName,
1264
+ visited
1265
+ );
1266
+ componentLibraryCache.set(cacheKey, nestedInfo);
1267
+ }
1268
+ if (nestedInfo.library) {
1269
+ internalLibraries.add(nestedInfo.library);
1270
+ libraryEvidence.push({
1271
+ componentName: usedComponent.name,
1272
+ library: nestedInfo.library
1273
+ });
1274
+ }
1275
+ for (const lib of nestedInfo.internalLibraries) {
1276
+ internalLibraries.add(lib);
1277
+ }
1278
+ for (const evidence of nestedInfo.libraryEvidence) {
1279
+ libraryEvidence.push({
1280
+ componentName: `${usedComponent.name} \u2192 ${evidence.componentName}`,
1281
+ library: evidence.library
1282
+ });
1283
+ }
1284
+ }
1285
+ }
1286
+ }
1287
+ }
1288
+ return {
1289
+ library: styleInfo.directLibrary,
1290
+ internalLibraries,
1291
+ libraryEvidence,
1292
+ isLocalComponent: true
1293
+ };
1294
+ }
1295
+
1296
+ // src/rules/no-mixed-component-libraries.ts
723
1297
  var no_mixed_component_libraries_default = createRule({
724
1298
  name: "no-mixed-component-libraries",
725
1299
  meta: {
726
1300
  type: "problem",
727
1301
  docs: {
728
- description: "Forbid mixing component libraries in the same file"
1302
+ description: "Forbid using non-preferred UI component libraries. Reports at JSX usage sites, including transitive usage."
729
1303
  },
730
1304
  messages: {
731
- mixedLibraries: "Mixing {{lib1}} and {{lib2}} components. Choose one library per file.",
732
- nonPreferredLibrary: "Using {{lib}} components, but {{preferred}} is the preferred library. Use {{preferred}} instead."
1305
+ nonPreferredLibrary: "Component <{{component}}> is from {{library}}, but {{preferred}} is the preferred library.",
1306
+ transitiveNonPreferred: "Component <{{component}}> internally uses {{libraries}} components ({{internalComponents}}). The preferred library is {{preferred}}."
733
1307
  },
734
1308
  schema: [
735
1309
  {
736
1310
  type: "object",
737
1311
  properties: {
738
- libraries: {
739
- type: "array",
740
- items: { type: "string", enum: ["shadcn", "mui"] }
741
- },
742
1312
  preferred: {
743
1313
  type: "string",
744
- enum: ["shadcn", "mui"]
1314
+ enum: ["shadcn", "mui", "chakra", "antd"],
1315
+ description: "The preferred UI library"
1316
+ },
1317
+ libraries: {
1318
+ type: "array",
1319
+ items: {
1320
+ type: "string",
1321
+ enum: ["shadcn", "mui", "chakra", "antd"]
1322
+ },
1323
+ description: "Libraries to detect (defaults to all)"
745
1324
  }
746
1325
  },
1326
+ required: ["preferred"],
747
1327
  additionalProperties: false
748
1328
  }
749
1329
  ]
750
1330
  },
751
- defaultOptions: [{ libraries: ["shadcn", "mui"] }],
1331
+ defaultOptions: [
1332
+ { preferred: "shadcn", libraries: ["shadcn", "mui", "chakra", "antd"] }
1333
+ ],
752
1334
  create(context) {
753
- const options = context.options[0] || {};
754
- const libraries = options.libraries || ["shadcn", "mui"];
1335
+ const options = context.options[0];
755
1336
  const preferred = options.preferred;
756
- const detected = /* @__PURE__ */ new Map();
1337
+ const importMap = /* @__PURE__ */ new Map();
1338
+ const componentUsages = [];
757
1339
  return {
758
1340
  ImportDeclaration(node) {
759
1341
  const source = node.source.value;
760
- for (const lib of libraries) {
761
- const patterns = LIBRARY_PATTERNS[lib];
762
- if (patterns?.some((p) => source.includes(p))) {
763
- if (!detected.has(lib)) {
764
- detected.set(lib, node);
765
- }
1342
+ for (const spec of node.specifiers) {
1343
+ if (spec.type === "ImportSpecifier") {
1344
+ importMap.set(spec.local.name, source);
1345
+ } else if (spec.type === "ImportDefaultSpecifier") {
1346
+ importMap.set(spec.local.name, source);
1347
+ } else if (spec.type === "ImportNamespaceSpecifier") {
1348
+ importMap.set(spec.local.name, source);
766
1349
  }
767
1350
  }
768
1351
  },
769
- "Program:exit"() {
770
- if (detected.size > 1) {
771
- const libs = [...detected.keys()];
772
- const secondLib = libs[1];
773
- const secondNode = detected.get(secondLib);
774
- context.report({
775
- node: secondNode,
776
- messageId: "mixedLibraries",
777
- data: { lib1: libs[0], lib2: secondLib }
778
- });
1352
+ JSXOpeningElement(node) {
1353
+ let componentName = null;
1354
+ if (node.name.type === "JSXIdentifier") {
1355
+ componentName = node.name.name;
1356
+ } else if (node.name.type === "JSXMemberExpression") {
1357
+ let current = node.name.object;
1358
+ while (current.type === "JSXMemberExpression") {
1359
+ current = current.object;
1360
+ }
1361
+ if (current.type === "JSXIdentifier") {
1362
+ componentName = current.name;
1363
+ }
1364
+ }
1365
+ if (!componentName || !/^[A-Z]/.test(componentName)) {
1366
+ return;
1367
+ }
1368
+ const importSource = importMap.get(componentName);
1369
+ if (!importSource) {
779
1370
  return;
780
1371
  }
781
- if (preferred && detected.size === 1) {
782
- const usedLib = [...detected.keys()][0];
783
- if (usedLib && usedLib !== preferred) {
784
- const node = detected.get(usedLib);
1372
+ componentUsages.push({
1373
+ node,
1374
+ componentName,
1375
+ importSource
1376
+ });
1377
+ },
1378
+ "Program:exit"() {
1379
+ const filename = context.filename || context.getFilename();
1380
+ for (const usage of componentUsages) {
1381
+ const libraryInfo = getComponentLibrary(
1382
+ filename,
1383
+ usage.componentName,
1384
+ usage.importSource
1385
+ );
1386
+ if (libraryInfo.library && libraryInfo.library !== preferred) {
785
1387
  context.report({
786
- node,
1388
+ node: usage.node,
787
1389
  messageId: "nonPreferredLibrary",
788
- data: { lib: usedLib, preferred }
1390
+ data: {
1391
+ component: usage.componentName,
1392
+ library: libraryInfo.library,
1393
+ preferred
1394
+ }
789
1395
  });
1396
+ continue;
1397
+ }
1398
+ if (libraryInfo.isLocalComponent && libraryInfo.internalLibraries.size > 0) {
1399
+ const nonPreferredLibs = [...libraryInfo.internalLibraries].filter(
1400
+ (lib) => lib !== preferred
1401
+ );
1402
+ if (nonPreferredLibs.length > 0) {
1403
+ const internalComponents = libraryInfo.libraryEvidence.filter((e) => e.library !== preferred).map((e) => e.componentName).slice(0, 3).join(", ");
1404
+ context.report({
1405
+ node: usage.node,
1406
+ messageId: "transitiveNonPreferred",
1407
+ data: {
1408
+ component: usage.componentName,
1409
+ libraries: nonPreferredLibs.join(", "),
1410
+ internalComponents: internalComponents || "unknown",
1411
+ preferred
1412
+ }
1413
+ });
1414
+ }
790
1415
  }
791
1416
  }
792
1417
  }
@@ -795,13 +1420,13 @@ var no_mixed_component_libraries_default = createRule({
795
1420
  });
796
1421
 
797
1422
  // src/rules/semantic.ts
798
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
1423
+ import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
799
1424
  import { spawnSync } from "child_process";
800
- import { dirname as dirname3, join as join3, relative } from "path";
1425
+ import { dirname as dirname4, join as join4, relative } from "path";
801
1426
 
802
1427
  // src/utils/cache.ts
803
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
804
- import { dirname, join } from "path";
1428
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
1429
+ import { dirname as dirname2, join as join2 } from "path";
805
1430
  var xxhashInstance = null;
806
1431
  async function getXxhash() {
807
1432
  if (!xxhashInstance) {
@@ -831,15 +1456,15 @@ function hashContentSync(content) {
831
1456
  var CACHE_VERSION = 1;
832
1457
  var CACHE_FILE = ".uilint/.cache/eslint-semantic.json";
833
1458
  function getCachePath(projectRoot) {
834
- return join(projectRoot, CACHE_FILE);
1459
+ return join2(projectRoot, CACHE_FILE);
835
1460
  }
836
1461
  function loadCache(projectRoot) {
837
1462
  const cachePath = getCachePath(projectRoot);
838
- if (!existsSync(cachePath)) {
1463
+ if (!existsSync2(cachePath)) {
839
1464
  return { version: CACHE_VERSION, entries: {} };
840
1465
  }
841
1466
  try {
842
- const content = readFileSync(cachePath, "utf-8");
1467
+ const content = readFileSync2(cachePath, "utf-8");
843
1468
  const cache = JSON.parse(content);
844
1469
  if (cache.version !== CACHE_VERSION) {
845
1470
  return { version: CACHE_VERSION, entries: {} };
@@ -852,8 +1477,8 @@ function loadCache(projectRoot) {
852
1477
  function saveCache(projectRoot, cache) {
853
1478
  const cachePath = getCachePath(projectRoot);
854
1479
  try {
855
- const cacheDir = dirname(cachePath);
856
- if (!existsSync(cacheDir)) {
1480
+ const cacheDir = dirname2(cachePath);
1481
+ if (!existsSync2(cacheDir)) {
857
1482
  mkdirSync(cacheDir, { recursive: true });
858
1483
  }
859
1484
  writeFileSync(cachePath, JSON.stringify(cache, null, 2), "utf-8");
@@ -884,8 +1509,8 @@ function clearCache(projectRoot) {
884
1509
  }
885
1510
 
886
1511
  // src/utils/styleguide-loader.ts
887
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
888
- import { dirname as dirname2, isAbsolute, join as join2, resolve } from "path";
1512
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
1513
+ import { dirname as dirname3, isAbsolute, join as join3, resolve } from "path";
889
1514
  var DEFAULT_STYLEGUIDE_PATHS = [
890
1515
  ".uilint/styleguide.md",
891
1516
  ".uilint/styleguide.yaml",
@@ -894,10 +1519,10 @@ var DEFAULT_STYLEGUIDE_PATHS = [
894
1519
  function findWorkspaceRoot(startDir) {
895
1520
  let dir = startDir;
896
1521
  for (let i = 0; i < 20; i++) {
897
- if (existsSync2(join2(dir, "pnpm-workspace.yaml")) || existsSync2(join2(dir, ".git"))) {
1522
+ if (existsSync3(join3(dir, "pnpm-workspace.yaml")) || existsSync3(join3(dir, ".git"))) {
898
1523
  return dir;
899
1524
  }
900
- const parent = dirname2(dir);
1525
+ const parent = dirname3(dir);
901
1526
  if (parent === dir) break;
902
1527
  dir = parent;
903
1528
  }
@@ -906,9 +1531,9 @@ function findWorkspaceRoot(startDir) {
906
1531
  function findNearestPackageRoot(startDir, workspaceRoot) {
907
1532
  let dir = startDir;
908
1533
  for (let i = 0; i < 30; i++) {
909
- if (existsSync2(join2(dir, "package.json"))) return dir;
1534
+ if (existsSync3(join3(dir, "package.json"))) return dir;
910
1535
  if (dir === workspaceRoot) break;
911
- const parent = dirname2(dir);
1536
+ const parent = dirname3(dir);
912
1537
  if (parent === dir) break;
913
1538
  dir = parent;
914
1539
  }
@@ -917,7 +1542,7 @@ function findNearestPackageRoot(startDir, workspaceRoot) {
917
1542
  function findStyleguidePath(startDir, explicitPath) {
918
1543
  if (explicitPath) {
919
1544
  if (isAbsolute(explicitPath)) {
920
- return existsSync2(explicitPath) ? explicitPath : null;
1545
+ return existsSync3(explicitPath) ? explicitPath : null;
921
1546
  }
922
1547
  const workspaceRoot2 = findWorkspaceRoot(startDir);
923
1548
  const packageRoot = findNearestPackageRoot(startDir, workspaceRoot2);
@@ -927,7 +1552,7 @@ function findStyleguidePath(startDir, explicitPath) {
927
1552
  resolve(workspaceRoot2, explicitPath)
928
1553
  ];
929
1554
  for (const p of candidates) {
930
- if (existsSync2(p)) return p;
1555
+ if (existsSync3(p)) return p;
931
1556
  }
932
1557
  return null;
933
1558
  }
@@ -935,13 +1560,13 @@ function findStyleguidePath(startDir, explicitPath) {
935
1560
  let dir = startDir;
936
1561
  while (true) {
937
1562
  for (const relativePath of DEFAULT_STYLEGUIDE_PATHS) {
938
- const fullPath = join2(dir, relativePath);
939
- if (existsSync2(fullPath)) {
1563
+ const fullPath = join3(dir, relativePath);
1564
+ if (existsSync3(fullPath)) {
940
1565
  return fullPath;
941
1566
  }
942
1567
  }
943
1568
  if (dir === workspaceRoot) break;
944
- const parent = dirname2(dir);
1569
+ const parent = dirname3(dir);
945
1570
  if (parent === dir) break;
946
1571
  dir = parent;
947
1572
  }
@@ -951,7 +1576,7 @@ function loadStyleguide(startDir, explicitPath) {
951
1576
  const path = findStyleguidePath(startDir, explicitPath);
952
1577
  if (!path) return null;
953
1578
  try {
954
- return readFileSync2(path, "utf-8");
1579
+ return readFileSync3(path, "utf-8");
955
1580
  } catch {
956
1581
  return null;
957
1582
  }
@@ -960,7 +1585,7 @@ function getStyleguide(startDir, explicitPath) {
960
1585
  const path = findStyleguidePath(startDir, explicitPath);
961
1586
  if (!path) return { path: null, content: null };
962
1587
  try {
963
- const content = readFileSync2(path, "utf-8");
1588
+ const content = readFileSync3(path, "utf-8");
964
1589
  return { path, content };
965
1590
  } catch {
966
1591
  return { path, content: null };
@@ -1003,7 +1628,7 @@ var semantic_default = createRule({
1003
1628
  create(context) {
1004
1629
  const options = context.options[0] || {};
1005
1630
  const filePath = context.filename;
1006
- const fileDir = dirname3(filePath);
1631
+ const fileDir = dirname4(filePath);
1007
1632
  const { path: styleguidePath, content: styleguide } = getStyleguide(
1008
1633
  fileDir,
1009
1634
  options.styleguidePath
@@ -1025,7 +1650,7 @@ var semantic_default = createRule({
1025
1650
  }
1026
1651
  let fileContent;
1027
1652
  try {
1028
- fileContent = readFileSync3(filePath, "utf-8");
1653
+ fileContent = readFileSync4(filePath, "utf-8");
1029
1654
  } catch {
1030
1655
  console.error(`[uilint] Failed to read file ${filePath}`);
1031
1656
  return {
@@ -1040,7 +1665,7 @@ var semantic_default = createRule({
1040
1665
  }
1041
1666
  const fileHash = hashContentSync(fileContent);
1042
1667
  const styleguideHash = hashContentSync(styleguide);
1043
- const projectRoot = findProjectRoot(fileDir);
1668
+ const projectRoot = findProjectRoot2(fileDir);
1044
1669
  const relativeFilePath = relative(projectRoot, filePath);
1045
1670
  const cached = getCacheEntry(
1046
1671
  projectRoot,
@@ -1093,13 +1718,13 @@ var semantic_default = createRule({
1093
1718
  };
1094
1719
  }
1095
1720
  });
1096
- function findProjectRoot(startDir) {
1721
+ function findProjectRoot2(startDir) {
1097
1722
  let dir = startDir;
1098
1723
  for (let i = 0; i < 20; i++) {
1099
- if (existsSync3(join3(dir, "package.json"))) {
1724
+ if (existsSync4(join4(dir, "package.json"))) {
1100
1725
  return dir;
1101
1726
  }
1102
- const parent = dirname3(dir);
1727
+ const parent = dirname4(dir);
1103
1728
  if (parent === dir) break;
1104
1729
  dir = parent;
1105
1730
  }