@adyen/eslint-plugin-bento 2.7.7 → 2.9.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.
Files changed (37) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +48 -15
  3. package/configs/tailwind.js +17 -0
  4. package/configs/vue-recommended.js +3 -0
  5. package/configs/vue-strict.js +3 -0
  6. package/index.js +5 -0
  7. package/package.json +1 -1
  8. package/rules/deprecation-bento-dropdown-size-prop/README.md +25 -0
  9. package/rules/deprecation-bento-dropdown-size-prop/__tests__/deprecation-bento-dropdown-size-prop.vue +9 -0
  10. package/rules/deprecation-bento-dropdown-size-prop/__tests__/eslint.config.js +26 -0
  11. package/rules/deprecation-bento-dropdown-size-prop/deprecation-bento-dropdown-size-prop.integration.test.ts +33 -0
  12. package/rules/deprecation-bento-dropdown-size-prop/deprecation-bento-dropdown-size-prop.js +78 -0
  13. package/rules/deprecation-bento-dropdown-size-prop/deprecation-bento-dropdown-size-prop.test.ts +101 -0
  14. package/rules/deprecation-components-error-prop/README.md +43 -0
  15. package/rules/deprecation-components-error-prop/__tests__/deprecation-components-error-prop.vue +17 -0
  16. package/rules/deprecation-components-error-prop/__tests__/eslint.config.js +26 -0
  17. package/rules/deprecation-components-error-prop/deprecation-components-error-prop.integration.test.ts +37 -0
  18. package/rules/deprecation-components-error-prop/deprecation-components-error-prop.js +79 -0
  19. package/rules/deprecation-components-error-prop/deprecation-components-error-prop.test.ts +120 -0
  20. package/rules/deprecation-components-input-emit/__tests__/eslint.config.js +1 -1
  21. package/rules/deprecation-components-input-emit/deprecation-components-input-emit.js +19 -4
  22. package/rules/deprecation-components-input-emit/deprecation-components-input-emit.test.ts +40 -0
  23. package/rules/deprecation-components-value-prop/__tests__/eslint.config.js +1 -1
  24. package/rules/deprecation-components-value-prop/deprecation-components-value-prop.js +24 -7
  25. package/rules/deprecation-components-value-prop/deprecation-components-value-prop.test.ts +56 -0
  26. package/rules/deprecation-filter-bar-deprecated-props/README.md +64 -0
  27. package/rules/deprecation-filter-bar-deprecated-props/__tests__/deprecation-filter-bar-deprecated-props.vue +48 -0
  28. package/rules/deprecation-filter-bar-deprecated-props/__tests__/eslint.config.js +25 -0
  29. package/rules/deprecation-filter-bar-deprecated-props/deprecation-filter-bar-deprecated-props.integration.test.ts +22 -0
  30. package/rules/deprecation-filter-bar-deprecated-props/deprecation-filter-bar-deprecated-props.js +187 -0
  31. package/rules/deprecation-filter-bar-deprecated-props/deprecation-filter-bar-deprecated-props.test.ts +421 -0
  32. package/rules/tailwind-v3-to-bento/README.md +114 -0
  33. package/rules/tailwind-v3-to-bento/__tests__/eslint.config.js +24 -0
  34. package/rules/tailwind-v3-to-bento/__tests__/tailwind-v3-to-bento.vue +15 -0
  35. package/rules/tailwind-v3-to-bento/tailwind-v3-to-bento.integration.test.ts +29 -0
  36. package/rules/tailwind-v3-to-bento/tailwind-v3-to-bento.js +377 -0
  37. package/rules/tailwind-v3-to-bento/tailwind-v3-to-bento.test.ts +403 -0
@@ -0,0 +1,377 @@
1
+ /**
2
+ * @typedef {import('eslint').Rule.RuleContext} RuleContext
3
+ */
4
+ /**
5
+ * @typedef {import('estree').Node} Node
6
+ */
7
+ /**
8
+ * @typedef {import('vue-eslint-parser').AST.VLiteral} VLiteral
9
+ */
10
+ /**
11
+ * @typedef {import('eslint-plugin-vue/lib/utils').RuleModule} RuleModule
12
+ */
13
+
14
+ const VueUtils = require('eslint-plugin-vue/lib/utils');
15
+
16
+ // Spacing: Tailwind 3 -> Bento spacer tokens
17
+ const SPACING_MAP = {
18
+ '0': '000',
19
+ '0.5': '010',
20
+ '1': '020',
21
+ '1.5': '030',
22
+ '2': '040',
23
+ '2.5': '050',
24
+ '3': '060',
25
+ '4': '070',
26
+ '5': '080',
27
+ '6': '090',
28
+ '8': '100',
29
+ '10': '110',
30
+ '12': '120',
31
+ '14': '130',
32
+ '16': '140',
33
+ };
34
+
35
+ // Border radius: Tailwind 3 -> Bento border radius tokens
36
+ const BORDER_RADIUS_MAP = {
37
+ 'rounded-sm': 'rounded-xs',
38
+ 'rounded-md': 'rounded-m',
39
+ 'rounded-lg': 'rounded-l',
40
+ 'rounded-xl': 'rounded-xl',
41
+ };
42
+
43
+ // Border width: Tailwind 3 -> Bento border width tokens
44
+ const BORDER_WIDTH_MAP = {
45
+ 'border-0': 'border-0',
46
+ 'border-2': 'border-m',
47
+ 'border-4': 'border-l',
48
+ };
49
+
50
+ // Box shadow: Tailwind 3 -> Bento shadow tokens
51
+ const SHADOW_MAP = {
52
+ 'shadow-sm': 'shadow-low',
53
+ 'shadow-md': 'shadow-medium',
54
+ 'shadow-lg': 'shadow-high',
55
+ 'shadow-xl': 'shadow-high',
56
+ 'shadow-2xl': 'shadow-high',
57
+ };
58
+
59
+ // Breakpoint prefixes: Tailwind 3 -> Bento breakpoint prefixes
60
+ const BREAKPOINT_MAP = {
61
+ 'sm:': 's-min:',
62
+ 'md:': 'm-min:',
63
+ 'lg:': 'l-min:',
64
+ 'xl:': 'xl-min:',
65
+ '2xl:': 'xxl:',
66
+ };
67
+
68
+ // Color utilities: Tailwind 3 -> Bento color tokens (best-effort mapping)
69
+ const COLOR_MAP = {
70
+ 'red-500': 'label-critical',
71
+ 'red-600': 'label-critical',
72
+ 'red-700': 'label-critical-hover',
73
+ 'red-100': 'critical-weak',
74
+ 'green-500': 'label-success',
75
+ 'green-600': 'label-success',
76
+ 'green-100': 'success-weak',
77
+ 'blue-500': 'label-highlight',
78
+ 'blue-600': 'label-highlight',
79
+ 'blue-100': 'highlight-weak',
80
+ 'yellow-500': 'label-warning',
81
+ 'yellow-600': 'label-warning',
82
+ 'yellow-100': 'warning-weak',
83
+ 'gray-50': 'secondary',
84
+ 'gray-100': 'secondary',
85
+ 'gray-200': 'tertiary',
86
+ 'gray-300': 'outline-primary',
87
+ 'gray-400': 'outline-secondary',
88
+ 'gray-500': 'label-secondary',
89
+ 'gray-600': 'label-secondary',
90
+ 'gray-700': 'label-primary',
91
+ 'gray-800': 'label-primary',
92
+ 'gray-900': 'label-primary',
93
+ white: 'primary',
94
+ black: 'label-primary',
95
+ };
96
+
97
+ // Color prefix utilities that use the COLOR_MAP
98
+ const COLOR_PREFIXES = ['text-', 'bg-', 'border-'];
99
+
100
+ // Spacing prefix patterns (padding, margin, gap, space, inset, etc.)
101
+ const SPACING_PREFIXES = [
102
+ 'p',
103
+ 'px',
104
+ 'py',
105
+ 'pt',
106
+ 'pr',
107
+ 'pb',
108
+ 'pl',
109
+ 'ps',
110
+ 'pe',
111
+ 'm',
112
+ 'mx',
113
+ 'my',
114
+ 'mt',
115
+ 'mr',
116
+ 'mb',
117
+ 'ml',
118
+ 'ms',
119
+ 'me',
120
+ 'gap',
121
+ 'gap-x',
122
+ 'gap-y',
123
+ 'space-x',
124
+ 'space-y',
125
+ 'inset',
126
+ 'inset-x',
127
+ 'inset-y',
128
+ 'top',
129
+ 'right',
130
+ 'bottom',
131
+ 'left',
132
+ 'start',
133
+ 'end',
134
+ 'w',
135
+ 'min-w',
136
+ 'max-w',
137
+ 'h',
138
+ 'min-h',
139
+ 'max-h',
140
+ 'size',
141
+ 'basis',
142
+ ];
143
+
144
+ // Typography utilities that should be flagged (no auto-fix, use bento-typography)
145
+ const TYPOGRAPHY_PATTERNS = [
146
+ /^text-(xs|sm|base|lg|xl|2xl|3xl|4xl|5xl|6xl|7xl|8xl|9xl)$/,
147
+ /^font-(thin|extralight|light|normal|medium|semibold|bold|extrabold|black)$/,
148
+ /^font-(sans|serif|mono)$/,
149
+ /^leading-(none|tight|snug|normal|relaxed|loose|\d+)$/,
150
+ /^tracking-(tighter|tight|normal|wide|wider|widest)$/,
151
+ ];
152
+
153
+ // Font size -> suggested bento-typography variant
154
+ const FONT_SIZE_SUGGESTION = {
155
+ 'text-xs': 'caption',
156
+ 'text-sm': 'caption',
157
+ 'text-base': 'body',
158
+ 'text-lg': 'title',
159
+ 'text-xl': 'title" medium',
160
+ 'text-2xl': 'title" large',
161
+ 'text-3xl': 'title" large',
162
+ 'text-4xl': 'title" large',
163
+ 'text-5xl': 'title" large',
164
+ 'text-6xl': 'title" large',
165
+ 'text-7xl': 'title" large',
166
+ 'text-8xl': 'title" large',
167
+ 'text-9xl': 'title" large',
168
+ };
169
+
170
+ // Font weight -> suggested bento-typography modifier prop
171
+ const FONT_WEIGHT_SUGGESTION = {
172
+ 'font-medium': 'stronger',
173
+ 'font-semibold': 'stronger',
174
+ 'font-bold': 'strongest',
175
+ 'font-extrabold': 'strongest',
176
+ 'font-black': 'strongest',
177
+ };
178
+
179
+ /**
180
+ * Build a suggested bento-typography usage from detected typography classes.
181
+ * @param {string[]} typographyClasses
182
+ * @returns {string}
183
+ */
184
+ function buildTypographySuggestion(typographyClasses) {
185
+ let variant = 'body';
186
+ const props = [];
187
+
188
+ for (const cls of typographyClasses) {
189
+ if (FONT_SIZE_SUGGESTION[cls]) {
190
+ const suggestion = FONT_SIZE_SUGGESTION[cls];
191
+ if (suggestion.includes('" ')) {
192
+ const [v, prop] = suggestion.split('" ');
193
+ variant = v;
194
+ props.push(prop);
195
+ } else {
196
+ variant = suggestion;
197
+ }
198
+ }
199
+ if (FONT_WEIGHT_SUGGESTION[cls]) {
200
+ props.push(FONT_WEIGHT_SUGGESTION[cls]);
201
+ }
202
+ }
203
+
204
+ const propsStr = props.length > 0 ? ' ' + props.join(' ') : '';
205
+ return `<bento-typography variant="${variant}"${propsStr}>`;
206
+ }
207
+
208
+ /**
209
+ * Try to replace a single CSS class with its Bento equivalent.
210
+ * @param {string} cssClass
211
+ * @returns {{ replaced: string, wasReplaced: boolean, isTypography: boolean }}
212
+ */
213
+ function replaceClass(cssClass) {
214
+ let workingClass = cssClass;
215
+ let breakpointPrefix = '';
216
+ let hasVariantReplacement = false;
217
+
218
+ // Extract all variants (e.g., md:hover:p-4 -> variants=['m-min:', 'hover:'], workingClass='p-4')
219
+ while (workingClass.includes(':')) {
220
+ const colonIndex = workingClass.indexOf(':') + 1;
221
+ const variant = workingClass.slice(0, colonIndex);
222
+ const mapped = BREAKPOINT_MAP[variant];
223
+ if (mapped) {
224
+ hasVariantReplacement = true;
225
+ breakpointPrefix += mapped;
226
+ } else {
227
+ breakpointPrefix += variant;
228
+ }
229
+ workingClass = workingClass.slice(colonIndex);
230
+ }
231
+
232
+ // Check typography (warn only)
233
+ for (const pattern of TYPOGRAPHY_PATTERNS) {
234
+ if (pattern.test(workingClass)) {
235
+ return { replaced: cssClass, wasReplaced: false, isTypography: true };
236
+ }
237
+ }
238
+
239
+ // Check border radius: bare "rounded" (without further suffix) maps to "rounded-s"
240
+ if (workingClass === 'rounded') {
241
+ return { replaced: breakpointPrefix + 'rounded-s', wasReplaced: true, isTypography: false };
242
+ }
243
+ if (BORDER_RADIUS_MAP[workingClass]) {
244
+ return { replaced: breakpointPrefix + BORDER_RADIUS_MAP[workingClass], wasReplaced: true, isTypography: false };
245
+ }
246
+
247
+ // Check border width: bare "border" (without color suffix) maps to "border-s"
248
+ if (workingClass === 'border') {
249
+ return { replaced: breakpointPrefix + 'border-s', wasReplaced: true, isTypography: false };
250
+ }
251
+ if (BORDER_WIDTH_MAP[workingClass]) {
252
+ return { replaced: breakpointPrefix + BORDER_WIDTH_MAP[workingClass], wasReplaced: true, isTypography: false };
253
+ }
254
+
255
+ // Check box shadow: bare "shadow" maps to "shadow-medium"
256
+ if (workingClass === 'shadow') {
257
+ return { replaced: breakpointPrefix + 'shadow-medium', wasReplaced: true, isTypography: false };
258
+ }
259
+ if (SHADOW_MAP[workingClass]) {
260
+ return { replaced: breakpointPrefix + SHADOW_MAP[workingClass], wasReplaced: true, isTypography: false };
261
+ }
262
+
263
+ // Check color utilities
264
+ for (const prefix of COLOR_PREFIXES) {
265
+ if (workingClass.startsWith(prefix)) {
266
+ const colorPart = workingClass.slice(prefix.length);
267
+ if (COLOR_MAP[colorPart]) {
268
+ const bentoColor = COLOR_MAP[colorPart];
269
+ return { replaced: breakpointPrefix + prefix + bentoColor, wasReplaced: true, isTypography: false };
270
+ }
271
+ }
272
+ }
273
+
274
+ // Check spacing utilities (including negative values like -m-4)
275
+ for (const spacingPrefix of SPACING_PREFIXES) {
276
+ const isNegative = workingClass.startsWith('-');
277
+ const baseClass = isNegative ? workingClass.slice(1) : workingClass;
278
+ const prefixWithDash = spacingPrefix + '-';
279
+ if (baseClass.startsWith(prefixWithDash)) {
280
+ const value = baseClass.slice(prefixWithDash.length);
281
+ if (SPACING_MAP[value]) {
282
+ return {
283
+ replaced: breakpointPrefix + (isNegative ? '-' : '') + prefixWithDash + SPACING_MAP[value],
284
+ wasReplaced: true,
285
+ isTypography: false,
286
+ };
287
+ }
288
+ }
289
+ }
290
+
291
+ // If a breakpoint variant was mapped but the base class wasn't, still report the replacement
292
+ if (hasVariantReplacement && workingClass) {
293
+ return { replaced: breakpointPrefix + workingClass, wasReplaced: true, isTypography: false };
294
+ }
295
+
296
+ return { replaced: cssClass, wasReplaced: false, isTypography: false };
297
+ }
298
+
299
+ /** @type {RuleModule} */
300
+ module.exports = {
301
+ meta: {
302
+ type: 'suggestion',
303
+ docs: {
304
+ description: 'Replace Tailwind CSS v3 utility classes with Bento design token equivalents',
305
+ category: 'Best Practices',
306
+ url: 'https://github.com/Adyen/bento/blob/main/packages/eslint/rules/tailwind-v3-to-bento/README.md',
307
+ recommended: true,
308
+ },
309
+ messages: {
310
+ updateTailwindClass:
311
+ 'Found Tailwind v3 class "{{ oldClass }}" — replace with Bento equivalent "{{ newClass }}".',
312
+ removeTailwindTypography:
313
+ 'Found Tailwind v3 typography class "{{ oldClass }}" — use {{ suggestion }} instead.',
314
+ },
315
+ fixable: 'code',
316
+ schema: [],
317
+ },
318
+ /** @param {RuleContext} context */
319
+ create(context) {
320
+ return VueUtils.defineTemplateBodyVisitor(context, {
321
+ VLiteral: (/** @type {any} */ node) => {
322
+ if (node.parent && node.parent.key && node.parent.key.name !== 'class') {
323
+ return;
324
+ }
325
+
326
+ if (typeof node.value !== 'string') {
327
+ return;
328
+ }
329
+
330
+ const classList = /** @type {VLiteral['value']} */ node.value;
331
+ let hasReplacements = false;
332
+ let hasTypography = false;
333
+
334
+ const classes = classList.split(/\s+/).filter(Boolean);
335
+ const results = classes.map(cssClass => {
336
+ const result = replaceClass(cssClass);
337
+ if (result.wasReplaced) {
338
+ hasReplacements = true;
339
+ }
340
+ if (result.isTypography) {
341
+ hasTypography = true;
342
+ }
343
+ return result;
344
+ });
345
+
346
+ if (hasReplacements) {
347
+ const fixed = results.map(r => r.replaced).join(' ');
348
+ context.report({
349
+ node,
350
+ messageId: 'updateTailwindClass',
351
+ data: {
352
+ oldClass: classList,
353
+ newClass: fixed,
354
+ },
355
+ fix(fixer) {
356
+ const quote = context.sourceCode.getText(node)[0];
357
+ return fixer.replaceText(node, `${quote}${fixed}${quote}`);
358
+ },
359
+ });
360
+ }
361
+
362
+ if (hasTypography) {
363
+ const typographyClasses = results.filter(r => r.isTypography).map(r => r.replaced);
364
+ const suggestion = buildTypographySuggestion(typographyClasses);
365
+ context.report({
366
+ node,
367
+ messageId: 'removeTailwindTypography',
368
+ data: {
369
+ oldClass: typographyClasses.join(', '),
370
+ suggestion,
371
+ },
372
+ });
373
+ }
374
+ },
375
+ });
376
+ },
377
+ };