@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.
- package/CHANGELOG.md +26 -0
- package/README.md +48 -15
- package/configs/tailwind.js +17 -0
- package/configs/vue-recommended.js +3 -0
- package/configs/vue-strict.js +3 -0
- package/index.js +5 -0
- package/package.json +1 -1
- package/rules/deprecation-bento-dropdown-size-prop/README.md +25 -0
- package/rules/deprecation-bento-dropdown-size-prop/__tests__/deprecation-bento-dropdown-size-prop.vue +9 -0
- package/rules/deprecation-bento-dropdown-size-prop/__tests__/eslint.config.js +26 -0
- package/rules/deprecation-bento-dropdown-size-prop/deprecation-bento-dropdown-size-prop.integration.test.ts +33 -0
- package/rules/deprecation-bento-dropdown-size-prop/deprecation-bento-dropdown-size-prop.js +78 -0
- package/rules/deprecation-bento-dropdown-size-prop/deprecation-bento-dropdown-size-prop.test.ts +101 -0
- package/rules/deprecation-components-error-prop/README.md +43 -0
- package/rules/deprecation-components-error-prop/__tests__/deprecation-components-error-prop.vue +17 -0
- package/rules/deprecation-components-error-prop/__tests__/eslint.config.js +26 -0
- package/rules/deprecation-components-error-prop/deprecation-components-error-prop.integration.test.ts +37 -0
- package/rules/deprecation-components-error-prop/deprecation-components-error-prop.js +79 -0
- package/rules/deprecation-components-error-prop/deprecation-components-error-prop.test.ts +120 -0
- package/rules/deprecation-components-input-emit/__tests__/eslint.config.js +1 -1
- package/rules/deprecation-components-input-emit/deprecation-components-input-emit.js +19 -4
- package/rules/deprecation-components-input-emit/deprecation-components-input-emit.test.ts +40 -0
- package/rules/deprecation-components-value-prop/__tests__/eslint.config.js +1 -1
- package/rules/deprecation-components-value-prop/deprecation-components-value-prop.js +24 -7
- package/rules/deprecation-components-value-prop/deprecation-components-value-prop.test.ts +56 -0
- package/rules/deprecation-filter-bar-deprecated-props/README.md +64 -0
- package/rules/deprecation-filter-bar-deprecated-props/__tests__/deprecation-filter-bar-deprecated-props.vue +48 -0
- package/rules/deprecation-filter-bar-deprecated-props/__tests__/eslint.config.js +25 -0
- package/rules/deprecation-filter-bar-deprecated-props/deprecation-filter-bar-deprecated-props.integration.test.ts +22 -0
- package/rules/deprecation-filter-bar-deprecated-props/deprecation-filter-bar-deprecated-props.js +187 -0
- package/rules/deprecation-filter-bar-deprecated-props/deprecation-filter-bar-deprecated-props.test.ts +421 -0
- package/rules/tailwind-v3-to-bento/README.md +114 -0
- package/rules/tailwind-v3-to-bento/__tests__/eslint.config.js +24 -0
- package/rules/tailwind-v3-to-bento/__tests__/tailwind-v3-to-bento.vue +15 -0
- package/rules/tailwind-v3-to-bento/tailwind-v3-to-bento.integration.test.ts +29 -0
- package/rules/tailwind-v3-to-bento/tailwind-v3-to-bento.js +377 -0
- 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
|
+
};
|