eslint-plugin-vuetify 2.5.3 → 2.7.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 +101 -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/eslint-typegen.d.ts +136 -0
- package/lib/index.d.ts +20 -0
- package/lib/index.js +5 -1
- package/lib/rules/custom-deprecated-components.js +111 -0
- package/lib/rules/custom-deprecated-events.js +103 -0
- package/lib/rules/custom-deprecated-props.js +100 -0
- package/lib/rules/custom-deprecated-slots.js +100 -0
- package/lib/rules/grid-unknown-attributes.js +1 -1
- package/lib/rules/no-border-prop.js +73 -0
- package/lib/rules/no-deprecated-components.js +0 -1
- 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/grid-attributes.js +3 -1
- package/lib/util/helpers.js +3 -2
- package/package.json +21 -13
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
classify,
|
|
5
|
+
isVueTemplate
|
|
6
|
+
} = require('../util/helpers');
|
|
7
|
+
module.exports = {
|
|
8
|
+
meta: {
|
|
9
|
+
docs: {
|
|
10
|
+
description: 'Disallow usage of specified component slots, with optional replacements'
|
|
11
|
+
},
|
|
12
|
+
fixable: 'code',
|
|
13
|
+
schema: [{
|
|
14
|
+
type: 'object',
|
|
15
|
+
additionalProperties: {
|
|
16
|
+
type: 'object',
|
|
17
|
+
additionalProperties: {
|
|
18
|
+
oneOf: [{
|
|
19
|
+
type: 'string'
|
|
20
|
+
}, {
|
|
21
|
+
type: 'boolean',
|
|
22
|
+
enum: [false]
|
|
23
|
+
}, {
|
|
24
|
+
type: 'object',
|
|
25
|
+
properties: {
|
|
26
|
+
message: {
|
|
27
|
+
type: 'string'
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
required: ['message'],
|
|
31
|
+
additionalProperties: false
|
|
32
|
+
}]
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}],
|
|
36
|
+
messages: {
|
|
37
|
+
replacedWith: `{{ component }}'s '{{ slot }}' slot has been replaced with '{{ newSlot }}'`,
|
|
38
|
+
removed: `{{ component }}'s '{{ slot }}' slot has been removed`,
|
|
39
|
+
removedWithMessage: `{{ component }}'s '{{ slot }}' slot has been removed: {{ message }}`
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
create(context) {
|
|
43
|
+
if (!isVueTemplate(context)) return {};
|
|
44
|
+
const options = context.options[0];
|
|
45
|
+
if (!options || !Object.keys(options).length) return {};
|
|
46
|
+
|
|
47
|
+
// Normalize keys to PascalCase
|
|
48
|
+
const replacements = new Map();
|
|
49
|
+
for (const [component, slots] of Object.entries(options)) {
|
|
50
|
+
replacements.set(classify(component), slots);
|
|
51
|
+
}
|
|
52
|
+
return context.sourceCode.parserServices.defineTemplateBodyVisitor({
|
|
53
|
+
VElement(node) {
|
|
54
|
+
if (node.name !== 'template' || node.parent.type !== 'VElement') return;
|
|
55
|
+
const tag = classify(node.parent.name);
|
|
56
|
+
if (!replacements.has(tag)) return;
|
|
57
|
+
const slots = replacements.get(tag);
|
|
58
|
+
const directive = node.startTag.attributes.find(attr => {
|
|
59
|
+
return attr.directive && attr.key.name.name === 'slot' && attr.key.argument?.name && slots[attr.key.argument.name] !== undefined;
|
|
60
|
+
});
|
|
61
|
+
if (!directive) return;
|
|
62
|
+
const slotName = directive.key.argument.name;
|
|
63
|
+
const replace = slots[slotName];
|
|
64
|
+
if (replace === false) {
|
|
65
|
+
context.report({
|
|
66
|
+
messageId: 'removed',
|
|
67
|
+
data: {
|
|
68
|
+
component: node.parent.name,
|
|
69
|
+
slot: slotName
|
|
70
|
+
},
|
|
71
|
+
node: directive
|
|
72
|
+
});
|
|
73
|
+
} else if (typeof replace === 'string') {
|
|
74
|
+
context.report({
|
|
75
|
+
messageId: 'replacedWith',
|
|
76
|
+
data: {
|
|
77
|
+
component: node.parent.name,
|
|
78
|
+
slot: slotName,
|
|
79
|
+
newSlot: replace
|
|
80
|
+
},
|
|
81
|
+
node: directive,
|
|
82
|
+
fix(fixer) {
|
|
83
|
+
return fixer.replaceText(directive.key.argument, replace);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
} else if (typeof replace === 'object' && replace !== null) {
|
|
87
|
+
context.report({
|
|
88
|
+
messageId: 'removedWithMessage',
|
|
89
|
+
data: {
|
|
90
|
+
component: node.parent.name,
|
|
91
|
+
slot: slotName,
|
|
92
|
+
message: replace.message
|
|
93
|
+
},
|
|
94
|
+
node: directive
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
};
|
|
@@ -33,7 +33,7 @@ const tags = Object.keys(VGrid).reduce((t, k) => {
|
|
|
33
33
|
module.exports = {
|
|
34
34
|
meta: {
|
|
35
35
|
docs: {
|
|
36
|
-
description: '
|
|
36
|
+
description: 'Warn about v1 grid attributes not being auto-converted to classes in v2.',
|
|
37
37
|
category: 'recommended'
|
|
38
38
|
},
|
|
39
39
|
fixable: 'code',
|
|
@@ -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
|
+
};
|
|
@@ -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"'
|
|
@@ -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
|
+
};
|