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 +20 -10
- package/{AGENTS.md → SKILL.md} +11 -13
- package/dist/eslint-plugin.cjs +181 -62
- package/dist/eslint-plugin.d.ts +6 -5
- package/dist/eslint-plugin.js +181 -62
- package/dist/index.cjs +339 -189
- package/dist/index.d.ts +19 -9
- package/dist/index.js +339 -189
- package/dist/src/eslint-plugin.d.ts +6 -5
- package/dist/src/sv.d.ts +37 -35
- package/package.json +16 -12
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.
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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: '
|
|
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: '
|
|
1106
|
+
defaultVariants: { size: 'sm' }
|
|
1097
1107
|
});
|
|
1098
1108
|
```
|
|
1099
1109
|
|
package/{AGENTS.md → SKILL.md}
RENAMED
|
@@ -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-
|
|
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-
|
|
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-
|
|
475
|
+
### `slot-variants/no-conflicting-classes`
|
|
478
476
|
|
|
479
|
-
Reports class tokens that
|
|
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
|
|
482
|
-
- Does **not** flag
|
|
483
|
-
- For `cn()` (and `sv()` called without a config — the cn-style calling convention), flags
|
|
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-
|
|
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. **
|
|
529
|
-
3. **
|
|
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
|
package/dist/eslint-plugin.cjs
CHANGED
|
@@ -105,14 +105,15 @@ var getStrictProperties = (obj) => {
|
|
|
105
105
|
if (!obj || obj.type !== "ObjectExpression") {
|
|
106
106
|
return null;
|
|
107
107
|
}
|
|
108
|
-
|
|
109
|
-
|
|
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" ||
|
|
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) =>
|
|
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) =>
|
|
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
|
-
|
|
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) => !/^(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
836
|
+
analyzeConfigForRule(context, call.config, call.args);
|
|
760
837
|
} else {
|
|
761
|
-
|
|
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
|
|
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-
|
|
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
|
|
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
|
package/dist/eslint-plugin.d.ts
CHANGED
|
@@ -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-
|
|
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
|
|
13
|
+
declare const plugin: {
|
|
14
14
|
meta: {
|
|
15
15
|
name: string;
|
|
16
16
|
};
|
|
17
17
|
rules: {
|
|
18
|
-
'no-
|
|
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 {
|
|
27
|
+
export { plugin as default, rules };
|