eslint-plugin-vuetify 2.5.2 → 2.6.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 +87 -4
- package/lib/configs/flat/recommended-v4.js +10 -0
- package/lib/configs/flat/tailwindcss.js +10 -0
- package/lib/configs/recommended-v4.js +10 -0
- package/lib/configs/tailwindcss.js +10 -0
- package/lib/index.js +5 -1
- package/lib/rules/no-border-prop.js +73 -0
- package/lib/rules/no-deprecated-classes.js +1 -1
- package/lib/rules/no-deprecated-components.js +0 -1
- package/lib/rules/no-deprecated-props.js +42 -11
- package/lib/rules/no-deprecated-snackbar.js +108 -0
- package/lib/rules/no-deprecated-typography.js +106 -0
- package/lib/rules/no-elevation-overflow.js +77 -0
- package/lib/rules/no-elevation-prop.js +58 -0
- package/lib/rules/no-legacy-grid-props.js +141 -0
- package/lib/rules/no-legacy-utilities.js +111 -0
- package/lib/rules/no-rounded-prop.js +84 -0
- package/lib/util/helpers.js +3 -2
- package/package.json +26 -11
package/README.md
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://cdn.vuetifyjs.com/docs/images/one/logos/veslplugin-logo-dark.png">
|
|
4
|
+
<img alt="Vuetify ESLint Plugin Logo" src="https://cdn.vuetifyjs.com/docs/images/one/logos/veslplugin-logo-light.png" height="100">
|
|
5
|
+
</picture>
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<p align="center">
|
|
9
|
+
<a href="https://www.npmjs.com/package/eslint-plugin-vuetify"><img src="https://img.shields.io/npm/v/eslint-plugin-vuetify.svg" alt="npm version"></a>
|
|
10
|
+
<a href="https://npm.chart.dev/eslint-plugin-vuetify"><img src="https://img.shields.io/npm/dm/eslint-plugin-vuetify?color=blue" alt="npm downloads"></a>
|
|
11
|
+
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License: MIT"></a>
|
|
12
|
+
<a href="https://community.vuetifyjs.com"><img src="https://discordapp.com/api/guilds/340160225338195969/widget.png" alt="Discord"></a>
|
|
13
|
+
</p>
|
|
14
|
+
|
|
1
15
|
# eslint-plugin-vuetify
|
|
2
16
|
|
|
3
|
-
This package
|
|
17
|
+
This package helps migrate between Vuetify major versions. It includes rules for **v2 → v3** and **v3 → v4** migrations.
|
|
18
|
+
|
|
19
|
+
Use [eslint-plugin-vuetify@vuetify-2](https://www.npmjs.com/package/eslint-plugin-vuetify/v/vuetify-2) for v1 to v2.
|
|
4
20
|
|
|
5
21
|
<br>
|
|
6
22
|
|
|
@@ -13,6 +29,53 @@ This package is for migrating from Vuetify v2 to v3, use [eslint-plugin-vuetify@
|
|
|
13
29
|
</a>
|
|
14
30
|
</p>
|
|
15
31
|
|
|
32
|
+
## Vuetify 4 Migration
|
|
33
|
+
|
|
34
|
+
This plugin includes four new rules for migrating from Vuetify v3 to v4:
|
|
35
|
+
|
|
36
|
+
- **`no-deprecated-typography`** — replaces MD2 typography classes (`text-h1`) with MD3 equivalents (`text-display-large`)
|
|
37
|
+
- **`no-legacy-grid-props`** — converts removed `VRow`/`VCol` props (`align`, `justify`, `dense`) to utility classes or renamed props
|
|
38
|
+
- **`no-elevation-overflow`** — flags elevation classes and props above the MD3 maximum of 5
|
|
39
|
+
- **`no-deprecated-snackbar`** — fixes renamed `VSnackbarQueue` slots and replaced `VSnackbar` props
|
|
40
|
+
|
|
41
|
+
### Using the recommended-v4 preset
|
|
42
|
+
|
|
43
|
+
Enable all v4 migration rules at once:
|
|
44
|
+
|
|
45
|
+
```js
|
|
46
|
+
// eslint.config.js
|
|
47
|
+
import vue from 'eslint-plugin-vue'
|
|
48
|
+
import vuetify from 'eslint-plugin-vuetify'
|
|
49
|
+
|
|
50
|
+
export default [
|
|
51
|
+
...vue.configs['flat/base'],
|
|
52
|
+
...vuetify.configs['flat/recommended-v4'],
|
|
53
|
+
]
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Selecting individual rules
|
|
57
|
+
|
|
58
|
+
You can also enable rules selectively instead of using the preset:
|
|
59
|
+
|
|
60
|
+
```js
|
|
61
|
+
// eslint.config.js
|
|
62
|
+
import vue from 'eslint-plugin-vue'
|
|
63
|
+
import vuetify from 'eslint-plugin-vuetify'
|
|
64
|
+
|
|
65
|
+
export default [
|
|
66
|
+
...vue.configs['flat/base'],
|
|
67
|
+
...vuetify.configs['flat/base'],
|
|
68
|
+
{
|
|
69
|
+
rules: {
|
|
70
|
+
'vuetify/no-deprecated-typography': 'error',
|
|
71
|
+
'vuetify/no-legacy-grid-props': 'error',
|
|
72
|
+
'vuetify/no-elevation-overflow': 'error',
|
|
73
|
+
'vuetify/no-deprecated-snackbar': 'error',
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
]
|
|
77
|
+
```
|
|
78
|
+
|
|
16
79
|
## 💿 Install
|
|
17
80
|
|
|
18
81
|
You should have [`eslint`](https://eslint.org/docs/latest/use/getting-started) and [`eslint-plugin-vue`](https://eslint.vuejs.org/user-guide/#installation) set up first.
|
|
@@ -34,7 +97,7 @@ export default [
|
|
|
34
97
|
]
|
|
35
98
|
```
|
|
36
99
|
|
|
37
|
-
|
|
100
|
+
ESLint 8 can alternatively use the older configuration format:
|
|
38
101
|
|
|
39
102
|
```js
|
|
40
103
|
// .eslintrc.js
|
|
@@ -46,7 +109,9 @@ module.exports = {
|
|
|
46
109
|
}
|
|
47
110
|
```
|
|
48
111
|
|
|
49
|
-
|
|
112
|
+
This plugin supports ESLint 8, 9, and 10. ESLint 10 only supports the flat config format (use `configs['flat/base']` or `configs['flat/recommended']`).
|
|
113
|
+
|
|
114
|
+
**NOTE** This plugin does not affect _**pug**_ templates due to [a limitation in vue-eslint-parser](https://github.com/vuejs/vue-eslint-parser/issues/29). I suggest converting your pug templates to HTML with [pug-to-html](https://github.com/leo-buneev/pug-to-html) in order to use this plugin.
|
|
50
115
|
|
|
51
116
|
|
|
52
117
|
## Rules
|
|
@@ -60,15 +125,28 @@ These rules will help you avoid deprecated components, props, and classes. They
|
|
|
60
125
|
- Prevent the use of events that have been removed from Vuetify ([`no-deprecated-events`])
|
|
61
126
|
- Prevent the use of classes that have been removed from Vuetify ([`no-deprecated-classes`])
|
|
62
127
|
- Prevent the use of the old theme class syntax ([`no-deprecated-colors`])
|
|
128
|
+
- Prevent the use of slots that have been removed from Vuetify ([`no-deprecated-slots`])
|
|
63
129
|
- Prevent the use of deprecated import paths ([`no-deprecated-imports`])
|
|
130
|
+
|
|
131
|
+
Additional rule (not included in presets):
|
|
132
|
+
|
|
64
133
|
- Ensure icon buttons have a variant defined ([`icon-button-variant`])
|
|
65
134
|
|
|
66
135
|
### Grid system
|
|
67
136
|
|
|
68
|
-
These rules are designed to help migrate to the new grid system in Vuetify
|
|
137
|
+
These rules are designed to help migrate to the new grid system in Vuetify v3. They are included in the `recommended` preset.
|
|
69
138
|
|
|
70
139
|
- Warn about unknown attributes not being converted to classes on new grid components ([`grid-unknown-attributes`])
|
|
71
140
|
|
|
141
|
+
### Vuetify 4
|
|
142
|
+
|
|
143
|
+
These rules help migrate from Vuetify v3 to v4. They are included in the `recommended-v4` preset.
|
|
144
|
+
|
|
145
|
+
- Disallow deprecated MD2 typography classes ([`no-deprecated-typography`])
|
|
146
|
+
- Prevent the use of removed grid props ([`no-legacy-grid-props`])
|
|
147
|
+
- Disallow elevation classes above the MD3 maximum ([`no-elevation-overflow`])
|
|
148
|
+
- Disallow deprecated props and slots on snackbar components ([`no-deprecated-snackbar`])
|
|
149
|
+
|
|
72
150
|
|
|
73
151
|
[`grid-unknown-attributes`]: ./docs/rules/grid-unknown-attributes.md
|
|
74
152
|
[`no-deprecated-components`]: ./docs/rules/no-deprecated-components.md
|
|
@@ -76,8 +154,13 @@ These rules are designed to help migrate to the new grid system in Vuetify v2. T
|
|
|
76
154
|
[`no-deprecated-events`]: ./docs/rules/no-deprecated-events.md
|
|
77
155
|
[`no-deprecated-classes`]: ./docs/rules/no-deprecated-classes.md
|
|
78
156
|
[`no-deprecated-colors`]: ./docs/rules/no-deprecated-colors.md
|
|
157
|
+
[`no-deprecated-slots`]: ./docs/rules/no-deprecated-slots.md
|
|
79
158
|
[`no-deprecated-imports`]: ./docs/rules/no-deprecated-imports.md
|
|
80
159
|
[`icon-button-variant`]: ./docs/rules/icon-button-variant.md
|
|
160
|
+
[`no-deprecated-typography`]: ./docs/rules/no-deprecated-typography.md
|
|
161
|
+
[`no-legacy-grid-props`]: ./docs/rules/no-legacy-grid-props.md
|
|
162
|
+
[`no-elevation-overflow`]: ./docs/rules/no-elevation-overflow.md
|
|
163
|
+
[`no-deprecated-snackbar`]: ./docs/rules/no-deprecated-snackbar.md
|
|
81
164
|
|
|
82
165
|
|
|
83
166
|
## 💪 Supporting Vuetify
|
package/lib/index.js
CHANGED
|
@@ -6,8 +6,12 @@ module.exports = {
|
|
|
6
6
|
configs: {
|
|
7
7
|
base: require('./configs/base'),
|
|
8
8
|
recommended: require('./configs/recommended'),
|
|
9
|
+
'recommended-v4': require('./configs/recommended-v4'),
|
|
9
10
|
'flat/base': require('./configs/flat/base'),
|
|
10
|
-
'flat/recommended': require('./configs/flat/recommended')
|
|
11
|
+
'flat/recommended': require('./configs/flat/recommended'),
|
|
12
|
+
'flat/recommended-v4': require('./configs/flat/recommended-v4'),
|
|
13
|
+
tailwindcss: require('./configs/tailwindcss'),
|
|
14
|
+
'flat/tailwindcss': require('./configs/flat/tailwindcss')
|
|
11
15
|
},
|
|
12
16
|
rules: requireindex(path.join(__dirname, './rules'))
|
|
13
17
|
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
isVueTemplate
|
|
5
|
+
} = require('../util/helpers');
|
|
6
|
+
const {
|
|
7
|
+
addClass,
|
|
8
|
+
removeAttr
|
|
9
|
+
} = require('../util/fixers');
|
|
10
|
+
module.exports = {
|
|
11
|
+
meta: {
|
|
12
|
+
docs: {
|
|
13
|
+
description: 'Disallow the `border` prop; use Tailwind border utilities instead.',
|
|
14
|
+
category: 'tailwindcss'
|
|
15
|
+
},
|
|
16
|
+
fixable: 'code',
|
|
17
|
+
schema: [],
|
|
18
|
+
messages: {
|
|
19
|
+
replacedWith: `'border="{{ value }}"' should be replaced with class="{{ className }}"`,
|
|
20
|
+
noFix: `'border' prop should be replaced with Tailwind border utility classes`
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
create(context) {
|
|
24
|
+
if (!isVueTemplate(context)) return {};
|
|
25
|
+
return context.sourceCode.parserServices.defineTemplateBodyVisitor({
|
|
26
|
+
VAttribute(attr) {
|
|
27
|
+
if (attr.directive && (attr.key.name.name !== 'bind' || !attr.key.argument)) return;
|
|
28
|
+
const propName = attr.directive ? attr.key.argument.rawName : attr.key.rawName;
|
|
29
|
+
if (propName !== 'border') return;
|
|
30
|
+
const element = attr.parent.parent;
|
|
31
|
+
const propNameNode = attr.directive ? attr.key.argument : attr.key;
|
|
32
|
+
|
|
33
|
+
// Boolean attribute (no value) — `border` with no `="..."`
|
|
34
|
+
if (!attr.directive && !attr.value) {
|
|
35
|
+
context.report({
|
|
36
|
+
messageId: 'replacedWith',
|
|
37
|
+
data: {
|
|
38
|
+
value: '',
|
|
39
|
+
className: 'border'
|
|
40
|
+
},
|
|
41
|
+
node: propNameNode,
|
|
42
|
+
fix(fixer) {
|
|
43
|
+
return [addClass(context, fixer, element, 'border'), removeAttr(context, fixer, attr)];
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Get static value
|
|
50
|
+
const value = attr.directive ? attr.value?.expression?.type === 'Literal' ? String(attr.value.expression.value) : null : attr.value?.value;
|
|
51
|
+
if (value != null) {
|
|
52
|
+
const className = value.split(/\s+/).filter(Boolean).map(part => `border-${part}`).join(' ');
|
|
53
|
+
context.report({
|
|
54
|
+
messageId: 'replacedWith',
|
|
55
|
+
data: {
|
|
56
|
+
value,
|
|
57
|
+
className
|
|
58
|
+
},
|
|
59
|
+
node: propNameNode,
|
|
60
|
+
fix(fixer) {
|
|
61
|
+
return [addClass(context, fixer, element, className), removeAttr(context, fixer, attr)];
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
} else {
|
|
65
|
+
context.report({
|
|
66
|
+
messageId: 'noFix',
|
|
67
|
+
node: propNameNode
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
};
|
|
@@ -15,7 +15,7 @@ const replacements = new Map([[/^rounded-(r|l|tr|tl|br|bl)(-.*)?$/, ([side, rest
|
|
|
15
15
|
bl: 'bs'
|
|
16
16
|
}[side];
|
|
17
17
|
return `rounded-${side}${rest || ''}`;
|
|
18
|
-
}], [/^text-xs-(left|right|center|justify)$/, ([align]) => `text-${align}`], [/^hidden-(xs|sm|md|lg|xl)-only$/, ([breakpoint]) => `hidden-${breakpoint}`], ['scroll-y', 'overflow-y-auto'], ['hide-overflow', 'overflow-hidden'], ['show-overflow', 'overflow-visible'], ['no-wrap', 'text-no-wrap'], ['ellipsis', 'text-truncate'], ['left', 'float-left'], ['right', 'float-right'], ['display-4', 'text-h1'], ['display-3', 'text-h2'], ['display-2', 'text-h3'], ['display-1', 'text-h4'], ['headline', 'text-h5'], ['title', 'text-h6'], ['
|
|
18
|
+
}], [/^text-xs-(left|right|center|justify)$/, ([align]) => `text-${align}`], [/^hidden-(xs|sm|md|lg|xl)-only$/, ([breakpoint]) => `hidden-${breakpoint}`], ['scroll-y', 'overflow-y-auto'], ['hide-overflow', 'overflow-hidden'], ['show-overflow', 'overflow-visible'], ['no-wrap', 'text-no-wrap'], ['ellipsis', 'text-truncate'], ['left', 'float-left'], ['right', 'float-right'], ['display-4', 'text-h1'], ['display-3', 'text-h2'], ['display-2', 'text-h3'], ['display-1', 'text-h4'], ['headline', 'text-h5'], ['title', 'text-h6'], ['subheading', 'text-subtitle-1'], ['subtitle-1', 'text-subtitle-1'], ['subtitle-2', 'text-subtitle-2'], ['body-1', 'text-body-1'], ['body-2', 'text-body-2'], ['caption', 'text-caption'], ['overline', 'text-overline'], [/^transition-(fast-out-slow-in|linear-out-slow-in|fast-out-linear-in|ease-in-out|fast-in-fast-out|swing)$/, false]]);
|
|
19
19
|
|
|
20
20
|
// ------------------------------------------------------------------------------
|
|
21
21
|
// Rule Definition
|
|
@@ -31,7 +31,6 @@ const replacements = {
|
|
|
31
31
|
custom: '`v-list-item` with `icon` props, or `v-icon` in the list item append or prepend slot'
|
|
32
32
|
},
|
|
33
33
|
VOverflowBtn: false,
|
|
34
|
-
VPicker: false,
|
|
35
34
|
VSimpleCheckbox: 'v-checkbox-btn',
|
|
36
35
|
VSubheader: {
|
|
37
36
|
custom: 'v-list-subheader or class="text-subtitle-2"'
|
|
@@ -144,7 +144,14 @@ const replacements = {
|
|
|
144
144
|
value: 'inverted'
|
|
145
145
|
},
|
|
146
146
|
outlined: 'border',
|
|
147
|
-
|
|
147
|
+
dense: {
|
|
148
|
+
name: 'density',
|
|
149
|
+
value: 'compact'
|
|
150
|
+
},
|
|
151
|
+
prominent: {
|
|
152
|
+
name: 'density',
|
|
153
|
+
value: 'prominent'
|
|
154
|
+
},
|
|
148
155
|
scrollOffScreen: false,
|
|
149
156
|
shaped: false,
|
|
150
157
|
short: false,
|
|
@@ -286,9 +293,11 @@ const replacements = {
|
|
|
286
293
|
},
|
|
287
294
|
round: 'rounded',
|
|
288
295
|
shaped: false,
|
|
289
|
-
text
|
|
290
|
-
|
|
291
|
-
|
|
296
|
+
text(attr) {
|
|
297
|
+
return !attr.directive && !attr.value || attr.directive && [true, false].includes(attr.value.expression.value) ? {
|
|
298
|
+
name: 'variant',
|
|
299
|
+
value: 'text'
|
|
300
|
+
} : true;
|
|
292
301
|
},
|
|
293
302
|
top: {
|
|
294
303
|
name: 'location',
|
|
@@ -417,6 +426,10 @@ const replacements = {
|
|
|
417
426
|
},
|
|
418
427
|
groupDesc: {
|
|
419
428
|
custom: 'group-by'
|
|
429
|
+
},
|
|
430
|
+
dense: {
|
|
431
|
+
name: 'density',
|
|
432
|
+
value: 'compact'
|
|
420
433
|
}
|
|
421
434
|
},
|
|
422
435
|
VDatePicker: {
|
|
@@ -425,8 +438,7 @@ const replacements = {
|
|
|
425
438
|
custom: 'separate month and year props'
|
|
426
439
|
},
|
|
427
440
|
locale: false,
|
|
428
|
-
localeFirstDayOfYear:
|
|
429
|
-
firstDayOfWeek: false,
|
|
441
|
+
localeFirstDayOfYear: 'firstDayOfYear',
|
|
430
442
|
dayFormat: false,
|
|
431
443
|
weekdayFormat: false,
|
|
432
444
|
monthFormat: false,
|
|
@@ -691,8 +703,8 @@ const replacements = {
|
|
|
691
703
|
VSlider: {
|
|
692
704
|
backgroundColor: false,
|
|
693
705
|
tickLabels: 'ticks',
|
|
694
|
-
ticks
|
|
695
|
-
|
|
706
|
+
ticks(attr) {
|
|
707
|
+
return !attr.directive && !attr.value || attr.directive && [true, false].includes(attr.value.expression.value) ? 'show-ticks' : true;
|
|
696
708
|
},
|
|
697
709
|
vertical: {
|
|
698
710
|
name: 'direction',
|
|
@@ -706,8 +718,8 @@ const replacements = {
|
|
|
706
718
|
VRangeSlider: {
|
|
707
719
|
backgroundColor: false,
|
|
708
720
|
tickLabels: 'ticks',
|
|
709
|
-
ticks
|
|
710
|
-
|
|
721
|
+
ticks(attr) {
|
|
722
|
+
return !attr.directive && !attr.value || attr.directive && [true, false].includes(attr.value.expression.value) ? 'show-ticks' : true;
|
|
711
723
|
},
|
|
712
724
|
vertical: {
|
|
713
725
|
name: 'direction',
|
|
@@ -830,6 +842,12 @@ const replacements = {
|
|
|
830
842
|
link: false,
|
|
831
843
|
...link
|
|
832
844
|
},
|
|
845
|
+
VTable: {
|
|
846
|
+
dense: {
|
|
847
|
+
name: 'density',
|
|
848
|
+
value: 'compact'
|
|
849
|
+
}
|
|
850
|
+
},
|
|
833
851
|
VThemeProvider: {
|
|
834
852
|
root: false
|
|
835
853
|
},
|
|
@@ -854,7 +872,14 @@ const replacements = {
|
|
|
854
872
|
VToolbar: {
|
|
855
873
|
bottom: false,
|
|
856
874
|
outlined: 'border',
|
|
857
|
-
|
|
875
|
+
dense: {
|
|
876
|
+
name: 'density',
|
|
877
|
+
value: 'compact'
|
|
878
|
+
},
|
|
879
|
+
prominent: {
|
|
880
|
+
name: 'density',
|
|
881
|
+
value: 'prominent'
|
|
882
|
+
},
|
|
858
883
|
shaped: false,
|
|
859
884
|
short: false,
|
|
860
885
|
src: 'image',
|
|
@@ -904,6 +929,12 @@ const replacements = {
|
|
|
904
929
|
value: 'model-value',
|
|
905
930
|
...overlay
|
|
906
931
|
},
|
|
932
|
+
VTreeview: {
|
|
933
|
+
dense: {
|
|
934
|
+
name: 'density',
|
|
935
|
+
value: 'compact'
|
|
936
|
+
}
|
|
937
|
+
},
|
|
907
938
|
VWindow: {
|
|
908
939
|
activeClass: 'selected-class',
|
|
909
940
|
showArrowsOnHover: false,
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
hyphenate,
|
|
5
|
+
classify,
|
|
6
|
+
isVueTemplate
|
|
7
|
+
} = require('../util/helpers');
|
|
8
|
+
|
|
9
|
+
// VSnackbarQueue: #default slot was renamed to #item in Vuetify 4
|
|
10
|
+
const slotRenames = [{
|
|
11
|
+
component: 'VSnackbarQueue',
|
|
12
|
+
from: 'default',
|
|
13
|
+
to: 'item'
|
|
14
|
+
}];
|
|
15
|
+
|
|
16
|
+
// VSnackbar props deprecated in Vuetify 4
|
|
17
|
+
const propReplacements = {
|
|
18
|
+
VSnackbar: {
|
|
19
|
+
multiLine: {
|
|
20
|
+
name: 'min-height',
|
|
21
|
+
value: '68'
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// ------------------------------------------------------------------------------
|
|
27
|
+
// Rule Definition
|
|
28
|
+
// ------------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
module.exports = {
|
|
31
|
+
meta: {
|
|
32
|
+
docs: {
|
|
33
|
+
description: 'Disallow deprecated props and slots on Vuetify snackbar components.',
|
|
34
|
+
category: 'recommended'
|
|
35
|
+
},
|
|
36
|
+
fixable: 'code',
|
|
37
|
+
schema: [],
|
|
38
|
+
messages: {
|
|
39
|
+
replacedWith: `'{{ a }}' has been replaced with '{{ b }}'`,
|
|
40
|
+
renamed: `{{ component }}'s '{{ slot }}' slot has been renamed to '{{ newSlot }}'`
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
create(context) {
|
|
44
|
+
if (!isVueTemplate(context)) return {};
|
|
45
|
+
return context.sourceCode.parserServices.defineTemplateBodyVisitor({
|
|
46
|
+
VAttribute(attr) {
|
|
47
|
+
if (attr.directive && (attr.key.name.name !== 'bind' || !attr.key.argument)) return;
|
|
48
|
+
const tag = classify(attr.parent.parent.rawName);
|
|
49
|
+
if (!Object.keys(propReplacements).includes(tag)) return;
|
|
50
|
+
const propName = attr.directive ? hyphenate(attr.key.argument.rawName) : hyphenate(attr.key.rawName);
|
|
51
|
+
const propNameNode = attr.directive ? attr.key.argument : attr.key;
|
|
52
|
+
Object.entries(propReplacements[tag]).forEach(([test, replace]) => {
|
|
53
|
+
if (hyphenate(test) !== propName) return;
|
|
54
|
+
const oldValue = attr.directive ? context.sourceCode.getText(attr.value.expression) : attr.value?.value;
|
|
55
|
+
const value = typeof replace.value === 'function' ? replace.value(oldValue) : replace.value;
|
|
56
|
+
if (value == null || value === oldValue) return;
|
|
57
|
+
context.report({
|
|
58
|
+
messageId: 'replacedWith',
|
|
59
|
+
data: {
|
|
60
|
+
a: propName,
|
|
61
|
+
b: `${replace.name}="${value}"`
|
|
62
|
+
},
|
|
63
|
+
node: propNameNode,
|
|
64
|
+
fix(fixer) {
|
|
65
|
+
if (attr.directive && replace.bind !== false) {
|
|
66
|
+
if (replace.bind) {
|
|
67
|
+
return [fixer.replaceText(propNameNode, replace.name), fixer.replaceText(attr.value, `"${value}"`)];
|
|
68
|
+
} else {
|
|
69
|
+
const expression = context.sourceCode.getText(attr.value.expression);
|
|
70
|
+
return [fixer.replaceText(propNameNode, replace.name), fixer.replaceText(attr.value, `"${expression} ? '${value}' : undefined"`)];
|
|
71
|
+
}
|
|
72
|
+
} else {
|
|
73
|
+
return fixer.replaceText(attr, `${replace.bind ? ':' : ''}${replace.name}="${value}"`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
},
|
|
79
|
+
VElement(node) {
|
|
80
|
+
if (node.name !== 'template' || node.parent.type !== 'VElement') return;
|
|
81
|
+
const parentName = classify(node.parent.name);
|
|
82
|
+
for (const {
|
|
83
|
+
component,
|
|
84
|
+
from,
|
|
85
|
+
to
|
|
86
|
+
} of slotRenames) {
|
|
87
|
+
if (parentName !== component) continue;
|
|
88
|
+
const directive = node.startTag.attributes.find(attr => {
|
|
89
|
+
return attr.directive && attr.key.name.name === 'slot' && attr.key.argument?.name === from;
|
|
90
|
+
});
|
|
91
|
+
if (!directive) continue;
|
|
92
|
+
context.report({
|
|
93
|
+
node: directive,
|
|
94
|
+
messageId: 'renamed',
|
|
95
|
+
data: {
|
|
96
|
+
component: node.parent.name,
|
|
97
|
+
slot: from,
|
|
98
|
+
newSlot: to
|
|
99
|
+
},
|
|
100
|
+
fix(fixer) {
|
|
101
|
+
return fixer.replaceText(directive.key.argument, to);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
isVueTemplate
|
|
5
|
+
} = require('../util/helpers');
|
|
6
|
+
const md2 = {
|
|
7
|
+
'display-4': 'text-h1',
|
|
8
|
+
'display-3': 'text-h2',
|
|
9
|
+
'display-2': 'text-h3',
|
|
10
|
+
'display-1': 'text-h4',
|
|
11
|
+
headline: 'text-h5',
|
|
12
|
+
title: 'text-h6',
|
|
13
|
+
subheading: 'text-subtitle-1',
|
|
14
|
+
'subtitle-1': 'text-subtitle-1',
|
|
15
|
+
'subtitle-2': 'text-subtitle-2',
|
|
16
|
+
'body-1': 'text-body-1',
|
|
17
|
+
'body-2': 'text-body-2',
|
|
18
|
+
caption: 'text-caption',
|
|
19
|
+
overline: 'text-overline'
|
|
20
|
+
};
|
|
21
|
+
const md3 = {
|
|
22
|
+
'text-h1': 'text-display-large',
|
|
23
|
+
'text-h2': 'text-display-medium',
|
|
24
|
+
'text-h3': 'text-display-small',
|
|
25
|
+
'text-h4': 'text-headline-large',
|
|
26
|
+
'text-h5': 'text-headline-medium',
|
|
27
|
+
'text-h6': 'text-headline-small',
|
|
28
|
+
'text-subtitle-1': 'text-body-large',
|
|
29
|
+
'text-subtitle-2': 'text-label-large',
|
|
30
|
+
'text-body-1': 'text-body-large',
|
|
31
|
+
'text-body-2': 'text-body-medium',
|
|
32
|
+
'text-caption': 'text-body-small',
|
|
33
|
+
'text-button': 'text-label-large',
|
|
34
|
+
'text-overline': 'text-label-small'
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// ------------------------------------------------------------------------------
|
|
38
|
+
// Rule Definition
|
|
39
|
+
// ------------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
module.exports = {
|
|
42
|
+
md2,
|
|
43
|
+
md3,
|
|
44
|
+
meta: {
|
|
45
|
+
docs: {
|
|
46
|
+
description: 'Disallow deprecated MD2 typography classes, with configurable replacements.',
|
|
47
|
+
category: 'recommended'
|
|
48
|
+
},
|
|
49
|
+
fixable: 'code',
|
|
50
|
+
schema: [{
|
|
51
|
+
type: 'object',
|
|
52
|
+
additionalProperties: {
|
|
53
|
+
oneOf: [{
|
|
54
|
+
type: 'string'
|
|
55
|
+
}, {
|
|
56
|
+
type: 'boolean',
|
|
57
|
+
enum: [false]
|
|
58
|
+
}]
|
|
59
|
+
}
|
|
60
|
+
}],
|
|
61
|
+
messages: {
|
|
62
|
+
replacedWith: `'{{ a }}' has been replaced with '{{ b }}'`,
|
|
63
|
+
removed: `'{{ name }}' has been removed`
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
create(context) {
|
|
67
|
+
if (!isVueTemplate(context)) return {};
|
|
68
|
+
const replacements = {
|
|
69
|
+
...(context.options[0] || md3)
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// Remove entries the user set to false
|
|
73
|
+
for (const key of Object.keys(replacements)) {
|
|
74
|
+
if (replacements[key] === false) delete replacements[key];
|
|
75
|
+
}
|
|
76
|
+
return context.sourceCode.parserServices.defineTemplateBodyVisitor({
|
|
77
|
+
'VAttribute[key.name="class"]'(node) {
|
|
78
|
+
if (!node.value || !node.value.value) return;
|
|
79
|
+
const classes = node.value.value.split(/\s+/).filter(Boolean);
|
|
80
|
+
classes.forEach(className => {
|
|
81
|
+
const replace = replacements[className];
|
|
82
|
+
if (replace == null) return;
|
|
83
|
+
const idx = node.value.value.indexOf(className) + 1;
|
|
84
|
+
const range = [node.value.range[0] + idx, node.value.range[0] + idx + className.length];
|
|
85
|
+
const loc = {
|
|
86
|
+
start: context.sourceCode.getLocFromIndex(range[0]),
|
|
87
|
+
end: context.sourceCode.getLocFromIndex(range[1])
|
|
88
|
+
};
|
|
89
|
+
if (typeof replace === 'string') {
|
|
90
|
+
context.report({
|
|
91
|
+
loc,
|
|
92
|
+
messageId: 'replacedWith',
|
|
93
|
+
data: {
|
|
94
|
+
a: className,
|
|
95
|
+
b: replace
|
|
96
|
+
},
|
|
97
|
+
fix(fixer) {
|
|
98
|
+
return fixer.replaceTextRange(range, replace);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
isVueTemplate
|
|
5
|
+
} = require('../util/helpers');
|
|
6
|
+
const MAX_ELEVATION = 5;
|
|
7
|
+
|
|
8
|
+
// Match elevation-N in a class string, return the number
|
|
9
|
+
const elevationRegex = /\belevation-(\d+)\b/g;
|
|
10
|
+
function checkClassValue(context, node, value) {
|
|
11
|
+
let match;
|
|
12
|
+
elevationRegex.lastIndex = 0;
|
|
13
|
+
while ((match = elevationRegex.exec(value)) !== null) {
|
|
14
|
+
const level = parseInt(match[1], 10);
|
|
15
|
+
if (level > MAX_ELEVATION) {
|
|
16
|
+
context.report({
|
|
17
|
+
messageId: 'overflow',
|
|
18
|
+
data: {
|
|
19
|
+
level: String(level),
|
|
20
|
+
max: String(MAX_ELEVATION)
|
|
21
|
+
},
|
|
22
|
+
node
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ------------------------------------------------------------------------------
|
|
29
|
+
// Rule Definition
|
|
30
|
+
// ------------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
module.exports = {
|
|
33
|
+
meta: {
|
|
34
|
+
docs: {
|
|
35
|
+
description: 'Disallow elevation classes above the MD3 maximum (0–5).',
|
|
36
|
+
category: 'recommended'
|
|
37
|
+
},
|
|
38
|
+
fixable: null,
|
|
39
|
+
schema: [],
|
|
40
|
+
messages: {
|
|
41
|
+
overflow: 'Elevation level {{ level }} exceeds the MD3 maximum of {{ max }}.'
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
create(context) {
|
|
45
|
+
if (!isVueTemplate(context)) return {};
|
|
46
|
+
return context.sourceCode.parserServices.defineTemplateBodyVisitor({
|
|
47
|
+
VAttribute(attr) {
|
|
48
|
+
// Static class="elevation-10"
|
|
49
|
+
if (!attr.directive && attr.key.rawName === 'class' && attr.value) {
|
|
50
|
+
checkClassValue(context, attr, attr.value.value);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// :class="'elevation-10'" or :class="`elevation-${n}`" — check literal strings
|
|
55
|
+
if (attr.directive && attr.key.name.name === 'bind' && attr.key.argument?.rawName === 'class' && attr.value?.expression?.type === 'Literal' && typeof attr.value.expression.value === 'string') {
|
|
56
|
+
checkClassValue(context, attr, attr.value.expression.value);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// :elevation="10" or elevation="10" on any component
|
|
61
|
+
const propName = attr.directive ? attr.key.argument?.rawName : attr.key.rawName;
|
|
62
|
+
if (propName !== 'elevation') return;
|
|
63
|
+
const value = attr.directive ? attr.value?.expression?.type === 'Literal' ? attr.value.expression.value : null : attr.value ? Number(attr.value.value) : null;
|
|
64
|
+
if (typeof value === 'number' && value > MAX_ELEVATION) {
|
|
65
|
+
context.report({
|
|
66
|
+
messageId: 'overflow',
|
|
67
|
+
data: {
|
|
68
|
+
level: String(value),
|
|
69
|
+
max: String(MAX_ELEVATION)
|
|
70
|
+
},
|
|
71
|
+
node: attr
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
isVueTemplate
|
|
5
|
+
} = require('../util/helpers');
|
|
6
|
+
const {
|
|
7
|
+
addClass,
|
|
8
|
+
removeAttr
|
|
9
|
+
} = require('../util/fixers');
|
|
10
|
+
const validElevations = new Set(['0', '1', '2', '3', '4', '5']);
|
|
11
|
+
module.exports = {
|
|
12
|
+
meta: {
|
|
13
|
+
docs: {
|
|
14
|
+
description: 'Disallow the `elevation` prop; use Tailwind shadow utilities instead.',
|
|
15
|
+
category: 'tailwindcss'
|
|
16
|
+
},
|
|
17
|
+
fixable: 'code',
|
|
18
|
+
schema: [],
|
|
19
|
+
messages: {
|
|
20
|
+
replacedWith: `'elevation="{{ value }}"' should be replaced with class="{{ className }}"`,
|
|
21
|
+
noFix: `'elevation' prop should be replaced with a Tailwind shadow utility class`
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
create(context) {
|
|
25
|
+
if (!isVueTemplate(context)) return {};
|
|
26
|
+
return context.sourceCode.parserServices.defineTemplateBodyVisitor({
|
|
27
|
+
VAttribute(attr) {
|
|
28
|
+
if (attr.directive && (attr.key.name.name !== 'bind' || !attr.key.argument)) return;
|
|
29
|
+
const propName = attr.directive ? attr.key.argument.rawName : attr.key.rawName;
|
|
30
|
+
if (propName !== 'elevation') return;
|
|
31
|
+
const element = attr.parent.parent;
|
|
32
|
+
const propNameNode = attr.directive ? attr.key.argument : attr.key;
|
|
33
|
+
|
|
34
|
+
// Get static value
|
|
35
|
+
const value = attr.directive ? attr.value?.expression?.type === 'Literal' ? String(attr.value.expression.value) : null : attr.value?.value;
|
|
36
|
+
if (value != null && validElevations.has(value)) {
|
|
37
|
+
const className = `elevation-${value}`;
|
|
38
|
+
context.report({
|
|
39
|
+
messageId: 'replacedWith',
|
|
40
|
+
data: {
|
|
41
|
+
value,
|
|
42
|
+
className
|
|
43
|
+
},
|
|
44
|
+
node: propNameNode,
|
|
45
|
+
fix(fixer) {
|
|
46
|
+
return [addClass(context, fixer, element, className), removeAttr(context, fixer, attr)];
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
} else {
|
|
50
|
+
context.report({
|
|
51
|
+
messageId: 'noFix',
|
|
52
|
+
node: propNameNode
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
hyphenate,
|
|
5
|
+
classify,
|
|
6
|
+
isVueTemplate
|
|
7
|
+
} = require('../util/helpers');
|
|
8
|
+
const {
|
|
9
|
+
addClass,
|
|
10
|
+
removeAttr
|
|
11
|
+
} = require('../util/fixers');
|
|
12
|
+
const breakpoints = ['sm', 'md', 'lg', 'xl', 'xxl'];
|
|
13
|
+
|
|
14
|
+
// VRow props that should become utility classes
|
|
15
|
+
const rowPropToClass = {
|
|
16
|
+
align: value => `align-${value}`,
|
|
17
|
+
justify: value => `justify-${value}`,
|
|
18
|
+
'align-content': value => `align-content-${value}`
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// Generate responsive variants: align-sm, align-md, justify-sm, etc.
|
|
22
|
+
for (const [prop, fn] of Object.entries({
|
|
23
|
+
...rowPropToClass
|
|
24
|
+
})) {
|
|
25
|
+
for (const bp of breakpoints) {
|
|
26
|
+
rowPropToClass[`${prop}-${bp}`] = value => `${fn(value).replace(value, '')}-${bp}-${value}`;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
// Fix responsive class generation — the base fn closures captured value incorrectly, redo properly
|
|
30
|
+
for (const bp of breakpoints) {
|
|
31
|
+
rowPropToClass[`align-${bp}`] = value => `align-${bp}-${value}`;
|
|
32
|
+
rowPropToClass[`justify-${bp}`] = value => `justify-${bp}-${value}`;
|
|
33
|
+
rowPropToClass[`align-content-${bp}`] = value => `align-content-${bp}-${value}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// VCol props that should become utility classes
|
|
37
|
+
const colPropToClass = {
|
|
38
|
+
order: value => `order-${value}`,
|
|
39
|
+
'align-self': value => `align-self-${value}`
|
|
40
|
+
};
|
|
41
|
+
for (const bp of breakpoints) {
|
|
42
|
+
colPropToClass[`order-${bp}`] = value => `order-${bp}-${value}`;
|
|
43
|
+
}
|
|
44
|
+
const replacements = {
|
|
45
|
+
VRow: {
|
|
46
|
+
props: rowPropToClass,
|
|
47
|
+
renamed: {
|
|
48
|
+
dense: {
|
|
49
|
+
name: 'density',
|
|
50
|
+
value: 'compact'
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
VCol: {
|
|
55
|
+
props: colPropToClass,
|
|
56
|
+
renamed: {}
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// ------------------------------------------------------------------------------
|
|
61
|
+
// Rule Definition
|
|
62
|
+
// ------------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
module.exports = {
|
|
65
|
+
meta: {
|
|
66
|
+
docs: {
|
|
67
|
+
description: 'Prevent the use of removed grid props.',
|
|
68
|
+
category: 'recommended'
|
|
69
|
+
},
|
|
70
|
+
fixable: 'code',
|
|
71
|
+
schema: [],
|
|
72
|
+
messages: {
|
|
73
|
+
movedToClass: `'{{ prop }}' has been removed, use class="{{ className }}" instead`,
|
|
74
|
+
replacedWith: `'{{ a }}' has been replaced with '{{ b }}'`
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
create(context) {
|
|
78
|
+
if (!isVueTemplate(context)) return {};
|
|
79
|
+
return context.sourceCode.parserServices.defineTemplateBodyVisitor({
|
|
80
|
+
VAttribute(attr) {
|
|
81
|
+
if (attr.directive && (attr.key.name.name !== 'bind' || !attr.key.argument)) return;
|
|
82
|
+
const tag = classify(attr.parent.parent.rawName);
|
|
83
|
+
const config = replacements[tag];
|
|
84
|
+
if (!config) return;
|
|
85
|
+
const propName = attr.directive ? hyphenate(attr.key.argument.rawName) : hyphenate(attr.key.rawName);
|
|
86
|
+
const propNameNode = attr.directive ? attr.key.argument : attr.key;
|
|
87
|
+
const element = attr.parent.parent;
|
|
88
|
+
|
|
89
|
+
// Check for props that should become utility classes
|
|
90
|
+
if (config.props[propName]) {
|
|
91
|
+
const toClass = config.props[propName];
|
|
92
|
+
|
|
93
|
+
// Need a static value to auto-fix
|
|
94
|
+
const value = attr.directive ? attr.value?.expression?.type === 'Literal' ? String(attr.value.expression.value) : null : attr.value?.value;
|
|
95
|
+
if (value) {
|
|
96
|
+
const className = toClass(value);
|
|
97
|
+
context.report({
|
|
98
|
+
messageId: 'movedToClass',
|
|
99
|
+
data: {
|
|
100
|
+
prop: propName,
|
|
101
|
+
className
|
|
102
|
+
},
|
|
103
|
+
node: propNameNode,
|
|
104
|
+
fix(fixer) {
|
|
105
|
+
return [addClass(context, fixer, element, className), removeAttr(context, fixer, attr)];
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
} else {
|
|
109
|
+
// Dynamic value — can't auto-fix but still report
|
|
110
|
+
const className = toClass('<value>');
|
|
111
|
+
context.report({
|
|
112
|
+
messageId: 'movedToClass',
|
|
113
|
+
data: {
|
|
114
|
+
prop: propName,
|
|
115
|
+
className
|
|
116
|
+
},
|
|
117
|
+
node: propNameNode
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Check for renamed props (e.g. dense -> density="compact")
|
|
124
|
+
const renamed = config.renamed[propName];
|
|
125
|
+
if (renamed) {
|
|
126
|
+
context.report({
|
|
127
|
+
messageId: 'replacedWith',
|
|
128
|
+
data: {
|
|
129
|
+
a: propName,
|
|
130
|
+
b: `${renamed.name}="${renamed.value}"`
|
|
131
|
+
},
|
|
132
|
+
node: propNameNode,
|
|
133
|
+
fix(fixer) {
|
|
134
|
+
return fixer.replaceText(attr, `${renamed.name}="${renamed.value}"`);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
isVueTemplate
|
|
5
|
+
} = require('../util/helpers');
|
|
6
|
+
const defaultReplacements = {
|
|
7
|
+
'd-flex': 'flex',
|
|
8
|
+
'd-inline-flex': 'inline-flex',
|
|
9
|
+
'd-block': 'block',
|
|
10
|
+
'd-inline-block': 'inline-block',
|
|
11
|
+
'd-inline': 'inline',
|
|
12
|
+
'd-none': 'hidden',
|
|
13
|
+
'd-grid': 'grid',
|
|
14
|
+
'align-center': 'items-center',
|
|
15
|
+
'align-start': 'items-start',
|
|
16
|
+
'align-end': 'items-end',
|
|
17
|
+
'align-baseline': 'items-baseline',
|
|
18
|
+
'align-stretch': 'items-stretch',
|
|
19
|
+
'justify-space-between': 'justify-between',
|
|
20
|
+
'justify-space-around': 'justify-around',
|
|
21
|
+
'justify-space-evenly': 'justify-evenly',
|
|
22
|
+
'flex-grow-1': 'grow',
|
|
23
|
+
'flex-grow-0': 'grow-0',
|
|
24
|
+
'flex-shrink-1': 'shrink',
|
|
25
|
+
'flex-shrink-0': 'shrink-0',
|
|
26
|
+
'flex-column': 'flex-col',
|
|
27
|
+
'font-weight-bold': 'font-bold',
|
|
28
|
+
'font-weight-medium': 'font-medium',
|
|
29
|
+
'font-weight-regular': 'font-normal',
|
|
30
|
+
'font-weight-light': 'font-light',
|
|
31
|
+
'font-weight-thin': 'font-thin',
|
|
32
|
+
'text-truncate': 'truncate',
|
|
33
|
+
'text-no-wrap': 'whitespace-nowrap',
|
|
34
|
+
'fill-height': 'h-full',
|
|
35
|
+
'w-100': 'w-full',
|
|
36
|
+
'h-100': 'h-full'
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Generate spacing utility mappings only for prefixes that differ between Vuetify and Tailwind
|
|
40
|
+
// ma → m, pa → p (all other prefixes like mx, mt, px, pt etc. are already identical)
|
|
41
|
+
for (let i = 0; i <= 16; i++) {
|
|
42
|
+
defaultReplacements[`ma-${i}`] = `m-${i}`;
|
|
43
|
+
defaultReplacements[`pa-${i}`] = `p-${i}`;
|
|
44
|
+
}
|
|
45
|
+
defaultReplacements['ma-auto'] = 'm-auto';
|
|
46
|
+
|
|
47
|
+
// Negative margins: ma-n1..ma-n16 → -m-1..-m-16
|
|
48
|
+
for (let i = 1; i <= 16; i++) {
|
|
49
|
+
defaultReplacements[`ma-n${i}`] = `-m-${i}`;
|
|
50
|
+
}
|
|
51
|
+
module.exports = {
|
|
52
|
+
defaultReplacements,
|
|
53
|
+
meta: {
|
|
54
|
+
docs: {
|
|
55
|
+
description: 'Disallow Vuetify utility classes; use Tailwind equivalents instead.',
|
|
56
|
+
category: 'tailwindcss'
|
|
57
|
+
},
|
|
58
|
+
fixable: 'code',
|
|
59
|
+
schema: [{
|
|
60
|
+
type: 'object',
|
|
61
|
+
additionalProperties: {
|
|
62
|
+
oneOf: [{
|
|
63
|
+
type: 'string'
|
|
64
|
+
}, {
|
|
65
|
+
type: 'boolean',
|
|
66
|
+
enum: [false]
|
|
67
|
+
}]
|
|
68
|
+
}
|
|
69
|
+
}],
|
|
70
|
+
messages: {
|
|
71
|
+
replacedWith: `'{{ a }}' has been replaced with '{{ b }}'`
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
create(context) {
|
|
75
|
+
if (!isVueTemplate(context)) return {};
|
|
76
|
+
const replacements = {
|
|
77
|
+
...defaultReplacements,
|
|
78
|
+
...(context.options[0] || {})
|
|
79
|
+
};
|
|
80
|
+
for (const key of Object.keys(replacements)) {
|
|
81
|
+
if (replacements[key] === false) delete replacements[key];
|
|
82
|
+
}
|
|
83
|
+
return context.sourceCode.parserServices.defineTemplateBodyVisitor({
|
|
84
|
+
'VAttribute[key.name="class"]'(node) {
|
|
85
|
+
if (!node.value || !node.value.value) return;
|
|
86
|
+
const classes = node.value.value.split(/\s+/).filter(Boolean);
|
|
87
|
+
classes.forEach(className => {
|
|
88
|
+
const replace = replacements[className];
|
|
89
|
+
if (replace == null) return;
|
|
90
|
+
const idx = node.value.value.indexOf(className) + 1;
|
|
91
|
+
const range = [node.value.range[0] + idx, node.value.range[0] + idx + className.length];
|
|
92
|
+
const loc = {
|
|
93
|
+
start: context.sourceCode.getLocFromIndex(range[0]),
|
|
94
|
+
end: context.sourceCode.getLocFromIndex(range[1])
|
|
95
|
+
};
|
|
96
|
+
context.report({
|
|
97
|
+
loc,
|
|
98
|
+
messageId: 'replacedWith',
|
|
99
|
+
data: {
|
|
100
|
+
a: className,
|
|
101
|
+
b: replace
|
|
102
|
+
},
|
|
103
|
+
fix(fixer) {
|
|
104
|
+
return fixer.replaceTextRange(range, replace);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
isVueTemplate
|
|
5
|
+
} = require('../util/helpers');
|
|
6
|
+
const {
|
|
7
|
+
addClass,
|
|
8
|
+
removeAttr
|
|
9
|
+
} = require('../util/fixers');
|
|
10
|
+
const roundedMap = {
|
|
11
|
+
'': 'rounded',
|
|
12
|
+
0: 'rounded-none',
|
|
13
|
+
sm: 'rounded-sm',
|
|
14
|
+
lg: 'rounded-lg',
|
|
15
|
+
xl: 'rounded-xl',
|
|
16
|
+
circle: 'rounded-full',
|
|
17
|
+
pill: 'rounded-full',
|
|
18
|
+
shaped: 'rounded-te-xl rounded-bs-xl'
|
|
19
|
+
};
|
|
20
|
+
module.exports = {
|
|
21
|
+
meta: {
|
|
22
|
+
docs: {
|
|
23
|
+
description: 'Disallow the `rounded` prop; use Tailwind rounded utilities instead.',
|
|
24
|
+
category: 'tailwindcss'
|
|
25
|
+
},
|
|
26
|
+
fixable: 'code',
|
|
27
|
+
schema: [],
|
|
28
|
+
messages: {
|
|
29
|
+
replacedWith: `'rounded{{ valueDisplay }}' should be replaced with class="{{ className }}"`,
|
|
30
|
+
noFix: `'rounded' prop should be replaced with a Tailwind rounded utility class`
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
create(context) {
|
|
34
|
+
if (!isVueTemplate(context)) return {};
|
|
35
|
+
return context.sourceCode.parserServices.defineTemplateBodyVisitor({
|
|
36
|
+
VAttribute(attr) {
|
|
37
|
+
if (attr.directive && (attr.key.name.name !== 'bind' || !attr.key.argument)) return;
|
|
38
|
+
const propName = attr.directive ? attr.key.argument.rawName : attr.key.rawName;
|
|
39
|
+
if (propName !== 'rounded') return;
|
|
40
|
+
const element = attr.parent.parent;
|
|
41
|
+
const propNameNode = attr.directive ? attr.key.argument : attr.key;
|
|
42
|
+
|
|
43
|
+
// Boolean attribute (no value) — `rounded` with no `="..."`
|
|
44
|
+
if (!attr.directive && !attr.value) {
|
|
45
|
+
const className = roundedMap[''];
|
|
46
|
+
context.report({
|
|
47
|
+
messageId: 'replacedWith',
|
|
48
|
+
data: {
|
|
49
|
+
valueDisplay: '',
|
|
50
|
+
className
|
|
51
|
+
},
|
|
52
|
+
node: propNameNode,
|
|
53
|
+
fix(fixer) {
|
|
54
|
+
return [addClass(context, fixer, element, className), removeAttr(context, fixer, attr)];
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Get static value
|
|
61
|
+
const value = attr.directive ? attr.value?.expression?.type === 'Literal' ? String(attr.value.expression.value) : null : attr.value?.value;
|
|
62
|
+
if (value != null && roundedMap[value] != null) {
|
|
63
|
+
const className = roundedMap[value];
|
|
64
|
+
context.report({
|
|
65
|
+
messageId: 'replacedWith',
|
|
66
|
+
data: {
|
|
67
|
+
valueDisplay: `="${value}"`,
|
|
68
|
+
className
|
|
69
|
+
},
|
|
70
|
+
node: propNameNode,
|
|
71
|
+
fix(fixer) {
|
|
72
|
+
return [addClass(context, fixer, element, className), removeAttr(context, fixer, attr)];
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
} else {
|
|
76
|
+
context.report({
|
|
77
|
+
messageId: 'noFix',
|
|
78
|
+
node: propNameNode
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
};
|
package/lib/util/helpers.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const path = require('node:path');
|
|
4
|
-
function hyphenate(/* istanbul ignore next */
|
|
4
|
+
function hyphenate( /* istanbul ignore next */
|
|
5
5
|
str = '') {
|
|
6
6
|
return str.replace(/\B([A-Z])/g, '-$1').toLowerCase();
|
|
7
7
|
}
|
|
@@ -44,7 +44,8 @@ function mergeDeep(source, target) {
|
|
|
44
44
|
}
|
|
45
45
|
function isVueTemplate(context) {
|
|
46
46
|
if (context.sourceCode.parserServices.defineTemplateBodyVisitor == null) {
|
|
47
|
-
|
|
47
|
+
const filename = context.filename ?? context.getFilename?.() ?? '';
|
|
48
|
+
return path.extname(filename) === '.vue';
|
|
48
49
|
}
|
|
49
50
|
return true;
|
|
50
51
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-vuetify",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"description": "An eslint plugin for Vuetify",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"author": "Kael Watts-Deuchar <kaelwd@gmail.com>",
|
|
@@ -9,34 +9,39 @@
|
|
|
9
9
|
"scripts": {
|
|
10
10
|
"build": "rimraf lib && babel src --out-dir lib",
|
|
11
11
|
"test": "mocha tests --recursive --reporter dot",
|
|
12
|
-
"test:8": "ESLINT8=true mocha tests --recursive --reporter dot",
|
|
12
|
+
"test:8": "cross-env ESLINT8=true mocha tests --recursive --reporter dot",
|
|
13
|
+
"test:9": "cross-env ESLINT9=true mocha tests --recursive --reporter dot",
|
|
13
14
|
"test:coverage": "nyc mocha tests --recursive --reporter dot",
|
|
14
15
|
"test:ci": "nyc --reporter=lcov mocha tests --recursive --reporter dot",
|
|
15
16
|
"lint": "eslint src tests",
|
|
16
|
-
"prepublishOnly": "npm run build"
|
|
17
|
+
"prepublishOnly": "npm run build",
|
|
18
|
+
"release": "bumpp -r"
|
|
17
19
|
},
|
|
18
20
|
"files": [
|
|
19
21
|
"lib"
|
|
20
22
|
],
|
|
21
23
|
"homepage": "https://github.com/vuetifyjs/eslint-plugin-vuetify#readme",
|
|
22
24
|
"dependencies": {
|
|
23
|
-
"eslint-plugin-vue": "
|
|
25
|
+
"eslint-plugin-vue": "^10.8.0",
|
|
24
26
|
"requireindex": "^1.2.0"
|
|
25
27
|
},
|
|
26
28
|
"devDependencies": {
|
|
27
29
|
"@babel/cli": "^7.19.3",
|
|
28
30
|
"@babel/core": "^7.19.6",
|
|
29
31
|
"@babel/preset-env": "^7.19.4",
|
|
30
|
-
"@stylistic/eslint-plugin": "^
|
|
32
|
+
"@stylistic/eslint-plugin": "^4.4.1",
|
|
33
|
+
"bumpp": "^10.1.0",
|
|
31
34
|
"conventional-changelog-cli": "^2.2.2",
|
|
32
|
-
"
|
|
35
|
+
"cross-env": "^7.0.3",
|
|
36
|
+
"conventional-changelog-vuetify": "^2.0.2",
|
|
33
37
|
"conventional-github-releaser": "^3.1.5",
|
|
34
|
-
"eslint": "^
|
|
35
|
-
"eslint8": "npm:eslint@8.57.1",
|
|
38
|
+
"eslint": "^10.0.0",
|
|
36
39
|
"eslint-plugin-vue": "^10.0.0",
|
|
40
|
+
"eslint8": "npm:eslint@8.57.1",
|
|
41
|
+
"eslint9": "npm:eslint@9",
|
|
37
42
|
"husky": "^8.0.1",
|
|
38
43
|
"mocha": "^10.1.0",
|
|
39
|
-
"neostandard": "^0.
|
|
44
|
+
"neostandard": "^0.13.0",
|
|
40
45
|
"nyc": "^15.1.0",
|
|
41
46
|
"rimraf": "^3.0.2",
|
|
42
47
|
"vue": "^3.5.13",
|
|
@@ -44,8 +49,18 @@
|
|
|
44
49
|
"vuetify": "^3.7.17"
|
|
45
50
|
},
|
|
46
51
|
"peerDependencies": {
|
|
47
|
-
"eslint": "^8.0.0 || ^9.0.0",
|
|
52
|
+
"eslint": "^8.0.0 || ^9.0.0 || ^10.0.0",
|
|
48
53
|
"vuetify": "^3.0.0"
|
|
49
54
|
},
|
|
50
|
-
"packageManager": "pnpm@
|
|
55
|
+
"packageManager": "pnpm@10.26.1",
|
|
56
|
+
"pnpm": {
|
|
57
|
+
"overrides": {
|
|
58
|
+
"@stylistic/eslint-plugin": "$@stylistic/eslint-plugin"
|
|
59
|
+
},
|
|
60
|
+
"peerDependencyRules": {
|
|
61
|
+
"allowedVersions": {
|
|
62
|
+
"eslint": "10"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
51
66
|
}
|