eslint-plugin-mui-v7 1.0.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/LICENSE +21 -0
- package/README.md +272 -0
- package/index.cjs +439 -0
- package/index.js +439 -0
- package/package.json +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Matheus Pimenta (Koda AI Studio)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
# eslint-plugin-mui-v7
|
|
2
|
+
|
|
3
|
+
> ESLint plugin for Material-UI v7 with educational and friendly error messages
|
|
4
|
+
|
|
5
|
+
Automatically detect incorrect usage of Material-UI V7 and teach developers the right way through helpful messages with emojis and examples!
|
|
6
|
+
|
|
7
|
+
## ✨ Features
|
|
8
|
+
|
|
9
|
+
- ❌ **Detect deprecated deep imports** - No more `import createTheme from '@mui/material/styles/createTheme'`
|
|
10
|
+
- ❌ **Catch Grid2 usage** - Grid2 was renamed to Grid in V7
|
|
11
|
+
- ❌ **Find moved @mui/lab components** - Alert, Skeleton, Rating, etc. are now in @mui/material
|
|
12
|
+
- ❌ **Detect deprecated props** - onBackdropClick, size="normal", Hidden component
|
|
13
|
+
- ❌ **Grid item prop detection** - Grid doesn't use `item` prop anymore, use `size` instead
|
|
14
|
+
- ⚠️ **Theme variables suggestion** - Use `theme.vars.*` for automatic dark mode support
|
|
15
|
+
- 🔧 **Auto-fix available** for most rules!
|
|
16
|
+
|
|
17
|
+
## 📦 Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install --save-dev eslint-plugin-mui-v7
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## 🚀 Usage
|
|
24
|
+
|
|
25
|
+
### ESLint 9+ (Flat Config)
|
|
26
|
+
|
|
27
|
+
```javascript
|
|
28
|
+
// eslint.config.js
|
|
29
|
+
import muiV7Plugin from 'eslint-plugin-mui-v7'
|
|
30
|
+
|
|
31
|
+
export default [
|
|
32
|
+
{
|
|
33
|
+
plugins: {
|
|
34
|
+
'mui-v7': muiV7Plugin,
|
|
35
|
+
},
|
|
36
|
+
rules: {
|
|
37
|
+
// Errors (block code)
|
|
38
|
+
'mui-v7/no-deep-imports': 'error',
|
|
39
|
+
'mui-v7/no-grid2-import': 'error',
|
|
40
|
+
'mui-v7/no-lab-imports': 'error',
|
|
41
|
+
'mui-v7/no-grid-item-prop': 'error',
|
|
42
|
+
'mui-v7/no-deprecated-props': 'error',
|
|
43
|
+
|
|
44
|
+
// Warnings (suggest improvements)
|
|
45
|
+
'mui-v7/no-old-grid-import': 'warn',
|
|
46
|
+
'mui-v7/prefer-theme-vars': 'warn',
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
]
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### ESLint <9 (Legacy Config)
|
|
53
|
+
|
|
54
|
+
```javascript
|
|
55
|
+
// .eslintrc.js
|
|
56
|
+
module.exports = {
|
|
57
|
+
plugins: ['mui-v7'],
|
|
58
|
+
rules: {
|
|
59
|
+
'mui-v7/no-deep-imports': 'error',
|
|
60
|
+
'mui-v7/no-grid2-import': 'error',
|
|
61
|
+
'mui-v7/no-lab-imports': 'error',
|
|
62
|
+
'mui-v7/no-grid-item-prop': 'error',
|
|
63
|
+
'mui-v7/no-deprecated-props': 'error',
|
|
64
|
+
'mui-v7/no-old-grid-import': 'warn',
|
|
65
|
+
'mui-v7/prefer-theme-vars': 'warn',
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Using Recommended Config
|
|
71
|
+
|
|
72
|
+
```javascript
|
|
73
|
+
// eslint.config.js
|
|
74
|
+
import muiV7Plugin from 'eslint-plugin-mui-v7'
|
|
75
|
+
|
|
76
|
+
export default [
|
|
77
|
+
muiV7Plugin.configs.recommended, // Applies all recommended rules
|
|
78
|
+
]
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## 📋 Rules
|
|
82
|
+
|
|
83
|
+
### 🚨 Error Rules (Must Fix)
|
|
84
|
+
|
|
85
|
+
#### `mui-v7/no-deep-imports`
|
|
86
|
+
|
|
87
|
+
Deep imports with more than one level are not supported in MUI V7.
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
// ❌ Bad
|
|
91
|
+
import createTheme from '@mui/material/styles/createTheme'
|
|
92
|
+
|
|
93
|
+
// ✅ Good
|
|
94
|
+
import { createTheme } from '@mui/material/styles'
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
#### `mui-v7/no-grid2-import`
|
|
98
|
+
|
|
99
|
+
Grid2 was renamed to Grid in V7.
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
// ❌ Bad
|
|
103
|
+
import Grid2 from '@mui/material/Grid2'
|
|
104
|
+
|
|
105
|
+
// ✅ Good
|
|
106
|
+
import Grid from '@mui/material/Grid'
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
#### `mui-v7/no-lab-imports`
|
|
110
|
+
|
|
111
|
+
Components moved from @mui/lab to @mui/material.
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
// ❌ Bad
|
|
115
|
+
import Alert from '@mui/lab/Alert'
|
|
116
|
+
import Skeleton from '@mui/lab/Skeleton'
|
|
117
|
+
|
|
118
|
+
// ✅ Good
|
|
119
|
+
import Alert from '@mui/material/Alert'
|
|
120
|
+
import Skeleton from '@mui/material/Skeleton'
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**Moved components:** Alert, AlertTitle, Autocomplete, AvatarGroup, Pagination, PaginationItem, Rating, Skeleton, SpeedDial, SpeedDialAction, SpeedDialIcon, TabContext, TabList, TabPanel, Timeline*, ToggleButton, ToggleButtonGroup, TreeView, TreeItem
|
|
124
|
+
|
|
125
|
+
#### `mui-v7/no-grid-item-prop`
|
|
126
|
+
|
|
127
|
+
Grid doesn't use `item` prop anymore, use `size` instead.
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
// ❌ Bad
|
|
131
|
+
<Grid item xs={12} md={6}>
|
|
132
|
+
Content
|
|
133
|
+
</Grid>
|
|
134
|
+
|
|
135
|
+
// ✅ Good
|
|
136
|
+
<Grid size={{ xs: 12, md: 6 }}>
|
|
137
|
+
Content
|
|
138
|
+
</Grid>
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
#### `mui-v7/no-deprecated-props`
|
|
142
|
+
|
|
143
|
+
Detects deprecated props in various components.
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
// ❌ Bad: Dialog.onBackdropClick
|
|
147
|
+
<Dialog onBackdropClick={handleClick}>
|
|
148
|
+
|
|
149
|
+
// ✅ Good
|
|
150
|
+
<Dialog onClose={(event, reason) => {
|
|
151
|
+
if (reason === 'backdropClick') {
|
|
152
|
+
// Your logic here
|
|
153
|
+
}
|
|
154
|
+
}}>
|
|
155
|
+
|
|
156
|
+
// ❌ Bad: InputLabel size="normal"
|
|
157
|
+
<InputLabel size="normal">
|
|
158
|
+
|
|
159
|
+
// ✅ Good
|
|
160
|
+
<InputLabel size="medium">
|
|
161
|
+
|
|
162
|
+
// ❌ Bad: Hidden component
|
|
163
|
+
<Hidden xlUp><Paper /></Hidden>
|
|
164
|
+
|
|
165
|
+
// ✅ Good: Use sx prop
|
|
166
|
+
<Paper sx={{ display: { xl: 'none' } }} />
|
|
167
|
+
|
|
168
|
+
// ✅ Good: Use useMediaQuery
|
|
169
|
+
const hidden = useMediaQuery(theme => theme.breakpoints.up('xl'))
|
|
170
|
+
return hidden ? null : <Paper />
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### ⚠️ Warning Rules (Suggestions)
|
|
174
|
+
|
|
175
|
+
#### `mui-v7/no-old-grid-import`
|
|
176
|
+
|
|
177
|
+
Suggests migrating from old Grid to the new one.
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
// ⚠️ If you want to keep old Grid
|
|
181
|
+
import Grid from '@mui/material/GridLegacy'
|
|
182
|
+
|
|
183
|
+
// ✅ Recommended: Migrate to new Grid
|
|
184
|
+
import Grid from '@mui/material/Grid'
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
#### `mui-v7/prefer-theme-vars`
|
|
188
|
+
|
|
189
|
+
When using `cssVariables: true`, use `theme.vars.*` for automatic dark mode support.
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
// ⚠️ Warning (doesn't change with dark mode)
|
|
193
|
+
const Custom = styled('div')(({ theme }) => ({
|
|
194
|
+
color: theme.palette.text.primary,
|
|
195
|
+
}))
|
|
196
|
+
|
|
197
|
+
// ✅ Good (changes automatically with dark mode)
|
|
198
|
+
const Custom = styled('div')(({ theme }) => ({
|
|
199
|
+
color: theme.vars.palette.text.primary,
|
|
200
|
+
}))
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## 🎓 Example Messages
|
|
204
|
+
|
|
205
|
+
The plugin provides educational messages with emojis and examples:
|
|
206
|
+
|
|
207
|
+
```
|
|
208
|
+
🎯 Grid no MUI V7 não usa mais a prop `item`!
|
|
209
|
+
|
|
210
|
+
🔧 Forma antiga (V6):
|
|
211
|
+
<Grid item xs={12} sm={6} md={4}>
|
|
212
|
+
|
|
213
|
+
✅ Forma nova (V7):
|
|
214
|
+
<Grid size={{ xs: 12, sm: 6, md: 4 }}>
|
|
215
|
+
|
|
216
|
+
💡 A nova sintaxe é mais limpa e poderosa!
|
|
217
|
+
Você pode usar offset, push, pull e mais.
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## 🔧 Configuration Options
|
|
221
|
+
|
|
222
|
+
### Severity Levels
|
|
223
|
+
|
|
224
|
+
```javascript
|
|
225
|
+
rules: {
|
|
226
|
+
'mui-v7/no-deep-imports': 'error', // Blocks code
|
|
227
|
+
'mui-v7/no-deep-imports': 'warn', // Shows warning
|
|
228
|
+
'mui-v7/no-deep-imports': 'off', // Disables rule
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Recommended Config
|
|
233
|
+
|
|
234
|
+
```javascript
|
|
235
|
+
import muiV7Plugin from 'eslint-plugin-mui-v7'
|
|
236
|
+
|
|
237
|
+
export default [
|
|
238
|
+
muiV7Plugin.configs.recommended, // All errors + warnings
|
|
239
|
+
]
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Strict Config
|
|
243
|
+
|
|
244
|
+
```javascript
|
|
245
|
+
import muiV7Plugin from 'eslint-plugin-mui-v7'
|
|
246
|
+
|
|
247
|
+
export default [
|
|
248
|
+
muiV7Plugin.configs.strict, // Everything as error
|
|
249
|
+
]
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## 🤝 Contributing
|
|
253
|
+
|
|
254
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
255
|
+
|
|
256
|
+
## 📄 License
|
|
257
|
+
|
|
258
|
+
MIT © Matheus Pimenta (Koda AI Studio)
|
|
259
|
+
|
|
260
|
+
## 🔗 Links
|
|
261
|
+
|
|
262
|
+
- [Material-UI V7 Migration Guide](https://mui.com/material-ui/migration/upgrade-to-v7/)
|
|
263
|
+
- [GitHub Repository](https://github.com/Just-mpm/eslint-plugin-mui-v7)
|
|
264
|
+
- [npm Package](https://www.npmjs.com/package/eslint-plugin-mui-v7)
|
|
265
|
+
|
|
266
|
+
## ❤️ Credits
|
|
267
|
+
|
|
268
|
+
Created by **Matheus Pimenta** (Koda AI Studio) + **Claude Code**
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
**Keywords:** eslint, mui, material-ui, mui-v7, react, typescript, linter, code-quality
|
package/index.cjs
ADDED
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint Plugin Customizado para MUI V7 (CommonJS version)
|
|
3
|
+
*
|
|
4
|
+
* Detecta automaticamente usos incorretos do Material-UI V7 e fornece
|
|
5
|
+
* mensagens educativas para ensinar a forma correta.
|
|
6
|
+
*
|
|
7
|
+
* @created 2025-01-26
|
|
8
|
+
* @author Matheus (Koda AI Studio) + Claude Code
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const muiV7Rules = {
|
|
12
|
+
'no-deep-imports': {
|
|
13
|
+
meta: {
|
|
14
|
+
type: 'problem',
|
|
15
|
+
docs: {
|
|
16
|
+
description: 'Proíbe deep imports (mais de um nível) do MUI V7',
|
|
17
|
+
category: 'Best Practices',
|
|
18
|
+
recommended: true,
|
|
19
|
+
},
|
|
20
|
+
messages: {
|
|
21
|
+
deepImport: '❌ Deep imports não são mais suportados no MUI V7.\n\n' +
|
|
22
|
+
'🔧 Forma incorreta:\n' +
|
|
23
|
+
' import createTheme from "@mui/material/styles/createTheme"\n\n' +
|
|
24
|
+
'✅ Forma correta:\n' +
|
|
25
|
+
' import { createTheme } from "@mui/material/styles"',
|
|
26
|
+
},
|
|
27
|
+
schema: [],
|
|
28
|
+
fixable: 'code',
|
|
29
|
+
},
|
|
30
|
+
create(context) {
|
|
31
|
+
return {
|
|
32
|
+
ImportDeclaration(node) {
|
|
33
|
+
const source = node.source.value;
|
|
34
|
+
|
|
35
|
+
// Detecta imports com mais de um nível (ex: @mui/material/styles/createTheme)
|
|
36
|
+
if (source.startsWith('@mui/')) {
|
|
37
|
+
const parts = source.split('/');
|
|
38
|
+
// @mui/material/styles/createTheme -> 4 partes (deep import)
|
|
39
|
+
// @mui/material/styles -> 3 partes (OK)
|
|
40
|
+
if (parts.length > 3) {
|
|
41
|
+
context.report({
|
|
42
|
+
node,
|
|
43
|
+
messageId: 'deepImport',
|
|
44
|
+
fix(fixer) {
|
|
45
|
+
// Tenta converter para named import
|
|
46
|
+
const specifier = node.specifiers[0];
|
|
47
|
+
if (specifier && specifier.type === 'ImportDefaultSpecifier') {
|
|
48
|
+
const importName = specifier.local.name;
|
|
49
|
+
const newPath = parts.slice(0, 3).join('/'); // Remove último nível
|
|
50
|
+
return fixer.replaceText(
|
|
51
|
+
node,
|
|
52
|
+
`import { ${importName} } from "${newPath}"`
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
'no-grid2-import': {
|
|
66
|
+
meta: {
|
|
67
|
+
type: 'problem',
|
|
68
|
+
docs: {
|
|
69
|
+
description: 'Grid2 foi renomeado para Grid no MUI V7',
|
|
70
|
+
category: 'Best Practices',
|
|
71
|
+
recommended: true,
|
|
72
|
+
},
|
|
73
|
+
messages: {
|
|
74
|
+
grid2Import: '⚠️ Grid2 foi renomeado para Grid no MUI V7!\n\n' +
|
|
75
|
+
'🔧 Forma antiga (V6):\n' +
|
|
76
|
+
' import Grid2 from "@mui/material/Grid2"\n' +
|
|
77
|
+
' import { grid2Classes } from "@mui/material/Grid2"\n\n' +
|
|
78
|
+
'✅ Forma nova (V7):\n' +
|
|
79
|
+
' import Grid from "@mui/material/Grid"\n' +
|
|
80
|
+
' import { gridClasses } from "@mui/material/Grid"\n\n' +
|
|
81
|
+
'💡 Dica: O novo Grid é mais poderoso e responsivo!',
|
|
82
|
+
},
|
|
83
|
+
schema: [],
|
|
84
|
+
fixable: 'code',
|
|
85
|
+
},
|
|
86
|
+
create(context) {
|
|
87
|
+
return {
|
|
88
|
+
ImportDeclaration(node) {
|
|
89
|
+
const source = node.source.value;
|
|
90
|
+
|
|
91
|
+
if (source === '@mui/material/Grid2') {
|
|
92
|
+
context.report({
|
|
93
|
+
node,
|
|
94
|
+
messageId: 'grid2Import',
|
|
95
|
+
fix(fixer) {
|
|
96
|
+
const fixes = [
|
|
97
|
+
fixer.replaceText(node.source, '"@mui/material/Grid"')
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
// Renomeia Grid2 -> Grid e grid2Classes -> gridClasses
|
|
101
|
+
node.specifiers.forEach(spec => {
|
|
102
|
+
if (spec.imported) {
|
|
103
|
+
const name = spec.imported.name;
|
|
104
|
+
if (name.includes('grid2')) {
|
|
105
|
+
const newName = name.replace('grid2', 'grid');
|
|
106
|
+
// Nota: Isso só funciona bem para casos simples
|
|
107
|
+
// Para casos complexos, o usuário precisa fazer manual
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return fixes;
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
'no-old-grid-import': {
|
|
122
|
+
meta: {
|
|
123
|
+
type: 'suggestion',
|
|
124
|
+
docs: {
|
|
125
|
+
description: 'Sugere migração do Grid antigo para o novo',
|
|
126
|
+
category: 'Best Practices',
|
|
127
|
+
recommended: false,
|
|
128
|
+
},
|
|
129
|
+
messages: {
|
|
130
|
+
oldGrid: '💡 O Grid antigo agora é GridLegacy. Considere migrar para o novo Grid!\n\n' +
|
|
131
|
+
'🔧 Se quiser manter o Grid antigo:\n' +
|
|
132
|
+
' import Grid from "@mui/material/GridLegacy"\n' +
|
|
133
|
+
' import { gridLegacyClasses } from "@mui/material/GridLegacy"\n\n' +
|
|
134
|
+
'✅ Recomendado: Migrar para o novo Grid:\n' +
|
|
135
|
+
' import Grid from "@mui/material/Grid"\n\n' +
|
|
136
|
+
'📚 O novo Grid usa `size` em vez de `xs/sm/md`:\n' +
|
|
137
|
+
' <Grid size={{ xs: 12, md: 6 }}>Conteúdo</Grid>',
|
|
138
|
+
},
|
|
139
|
+
schema: [],
|
|
140
|
+
},
|
|
141
|
+
create(context) {
|
|
142
|
+
const sourceCode = context.getSourceCode();
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
ImportDeclaration(node) {
|
|
146
|
+
const source = node.source.value;
|
|
147
|
+
|
|
148
|
+
// Detecta import Grid from '@mui/material/Grid'
|
|
149
|
+
if (source === '@mui/material/Grid') {
|
|
150
|
+
const defaultImport = node.specifiers.find(
|
|
151
|
+
spec => spec.type === 'ImportDefaultSpecifier'
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
if (defaultImport) {
|
|
155
|
+
// Verifica se está usando props antigas (xs, sm, md) no código
|
|
156
|
+
// Isso é apenas um aviso suave, não um erro
|
|
157
|
+
context.report({
|
|
158
|
+
node,
|
|
159
|
+
messageId: 'oldGrid',
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
'no-lab-imports': {
|
|
169
|
+
meta: {
|
|
170
|
+
type: 'problem',
|
|
171
|
+
docs: {
|
|
172
|
+
description: 'Componentes movidos de @mui/lab para @mui/material',
|
|
173
|
+
category: 'Best Practices',
|
|
174
|
+
recommended: true,
|
|
175
|
+
},
|
|
176
|
+
messages: {
|
|
177
|
+
labImport: '✨ Este componente foi movido para @mui/material no V7!\n\n' +
|
|
178
|
+
'🔧 Forma antiga (V6):\n' +
|
|
179
|
+
' import {{ component }} from "@mui/lab/{{ component }}"\n\n' +
|
|
180
|
+
'✅ Forma nova (V7):\n' +
|
|
181
|
+
' import {{ component }} from "@mui/material/{{ component }}"\n\n' +
|
|
182
|
+
'📦 Componentes movidos: Alert, Autocomplete, Pagination, Rating,\n' +
|
|
183
|
+
' Skeleton, SpeedDial, ToggleButton, AvatarGroup, e mais!',
|
|
184
|
+
},
|
|
185
|
+
schema: [],
|
|
186
|
+
fixable: 'code',
|
|
187
|
+
},
|
|
188
|
+
create(context) {
|
|
189
|
+
const movedComponents = [
|
|
190
|
+
'Alert', 'AlertTitle',
|
|
191
|
+
'Autocomplete',
|
|
192
|
+
'AvatarGroup',
|
|
193
|
+
'Pagination', 'PaginationItem',
|
|
194
|
+
'Rating',
|
|
195
|
+
'Skeleton',
|
|
196
|
+
'SpeedDial', 'SpeedDialAction', 'SpeedDialIcon',
|
|
197
|
+
'TabContext', 'TabList', 'TabPanel',
|
|
198
|
+
'Timeline', 'TimelineConnector', 'TimelineContent', 'TimelineDot',
|
|
199
|
+
'TimelineItem', 'TimelineOppositeContent', 'TimelineSeparator',
|
|
200
|
+
'ToggleButton', 'ToggleButtonGroup',
|
|
201
|
+
'TreeView', 'TreeItem',
|
|
202
|
+
];
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
ImportDeclaration(node) {
|
|
206
|
+
const source = node.source.value;
|
|
207
|
+
|
|
208
|
+
// Detecta imports de @mui/lab
|
|
209
|
+
if (source.startsWith('@mui/lab')) {
|
|
210
|
+
node.specifiers.forEach(spec => {
|
|
211
|
+
const componentName = spec.local.name;
|
|
212
|
+
|
|
213
|
+
if (movedComponents.includes(componentName)) {
|
|
214
|
+
context.report({
|
|
215
|
+
node,
|
|
216
|
+
messageId: 'labImport',
|
|
217
|
+
data: { component: componentName },
|
|
218
|
+
fix(fixer) {
|
|
219
|
+
return fixer.replaceText(
|
|
220
|
+
node.source,
|
|
221
|
+
`"@mui/material/${componentName}"`
|
|
222
|
+
);
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
|
|
233
|
+
'no-grid-item-prop': {
|
|
234
|
+
meta: {
|
|
235
|
+
type: 'problem',
|
|
236
|
+
docs: {
|
|
237
|
+
description: 'Grid não usa mais a prop `item`, agora usa `size`',
|
|
238
|
+
category: 'Best Practices',
|
|
239
|
+
recommended: true,
|
|
240
|
+
},
|
|
241
|
+
messages: {
|
|
242
|
+
gridItemProp: '🎯 Grid no MUI V7 não usa mais a prop `item`!\n\n' +
|
|
243
|
+
'🔧 Forma antiga (V6):\n' +
|
|
244
|
+
' <Grid item xs={12} sm={6} md={4}>\n\n' +
|
|
245
|
+
'✅ Forma nova (V7):\n' +
|
|
246
|
+
' <Grid size={{ xs: 12, sm: 6, md: 4 }}>\n\n' +
|
|
247
|
+
'💡 A nova sintaxe é mais limpa e poderosa!\n' +
|
|
248
|
+
' Você pode usar offset, push, pull e mais.',
|
|
249
|
+
},
|
|
250
|
+
schema: [],
|
|
251
|
+
},
|
|
252
|
+
create(context) {
|
|
253
|
+
return {
|
|
254
|
+
JSXOpeningElement(node) {
|
|
255
|
+
if (node.name.name === 'Grid') {
|
|
256
|
+
const hasItemProp = node.attributes.some(
|
|
257
|
+
attr => attr.type === 'JSXAttribute' && attr.name.name === 'item'
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
const hasBreakpointProps = node.attributes.some(
|
|
261
|
+
attr => attr.type === 'JSXAttribute' &&
|
|
262
|
+
['xs', 'sm', 'md', 'lg', 'xl'].includes(attr.name.name)
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
if (hasItemProp || hasBreakpointProps) {
|
|
266
|
+
context.report({
|
|
267
|
+
node,
|
|
268
|
+
messageId: 'gridItemProp',
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
|
|
277
|
+
'no-deprecated-props': {
|
|
278
|
+
meta: {
|
|
279
|
+
type: 'problem',
|
|
280
|
+
docs: {
|
|
281
|
+
description: 'Detecta props depreciadas no MUI V7',
|
|
282
|
+
category: 'Best Practices',
|
|
283
|
+
recommended: true,
|
|
284
|
+
},
|
|
285
|
+
messages: {
|
|
286
|
+
onBackdropClick: '🔄 Dialog.onBackdropClick foi removido no V7!\n\n' +
|
|
287
|
+
'🔧 Forma antiga (V6):\n' +
|
|
288
|
+
' <Dialog onBackdropClick={handleClick}>\n\n' +
|
|
289
|
+
'✅ Forma nova (V7):\n' +
|
|
290
|
+
' <Dialog onClose={(event, reason) => {\n' +
|
|
291
|
+
' if (reason === "backdropClick") {\n' +
|
|
292
|
+
' // Sua lógica aqui\n' +
|
|
293
|
+
' }\n' +
|
|
294
|
+
' }}>',
|
|
295
|
+
|
|
296
|
+
inputLabelNormal: '📏 InputLabel.size="normal" foi renomeado!\n\n' +
|
|
297
|
+
'🔧 Forma antiga (V6):\n' +
|
|
298
|
+
' <InputLabel size="normal">\n\n' +
|
|
299
|
+
'✅ Forma nova (V7):\n' +
|
|
300
|
+
' <InputLabel size="medium">',
|
|
301
|
+
|
|
302
|
+
hiddenComponent: '👻 Hidden component foi removido no V7!\n\n' +
|
|
303
|
+
'🔧 Forma antiga (V6):\n' +
|
|
304
|
+
' <Hidden xlUp><Paper /></Hidden>\n\n' +
|
|
305
|
+
'✅ Opção 1 - Use sx prop:\n' +
|
|
306
|
+
' <Paper sx={{ display: { xl: "none" } }} />\n\n' +
|
|
307
|
+
'✅ Opção 2 - Use useMediaQuery:\n' +
|
|
308
|
+
' const hidden = useMediaQuery(theme => theme.breakpoints.up("xl"))\n' +
|
|
309
|
+
' return hidden ? null : <Paper />',
|
|
310
|
+
},
|
|
311
|
+
schema: [],
|
|
312
|
+
},
|
|
313
|
+
create(context) {
|
|
314
|
+
return {
|
|
315
|
+
JSXOpeningElement(node) {
|
|
316
|
+
const componentName = node.name.name;
|
|
317
|
+
|
|
318
|
+
// Dialog.onBackdropClick
|
|
319
|
+
if (componentName === 'Dialog') {
|
|
320
|
+
const hasOnBackdropClick = node.attributes.some(
|
|
321
|
+
attr => attr.type === 'JSXAttribute' &&
|
|
322
|
+
attr.name.name === 'onBackdropClick'
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
if (hasOnBackdropClick) {
|
|
326
|
+
context.report({
|
|
327
|
+
node,
|
|
328
|
+
messageId: 'onBackdropClick',
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// InputLabel size="normal"
|
|
334
|
+
if (componentName === 'InputLabel') {
|
|
335
|
+
node.attributes.forEach(attr => {
|
|
336
|
+
if (attr.type === 'JSXAttribute' &&
|
|
337
|
+
attr.name.name === 'size' &&
|
|
338
|
+
attr.value &&
|
|
339
|
+
attr.value.type === 'Literal' &&
|
|
340
|
+
attr.value.value === 'normal') {
|
|
341
|
+
context.report({
|
|
342
|
+
node: attr,
|
|
343
|
+
messageId: 'inputLabelNormal',
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Hidden component
|
|
350
|
+
if (componentName === 'Hidden') {
|
|
351
|
+
context.report({
|
|
352
|
+
node,
|
|
353
|
+
messageId: 'hiddenComponent',
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
},
|
|
357
|
+
};
|
|
358
|
+
},
|
|
359
|
+
},
|
|
360
|
+
|
|
361
|
+
'prefer-theme-vars': {
|
|
362
|
+
meta: {
|
|
363
|
+
type: 'suggestion',
|
|
364
|
+
docs: {
|
|
365
|
+
description: 'Recomenda uso de theme.vars para CSS variables',
|
|
366
|
+
category: 'Best Practices',
|
|
367
|
+
recommended: false,
|
|
368
|
+
},
|
|
369
|
+
messages: {
|
|
370
|
+
useThemeVars: '💡 Quando `cssVariables: true`, use theme.vars!\n\n' +
|
|
371
|
+
'⚠️ Forma que NÃO muda com dark mode:\n' +
|
|
372
|
+
' color: theme.palette.text.primary\n\n' +
|
|
373
|
+
'✅ Forma que muda automaticamente:\n' +
|
|
374
|
+
' color: theme.vars.palette.text.primary\n\n' +
|
|
375
|
+
'📚 Benefícios: Performance + Dark mode automático!',
|
|
376
|
+
},
|
|
377
|
+
schema: [],
|
|
378
|
+
},
|
|
379
|
+
create(context) {
|
|
380
|
+
const sourceCode = context.getSourceCode();
|
|
381
|
+
|
|
382
|
+
return {
|
|
383
|
+
MemberExpression(node) {
|
|
384
|
+
// Detecta theme.palette.* (sem .vars)
|
|
385
|
+
if (
|
|
386
|
+
node.object &&
|
|
387
|
+
node.object.type === 'MemberExpression' &&
|
|
388
|
+
node.object.object &&
|
|
389
|
+
node.object.object.name === 'theme' &&
|
|
390
|
+
node.object.property &&
|
|
391
|
+
node.object.property.name === 'palette'
|
|
392
|
+
) {
|
|
393
|
+
// Verifica se não é theme.vars.palette
|
|
394
|
+
const parent = node.object.object;
|
|
395
|
+
if (parent.type === 'Identifier' && parent.name === 'theme') {
|
|
396
|
+
context.report({
|
|
397
|
+
node,
|
|
398
|
+
messageId: 'useThemeVars',
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
},
|
|
403
|
+
};
|
|
404
|
+
},
|
|
405
|
+
},
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
// Exporta o plugin (CommonJS)
|
|
409
|
+
const plugin = {
|
|
410
|
+
rules: muiV7Rules,
|
|
411
|
+
configs: {
|
|
412
|
+
recommended: {
|
|
413
|
+
plugins: ['mui-v7'],
|
|
414
|
+
rules: {
|
|
415
|
+
'mui-v7/no-deep-imports': 'error',
|
|
416
|
+
'mui-v7/no-grid2-import': 'error',
|
|
417
|
+
'mui-v7/no-lab-imports': 'error',
|
|
418
|
+
'mui-v7/no-grid-item-prop': 'error',
|
|
419
|
+
'mui-v7/no-deprecated-props': 'error',
|
|
420
|
+
'mui-v7/no-old-grid-import': 'warn',
|
|
421
|
+
'mui-v7/prefer-theme-vars': 'warn',
|
|
422
|
+
},
|
|
423
|
+
},
|
|
424
|
+
strict: {
|
|
425
|
+
plugins: ['mui-v7'],
|
|
426
|
+
rules: {
|
|
427
|
+
'mui-v7/no-deep-imports': 'error',
|
|
428
|
+
'mui-v7/no-grid2-import': 'error',
|
|
429
|
+
'mui-v7/no-lab-imports': 'error',
|
|
430
|
+
'mui-v7/no-grid-item-prop': 'error',
|
|
431
|
+
'mui-v7/no-deprecated-props': 'error',
|
|
432
|
+
'mui-v7/no-old-grid-import': 'error',
|
|
433
|
+
'mui-v7/prefer-theme-vars': 'error',
|
|
434
|
+
},
|
|
435
|
+
},
|
|
436
|
+
},
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
module.exports = plugin;
|
package/index.js
ADDED
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint Plugin Customizado para MUI V7
|
|
3
|
+
*
|
|
4
|
+
* Detecta automaticamente usos incorretos do Material-UI V7 e fornece
|
|
5
|
+
* mensagens educativas para ensinar a forma correta.
|
|
6
|
+
*
|
|
7
|
+
* @created 2025-01-26
|
|
8
|
+
* @author Matheus (Koda AI Studio) + Claude Code
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const muiV7Rules = {
|
|
12
|
+
'no-deep-imports': {
|
|
13
|
+
meta: {
|
|
14
|
+
type: 'problem',
|
|
15
|
+
docs: {
|
|
16
|
+
description: 'Proíbe deep imports (mais de um nível) do MUI V7',
|
|
17
|
+
category: 'Best Practices',
|
|
18
|
+
recommended: true,
|
|
19
|
+
},
|
|
20
|
+
messages: {
|
|
21
|
+
deepImport: '❌ Deep imports não são mais suportados no MUI V7.\n\n' +
|
|
22
|
+
'🔧 Forma incorreta:\n' +
|
|
23
|
+
' import createTheme from "@mui/material/styles/createTheme"\n\n' +
|
|
24
|
+
'✅ Forma correta:\n' +
|
|
25
|
+
' import { createTheme } from "@mui/material/styles"',
|
|
26
|
+
},
|
|
27
|
+
schema: [],
|
|
28
|
+
fixable: 'code',
|
|
29
|
+
},
|
|
30
|
+
create(context) {
|
|
31
|
+
return {
|
|
32
|
+
ImportDeclaration(node) {
|
|
33
|
+
const source = node.source.value;
|
|
34
|
+
|
|
35
|
+
// Detecta imports com mais de um nível (ex: @mui/material/styles/createTheme)
|
|
36
|
+
if (source.startsWith('@mui/')) {
|
|
37
|
+
const parts = source.split('/');
|
|
38
|
+
// @mui/material/styles/createTheme -> 4 partes (deep import)
|
|
39
|
+
// @mui/material/styles -> 3 partes (OK)
|
|
40
|
+
if (parts.length > 3) {
|
|
41
|
+
context.report({
|
|
42
|
+
node,
|
|
43
|
+
messageId: 'deepImport',
|
|
44
|
+
fix(fixer) {
|
|
45
|
+
// Tenta converter para named import
|
|
46
|
+
const specifier = node.specifiers[0];
|
|
47
|
+
if (specifier && specifier.type === 'ImportDefaultSpecifier') {
|
|
48
|
+
const importName = specifier.local.name;
|
|
49
|
+
const newPath = parts.slice(0, 3).join('/'); // Remove último nível
|
|
50
|
+
return fixer.replaceText(
|
|
51
|
+
node,
|
|
52
|
+
`import { ${importName} } from "${newPath}"`
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
'no-grid2-import': {
|
|
66
|
+
meta: {
|
|
67
|
+
type: 'problem',
|
|
68
|
+
docs: {
|
|
69
|
+
description: 'Grid2 foi renomeado para Grid no MUI V7',
|
|
70
|
+
category: 'Best Practices',
|
|
71
|
+
recommended: true,
|
|
72
|
+
},
|
|
73
|
+
messages: {
|
|
74
|
+
grid2Import: '⚠️ Grid2 foi renomeado para Grid no MUI V7!\n\n' +
|
|
75
|
+
'🔧 Forma antiga (V6):\n' +
|
|
76
|
+
' import Grid2 from "@mui/material/Grid2"\n' +
|
|
77
|
+
' import { grid2Classes } from "@mui/material/Grid2"\n\n' +
|
|
78
|
+
'✅ Forma nova (V7):\n' +
|
|
79
|
+
' import Grid from "@mui/material/Grid"\n' +
|
|
80
|
+
' import { gridClasses } from "@mui/material/Grid"\n\n' +
|
|
81
|
+
'💡 Dica: O novo Grid é mais poderoso e responsivo!',
|
|
82
|
+
},
|
|
83
|
+
schema: [],
|
|
84
|
+
fixable: 'code',
|
|
85
|
+
},
|
|
86
|
+
create(context) {
|
|
87
|
+
return {
|
|
88
|
+
ImportDeclaration(node) {
|
|
89
|
+
const source = node.source.value;
|
|
90
|
+
|
|
91
|
+
if (source === '@mui/material/Grid2') {
|
|
92
|
+
context.report({
|
|
93
|
+
node,
|
|
94
|
+
messageId: 'grid2Import',
|
|
95
|
+
fix(fixer) {
|
|
96
|
+
const fixes = [
|
|
97
|
+
fixer.replaceText(node.source, '"@mui/material/Grid"')
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
// Renomeia Grid2 -> Grid e grid2Classes -> gridClasses
|
|
101
|
+
node.specifiers.forEach(spec => {
|
|
102
|
+
if (spec.imported) {
|
|
103
|
+
const name = spec.imported.name;
|
|
104
|
+
if (name.includes('grid2')) {
|
|
105
|
+
const newName = name.replace('grid2', 'grid');
|
|
106
|
+
// Nota: Isso só funciona bem para casos simples
|
|
107
|
+
// Para casos complexos, o usuário precisa fazer manual
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return fixes;
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
'no-old-grid-import': {
|
|
122
|
+
meta: {
|
|
123
|
+
type: 'suggestion',
|
|
124
|
+
docs: {
|
|
125
|
+
description: 'Sugere migração do Grid antigo para o novo',
|
|
126
|
+
category: 'Best Practices',
|
|
127
|
+
recommended: false,
|
|
128
|
+
},
|
|
129
|
+
messages: {
|
|
130
|
+
oldGrid: '💡 O Grid antigo agora é GridLegacy. Considere migrar para o novo Grid!\n\n' +
|
|
131
|
+
'🔧 Se quiser manter o Grid antigo:\n' +
|
|
132
|
+
' import Grid from "@mui/material/GridLegacy"\n' +
|
|
133
|
+
' import { gridLegacyClasses } from "@mui/material/GridLegacy"\n\n' +
|
|
134
|
+
'✅ Recomendado: Migrar para o novo Grid:\n' +
|
|
135
|
+
' import Grid from "@mui/material/Grid"\n\n' +
|
|
136
|
+
'📚 O novo Grid usa `size` em vez de `xs/sm/md`:\n' +
|
|
137
|
+
' <Grid size={{ xs: 12, md: 6 }}>Conteúdo</Grid>',
|
|
138
|
+
},
|
|
139
|
+
schema: [],
|
|
140
|
+
},
|
|
141
|
+
create(context) {
|
|
142
|
+
const sourceCode = context.getSourceCode();
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
ImportDeclaration(node) {
|
|
146
|
+
const source = node.source.value;
|
|
147
|
+
|
|
148
|
+
// Detecta import Grid from '@mui/material/Grid'
|
|
149
|
+
if (source === '@mui/material/Grid') {
|
|
150
|
+
const defaultImport = node.specifiers.find(
|
|
151
|
+
spec => spec.type === 'ImportDefaultSpecifier'
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
if (defaultImport) {
|
|
155
|
+
// Verifica se está usando props antigas (xs, sm, md) no código
|
|
156
|
+
// Isso é apenas um aviso suave, não um erro
|
|
157
|
+
context.report({
|
|
158
|
+
node,
|
|
159
|
+
messageId: 'oldGrid',
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
'no-lab-imports': {
|
|
169
|
+
meta: {
|
|
170
|
+
type: 'problem',
|
|
171
|
+
docs: {
|
|
172
|
+
description: 'Componentes movidos de @mui/lab para @mui/material',
|
|
173
|
+
category: 'Best Practices',
|
|
174
|
+
recommended: true,
|
|
175
|
+
},
|
|
176
|
+
messages: {
|
|
177
|
+
labImport: '✨ Este componente foi movido para @mui/material no V7!\n\n' +
|
|
178
|
+
'🔧 Forma antiga (V6):\n' +
|
|
179
|
+
' import {{ component }} from "@mui/lab/{{ component }}"\n\n' +
|
|
180
|
+
'✅ Forma nova (V7):\n' +
|
|
181
|
+
' import {{ component }} from "@mui/material/{{ component }}"\n\n' +
|
|
182
|
+
'📦 Componentes movidos: Alert, Autocomplete, Pagination, Rating,\n' +
|
|
183
|
+
' Skeleton, SpeedDial, ToggleButton, AvatarGroup, e mais!',
|
|
184
|
+
},
|
|
185
|
+
schema: [],
|
|
186
|
+
fixable: 'code',
|
|
187
|
+
},
|
|
188
|
+
create(context) {
|
|
189
|
+
const movedComponents = [
|
|
190
|
+
'Alert', 'AlertTitle',
|
|
191
|
+
'Autocomplete',
|
|
192
|
+
'AvatarGroup',
|
|
193
|
+
'Pagination', 'PaginationItem',
|
|
194
|
+
'Rating',
|
|
195
|
+
'Skeleton',
|
|
196
|
+
'SpeedDial', 'SpeedDialAction', 'SpeedDialIcon',
|
|
197
|
+
'TabContext', 'TabList', 'TabPanel',
|
|
198
|
+
'Timeline', 'TimelineConnector', 'TimelineContent', 'TimelineDot',
|
|
199
|
+
'TimelineItem', 'TimelineOppositeContent', 'TimelineSeparator',
|
|
200
|
+
'ToggleButton', 'ToggleButtonGroup',
|
|
201
|
+
'TreeView', 'TreeItem',
|
|
202
|
+
];
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
ImportDeclaration(node) {
|
|
206
|
+
const source = node.source.value;
|
|
207
|
+
|
|
208
|
+
// Detecta imports de @mui/lab
|
|
209
|
+
if (source.startsWith('@mui/lab')) {
|
|
210
|
+
node.specifiers.forEach(spec => {
|
|
211
|
+
const componentName = spec.local.name;
|
|
212
|
+
|
|
213
|
+
if (movedComponents.includes(componentName)) {
|
|
214
|
+
context.report({
|
|
215
|
+
node,
|
|
216
|
+
messageId: 'labImport',
|
|
217
|
+
data: { component: componentName },
|
|
218
|
+
fix(fixer) {
|
|
219
|
+
return fixer.replaceText(
|
|
220
|
+
node.source,
|
|
221
|
+
`"@mui/material/${componentName}"`
|
|
222
|
+
);
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
|
|
233
|
+
'no-grid-item-prop': {
|
|
234
|
+
meta: {
|
|
235
|
+
type: 'problem',
|
|
236
|
+
docs: {
|
|
237
|
+
description: 'Grid não usa mais a prop `item`, agora usa `size`',
|
|
238
|
+
category: 'Best Practices',
|
|
239
|
+
recommended: true,
|
|
240
|
+
},
|
|
241
|
+
messages: {
|
|
242
|
+
gridItemProp: '🎯 Grid no MUI V7 não usa mais a prop `item`!\n\n' +
|
|
243
|
+
'🔧 Forma antiga (V6):\n' +
|
|
244
|
+
' <Grid item xs={12} sm={6} md={4}>\n\n' +
|
|
245
|
+
'✅ Forma nova (V7):\n' +
|
|
246
|
+
' <Grid size={{ xs: 12, sm: 6, md: 4 }}>\n\n' +
|
|
247
|
+
'💡 A nova sintaxe é mais limpa e poderosa!\n' +
|
|
248
|
+
' Você pode usar offset, push, pull e mais.',
|
|
249
|
+
},
|
|
250
|
+
schema: [],
|
|
251
|
+
},
|
|
252
|
+
create(context) {
|
|
253
|
+
return {
|
|
254
|
+
JSXOpeningElement(node) {
|
|
255
|
+
if (node.name.name === 'Grid') {
|
|
256
|
+
const hasItemProp = node.attributes.some(
|
|
257
|
+
attr => attr.type === 'JSXAttribute' && attr.name.name === 'item'
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
const hasBreakpointProps = node.attributes.some(
|
|
261
|
+
attr => attr.type === 'JSXAttribute' &&
|
|
262
|
+
['xs', 'sm', 'md', 'lg', 'xl'].includes(attr.name.name)
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
if (hasItemProp || hasBreakpointProps) {
|
|
266
|
+
context.report({
|
|
267
|
+
node,
|
|
268
|
+
messageId: 'gridItemProp',
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
|
|
277
|
+
'no-deprecated-props': {
|
|
278
|
+
meta: {
|
|
279
|
+
type: 'problem',
|
|
280
|
+
docs: {
|
|
281
|
+
description: 'Detecta props depreciadas no MUI V7',
|
|
282
|
+
category: 'Best Practices',
|
|
283
|
+
recommended: true,
|
|
284
|
+
},
|
|
285
|
+
messages: {
|
|
286
|
+
onBackdropClick: '🔄 Dialog.onBackdropClick foi removido no V7!\n\n' +
|
|
287
|
+
'🔧 Forma antiga (V6):\n' +
|
|
288
|
+
' <Dialog onBackdropClick={handleClick}>\n\n' +
|
|
289
|
+
'✅ Forma nova (V7):\n' +
|
|
290
|
+
' <Dialog onClose={(event, reason) => {\n' +
|
|
291
|
+
' if (reason === "backdropClick") {\n' +
|
|
292
|
+
' // Sua lógica aqui\n' +
|
|
293
|
+
' }\n' +
|
|
294
|
+
' }}>',
|
|
295
|
+
|
|
296
|
+
inputLabelNormal: '📏 InputLabel.size="normal" foi renomeado!\n\n' +
|
|
297
|
+
'🔧 Forma antiga (V6):\n' +
|
|
298
|
+
' <InputLabel size="normal">\n\n' +
|
|
299
|
+
'✅ Forma nova (V7):\n' +
|
|
300
|
+
' <InputLabel size="medium">',
|
|
301
|
+
|
|
302
|
+
hiddenComponent: '👻 Hidden component foi removido no V7!\n\n' +
|
|
303
|
+
'🔧 Forma antiga (V6):\n' +
|
|
304
|
+
' <Hidden xlUp><Paper /></Hidden>\n\n' +
|
|
305
|
+
'✅ Opção 1 - Use sx prop:\n' +
|
|
306
|
+
' <Paper sx={{ display: { xl: "none" } }} />\n\n' +
|
|
307
|
+
'✅ Opção 2 - Use useMediaQuery:\n' +
|
|
308
|
+
' const hidden = useMediaQuery(theme => theme.breakpoints.up("xl"))\n' +
|
|
309
|
+
' return hidden ? null : <Paper />',
|
|
310
|
+
},
|
|
311
|
+
schema: [],
|
|
312
|
+
},
|
|
313
|
+
create(context) {
|
|
314
|
+
return {
|
|
315
|
+
JSXOpeningElement(node) {
|
|
316
|
+
const componentName = node.name.name;
|
|
317
|
+
|
|
318
|
+
// Dialog.onBackdropClick
|
|
319
|
+
if (componentName === 'Dialog') {
|
|
320
|
+
const hasOnBackdropClick = node.attributes.some(
|
|
321
|
+
attr => attr.type === 'JSXAttribute' &&
|
|
322
|
+
attr.name.name === 'onBackdropClick'
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
if (hasOnBackdropClick) {
|
|
326
|
+
context.report({
|
|
327
|
+
node,
|
|
328
|
+
messageId: 'onBackdropClick',
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// InputLabel size="normal"
|
|
334
|
+
if (componentName === 'InputLabel') {
|
|
335
|
+
node.attributes.forEach(attr => {
|
|
336
|
+
if (attr.type === 'JSXAttribute' &&
|
|
337
|
+
attr.name.name === 'size' &&
|
|
338
|
+
attr.value &&
|
|
339
|
+
attr.value.type === 'Literal' &&
|
|
340
|
+
attr.value.value === 'normal') {
|
|
341
|
+
context.report({
|
|
342
|
+
node: attr,
|
|
343
|
+
messageId: 'inputLabelNormal',
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Hidden component
|
|
350
|
+
if (componentName === 'Hidden') {
|
|
351
|
+
context.report({
|
|
352
|
+
node,
|
|
353
|
+
messageId: 'hiddenComponent',
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
},
|
|
357
|
+
};
|
|
358
|
+
},
|
|
359
|
+
},
|
|
360
|
+
|
|
361
|
+
'prefer-theme-vars': {
|
|
362
|
+
meta: {
|
|
363
|
+
type: 'suggestion',
|
|
364
|
+
docs: {
|
|
365
|
+
description: 'Recomenda uso de theme.vars para CSS variables',
|
|
366
|
+
category: 'Best Practices',
|
|
367
|
+
recommended: false,
|
|
368
|
+
},
|
|
369
|
+
messages: {
|
|
370
|
+
useThemeVars: '💡 Quando `cssVariables: true`, use theme.vars!\n\n' +
|
|
371
|
+
'⚠️ Forma que NÃO muda com dark mode:\n' +
|
|
372
|
+
' color: theme.palette.text.primary\n\n' +
|
|
373
|
+
'✅ Forma que muda automaticamente:\n' +
|
|
374
|
+
' color: theme.vars.palette.text.primary\n\n' +
|
|
375
|
+
'📚 Benefícios: Performance + Dark mode automático!',
|
|
376
|
+
},
|
|
377
|
+
schema: [],
|
|
378
|
+
},
|
|
379
|
+
create(context) {
|
|
380
|
+
const sourceCode = context.getSourceCode();
|
|
381
|
+
|
|
382
|
+
return {
|
|
383
|
+
MemberExpression(node) {
|
|
384
|
+
// Detecta theme.palette.* (sem .vars)
|
|
385
|
+
if (
|
|
386
|
+
node.object &&
|
|
387
|
+
node.object.type === 'MemberExpression' &&
|
|
388
|
+
node.object.object &&
|
|
389
|
+
node.object.object.name === 'theme' &&
|
|
390
|
+
node.object.property &&
|
|
391
|
+
node.object.property.name === 'palette'
|
|
392
|
+
) {
|
|
393
|
+
// Verifica se não é theme.vars.palette
|
|
394
|
+
const parent = node.object.object;
|
|
395
|
+
if (parent.type === 'Identifier' && parent.name === 'theme') {
|
|
396
|
+
context.report({
|
|
397
|
+
node,
|
|
398
|
+
messageId: 'useThemeVars',
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
},
|
|
403
|
+
};
|
|
404
|
+
},
|
|
405
|
+
},
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
// Exporta o plugin (ESM e CommonJS compatível)
|
|
409
|
+
const plugin = {
|
|
410
|
+
rules: muiV7Rules,
|
|
411
|
+
configs: {
|
|
412
|
+
recommended: {
|
|
413
|
+
plugins: ['mui-v7'],
|
|
414
|
+
rules: {
|
|
415
|
+
'mui-v7/no-deep-imports': 'error',
|
|
416
|
+
'mui-v7/no-grid2-import': 'error',
|
|
417
|
+
'mui-v7/no-lab-imports': 'error',
|
|
418
|
+
'mui-v7/no-grid-item-prop': 'error',
|
|
419
|
+
'mui-v7/no-deprecated-props': 'error',
|
|
420
|
+
'mui-v7/no-old-grid-import': 'warn',
|
|
421
|
+
'mui-v7/prefer-theme-vars': 'warn',
|
|
422
|
+
},
|
|
423
|
+
},
|
|
424
|
+
strict: {
|
|
425
|
+
plugins: ['mui-v7'],
|
|
426
|
+
rules: {
|
|
427
|
+
'mui-v7/no-deep-imports': 'error',
|
|
428
|
+
'mui-v7/no-grid2-import': 'error',
|
|
429
|
+
'mui-v7/no-lab-imports': 'error',
|
|
430
|
+
'mui-v7/no-grid-item-prop': 'error',
|
|
431
|
+
'mui-v7/no-deprecated-props': 'error',
|
|
432
|
+
'mui-v7/no-old-grid-import': 'error',
|
|
433
|
+
'mui-v7/prefer-theme-vars': 'error',
|
|
434
|
+
},
|
|
435
|
+
},
|
|
436
|
+
},
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
export default plugin;
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "eslint-plugin-mui-v7",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "ESLint plugin for Material-UI v7 with educational error messages",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"eslint",
|
|
7
|
+
"eslintplugin",
|
|
8
|
+
"eslint-plugin",
|
|
9
|
+
"mui",
|
|
10
|
+
"material-ui",
|
|
11
|
+
"mui-v7",
|
|
12
|
+
"react",
|
|
13
|
+
"typescript",
|
|
14
|
+
"linter",
|
|
15
|
+
"code-quality"
|
|
16
|
+
],
|
|
17
|
+
"author": "Matheus Pimenta <studio.kodaai@gmail.com> (https://kodaai.app)",
|
|
18
|
+
"homepage": "https://github.com/Just-mpm/eslint-plugin-mui-v7#readme",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/Just-mpm/eslint-plugin-mui-v7.git"
|
|
22
|
+
},
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/Just-mpm/eslint-plugin-mui-v7/issues"
|
|
25
|
+
},
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"type": "module",
|
|
28
|
+
"main": "./index.js",
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"import": "./index.js",
|
|
32
|
+
"require": "./index.cjs"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"index.js",
|
|
37
|
+
"index.cjs",
|
|
38
|
+
"README.md",
|
|
39
|
+
"LICENSE"
|
|
40
|
+
],
|
|
41
|
+
"scripts": {
|
|
42
|
+
"test": "echo \"No tests yet\" && exit 0",
|
|
43
|
+
"lint": "echo \"Linting skipped for plugin package\"",
|
|
44
|
+
"prepublishOnly": "echo \"Ready to publish!\""
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"eslint": ">=8.0.0"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"eslint": "^9.38.0"
|
|
51
|
+
},
|
|
52
|
+
"engines": {
|
|
53
|
+
"node": ">=18.0.0"
|
|
54
|
+
}
|
|
55
|
+
}
|