slot-variants 1.3.0 → 1.4.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 CHANGED
@@ -668,7 +668,7 @@ The `postProcess` function is applied to each slot's final class string independ
668
668
 
669
669
  ### Caching
670
670
 
671
- Results are cached automatically for performance. The default cache size is **256** entries. Calls with `class` or `className` props bypass the cache.
671
+ Results are cached automatically for performance. The default cache size is **256** entries.
672
672
 
673
673
  ```typescript
674
674
  const button = sv('btn', {
@@ -896,20 +896,30 @@ Class values inside the config (`base`, `variants`, `slots`, and `compound*` `cl
896
896
 
897
897
  ### Rules
898
898
 
899
- - **`slot-variants/no-duplicate-classes`** — flags class name tokens that will appear more than once in the output of an `sv()` or `cn()` call. For `sv()`, detects duplicates within `base`, across different variant keys, inside compound variants and compound slots, between `base` and a variant value, and within a single literal. For `cn()` (and the cn-style calling convention of `sv()` without a config), flags any token that appears in more than one always-present source — across args, inside arrays, template literals, or within a single literal.
899
+ - **`slot-variants/no-conflicting-classes`** — flags class tokens that collide within the output of an `sv()` or `cn()` call: both exact-duplicate tokens that will appear more than once and distinct tokens that target the same Tailwind-style utility namespace (e.g. `w-100` and `w-200`). For `sv()`, detects collisions within `base`, across different variant keys, inside compound variants and compound slots, between `base` and a variant value, and within a single literal. For `cn()` (and the cn-style calling convention of `sv()` without a config), flags collisions across args, inside arrays, template literals, or within a single literal. Tokens with different variant prefixes (`w-100` vs `hover:w-200`) don't conflict, the trailing `!` important marker is ignored when computing the namespace, and tokens that only co-occur across mutually-exclusive variant values are skipped.
900
900
 
901
901
  - **`slot-variants/no-dynamic-classes`** — flags class-bearing positions in `sv()` and `cn()` calls that aren't statically inferrable. Only string literals, template literals without expressions, flat arrays of those in config, and explicit `undefined` config class values are accepted, and config objects must use static keys (no spreads, no computed keys). Identifiers, member access, calls, spreads, non-string literals, templates with expressions, nested config arrays, and runtime conditional records are reported. Non-class-bearing config keys (`defaultVariants`, `presets`, `requiredVariants`, `cacheSize`, `postProcess`, `introspection`) are not validated, and runtime variant matchers inside compound entries are left alone — only the `class`/`className` value (and the `slots` array of `compoundSlots`) is checked.
902
902
 
903
903
  - **`slot-variants/no-empty-classes`** — flags empty class values — empty strings, empty arrays, and empty objects — at any class-bearing position reachable from an `sv()` or `cn()` call, plus zero-argument `sv()` / `cn()` calls themselves (which always produce an empty class string). Reports empties in positional arguments (and inside arrays nested in those), in `base`, in variants including slot-keyed variant branches, in the `class`/`className` of `compoundVariants` and `compoundSlots` entries, and in the top-level `slots`, `variants`, `compoundVariants`, and `compoundSlots` containers themselves. Inside an `sv()` config, an empty string is still allowed as a direct `slots[key]` value — declaring a slot with no default classes is a valid use case.
904
904
 
905
- - **`slot-variants/no-redundant-spaces`** — flags class strings whose whitespace isn't canonical. Inside a class string, whitespace is canonical only as a single ASCII space between two non-whitespace tokens, so leading or trailing whitespace, repeated spaces, and non-space whitespace (tabs, newlines, etc.) are reported. The rule walks every string and expressionless template literal reachable from a call's arguments — including values nested inside arrays and objects — and bails silently on dynamic expressions.
905
+ - **`slot-variants/no-redundant-spaces`** — flags class strings whose whitespace isn't canonical. Inside a class string, whitespace is canonical only as a single ASCII space between two non-whitespace tokens, so leading or trailing whitespace, repeated spaces, and non-space whitespace (tabs, newlines, etc.) are reported. The rule walks every string and expressionless template literal reachable from a call's arguments — including values nested inside arrays and objects — and bails silently on dynamic expressions. **Auto-fixable**: `eslint --fix` rewrites each offending literal in place, preserving its original quote style.
906
906
 
907
907
  - **`slot-variants/no-shared-tokens`** — flags class tokens that appear in every value of an exhaustively-covered variant, where “exhaustive” means the variant has a statically defined default value or is listed in `requiredVariants`. Those tokens are constant in the rendered output, so they belong in `base` or the corresponding `slots[slot]` entry rather than being repeated in every variant value. The rule only analyzes `sv()` calls with a config, compares tokens per-slot, skips non-exhaustive variants, single-value variants, boolean shorthand, undefined or dynamic defaults, and dynamic or partially-analyzable variant value records, and reports every repeated occurrence that should be lifted out.
908
908
 
909
- Only calls where `sv` or `cn` is a named import from `'slot-variants'` are analyzed. `no-duplicate-classes` skips dynamic inputs silently to avoid false positives; `no-dynamic-classes` is the opposite — it flags exactly those positions so the static analyzer can fully reason about every call. `no-shared-tokens` sits between them: it needs a fully statically analyzable, exhaustive variant before it can prove a token is constant across every value. `no-empty-classes` and `no-redundant-spaces` are independent and complement the structural rules: they cover empty and badly-shaped literals reachable from a call's arguments, regardless of whether the surrounding call is fully static.
909
+ Only calls where `sv` or `cn` is a named import from `'slot-variants'` are analyzed. `no-conflicting-classes` skips dynamic inputs silently to avoid false positives; `no-dynamic-classes` is the opposite — it flags exactly those positions so the static analyzer can fully reason about every call. `no-shared-tokens` sits between them: it needs a fully statically analyzable, exhaustive variant before it can prove a token is constant across every value. `no-empty-classes` and `no-redundant-spaces` are independent and complement the structural rules: they cover empty and badly-shaped literals reachable from a call's arguments, regardless of whether the surrounding call is fully static.
910
910
 
911
911
  ### ESLint (flat config)
912
912
 
913
+ Use the `recommended` preset to enable every rule at `error` in one line:
914
+
915
+ ```js
916
+ import svPlugin from 'slot-variants/eslint-plugin';
917
+
918
+ export default [svPlugin.configs.recommended];
919
+ ```
920
+
921
+ Or wire each rule by hand if you want per-rule control:
922
+
913
923
  ```js
914
924
  import svPlugin from 'slot-variants/eslint-plugin';
915
925
 
@@ -917,7 +927,7 @@ export default [
917
927
  {
918
928
  plugins: { 'slot-variants': svPlugin },
919
929
  rules: {
920
- 'slot-variants/no-duplicate-classes': 'error',
930
+ 'slot-variants/no-conflicting-classes': 'error',
921
931
  'slot-variants/no-dynamic-classes': 'error',
922
932
  'slot-variants/no-empty-classes': 'error',
923
933
  'slot-variants/no-redundant-spaces': 'error',
@@ -933,7 +943,7 @@ export default [
933
943
  {
934
944
  "jsPlugins": ["slot-variants/eslint-plugin"],
935
945
  "rules": {
936
- "slot-variants/no-duplicate-classes": "error",
946
+ "slot-variants/no-conflicting-classes": "error",
937
947
  "slot-variants/no-dynamic-classes": "error",
938
948
  "slot-variants/no-empty-classes": "error",
939
949
  "slot-variants/no-redundant-spaces": "error",
@@ -960,7 +970,7 @@ const button = sv({
960
970
  cn('flex items-center', 'flex'); // 'flex' duplicated across args
961
971
  ```
962
972
 
963
- `no-duplicate-classes` reports `flex` on the `base` literal and on both variant values; for the `cn()` call, both occurrences of `'flex'` are flagged. Move the shared class into `base` — or use compound variants — so each class has a single source.
973
+ `no-conflicting-classes` reports `flex` on the `base` literal and on both variant values; for the `cn()` call, both occurrences of `'flex'` are flagged. Move the shared class into `base` — or use compound variants — so each class has a single source.
964
974
 
965
975
  ```typescript
966
976
  import { sv, cn } from 'slot-variants';
@@ -1000,7 +1010,7 @@ sv({ slots: { body: 'p-4 ' } }); // trailing space
1000
1010
  cn(`flex\titems-center`); // tab between tokens
1001
1011
  ```
1002
1012
 
1003
- `no-redundant-spaces` reports each literal whose whitespace deviates from the canonical "tokens separated by exactly one space" form. Trim and collapse the strings — or split them into array entries — so the stored class output is byte-stable and easy to scan in diffs.
1013
+ `no-redundant-spaces` reports each literal whose whitespace deviates from the canonical "tokens separated by exactly one space" form. Trim and collapse the strings — or split them into array entries — so the stored class output is byte-stable and easy to scan in diffs. Run `eslint --fix` to apply the canonical form automatically; the fixer rewrites the literal in place using the original quote style.
1004
1014
 
1005
1015
  ```typescript
1006
1016
  import { sv } from 'slot-variants';
@@ -1070,7 +1080,7 @@ module.exports = {
1070
1080
  size: { sm: 'text-sm', lg: 'text-lg' },
1071
1081
  intent: { primary: 'bg-blue-500', danger: 'bg-red-500' }
1072
1082
  },
1073
- defaultVariants: { size: 'md' },
1083
+ defaultVariants: { size: 'sm' },
1074
1084
  compoundVariants: [
1075
1085
  { size: 'lg', intent: 'primary', class: 'uppercase' }
1076
1086
  ]
@@ -1093,7 +1103,7 @@ Everything else works identically — the config shape, `class`/`className` over
1093
1103
  variants: {
1094
1104
  size: { sm: 'text-sm', lg: 'text-lg' }
1095
1105
  },
1096
- defaultVariants: { size: 'md' }
1106
+ defaultVariants: { size: 'sm' }
1097
1107
  });
1098
1108
  ```
1099
1109
 
@@ -215,8 +215,6 @@ const button = sv('btn', {
215
215
 
216
216
  Cache inspection methods (`getCacheSize`, `clearCache`) are only exposed when `introspection: true` is set — see rule 11.
217
217
 
218
- Note: Using `class` or `className` prop bypasses caching.
219
-
220
218
  ### 10. Use Presets for Reusable Variant Combinations
221
219
 
222
220
  Define presets for frequently used combinations of variants (rarely needed):
@@ -449,7 +447,7 @@ import svPlugin from 'slot-variants/eslint-plugin';
449
447
  export default [{
450
448
  plugins: { 'slot-variants': svPlugin },
451
449
  rules: {
452
- 'slot-variants/no-duplicate-classes': 'error',
450
+ 'slot-variants/no-conflicting-classes': 'error',
453
451
  'slot-variants/no-dynamic-classes': 'error',
454
452
  'slot-variants/no-empty-classes': 'error',
455
453
  'slot-variants/no-redundant-spaces': 'error',
@@ -463,7 +461,7 @@ export default [{
463
461
  {
464
462
  "jsPlugins": ["slot-variants/eslint-plugin"],
465
463
  "rules": {
466
- "slot-variants/no-duplicate-classes": "error",
464
+ "slot-variants/no-conflicting-classes": "error",
467
465
  "slot-variants/no-dynamic-classes": "error",
468
466
  "slot-variants/no-empty-classes": "error",
469
467
  "slot-variants/no-redundant-spaces": "error",
@@ -474,18 +472,19 @@ export default [{
474
472
 
475
473
  All rules only analyze calls where `sv` or `cn` is a **named import** from `'slot-variants'`. Default, namespace, and aliased-to-other-identifier imports are ignored.
476
474
 
477
- ### `slot-variants/no-duplicate-classes`
475
+ ### `slot-variants/no-conflicting-classes`
478
476
 
479
- Reports class tokens that will appear more than once in the output of an `sv()` or `cn()` call.
477
+ Reports class tokens that collide within the output of an `sv()` or `cn()` call: exact-duplicate tokens that will appear more than once, and distinct tokens that target the same Tailwind-style utility namespace (e.g. `w-100` and `w-200`).
480
478
 
481
- - For `sv()` with a config, flags a token as duplicated when two of its sources can both be active at runtime: base + any variant, base + compound, two variants with different keys, two compound entries, or the same literal token repeated inside a single source.
482
- - Does **not** flag the same token appearing in different values of the **same** variant key (those are mutually exclusive).
483
- - For `cn()` (and `sv()` called without a config — the cn-style calling convention), flags any token that appears in more than one always-present source: across args, inside arrays, template literals without expressions, or within a single literal.
479
+ - For `sv()` with a config, flags a collision when two of its sources can both be active at runtime: base + any variant, base + compound, two variants with different keys, two compound entries, or the same literal token repeated inside a single source.
480
+ - Does **not** flag tokens that only co-occur across different values of the **same** variant key (those are mutually exclusive at runtime).
481
+ - For `cn()` (and `sv()` called without a config — the cn-style calling convention), flags collisions across args, inside arrays, template literals without expressions, or within a single literal.
482
+ - Tokens with different variant prefixes (`w-100` vs `hover:w-200`) don't conflict; the trailing `!` important marker is ignored when computing the namespace; single-word utilities (no `-`) participate only in exact-duplicate detection.
484
483
  - Bails out silently on dynamic inputs (identifiers, spreads, computed keys, template literals with expressions, cn-style `{ cls: condition }` records) — no false positives for code it can't statically resolve.
485
484
 
486
485
  ### `slot-variants/no-dynamic-classes`
487
486
 
488
- Reports class-bearing positions in `sv()` and `cn()` calls that aren't statically inferrable. Pair this rule with `no-duplicate-classes` to guarantee every call is fully analyzable.
487
+ Reports class-bearing positions in `sv()` and `cn()` calls that aren't statically inferrable. Pair this rule with `no-conflicting-classes` to guarantee every call is fully analyzable.
489
488
 
490
489
  - Accepts only string literals, template literals without expressions, and arrays of those as class values. Identifiers, member access, calls, spreads, non-string literals, templates with expressions, and object records are reported.
491
490
  - For `sv()` config, validates `base`, `slots` values, `variants` values (both record form and boolean shorthand), and the `class` / `className` of `compoundVariants` / `compoundSlots` entries. The `slots` array of `compoundSlots` must be a static array of string literals.
@@ -525,6 +524,5 @@ Reports class tokens that appear in every value of an exhaustively-covered varia
525
524
  ## Performance Notes
526
525
 
527
526
  1. **Caching is automatic** - Results are cached by default
528
- 2. **Class prop bypasses cache** - Dynamic classes at runtime should use `class` prop
529
- 3. **Complex components benefit from larger cache** - Increase `cacheSize` for components with many variant combinations
530
- 4. **Prefer static defaults** - Function-based defaults are called on every invocation
527
+ 2. **Complex components benefit from larger cache** - Increase `cacheSize` for components with many variant combinations
528
+ 3. **Prefer static defaults** - Function-based defaults are called on every invocation
@@ -105,14 +105,15 @@ var getStrictProperties = (obj) => {
105
105
  if (!obj || obj.type !== "ObjectExpression") {
106
106
  return null;
107
107
  }
108
- if (strictPropertiesCache.has(obj)) {
109
- return strictPropertiesCache.get(obj) ?? null;
108
+ const cached = strictPropertiesCache.get(obj);
109
+ if (cached !== void 0) {
110
+ return cached;
110
111
  }
111
112
  const map = buildStrictPropertiesMap(obj);
112
113
  strictPropertiesCache.set(obj, map);
113
114
  return map;
114
115
  };
115
- var isSlotKeyedPropertyKey = (key, slotNames) => key === "base" || key !== null && slotNames.has(key);
116
+ var isSlotKeyedPropertyKey = (key, slotNames) => key !== null && (key === "base" || slotNames.has(key));
116
117
  var buildSlotKeyedMap = (obj, slotNames) => {
117
118
  const result = /* @__PURE__ */ new Map();
118
119
  for (const prop of obj.properties) {
@@ -133,7 +134,12 @@ var collectSlotKeyedProperties = (node, slotNames) => {
133
134
  }
134
135
  return buildSlotKeyedMap(node, slotNames);
135
136
  };
136
- var getIdentifierCalleeName = (node) => node.callee.type === "Identifier" ? node.callee.name : null;
137
+ var getIdentifierCalleeName = (node) => {
138
+ if (node.callee.type === "Identifier") {
139
+ return node.callee.name;
140
+ }
141
+ return null;
142
+ };
137
143
  var matchSvCall = (node) => {
138
144
  const args = node.arguments;
139
145
  const last = args[args.length - 1];
@@ -172,7 +178,12 @@ var isConfigLike = (node) => {
172
178
  };
173
179
  var baseSource = { kind: "base" };
174
180
  var compoundSource = { kind: "compound" };
175
- var getVariantSource = (entry) => entry.source.kind === "variant" ? entry.source : null;
181
+ var getVariantSource = (entry) => {
182
+ if (entry.source.kind === "variant") {
183
+ return entry.source;
184
+ }
185
+ return null;
186
+ };
176
187
  var sharesVariantKey = (sharedKey, source) => sharedKey === null || sharedKey === source.key;
177
188
  var isExclusiveVariantSource = (source, sharedKey, seenValues) => source !== null && sharesVariantKey(sharedKey, source) && !seenValues.has(source.value);
178
189
  var isMutuallyExclusiveVariants = (list) => {
@@ -365,45 +376,6 @@ var collectConfigEntries = (config, slotNames, baseArgs, sourceCode) => {
365
376
  );
366
377
  return entries;
367
378
  };
368
- var reportDuplicatesBySlot = (context, bySlot) => {
369
- for (const [slotKey, tokenMap] of bySlot.entries()) {
370
- reportDuplicateTokens(context, tokenMap, "duplicate", {
371
- slot: slotKey
372
- });
373
- }
374
- };
375
- var analyzeConfig = (context, configNode, baseArgs) => {
376
- const config = getProperties(configNode);
377
- const slotNames = getConfigSlotNames(config);
378
- reportDuplicatesBySlot(
379
- context,
380
- indexEntriesBySlotAndToken(
381
- collectConfigEntries(
382
- config,
383
- slotNames,
384
- baseArgs,
385
- context.sourceCode
386
- )
387
- )
388
- );
389
- };
390
- var analyzeCnCall = (context, args) => {
391
- const entries = [];
392
- for (const arg of args) {
393
- extractTokens(
394
- arg,
395
- "base",
396
- baseSource,
397
- EMPTY_SLOT_NAMES,
398
- entries,
399
- context.sourceCode
400
- );
401
- }
402
- const tokenMap = indexEntriesBySlotAndToken(entries).get("base");
403
- if (tokenMap) {
404
- reportDuplicateTokens(context, tokenMap, "duplicateCn", {});
405
- }
406
- };
407
379
  var reportDynamic = (context, node) => {
408
380
  context.report({ node, messageId: "dynamic" });
409
381
  };
@@ -540,7 +512,10 @@ var getCompoundEntryValueChecker = (key, hasSlotsKey) => {
540
512
  return checkConfigClassValueIsStatic;
541
513
  }
542
514
  if (key === "slots") {
543
- return hasSlotsKey ? checkCompoundSlotsArray : null;
515
+ if (hasSlotsKey) {
516
+ return checkCompoundSlotsArray;
517
+ }
518
+ return null;
544
519
  }
545
520
  return null;
546
521
  };
@@ -652,15 +627,38 @@ var noDynamicClasses = {
652
627
  });
653
628
  }
654
629
  };
655
- var hasRedundantSpaces = (value) => !/^(?:[^\s]+(?: [^\s]+)*)?$/.test(value);
630
+ var hasRedundantSpaces = (value) => !/^(?:\S+(?: \S+)*)?$/.test(value);
631
+ var canonicalizeWhitespace = (value) => value.split(/\s+/).filter(Boolean).join(" ");
632
+ var canHoistAsLiteral = (canonical, quote) => {
633
+ if (canonical.includes("\\") || canonical.includes(quote)) {
634
+ return false;
635
+ }
636
+ return quote !== "`" || !canonical.includes("${");
637
+ };
656
638
  var reportRedundantSpaces = (context, node, value) => {
657
- if (hasRedundantSpaces(value)) {
658
- context.report({ node, messageId: "redundant" });
639
+ if (!hasRedundantSpaces(value)) {
640
+ return;
659
641
  }
642
+ const raw = context.sourceCode.getText(node);
643
+ const quote = raw[0] ?? "";
644
+ const canonical = canonicalizeWhitespace(value);
645
+ context.report({
646
+ node,
647
+ messageId: "redundant",
648
+ fix: (fixer) => {
649
+ if (!canHoistAsLiteral(canonical, quote)) {
650
+ return null;
651
+ }
652
+ return fixer.replaceText(node, `${quote}${canonical}${quote}`);
653
+ }
654
+ });
660
655
  };
661
656
  var getStaticStringText = (node) => {
662
657
  if (node.type === "Literal") {
663
- return typeof node.value === "string" ? node.value : null;
658
+ if (typeof node.value === "string") {
659
+ return node.value;
660
+ }
661
+ return null;
664
662
  }
665
663
  if (node.type !== "TemplateLiteral" || node.expressions.length > 0) {
666
664
  return null;
@@ -721,6 +719,7 @@ var noRedundantSpaces = {
721
719
  docs: {
722
720
  description: "Disallow redundant whitespace inside class strings passed to sv() and cn() calls"
723
721
  },
722
+ fixable: "code",
724
723
  schema: [],
725
724
  messages: {
726
725
  redundant: "Redundant whitespace in class string."
@@ -741,24 +740,102 @@ var noRedundantSpaces = {
741
740
  });
742
741
  }
743
742
  };
744
- var noDuplicateClasses = {
743
+ var getConflictKey = (token) => {
744
+ let stripped = token;
745
+ if (token.endsWith("!")) {
746
+ stripped = token.slice(0, -1);
747
+ }
748
+ const lastColon = stripped.lastIndexOf(":");
749
+ let variantPrefix = "";
750
+ let utility = stripped;
751
+ if (lastColon !== -1) {
752
+ variantPrefix = stripped.slice(0, lastColon);
753
+ utility = stripped.slice(lastColon + 1);
754
+ }
755
+ let utilStart = 0;
756
+ if (utility.startsWith("-")) {
757
+ utilStart = 1;
758
+ }
759
+ const firstDash = utility.indexOf("-", utilStart + 1);
760
+ if (firstDash === -1) {
761
+ return null;
762
+ }
763
+ return `${variantPrefix}|${utility.slice(utilStart, firstDash)}`;
764
+ };
765
+ var groupEntriesByConflictKey = (tokenMap) => {
766
+ const groups = /* @__PURE__ */ new Map();
767
+ for (const [token, list] of tokenMap) {
768
+ const key = getConflictKey(token);
769
+ if (key === null) {
770
+ continue;
771
+ }
772
+ const group = getOrCreate(groups, key, () => ({
773
+ tokens: /* @__PURE__ */ new Set(),
774
+ entries: []
775
+ }));
776
+ group.tokens.add(token);
777
+ group.entries.push(...list);
778
+ }
779
+ return groups;
780
+ };
781
+ var reportConflicts = (context, tokenMap, messageId, data) => {
782
+ for (const group of groupEntriesByConflictKey(tokenMap).values()) {
783
+ if (group.tokens.size < 2 || isMutuallyExclusiveVariants(group.entries)) {
784
+ continue;
785
+ }
786
+ const tokens = [...group.tokens].sort().join(", ");
787
+ reportEntryList(context, group.entries, messageId, { tokens, ...data });
788
+ }
789
+ };
790
+ var analyzeConfigForRule = (context, configNode, baseArgs) => {
791
+ const config = getProperties(configNode);
792
+ const slotNames = getConfigSlotNames(config);
793
+ const bySlot = indexEntriesBySlotAndToken(
794
+ collectConfigEntries(config, slotNames, baseArgs, context.sourceCode)
795
+ );
796
+ for (const [slot, tokenMap] of bySlot) {
797
+ reportDuplicateTokens(context, tokenMap, "duplicate", { slot });
798
+ reportConflicts(context, tokenMap, "conflict", { slot });
799
+ }
800
+ };
801
+ var analyzeCnForRule = (context, args) => {
802
+ const entries = [];
803
+ for (const arg of args) {
804
+ extractTokens(
805
+ arg,
806
+ "base",
807
+ baseSource,
808
+ EMPTY_SLOT_NAMES,
809
+ entries,
810
+ context.sourceCode
811
+ );
812
+ }
813
+ const tokenMap = indexEntriesBySlotAndToken(entries).get("base");
814
+ if (tokenMap) {
815
+ reportDuplicateTokens(context, tokenMap, "duplicateCn", {});
816
+ reportConflicts(context, tokenMap, "conflictCn", {});
817
+ }
818
+ };
819
+ var noConflictingClasses = {
745
820
  meta: {
746
821
  type: "problem",
747
822
  docs: {
748
- description: "Disallow duplicate class names in sv() and cn() outputs across slots, variants, and compounds"
823
+ description: "Disallow duplicate class tokens and tokens targeting the same utility namespace within an sv() or cn() output"
749
824
  },
750
825
  schema: [],
751
826
  messages: {
752
827
  duplicate: 'Class "{{token}}" will appear more than once in the "{{slot}}" slot output.',
753
- duplicateCn: 'Class "{{token}}" will appear more than once in the call output.'
828
+ duplicateCn: 'Class "{{token}}" will appear more than once in the call output.',
829
+ conflict: 'Conflicting classes "{{tokens}}" target the same utility namespace in the "{{slot}}" slot output.',
830
+ conflictCn: 'Conflicting classes "{{tokens}}" target the same utility namespace in the call output.'
754
831
  }
755
832
  },
756
833
  create(context) {
757
834
  return createTrackedCallListeners((_node, call) => {
758
835
  if (call.config) {
759
- analyzeConfig(context, call.config, call.args);
836
+ analyzeConfigForRule(context, call.config, call.args);
760
837
  } else {
761
- analyzeCnCall(context, call.args);
838
+ analyzeCnForRule(context, call.args);
762
839
  }
763
840
  });
764
841
  }
@@ -945,23 +1022,48 @@ var noSharedTokens = {
945
1022
  var isEmptyStringNode = (node) => getStaticStringText(node) === "";
946
1023
  var shouldReportEmptyString = (node, allowEmptyString) => !allowEmptyString && isEmptyStringNode(node);
947
1024
  var isEmptyObjectExpression = (node) => node.type === "ObjectExpression" && node.properties.length === 0;
948
- var visitForEmptyClasses = (context, node, allowEmptyString) => {
1025
+ var removeFromList = (fixer, sourceCode, node, list) => {
1026
+ let nonNullCount = 0;
1027
+ for (const item of list) {
1028
+ if (item) {
1029
+ nonNullCount += 1;
1030
+ }
1031
+ }
1032
+ if (nonNullCount <= 1) {
1033
+ return null;
1034
+ }
1035
+ const [start, end] = sourceCode.getRange(node);
1036
+ const after = sourceCode.getTokenAfter(node);
1037
+ if (after && after.value === ",") {
1038
+ return fixer.removeRange([start, after.range[1]]);
1039
+ }
1040
+ const before = sourceCode.getTokenBefore(node);
1041
+ if (!before || before.value !== ",") {
1042
+ return null;
1043
+ }
1044
+ return fixer.removeRange([before.range[0], end]);
1045
+ };
1046
+ var visitForEmptyClasses = (context, node, allowEmptyString, list) => {
1047
+ let fix;
1048
+ if (list) {
1049
+ fix = (fixer) => removeFromList(fixer, context.sourceCode, node, list);
1050
+ }
949
1051
  if (shouldReportEmptyString(node, allowEmptyString)) {
950
- context.report({ node, messageId: "emptyString" });
1052
+ context.report({ node, messageId: "emptyString", fix });
951
1053
  return;
952
1054
  }
953
1055
  if (node.type === "ArrayExpression") {
954
1056
  if (node.elements.length === 0) {
955
- context.report({ node, messageId: "emptyArray" });
1057
+ context.report({ node, messageId: "emptyArray", fix });
956
1058
  return;
957
1059
  }
958
1060
  forEachStaticItem(node.elements, (element) => {
959
- visitForEmptyClasses(context, element, false);
1061
+ visitForEmptyClasses(context, element, false, node.elements);
960
1062
  });
961
1063
  return;
962
1064
  }
963
1065
  if (isEmptyObjectExpression(node)) {
964
- context.report({ node, messageId: "emptyObject" });
1066
+ context.report({ node, messageId: "emptyObject", fix });
965
1067
  }
966
1068
  };
967
1069
  var visitVariantRecordForEmpty = (context, node) => {
@@ -1025,6 +1127,7 @@ var noEmptyClasses = {
1025
1127
  docs: {
1026
1128
  description: "Disallow empty class values (empty strings, arrays, or objects) and zero-argument calls in sv() and cn()"
1027
1129
  },
1130
+ fixable: "code",
1028
1131
  schema: [],
1029
1132
  messages: {
1030
1133
  emptyString: "Empty class string is not allowed.",
@@ -1040,7 +1143,7 @@ var noEmptyClasses = {
1040
1143
  return;
1041
1144
  }
1042
1145
  forEachStaticItem(call.args, (arg) => {
1043
- visitForEmptyClasses(context, arg, false);
1146
+ visitForEmptyClasses(context, arg, false, node.arguments);
1044
1147
  });
1045
1148
  if (call.config) {
1046
1149
  dispatchSvConfigCheckers(
@@ -1053,14 +1156,30 @@ var noEmptyClasses = {
1053
1156
  }
1054
1157
  };
1055
1158
  var rules = {
1056
- "no-duplicate-classes": noDuplicateClasses,
1159
+ "no-conflicting-classes": noConflictingClasses,
1057
1160
  "no-dynamic-classes": noDynamicClasses,
1058
1161
  "no-empty-classes": noEmptyClasses,
1059
1162
  "no-redundant-spaces": noRedundantSpaces,
1060
1163
  "no-shared-tokens": noSharedTokens
1061
1164
  };
1062
1165
  var meta = { name: "slot-variants" };
1063
- var eslint_plugin_default = { meta, rules };
1166
+ var recommendedRules = {
1167
+ "slot-variants/no-conflicting-classes": "error",
1168
+ "slot-variants/no-dynamic-classes": "error",
1169
+ "slot-variants/no-empty-classes": "error",
1170
+ "slot-variants/no-redundant-spaces": "error",
1171
+ "slot-variants/no-shared-tokens": "error"
1172
+ };
1173
+ var plugin = {
1174
+ meta,
1175
+ rules,
1176
+ configs: {}
1177
+ };
1178
+ plugin.configs.recommended = {
1179
+ plugins: { "slot-variants": plugin },
1180
+ rules: recommendedRules
1181
+ };
1182
+ var eslint_plugin_default = plugin;
1064
1183
  // Annotate the CommonJS export names for ESM import in node:
1065
1184
  0 && (module.exports = {
1066
1185
  rules
@@ -1,26 +1,27 @@
1
- import { Rule } from 'eslint';
1
+ import { Rule, Linter } from 'eslint';
2
2
 
3
3
  /**
4
4
  * Rules exported by the plugin.
5
5
  */
6
6
  declare const rules: {
7
- 'no-duplicate-classes': Rule.RuleModule;
7
+ 'no-conflicting-classes': Rule.RuleModule;
8
8
  'no-dynamic-classes': Rule.RuleModule;
9
9
  'no-empty-classes': Rule.RuleModule;
10
10
  'no-redundant-spaces': Rule.RuleModule;
11
11
  'no-shared-tokens': Rule.RuleModule;
12
12
  };
13
- declare const _default: {
13
+ declare const plugin: {
14
14
  meta: {
15
15
  name: string;
16
16
  };
17
17
  rules: {
18
- 'no-duplicate-classes': Rule.RuleModule;
18
+ 'no-conflicting-classes': Rule.RuleModule;
19
19
  'no-dynamic-classes': Rule.RuleModule;
20
20
  'no-empty-classes': Rule.RuleModule;
21
21
  'no-redundant-spaces': Rule.RuleModule;
22
22
  'no-shared-tokens': Rule.RuleModule;
23
23
  };
24
+ configs: Record<string, Linter.Config>;
24
25
  };
25
26
 
26
- export { _default as default, rules };
27
+ export { plugin as default, rules };