uilint-eslint 0.2.164 → 0.2.165

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.d.ts CHANGED
@@ -529,7 +529,7 @@ declare const rules: {
529
529
  }], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
530
530
  name: string;
531
531
  };
532
- "prefer-tailwind": _typescript_eslint_utils_ts_eslint.RuleModule<"preferTailwind" | "preferSemanticColors" | "preferSemanticColorsWithSuggestion" | "preferSemanticClassGroups" | "semanticOpacityModifier", [({
532
+ "prefer-tailwind": _typescript_eslint_utils_ts_eslint.RuleModule<"preferTailwind" | "preferSemanticColors" | "preferSemanticColorsWithSuggestion" | "preferSemanticClassGroups" | "semanticOpacityModifier" | "componentVariantLeakage", [({
533
533
  styleRatioThreshold?: number;
534
534
  minElementsForAnalysis?: number;
535
535
  allowedStyleProperties?: string[];
@@ -543,6 +543,11 @@ declare const rules: {
543
543
  disallowSemanticOpacityModifiers?: boolean;
544
544
  allowedOpacityModifierClasses?: string[];
545
545
  allowedVisualUtilityClasses?: string[];
546
+ preferComponentVariants?: boolean;
547
+ componentVariantComponents?: string[];
548
+ componentVariantProps?: string[];
549
+ componentVariantClassThreshold?: number;
550
+ allowedComponentVariantClasses?: string[];
546
551
  } | undefined)?], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
547
552
  name: string;
548
553
  };
@@ -655,7 +660,7 @@ declare const plugin: {
655
660
  }], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
656
661
  name: string;
657
662
  };
658
- "prefer-tailwind": _typescript_eslint_utils_ts_eslint.RuleModule<"preferTailwind" | "preferSemanticColors" | "preferSemanticColorsWithSuggestion" | "preferSemanticClassGroups" | "semanticOpacityModifier", [({
663
+ "prefer-tailwind": _typescript_eslint_utils_ts_eslint.RuleModule<"preferTailwind" | "preferSemanticColors" | "preferSemanticColorsWithSuggestion" | "preferSemanticClassGroups" | "semanticOpacityModifier" | "componentVariantLeakage", [({
659
664
  styleRatioThreshold?: number;
660
665
  minElementsForAnalysis?: number;
661
666
  allowedStyleProperties?: string[];
@@ -669,6 +674,11 @@ declare const plugin: {
669
674
  disallowSemanticOpacityModifiers?: boolean;
670
675
  allowedOpacityModifierClasses?: string[];
671
676
  allowedVisualUtilityClasses?: string[];
677
+ preferComponentVariants?: boolean;
678
+ componentVariantComponents?: string[];
679
+ componentVariantProps?: string[];
680
+ componentVariantClassThreshold?: number;
681
+ allowedComponentVariantClasses?: string[];
672
682
  } | undefined)?], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
673
683
  name: string;
674
684
  };
package/dist/index.js CHANGED
@@ -4988,7 +4988,7 @@ function formatSuggestionsForMessage(suggestions, limit = 2) {
4988
4988
  // src/rules/prefer-tailwind/index.ts
4989
4989
  var meta14 = defineRuleMeta({
4990
4990
  id: "prefer-tailwind",
4991
- version: "1.2.0",
4991
+ version: "1.3.0",
4992
4992
  name: "Prefer Tailwind",
4993
4993
  description: "Encourage Tailwind className over inline style attributes",
4994
4994
  defaultSeverity: "warn",
@@ -5011,7 +5011,12 @@ var meta14 = defineRuleMeta({
5011
5011
  visualUtilityMinGroups: 2,
5012
5012
  disallowSemanticOpacityModifiers: true,
5013
5013
  allowedOpacityModifierClasses: [],
5014
- allowedVisualUtilityClasses: []
5014
+ allowedVisualUtilityClasses: [],
5015
+ preferComponentVariants: true,
5016
+ componentVariantComponents: [],
5017
+ componentVariantProps: ["variant", "size"],
5018
+ componentVariantClassThreshold: 4,
5019
+ allowedComponentVariantClasses: []
5015
5020
  }
5016
5021
  ],
5017
5022
  optionSchema: {
@@ -5106,6 +5111,41 @@ var meta14 = defineRuleMeta({
5106
5111
  type: "text",
5107
5112
  defaultValue: "",
5108
5113
  description: "Comma-separated exact visual utility classes to ignore in cluster detection"
5114
+ },
5115
+ {
5116
+ key: "preferComponentVariants",
5117
+ label: "Prefer component variants",
5118
+ type: "boolean",
5119
+ defaultValue: true,
5120
+ description: "Warn when design-system components with variant or size props also carry bespoke styling classes"
5121
+ },
5122
+ {
5123
+ key: "componentVariantComponents",
5124
+ label: "Component variant components",
5125
+ type: "text",
5126
+ defaultValue: "",
5127
+ description: "Optional comma-separated component names to inspect; empty means any custom JSX component with variant props"
5128
+ },
5129
+ {
5130
+ key: "componentVariantProps",
5131
+ label: "Component variant props",
5132
+ type: "text",
5133
+ defaultValue: "variant,size",
5134
+ description: "Comma-separated prop names that indicate a component already exposes styling variants"
5135
+ },
5136
+ {
5137
+ key: "componentVariantClassThreshold",
5138
+ label: "Component variant class threshold",
5139
+ type: "number",
5140
+ defaultValue: 4,
5141
+ description: "Minimum number of styling override classes before warning on a variant component"
5142
+ },
5143
+ {
5144
+ key: "allowedComponentVariantClasses",
5145
+ label: "Allowed component variant classes",
5146
+ type: "text",
5147
+ defaultValue: "",
5148
+ description: "Comma-separated exact classes to ignore when checking component variant leakage"
5109
5149
  }
5110
5150
  ]
5111
5151
  },
@@ -5221,6 +5261,34 @@ opacity suffixes are reported:
5221
5261
  Prefer a fully semantic token such as \`text-muted-foreground\`, or define a new
5222
5262
  theme token/class when the opacity represents a reusable state.
5223
5263
 
5264
+ ## Component Variant Leakage
5265
+
5266
+ When \`preferComponentVariants\` is enabled, the rule checks custom JSX
5267
+ components that already use style-like variant props such as \`variant\` or
5268
+ \`size\`. It ignores lowercase HTML elements by default. When \`className\` adds
5269
+ several bespoke styling overrides across static strings, template chunks, and
5270
+ common class combiners, the rule warns because the styling probably belongs in a
5271
+ component variant or semantic project class.
5272
+
5273
+ ### \u274C Bespoke overrides on a variant component
5274
+
5275
+ \`\`\`tsx
5276
+ <Button
5277
+ variant="ghost"
5278
+ size="xs"
5279
+ className={cn(
5280
+ "h-auto gap-1 rounded-md px-2 py-0.5 text-[11px] font-medium",
5281
+ running ? "cursor-not-allowed opacity-40" : "bg-primary/10 text-primary hover:bg-primary/20"
5282
+ )}
5283
+ />
5284
+ \`\`\`
5285
+
5286
+ ### \u2705 Semantic component variant
5287
+
5288
+ \`\`\`tsx
5289
+ <Button variant="primarySubtle" size="xs" />
5290
+ \`\`\`
5291
+
5224
5292
  ## LLM-Powered Suggestions
5225
5293
 
5226
5294
  When \`useLlmSuggestions\` is enabled and Ollama is running locally, the rule will:
@@ -5268,6 +5336,10 @@ function getComponentName2(node) {
5268
5336
  }
5269
5337
  return "";
5270
5338
  }
5339
+ function isCustomComponentName(componentName) {
5340
+ const localName = componentName.split(".").at(-1) ?? componentName;
5341
+ return /^[A-Z]/.test(localName);
5342
+ }
5271
5343
  function getStylePropertyNames(value) {
5272
5344
  const expr = value.expression;
5273
5345
  if (expr.type === "ObjectExpression") {
@@ -5399,6 +5471,7 @@ var SHADOW_SIZE_VALUES = /* @__PURE__ */ new Set([
5399
5471
  "inner",
5400
5472
  "none"
5401
5473
  ]);
5474
+ var DEFAULT_COMPONENT_VARIANT_PROPS = ["variant", "size"];
5402
5475
  function stripImportant(value) {
5403
5476
  return value.replace(/^!/, "").replace(/!$/, "");
5404
5477
  }
@@ -5532,6 +5605,112 @@ function findSemanticOpacityModifiers(className, allowedClasses) {
5532
5605
  }
5533
5606
  return matches;
5534
5607
  }
5608
+ function isSpacingOverride(baseClass) {
5609
+ return /^-?(p|px|py|pt|pr|pb|pl|m|mx|my|mt|mr|mb|ml|gap|gap-x|gap-y)-/.test(
5610
+ baseClass
5611
+ ) || baseClass === "gap";
5612
+ }
5613
+ function isSizingOverride(baseClass) {
5614
+ return /^(size|h|min-h|max-h|w|min-w|max-w)-/.test(baseClass);
5615
+ }
5616
+ function isTypographyOverride(baseClass) {
5617
+ return baseClass.startsWith("text-[") || baseClass.startsWith("leading-") || baseClass.startsWith("tracking-") || baseClass.startsWith("font-");
5618
+ }
5619
+ function getComponentOverrideGroup(baseClass) {
5620
+ const visualGroup = getVisualUtilityGroup(baseClass);
5621
+ if (visualGroup) {
5622
+ return visualGroup;
5623
+ }
5624
+ if (isSpacingOverride(baseClass)) {
5625
+ return "spacing";
5626
+ }
5627
+ if (isSizingOverride(baseClass)) {
5628
+ return "sizing";
5629
+ }
5630
+ if (isTypographyOverride(baseClass)) {
5631
+ return "typography";
5632
+ }
5633
+ return null;
5634
+ }
5635
+ function findComponentVariantLeakageClasses(classNames, threshold, allowedClasses) {
5636
+ const matches = [];
5637
+ for (const className of classNames) {
5638
+ for (const token of extractClassTokens(className)) {
5639
+ if (classIsAllowed(token, allowedClasses)) {
5640
+ continue;
5641
+ }
5642
+ const group = getComponentOverrideGroup(token.base);
5643
+ if (group) {
5644
+ matches.push({ token: token.original, group });
5645
+ }
5646
+ }
5647
+ }
5648
+ if (matches.length < threshold) {
5649
+ return [];
5650
+ }
5651
+ const seen = /* @__PURE__ */ new Set();
5652
+ return matches.map((match) => match.token).filter((token) => {
5653
+ if (seen.has(token)) {
5654
+ return false;
5655
+ }
5656
+ seen.add(token);
5657
+ return true;
5658
+ });
5659
+ }
5660
+ function extractStaticClassStringsFromExpression(expression) {
5661
+ if (!expression || expression.type === "JSXEmptyExpression") {
5662
+ return [];
5663
+ }
5664
+ if (expression.type === "SpreadElement") {
5665
+ return [];
5666
+ }
5667
+ switch (expression.type) {
5668
+ case "Literal":
5669
+ return typeof expression.value === "string" ? [expression.value] : [];
5670
+ case "TemplateLiteral":
5671
+ return expression.quasis.map((quasi) => quasi.value.raw);
5672
+ case "ConditionalExpression":
5673
+ return [
5674
+ ...extractStaticClassStringsFromExpression(expression.consequent),
5675
+ ...extractStaticClassStringsFromExpression(expression.alternate)
5676
+ ];
5677
+ case "LogicalExpression":
5678
+ return [
5679
+ ...extractStaticClassStringsFromExpression(expression.left),
5680
+ ...extractStaticClassStringsFromExpression(expression.right)
5681
+ ];
5682
+ case "CallExpression":
5683
+ if (expression.callee.type !== "Identifier" || !CLASS_COMBINER_NAMES.has(expression.callee.name)) {
5684
+ return [];
5685
+ }
5686
+ return expression.arguments.flatMap(
5687
+ (arg) => extractStaticClassStringsFromExpression(arg)
5688
+ );
5689
+ case "ArrayExpression":
5690
+ return expression.elements.flatMap(
5691
+ (element) => extractStaticClassStringsFromExpression(element)
5692
+ );
5693
+ case "ObjectExpression":
5694
+ return expression.properties.flatMap((property) => {
5695
+ if (property.type !== "Property") {
5696
+ return [];
5697
+ }
5698
+ if (property.key.type === "Literal" && typeof property.key.value === "string") {
5699
+ return [property.key.value];
5700
+ }
5701
+ if (property.key.type === "Identifier") {
5702
+ return [property.key.name];
5703
+ }
5704
+ return [];
5705
+ });
5706
+ case "TSAsExpression":
5707
+ case "TSTypeAssertion":
5708
+ case "TSNonNullExpression":
5709
+ return extractStaticClassStringsFromExpression(expression.expression);
5710
+ default:
5711
+ return [];
5712
+ }
5713
+ }
5535
5714
  var prefer_tailwind_default = createRule({
5536
5715
  name: "prefer-tailwind",
5537
5716
  meta: {
@@ -5544,7 +5723,8 @@ var prefer_tailwind_default = createRule({
5544
5723
  preferSemanticColors: "Hard-coded colors: {{colors}}. Use semantic classes instead.",
5545
5724
  preferSemanticColorsWithSuggestion: "Hard-coded colors: {{colors}}. {{suggestion}}",
5546
5725
  preferSemanticClassGroups: "Dense visual utility cluster: {{classes}}. Move repeated panel/card styling into a semantic class.",
5547
- semanticOpacityModifier: "Semantic color opacity modifiers: {{classes}}. Use fully semantic classes or tokens instead."
5726
+ semanticOpacityModifier: "Semantic color opacity modifiers: {{classes}}. Use fully semantic classes or tokens instead.",
5727
+ componentVariantLeakage: "{{component}} already uses {{props}}; move bespoke styling overrides into a component variant or semantic class instead of className: {{classes}}."
5548
5728
  },
5549
5729
  schema: [
5550
5730
  {
@@ -5611,6 +5791,30 @@ var prefer_tailwind_default = createRule({
5611
5791
  type: "array",
5612
5792
  items: { type: "string" },
5613
5793
  description: "Exact visual utility classes to ignore in cluster detection"
5794
+ },
5795
+ preferComponentVariants: {
5796
+ type: "boolean",
5797
+ description: "Warn when design-system components with variant props also use bespoke styling classes"
5798
+ },
5799
+ componentVariantComponents: {
5800
+ type: "array",
5801
+ items: { type: "string" },
5802
+ description: "Optional component names to inspect for component variant leakage; empty means any custom JSX component"
5803
+ },
5804
+ componentVariantProps: {
5805
+ type: "array",
5806
+ items: { type: "string" },
5807
+ description: "Prop names that indicate a component exposes styling variants"
5808
+ },
5809
+ componentVariantClassThreshold: {
5810
+ type: "number",
5811
+ minimum: 1,
5812
+ description: "Minimum styling override class count before warning on a variant component"
5813
+ },
5814
+ allowedComponentVariantClasses: {
5815
+ type: "array",
5816
+ items: { type: "string" },
5817
+ description: "Exact classes to ignore in component variant leakage detection"
5614
5818
  }
5615
5819
  },
5616
5820
  additionalProperties: false
@@ -5631,7 +5835,12 @@ var prefer_tailwind_default = createRule({
5631
5835
  visualUtilityMinGroups: 2,
5632
5836
  disallowSemanticOpacityModifiers: true,
5633
5837
  allowedOpacityModifierClasses: [],
5634
- allowedVisualUtilityClasses: []
5838
+ allowedVisualUtilityClasses: [],
5839
+ preferComponentVariants: true,
5840
+ componentVariantComponents: [],
5841
+ componentVariantProps: DEFAULT_COMPONENT_VARIANT_PROPS,
5842
+ componentVariantClassThreshold: 4,
5843
+ allowedComponentVariantClasses: []
5635
5844
  }
5636
5845
  ],
5637
5846
  create(context) {
@@ -5649,6 +5858,11 @@ var prefer_tailwind_default = createRule({
5649
5858
  const disallowSemanticOpacityModifiers = options.disallowSemanticOpacityModifiers ?? true;
5650
5859
  const allowedOpacityModifierClasses = options.allowedOpacityModifierClasses ?? [];
5651
5860
  const allowedVisualUtilityClasses = options.allowedVisualUtilityClasses ?? [];
5861
+ const preferComponentVariants = options.preferComponentVariants ?? true;
5862
+ const componentVariantComponents = options.componentVariantComponents ?? [];
5863
+ const componentVariantProps = options.componentVariantProps ?? DEFAULT_COMPONENT_VARIANT_PROPS;
5864
+ const componentVariantClassThreshold = options.componentVariantClassThreshold ?? 4;
5865
+ const allowedComponentVariantClasses = options.allowedComponentVariantClasses ?? [];
5652
5866
  let fileContent = null;
5653
5867
  const filePath = context.filename;
5654
5868
  const fileDir = dirname5(filePath);
@@ -5669,6 +5883,69 @@ var prefer_tailwind_default = createRule({
5669
5883
  function isClassNameAttribute(attr) {
5670
5884
  return attr.name.type === "JSXIdentifier" && (attr.name.name === "className" || attr.name.name === "class");
5671
5885
  }
5886
+ function getJSXAttributeName(attr) {
5887
+ return attr.name.type === "JSXIdentifier" ? attr.name.name : null;
5888
+ }
5889
+ function getClassAttributeStaticStrings(attr) {
5890
+ const value = attr.value;
5891
+ if (value?.type === "Literal" && typeof value.value === "string") {
5892
+ return [value.value];
5893
+ }
5894
+ if (value?.type === "JSXExpressionContainer") {
5895
+ return extractStaticClassStringsFromExpression(value.expression);
5896
+ }
5897
+ return [];
5898
+ }
5899
+ function getComponentVariantPropNames(node) {
5900
+ const propNames = /* @__PURE__ */ new Set();
5901
+ for (const attr of node.attributes) {
5902
+ if (attr.type !== "JSXAttribute") {
5903
+ continue;
5904
+ }
5905
+ const attrName = getJSXAttributeName(attr);
5906
+ if (attrName && componentVariantProps.includes(attrName)) {
5907
+ propNames.add(attrName);
5908
+ }
5909
+ }
5910
+ return [...propNames];
5911
+ }
5912
+ function shouldInspectComponentVariantLeakage(componentName) {
5913
+ if (!preferComponentVariants || !isCustomComponentName(componentName)) {
5914
+ return false;
5915
+ }
5916
+ if (componentVariantComponents.length === 0) {
5917
+ return true;
5918
+ }
5919
+ return componentVariantComponents.includes(componentName);
5920
+ }
5921
+ function checkComponentVariantLeakage(node, classNameAttr) {
5922
+ const componentName = getComponentName2(node);
5923
+ if (!shouldInspectComponentVariantLeakage(componentName)) {
5924
+ return;
5925
+ }
5926
+ const variantPropNames = getComponentVariantPropNames(node);
5927
+ if (variantPropNames.length === 0 || !classNameAttr) {
5928
+ return;
5929
+ }
5930
+ const classStrings = getClassAttributeStaticStrings(classNameAttr);
5931
+ const leakageClasses = findComponentVariantLeakageClasses(
5932
+ classStrings,
5933
+ componentVariantClassThreshold,
5934
+ allowedComponentVariantClasses
5935
+ );
5936
+ if (leakageClasses.length === 0) {
5937
+ return;
5938
+ }
5939
+ context.report({
5940
+ node: classNameAttr,
5941
+ messageId: "componentVariantLeakage",
5942
+ data: {
5943
+ component: componentName,
5944
+ props: variantPropNames.join("/"),
5945
+ classes: leakageClasses.slice(0, 12).join(", ")
5946
+ }
5947
+ });
5948
+ }
5672
5949
  function checkClassString(node, className) {
5673
5950
  if (preferSemanticColors) {
5674
5951
  const hardCodedColors = findHardCodedColors(
@@ -5764,6 +6041,7 @@ var prefer_tailwind_default = createRule({
5764
6041
  let hasStyle = false;
5765
6042
  let hasClassName = false;
5766
6043
  let styleProperties = [];
6044
+ let classNameAttr = null;
5767
6045
  for (const attr of node.attributes) {
5768
6046
  if (attr.type === "JSXAttribute") {
5769
6047
  if (isStyleAttribute(attr)) {
@@ -5774,10 +6052,12 @@ var prefer_tailwind_default = createRule({
5774
6052
  }
5775
6053
  if (isClassNameAttribute(attr)) {
5776
6054
  hasClassName = true;
6055
+ classNameAttr = attr;
5777
6056
  processClassAttribute(attr);
5778
6057
  }
5779
6058
  }
5780
6059
  }
6060
+ checkComponentVariantLeakage(node, classNameAttr);
5781
6061
  if (hasStyle || hasClassName) {
5782
6062
  styledElements.push({
5783
6063
  node,
@@ -5817,23 +6097,8 @@ var prefer_tailwind_default = createRule({
5817
6097
  if (!CLASS_COMBINER_NAMES.has(node.callee.name)) {
5818
6098
  return;
5819
6099
  }
5820
- for (const arg of node.arguments) {
5821
- if (arg.type === "Literal" && typeof arg.value === "string") {
5822
- checkClassString(arg, arg.value);
5823
- }
5824
- if (arg.type === "TemplateLiteral") {
5825
- processTemplateLiteral(arg);
5826
- }
5827
- if (arg.type === "ArrayExpression") {
5828
- for (const element of arg.elements) {
5829
- if (element?.type === "Literal" && typeof element.value === "string") {
5830
- checkClassString(element, element.value);
5831
- }
5832
- if (element?.type === "TemplateLiteral") {
5833
- processTemplateLiteral(element);
5834
- }
5835
- }
5836
- }
6100
+ for (const className of extractStaticClassStringsFromExpression(node)) {
6101
+ checkClassString(node, className);
5837
6102
  }
5838
6103
  }
5839
6104
  };
@@ -6265,7 +6530,15 @@ var recommendedConfig = {
6265
6530
  "visualUtilityMinGroups": 2,
6266
6531
  "disallowSemanticOpacityModifiers": true,
6267
6532
  "allowedOpacityModifierClasses": [],
6268
- "allowedVisualUtilityClasses": []
6533
+ "allowedVisualUtilityClasses": [],
6534
+ "preferComponentVariants": true,
6535
+ "componentVariantComponents": [],
6536
+ "componentVariantProps": [
6537
+ "variant",
6538
+ "size"
6539
+ ],
6540
+ "componentVariantClassThreshold": 4,
6541
+ "allowedComponentVariantClasses": []
6269
6542
  }
6270
6543
  ]]
6271
6544
  }
@@ -6405,7 +6678,15 @@ var strictConfig = {
6405
6678
  "visualUtilityMinGroups": 2,
6406
6679
  "disallowSemanticOpacityModifiers": true,
6407
6680
  "allowedOpacityModifierClasses": [],
6408
- "allowedVisualUtilityClasses": []
6681
+ "allowedVisualUtilityClasses": [],
6682
+ "preferComponentVariants": true,
6683
+ "componentVariantComponents": [],
6684
+ "componentVariantProps": [
6685
+ "variant",
6686
+ "size"
6687
+ ],
6688
+ "componentVariantClassThreshold": 4,
6689
+ "allowedComponentVariantClasses": []
6409
6690
  }
6410
6691
  ]]
6411
6692
  }