eslint-plugin-mui-v7 1.2.1 → 1.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 +686 -322
- package/index.cjs +531 -82
- package/index.js +535 -82
- package/package.json +56 -55
package/index.cjs
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ESLint Plugin
|
|
2
|
+
* ESLint Plugin for MUI V7 - Focuses on Breaking Changes
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Automatically detects code that BREAKS when migrating from V6 → V7
|
|
5
|
+
* and provides educational messages to fix it.
|
|
6
6
|
*
|
|
7
|
-
* @version 1.
|
|
7
|
+
* @version 1.6.0
|
|
8
8
|
* @created 2025-01-26
|
|
9
|
-
* @updated 2025-
|
|
9
|
+
* @updated 2025-11-14
|
|
10
10
|
* @author Matheus (Koda AI Studio) + Claude Code
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
const { readFileSync } = require('fs');
|
|
14
|
+
const { join } = require('path');
|
|
15
|
+
const packageJson = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf-8'));
|
|
16
|
+
|
|
13
17
|
// Define moved components at module scope to avoid recreation on every rule invocation
|
|
18
|
+
// ⚠️ OFFICIAL LIST from MUI v7.0.0/lab-removed-components codemod
|
|
19
|
+
// Source: https://github.com/mui/material-ui/blob/master/packages/mui-codemod/README.md
|
|
14
20
|
const MOVED_COMPONENTS = new Set([
|
|
15
21
|
'Alert', 'AlertTitle',
|
|
16
22
|
'Autocomplete',
|
|
@@ -19,11 +25,11 @@ const MOVED_COMPONENTS = new Set([
|
|
|
19
25
|
'Rating',
|
|
20
26
|
'Skeleton',
|
|
21
27
|
'SpeedDial', 'SpeedDialAction', 'SpeedDialIcon',
|
|
22
|
-
'TabContext', 'TabList', 'TabPanel',
|
|
23
|
-
'Timeline', 'TimelineConnector', 'TimelineContent', 'TimelineDot',
|
|
24
|
-
'TimelineItem', 'TimelineOppositeContent', 'TimelineSeparator',
|
|
25
28
|
'ToggleButton', 'ToggleButtonGroup',
|
|
26
|
-
|
|
29
|
+
// ❌ NOT INCLUDED (still in @mui/lab or moved to MUI X):
|
|
30
|
+
// - TabContext, TabList, TabPanel → Still in @mui/lab
|
|
31
|
+
// - Timeline* (7 components) → Still in @mui/lab
|
|
32
|
+
// - TreeView, TreeItem → Moved to @mui/x-tree-view
|
|
27
33
|
]);
|
|
28
34
|
|
|
29
35
|
const muiV7Rules = {
|
|
@@ -31,18 +37,18 @@ const muiV7Rules = {
|
|
|
31
37
|
meta: {
|
|
32
38
|
type: 'problem',
|
|
33
39
|
docs: {
|
|
34
|
-
description: 'Unstable_Grid2
|
|
40
|
+
description: 'Unstable_Grid2 was promoted to Grid in MUI V7',
|
|
35
41
|
category: 'Breaking Changes',
|
|
36
42
|
recommended: true,
|
|
37
43
|
},
|
|
38
44
|
messages: {
|
|
39
|
-
unstableGrid: '🚀 Unstable_Grid2
|
|
40
|
-
'🔧
|
|
45
|
+
unstableGrid: '🚀 Unstable_Grid2 was promoted to stable Grid in MUI V7!\n\n' +
|
|
46
|
+
'🔧 Old way (V6):\n' +
|
|
41
47
|
' import Grid from "@mui/material/Unstable_Grid2"\n' +
|
|
42
48
|
' import Grid2 from "@mui/material/Unstable_Grid2"\n\n' +
|
|
43
|
-
'✅
|
|
49
|
+
'✅ New way (V7):\n' +
|
|
44
50
|
' import { Grid } from "@mui/material"\n\n' +
|
|
45
|
-
'💡
|
|
51
|
+
'💡 Grid is now stable and uses the `size` prop!',
|
|
46
52
|
},
|
|
47
53
|
schema: [],
|
|
48
54
|
fixable: 'code',
|
|
@@ -70,19 +76,19 @@ const muiV7Rules = {
|
|
|
70
76
|
meta: {
|
|
71
77
|
type: 'problem',
|
|
72
78
|
docs: {
|
|
73
|
-
description: 'Grid2
|
|
79
|
+
description: 'Grid2 was renamed to Grid in MUI V7',
|
|
74
80
|
category: 'Breaking Changes',
|
|
75
81
|
recommended: true,
|
|
76
82
|
},
|
|
77
83
|
messages: {
|
|
78
|
-
grid2Import: '⚠️ Grid2
|
|
79
|
-
'🔧
|
|
84
|
+
grid2Import: '⚠️ Grid2 was renamed to Grid in MUI V7!\n\n' +
|
|
85
|
+
'🔧 Old way (V6):\n' +
|
|
80
86
|
' import Grid2 from "@mui/material/Grid2"\n' +
|
|
81
87
|
' import { grid2Classes } from "@mui/material/Grid2"\n\n' +
|
|
82
|
-
'✅
|
|
88
|
+
'✅ Recommended:\n' +
|
|
83
89
|
' import { Grid } from "@mui/material"\n' +
|
|
84
90
|
' import { gridClasses } from "@mui/material"\n\n' +
|
|
85
|
-
'💡
|
|
91
|
+
'💡 The new Grid is more powerful and uses the `size` prop!',
|
|
86
92
|
},
|
|
87
93
|
schema: [],
|
|
88
94
|
fixable: 'code',
|
|
@@ -97,7 +103,30 @@ const muiV7Rules = {
|
|
|
97
103
|
node,
|
|
98
104
|
messageId: 'grid2Import',
|
|
99
105
|
fix(fixer) {
|
|
100
|
-
|
|
106
|
+
const fixes = [fixer.replaceText(node.source, '"@mui/material"')];
|
|
107
|
+
|
|
108
|
+
// Rename Grid2 → Grid and grid2Classes → gridClasses
|
|
109
|
+
node.specifiers.forEach(spec => {
|
|
110
|
+
if (spec.type === 'ImportDefaultSpecifier') {
|
|
111
|
+
// import Grid2 from '@mui/material/Grid2' → import { Grid } from '@mui/material'
|
|
112
|
+
const localName = spec.local.name;
|
|
113
|
+
if (localName === 'Grid2') {
|
|
114
|
+
fixes.push(fixer.replaceText(spec, '{ Grid }'));
|
|
115
|
+
} else {
|
|
116
|
+
// Keep the alias: import MyGrid from ... → import { Grid as MyGrid } from ...
|
|
117
|
+
fixes.push(fixer.replaceText(spec, `{ Grid as ${localName} }`));
|
|
118
|
+
}
|
|
119
|
+
} else if (spec.type === 'ImportSpecifier') {
|
|
120
|
+
const importedName = spec.imported.name;
|
|
121
|
+
if (importedName === 'grid2Classes') {
|
|
122
|
+
fixes.push(fixer.replaceText(spec.imported, 'gridClasses'));
|
|
123
|
+
} else if (importedName === 'Grid2') {
|
|
124
|
+
fixes.push(fixer.replaceText(spec.imported, 'Grid'));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
return fixes;
|
|
101
130
|
},
|
|
102
131
|
});
|
|
103
132
|
}
|
|
@@ -110,18 +139,18 @@ const muiV7Rules = {
|
|
|
110
139
|
meta: {
|
|
111
140
|
type: 'problem',
|
|
112
141
|
docs: {
|
|
113
|
-
description: '
|
|
142
|
+
description: 'Components moved from @mui/lab to @mui/material',
|
|
114
143
|
category: 'Breaking Changes',
|
|
115
144
|
recommended: true,
|
|
116
145
|
},
|
|
117
146
|
messages: {
|
|
118
|
-
labImport: '✨ {{ count }}
|
|
119
|
-
'🔧
|
|
147
|
+
labImport: '✨ {{ count }} component(s) moved to @mui/material in V7!\n\n' +
|
|
148
|
+
'🔧 Old way (V6):\n' +
|
|
120
149
|
' import { {{ components }} } from "@mui/lab"\n\n' +
|
|
121
|
-
'✅
|
|
150
|
+
'✅ Recommended:\n' +
|
|
122
151
|
' import { {{ components }} } from "@mui/material"\n\n' +
|
|
123
|
-
'📦
|
|
124
|
-
' Skeleton, SpeedDial, ToggleButton, AvatarGroup,
|
|
152
|
+
'📦 All moved components: Alert, Autocomplete, Pagination, Rating,\n' +
|
|
153
|
+
' Skeleton, SpeedDial, ToggleButton, AvatarGroup, and more!',
|
|
125
154
|
},
|
|
126
155
|
schema: [],
|
|
127
156
|
fixable: 'code',
|
|
@@ -131,7 +160,7 @@ const muiV7Rules = {
|
|
|
131
160
|
ImportDeclaration(node) {
|
|
132
161
|
const source = node.source.value;
|
|
133
162
|
|
|
134
|
-
//
|
|
163
|
+
// Detect imports from @mui/lab
|
|
135
164
|
if (source.startsWith('@mui/lab')) {
|
|
136
165
|
// Collect ALL moved components (O(n) with Set.has O(1) lookup)
|
|
137
166
|
const movedComponentsList = node.specifiers
|
|
@@ -161,38 +190,148 @@ const muiV7Rules = {
|
|
|
161
190
|
meta: {
|
|
162
191
|
type: 'problem',
|
|
163
192
|
docs: {
|
|
164
|
-
description: 'Grid
|
|
193
|
+
description: 'Grid no longer uses the `item` prop, now uses `size`',
|
|
165
194
|
category: 'Breaking Changes',
|
|
166
195
|
recommended: true,
|
|
167
196
|
},
|
|
168
197
|
messages: {
|
|
169
|
-
gridItemProp: '🎯 Grid
|
|
170
|
-
'🔧
|
|
171
|
-
' <Grid item xs={12} sm={6}
|
|
172
|
-
'✅
|
|
173
|
-
' <Grid size={{ xs: 12, sm: 6
|
|
174
|
-
'💡
|
|
175
|
-
'
|
|
198
|
+
gridItemProp: '🎯 Grid in MUI V7 no longer uses the `item` prop!\n\n' +
|
|
199
|
+
'🔧 Old way (V6):\n' +
|
|
200
|
+
' <Grid item xs={12} sm={6}>\n\n' +
|
|
201
|
+
'✅ New way (V7):\n' +
|
|
202
|
+
' <Grid size={12}> or <Grid size={{ "{"}xs: 12, sm: 6{"}"} }}>\n\n' +
|
|
203
|
+
'💡 The new syntax is cleaner and more powerful!\n' +
|
|
204
|
+
' You can use: size, offset, responsive spacing, and more.',
|
|
176
205
|
},
|
|
177
206
|
schema: [],
|
|
207
|
+
fixable: 'code',
|
|
178
208
|
},
|
|
179
209
|
create(context) {
|
|
210
|
+
const sourceCode = context.getSourceCode();
|
|
211
|
+
|
|
180
212
|
return {
|
|
181
213
|
JSXOpeningElement(node) {
|
|
182
|
-
if (node.name
|
|
183
|
-
|
|
184
|
-
|
|
214
|
+
if (node.name?.name === 'Grid') {
|
|
215
|
+
// Check if it's a Grid container (should not report error)
|
|
216
|
+
const hasContainerProp = node.attributes.some(
|
|
217
|
+
attr => attr.type === 'JSXAttribute' && attr.name?.name === 'container'
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
// Grid container can have breakpoint props (xs, sm, etc) without issue
|
|
221
|
+
if (hasContainerProp) return;
|
|
222
|
+
|
|
223
|
+
const itemProp = node.attributes.find(
|
|
224
|
+
attr => attr.type === 'JSXAttribute' && attr.name?.name === 'item'
|
|
185
225
|
);
|
|
186
226
|
|
|
187
|
-
const
|
|
227
|
+
const breakpointProps = node.attributes.filter(
|
|
188
228
|
attr => attr.type === 'JSXAttribute' &&
|
|
189
|
-
['xs', 'sm', 'md', 'lg', 'xl'].includes(attr.name
|
|
229
|
+
['xs', 'sm', 'md', 'lg', 'xl'].includes(attr.name?.name)
|
|
190
230
|
);
|
|
191
231
|
|
|
192
|
-
if (
|
|
232
|
+
if (itemProp || breakpointProps.length > 0) {
|
|
193
233
|
context.report({
|
|
194
234
|
node,
|
|
195
235
|
messageId: 'gridItemProp',
|
|
236
|
+
fix(fixer) {
|
|
237
|
+
// 🔒 SAFETY CHECK: Don't autofix if there are spread props
|
|
238
|
+
// Spread props may contain item/xs/sm/etc and override our fix
|
|
239
|
+
const hasSpreadProps = node.attributes.some(
|
|
240
|
+
attr => attr.type === 'JSXSpreadAttribute'
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
if (hasSpreadProps) {
|
|
244
|
+
// Only report the problem, no autofix (too risky)
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const fixes = [];
|
|
249
|
+
const propsToRemove = [];
|
|
250
|
+
|
|
251
|
+
// Add item prop for removal
|
|
252
|
+
if (itemProp) {
|
|
253
|
+
propsToRemove.push(itemProp);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Only autofix breakpoints if they are simple literal values
|
|
257
|
+
if (breakpointProps.length > 0) {
|
|
258
|
+
// Check if all values are simple literals
|
|
259
|
+
const allSimpleLiterals = breakpointProps.every(prop => {
|
|
260
|
+
if (!prop.value) return false;
|
|
261
|
+
// Direct literal: xs="12" or xs={12}
|
|
262
|
+
if (prop.value.type === 'Literal') return true;
|
|
263
|
+
// JSXExpressionContainer with Literal: xs={12}
|
|
264
|
+
if (prop.value.type === 'JSXExpressionContainer' &&
|
|
265
|
+
prop.value.expression?.type === 'Literal') return true;
|
|
266
|
+
return false;
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
if (allSimpleLiterals) {
|
|
270
|
+
// Extract values
|
|
271
|
+
const breakpointValues = breakpointProps.map(prop => {
|
|
272
|
+
const name = prop.name.name;
|
|
273
|
+
let value;
|
|
274
|
+
|
|
275
|
+
if (prop.value.type === 'Literal') {
|
|
276
|
+
value = prop.value.value;
|
|
277
|
+
} else if (prop.value.type === 'JSXExpressionContainer') {
|
|
278
|
+
value = prop.value.expression.value;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return { name, value };
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// Add breakpoint props for removal
|
|
285
|
+
propsToRemove.push(...breakpointProps);
|
|
286
|
+
|
|
287
|
+
// Remove all props (including spaces)
|
|
288
|
+
propsToRemove.forEach((prop, index) => {
|
|
289
|
+
const sourceCodeText = sourceCode.getText();
|
|
290
|
+
let start = prop.range[0];
|
|
291
|
+
let end = prop.range[1];
|
|
292
|
+
|
|
293
|
+
// Remove space before the prop
|
|
294
|
+
while (start > 0 && /\s/.test(sourceCodeText[start - 1])) {
|
|
295
|
+
start--;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
fixes.push(fixer.removeRange([start, end]));
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// Create the new size prop
|
|
302
|
+
let sizeValue;
|
|
303
|
+
if (breakpointValues.length === 1) {
|
|
304
|
+
// Simple case: size={12}
|
|
305
|
+
const { value } = breakpointValues[0];
|
|
306
|
+
sizeValue = `size={${JSON.stringify(value)}}`;
|
|
307
|
+
} else {
|
|
308
|
+
// Multiple breakpoints: size={{ xs: 12, sm: 6 }}
|
|
309
|
+
const objPairs = breakpointValues
|
|
310
|
+
.map(({ name, value }) => `${name}: ${JSON.stringify(value)}`)
|
|
311
|
+
.join(', ');
|
|
312
|
+
sizeValue = `size={{ ${objPairs} }}`;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Insert the size prop after the opening tag
|
|
316
|
+
const insertPosition = node.name.range[1];
|
|
317
|
+
fixes.push(fixer.insertTextAfterRange([insertPosition, insertPosition], ` ${sizeValue}`));
|
|
318
|
+
}
|
|
319
|
+
} else if (itemProp) {
|
|
320
|
+
// Only remove the item prop (without adding size)
|
|
321
|
+
const sourceCodeText = sourceCode.getText();
|
|
322
|
+
let start = itemProp.range[0];
|
|
323
|
+
let end = itemProp.range[1];
|
|
324
|
+
|
|
325
|
+
// Remove space before the prop
|
|
326
|
+
while (start > 0 && /\s/.test(sourceCodeText[start - 1])) {
|
|
327
|
+
start--;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
fixes.push(fixer.removeRange([start, end]));
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return fixes.length > 0 ? fixes : null;
|
|
334
|
+
},
|
|
196
335
|
});
|
|
197
336
|
}
|
|
198
337
|
}
|
|
@@ -205,54 +344,66 @@ const muiV7Rules = {
|
|
|
205
344
|
meta: {
|
|
206
345
|
type: 'problem',
|
|
207
346
|
docs: {
|
|
208
|
-
description: '
|
|
347
|
+
description: 'Detects deprecated props and components in MUI V7',
|
|
209
348
|
category: 'Breaking Changes',
|
|
210
349
|
recommended: true,
|
|
211
350
|
},
|
|
212
351
|
messages: {
|
|
213
|
-
onBackdropClick: '🔄
|
|
214
|
-
'🔧
|
|
215
|
-
' <
|
|
216
|
-
'✅
|
|
217
|
-
' <
|
|
352
|
+
onBackdropClick: '🔄 {{ component }}.onBackdropClick was removed in V7!\n\n' +
|
|
353
|
+
'🔧 Old way (V6):\n' +
|
|
354
|
+
' <{{ component }} onBackdropClick={handleClick}>\n\n' +
|
|
355
|
+
'✅ New way (V7):\n' +
|
|
356
|
+
' <{{ component }} onClose={(event, reason) => {\n' +
|
|
218
357
|
' if (reason === "backdropClick") {\n' +
|
|
219
|
-
' //
|
|
358
|
+
' // Your logic here\n' +
|
|
220
359
|
' }\n' +
|
|
221
360
|
' }}>',
|
|
222
361
|
|
|
223
|
-
inputLabelNormal: '📏 InputLabel.size="normal"
|
|
224
|
-
'🔧
|
|
362
|
+
inputLabelNormal: '📏 InputLabel.size="normal" was renamed!\n\n' +
|
|
363
|
+
'🔧 Old way (V6):\n' +
|
|
225
364
|
' <InputLabel size="normal">\n\n' +
|
|
226
|
-
'✅
|
|
365
|
+
'✅ New way (V7):\n' +
|
|
227
366
|
' <InputLabel size="medium">',
|
|
228
367
|
|
|
229
|
-
hiddenComponent: '👻 Hidden component
|
|
230
|
-
'🔧
|
|
368
|
+
hiddenComponent: '👻 Hidden component was removed in V7!\n\n' +
|
|
369
|
+
'🔧 Old way (V6):\n' +
|
|
231
370
|
' <Hidden xlUp><Paper /></Hidden>\n\n' +
|
|
232
|
-
'✅
|
|
371
|
+
'✅ Option 1 - Use sx prop:\n' +
|
|
233
372
|
' <Paper sx={{ display: { xl: "none" } }} />\n\n' +
|
|
234
|
-
'✅
|
|
373
|
+
'✅ Option 2 - Use useMediaQuery:\n' +
|
|
374
|
+
' const hidden = useMediaQuery(theme => theme.breakpoints.up("xl"))\n' +
|
|
375
|
+
' return hidden ? null : <Paper />',
|
|
376
|
+
|
|
377
|
+
pigmentHiddenComponent: '👻 PigmentHidden component was removed in V7!\n\n' +
|
|
378
|
+
'🔧 Old way (V6):\n' +
|
|
379
|
+
' <PigmentHidden xlUp><Paper /></PigmentHidden>\n\n' +
|
|
380
|
+
'✅ Option 1 - Use sx prop:\n' +
|
|
381
|
+
' <Paper sx={{ display: { xl: "none" } }} />\n\n' +
|
|
382
|
+
'✅ Option 2 - Use useMediaQuery:\n' +
|
|
235
383
|
' const hidden = useMediaQuery(theme => theme.breakpoints.up("xl"))\n' +
|
|
236
384
|
' return hidden ? null : <Paper />',
|
|
237
385
|
},
|
|
238
386
|
schema: [],
|
|
387
|
+
fixable: 'code',
|
|
239
388
|
},
|
|
240
389
|
create(context) {
|
|
241
390
|
return {
|
|
242
391
|
JSXOpeningElement(node) {
|
|
243
|
-
const componentName = node.name
|
|
392
|
+
const componentName = node.name?.name;
|
|
393
|
+
if (!componentName) return;
|
|
244
394
|
|
|
245
|
-
// Dialog.onBackdropClick
|
|
246
|
-
if (componentName === 'Dialog') {
|
|
395
|
+
// Dialog.onBackdropClick and Modal.onBackdropClick
|
|
396
|
+
if (componentName === 'Dialog' || componentName === 'Modal') {
|
|
247
397
|
const hasOnBackdropClick = node.attributes.some(
|
|
248
398
|
attr => attr.type === 'JSXAttribute' &&
|
|
249
|
-
attr.name
|
|
399
|
+
attr.name?.name === 'onBackdropClick'
|
|
250
400
|
);
|
|
251
401
|
|
|
252
402
|
if (hasOnBackdropClick) {
|
|
253
403
|
context.report({
|
|
254
404
|
node,
|
|
255
405
|
messageId: 'onBackdropClick',
|
|
406
|
+
data: { component: componentName },
|
|
256
407
|
});
|
|
257
408
|
}
|
|
258
409
|
}
|
|
@@ -261,13 +412,16 @@ const muiV7Rules = {
|
|
|
261
412
|
if (componentName === 'InputLabel') {
|
|
262
413
|
node.attributes.forEach(attr => {
|
|
263
414
|
if (attr.type === 'JSXAttribute' &&
|
|
264
|
-
attr.name
|
|
415
|
+
attr.name?.name === 'size' &&
|
|
265
416
|
attr.value &&
|
|
266
417
|
attr.value.type === 'Literal' &&
|
|
267
418
|
attr.value.value === 'normal') {
|
|
268
419
|
context.report({
|
|
269
420
|
node: attr,
|
|
270
421
|
messageId: 'inputLabelNormal',
|
|
422
|
+
fix(fixer) {
|
|
423
|
+
return fixer.replaceText(attr.value, '"medium"');
|
|
424
|
+
},
|
|
271
425
|
});
|
|
272
426
|
}
|
|
273
427
|
});
|
|
@@ -280,6 +434,151 @@ const muiV7Rules = {
|
|
|
280
434
|
messageId: 'hiddenComponent',
|
|
281
435
|
});
|
|
282
436
|
}
|
|
437
|
+
|
|
438
|
+
// PigmentHidden component
|
|
439
|
+
if (componentName === 'PigmentHidden') {
|
|
440
|
+
context.report({
|
|
441
|
+
node,
|
|
442
|
+
messageId: 'pigmentHiddenComponent',
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
},
|
|
446
|
+
};
|
|
447
|
+
},
|
|
448
|
+
},
|
|
449
|
+
|
|
450
|
+
'no-deprecated-imports': {
|
|
451
|
+
meta: {
|
|
452
|
+
type: 'problem',
|
|
453
|
+
docs: {
|
|
454
|
+
description: 'Detects deprecated imports in MUI V7',
|
|
455
|
+
category: 'Breaking Changes',
|
|
456
|
+
recommended: true,
|
|
457
|
+
},
|
|
458
|
+
messages: {
|
|
459
|
+
createMuiTheme: '🎨 createMuiTheme was removed in V7!\n\n' +
|
|
460
|
+
'🔧 Old way (V6):\n' +
|
|
461
|
+
' import { createMuiTheme } from "@mui/material/styles"\n\n' +
|
|
462
|
+
'✅ New way (V7):\n' +
|
|
463
|
+
' import { createTheme } from "@mui/material/styles"\n\n' +
|
|
464
|
+
'💡 The functionality is identical, only the name changed!',
|
|
465
|
+
|
|
466
|
+
experimentalStyled: '🎨 experimentalStyled was removed in V7!\n\n' +
|
|
467
|
+
'🔧 Old way (V6):\n' +
|
|
468
|
+
' import { experimentalStyled } from "@mui/material/styles"\n\n' +
|
|
469
|
+
'✅ New way (V7):\n' +
|
|
470
|
+
' import { styled } from "@mui/material/styles"\n\n' +
|
|
471
|
+
'💡 styled is now stable and fully supported!',
|
|
472
|
+
|
|
473
|
+
styledEngineProvider: '⚙️ StyledEngineProvider can no longer be imported from @mui/material!\n\n' +
|
|
474
|
+
'🔧 Old way (V6):\n' +
|
|
475
|
+
' import { StyledEngineProvider } from "@mui/material"\n\n' +
|
|
476
|
+
'✅ New way (V7):\n' +
|
|
477
|
+
' import { StyledEngineProvider } from "@mui/material/styles"\n\n' +
|
|
478
|
+
'💡 The correct location since V5 is @mui/material/styles!',
|
|
479
|
+
},
|
|
480
|
+
schema: [],
|
|
481
|
+
fixable: 'code',
|
|
482
|
+
},
|
|
483
|
+
create(context) {
|
|
484
|
+
return {
|
|
485
|
+
ImportDeclaration(node) {
|
|
486
|
+
const source = node.source.value;
|
|
487
|
+
|
|
488
|
+
// Detect StyledEngineProvider being imported from @mui/material (wrong)
|
|
489
|
+
if (source === '@mui/material') {
|
|
490
|
+
node.specifiers.forEach(spec => {
|
|
491
|
+
if (spec.type === 'ImportSpecifier' && spec.imported.name === 'StyledEngineProvider') {
|
|
492
|
+
context.report({
|
|
493
|
+
node,
|
|
494
|
+
messageId: 'styledEngineProvider',
|
|
495
|
+
fix(fixer) {
|
|
496
|
+
// Change the source from '@mui/material' to '@mui/material/styles'
|
|
497
|
+
return fixer.replaceText(node.source, '"@mui/material/styles"');
|
|
498
|
+
},
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Detect imports from @mui/material/styles
|
|
505
|
+
if (source === '@mui/material/styles' || source === '@mui/material') {
|
|
506
|
+
node.specifiers.forEach(spec => {
|
|
507
|
+
if (spec.type === 'ImportSpecifier') {
|
|
508
|
+
const importedName = spec.imported.name;
|
|
509
|
+
|
|
510
|
+
// createMuiTheme → createTheme
|
|
511
|
+
if (importedName === 'createMuiTheme') {
|
|
512
|
+
context.report({
|
|
513
|
+
node: spec,
|
|
514
|
+
messageId: 'createMuiTheme',
|
|
515
|
+
fix(fixer) {
|
|
516
|
+
return fixer.replaceText(spec.imported, 'createTheme');
|
|
517
|
+
},
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// experimentalStyled → styled
|
|
522
|
+
if (importedName === 'experimentalStyled') {
|
|
523
|
+
context.report({
|
|
524
|
+
node: spec,
|
|
525
|
+
messageId: 'experimentalStyled',
|
|
526
|
+
fix(fixer) {
|
|
527
|
+
return fixer.replaceText(spec.imported, 'styled');
|
|
528
|
+
},
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
},
|
|
535
|
+
};
|
|
536
|
+
},
|
|
537
|
+
},
|
|
538
|
+
|
|
539
|
+
'prefer-slots-api': {
|
|
540
|
+
meta: {
|
|
541
|
+
type: 'suggestion',
|
|
542
|
+
docs: {
|
|
543
|
+
description: 'Recommends using slots/slotProps instead of components/componentsProps',
|
|
544
|
+
category: 'Best Practices',
|
|
545
|
+
recommended: true,
|
|
546
|
+
},
|
|
547
|
+
messages: {
|
|
548
|
+
useSlots: '🔧 The components/componentsProps API was deprecated!\n\n' +
|
|
549
|
+
'⚠️ Old way (deprecated):\n' +
|
|
550
|
+
' <TextField components={{"{"}...{"}"} componentsProps={{"{"}...{"}"} } />\n\n' +
|
|
551
|
+
'✅ New way (recommended):\n' +
|
|
552
|
+
' <TextField slots={{"{"}...{"}"} slotProps={{"{"}...{"}"} } />\n\n' +
|
|
553
|
+
'💡 The new API is more consistent and flexible!',
|
|
554
|
+
},
|
|
555
|
+
schema: [],
|
|
556
|
+
fixable: 'code',
|
|
557
|
+
},
|
|
558
|
+
create(context) {
|
|
559
|
+
return {
|
|
560
|
+
JSXOpeningElement(node) {
|
|
561
|
+
const deprecatedProps = node.attributes.filter(
|
|
562
|
+
attr => attr.type === 'JSXAttribute' &&
|
|
563
|
+
(attr.name?.name === 'components' || attr.name?.name === 'componentsProps')
|
|
564
|
+
);
|
|
565
|
+
|
|
566
|
+
if (deprecatedProps.length > 0) {
|
|
567
|
+
context.report({
|
|
568
|
+
node,
|
|
569
|
+
messageId: 'useSlots',
|
|
570
|
+
fix(fixer) {
|
|
571
|
+
// Rename each deprecated prop
|
|
572
|
+
return deprecatedProps.map(attr => {
|
|
573
|
+
if (attr.name.name === 'components') {
|
|
574
|
+
return fixer.replaceText(attr.name, 'slots');
|
|
575
|
+
} else if (attr.name.name === 'componentsProps') {
|
|
576
|
+
return fixer.replaceText(attr.name, 'slotProps');
|
|
577
|
+
}
|
|
578
|
+
}).filter(Boolean);
|
|
579
|
+
},
|
|
580
|
+
});
|
|
581
|
+
}
|
|
283
582
|
},
|
|
284
583
|
};
|
|
285
584
|
},
|
|
@@ -289,47 +588,48 @@ const muiV7Rules = {
|
|
|
289
588
|
meta: {
|
|
290
589
|
type: 'suggestion',
|
|
291
590
|
docs: {
|
|
292
|
-
description: '
|
|
591
|
+
description: 'Recommends using theme.vars for CSS variables',
|
|
293
592
|
category: 'Best Practices',
|
|
294
593
|
recommended: false,
|
|
295
594
|
},
|
|
296
595
|
messages: {
|
|
297
|
-
useThemeVars: '💡
|
|
298
|
-
'⚠️
|
|
596
|
+
useThemeVars: '💡 When `cssVariables: true`, use theme.vars!\n\n' +
|
|
597
|
+
'⚠️ Way that does NOT change with dark mode:\n' +
|
|
299
598
|
' color: theme.palette.text.primary\n\n' +
|
|
300
|
-
'✅
|
|
599
|
+
'✅ Way that changes automatically:\n' +
|
|
301
600
|
' color: theme.vars.palette.text.primary\n\n' +
|
|
302
|
-
'📚
|
|
601
|
+
'📚 Benefits: Performance + Automatic dark mode!',
|
|
303
602
|
},
|
|
304
603
|
schema: [],
|
|
604
|
+
fixable: 'code',
|
|
305
605
|
},
|
|
306
606
|
create(context) {
|
|
307
607
|
const sourceCode = context.getSourceCode();
|
|
308
608
|
|
|
309
609
|
/**
|
|
310
|
-
*
|
|
311
|
-
*
|
|
610
|
+
* Check if the node is inside a ternary that checks theme.vars
|
|
611
|
+
* Example: theme.vars ? `${theme.vars.palette.primary.main}` : `${theme.palette.primary.main}`
|
|
312
612
|
*/
|
|
313
613
|
function isInsideThemeVarsConditional(node) {
|
|
314
614
|
let current = node;
|
|
315
615
|
let depth = 0;
|
|
316
616
|
const MAX_DEPTH = 10;
|
|
317
617
|
|
|
318
|
-
//
|
|
618
|
+
// Go up MAX_DEPTH levels in the AST tree looking for ConditionalExpression
|
|
319
619
|
while (current.parent && depth < MAX_DEPTH) {
|
|
320
620
|
current = current.parent;
|
|
321
621
|
depth++;
|
|
322
622
|
|
|
323
|
-
//
|
|
623
|
+
// If a ternary (ConditionalExpression) is found
|
|
324
624
|
if (current.type === 'ConditionalExpression') {
|
|
325
625
|
const test = current.test;
|
|
326
|
-
//
|
|
626
|
+
// Check if the test is "theme.vars"
|
|
327
627
|
if (
|
|
328
628
|
test?.type === 'MemberExpression' &&
|
|
329
629
|
test.object?.name === 'theme' &&
|
|
330
630
|
test.property?.name === 'vars'
|
|
331
631
|
) {
|
|
332
|
-
return true; //
|
|
632
|
+
return true; // Ignore warnings when inside ternary with theme.vars
|
|
333
633
|
}
|
|
334
634
|
}
|
|
335
635
|
}
|
|
@@ -343,8 +643,8 @@ const muiV7Rules = {
|
|
|
343
643
|
const sourceTextCache = new WeakMap();
|
|
344
644
|
|
|
345
645
|
/**
|
|
346
|
-
*
|
|
347
|
-
*
|
|
646
|
+
* Check if inside an sx function that uses theme.vars!
|
|
647
|
+
* Example: sx={(theme) => ({ background: `${theme.vars!.palette...}` })}
|
|
348
648
|
*/
|
|
349
649
|
function isUsingNonNullAssertion(node) {
|
|
350
650
|
if (!node.parent) return false;
|
|
@@ -355,52 +655,197 @@ const muiV7Rules = {
|
|
|
355
655
|
sourceTextCache.set(node.parent, sourceText);
|
|
356
656
|
}
|
|
357
657
|
|
|
358
|
-
//
|
|
658
|
+
// Look for theme.vars! (non-null assertion)
|
|
359
659
|
return sourceText.includes('theme.vars!');
|
|
360
660
|
}
|
|
361
661
|
|
|
362
662
|
return {
|
|
363
663
|
MemberExpression(node) {
|
|
364
|
-
//
|
|
664
|
+
// Detect theme.palette.* (without .vars)
|
|
365
665
|
// Optimized: use optional chaining and early returns
|
|
366
666
|
if (node.object?.type !== 'MemberExpression') return;
|
|
367
667
|
if (node.object.object?.name !== 'theme') return;
|
|
368
668
|
if (node.object.property?.name !== 'palette') return;
|
|
369
669
|
|
|
370
|
-
//
|
|
670
|
+
// Check it's not theme.vars.palette
|
|
371
671
|
const parent = node.object.object;
|
|
372
672
|
if (parent.type !== 'Identifier' || parent.name !== 'theme') return;
|
|
373
673
|
|
|
374
|
-
//
|
|
674
|
+
// Ignore if inside a ternary that checks theme.vars
|
|
375
675
|
if (isInsideThemeVarsConditional(node)) return;
|
|
376
676
|
|
|
377
|
-
//
|
|
677
|
+
// Ignore if already using theme.vars! (non-null assertion)
|
|
378
678
|
if (isUsingNonNullAssertion(node)) return;
|
|
379
679
|
|
|
380
680
|
context.report({
|
|
381
681
|
node,
|
|
382
682
|
messageId: 'useThemeVars',
|
|
683
|
+
fix(fixer) {
|
|
684
|
+
// Transform: theme.palette.* → theme.vars.palette.*
|
|
685
|
+
// node.object.object is 'theme'
|
|
686
|
+
// node.object.property is 'palette'
|
|
687
|
+
const themeNode = node.object.object;
|
|
688
|
+
|
|
689
|
+
// Insert '.vars' after 'theme'
|
|
690
|
+
const insertPosition = themeNode.range[1];
|
|
691
|
+
return fixer.insertTextAfterRange(
|
|
692
|
+
[insertPosition, insertPosition],
|
|
693
|
+
'.vars'
|
|
694
|
+
);
|
|
695
|
+
},
|
|
383
696
|
});
|
|
384
697
|
},
|
|
385
698
|
};
|
|
386
699
|
},
|
|
387
700
|
},
|
|
701
|
+
|
|
702
|
+
'no-deep-imports': {
|
|
703
|
+
meta: {
|
|
704
|
+
type: 'problem',
|
|
705
|
+
docs: {
|
|
706
|
+
description: 'Deep imports break in MUI V7 due to new exports field',
|
|
707
|
+
category: 'Breaking Changes',
|
|
708
|
+
recommended: true,
|
|
709
|
+
},
|
|
710
|
+
messages: {
|
|
711
|
+
deepImport: '📦 Deep imports no longer work in MUI V7!\n\n' +
|
|
712
|
+
'🔧 Old way (V6):\n' +
|
|
713
|
+
' import {{ importName }} from "{{ source }}"\n\n' +
|
|
714
|
+
'✅ New way (V7):\n' +
|
|
715
|
+
' import { {{ importName }} } from "{{ suggestedSource }}"\n\n' +
|
|
716
|
+
'💡 MUI V7 uses exports field in package.json, which blocks deep imports.',
|
|
717
|
+
},
|
|
718
|
+
schema: [],
|
|
719
|
+
fixable: 'code',
|
|
720
|
+
},
|
|
721
|
+
create(context) {
|
|
722
|
+
return {
|
|
723
|
+
ImportDeclaration(node) {
|
|
724
|
+
const source = node.source.value;
|
|
725
|
+
|
|
726
|
+
// Detect MUI deep imports (more than one level deep)
|
|
727
|
+
// Example: @mui/material/styles/createTheme
|
|
728
|
+
const muiDeepImportRegex = /^@mui\/(material|system|joy)\/([^/]+)\/(.+)$/;
|
|
729
|
+
const match = source.match(muiDeepImportRegex);
|
|
730
|
+
|
|
731
|
+
if (match) {
|
|
732
|
+
const [, package_, componentDir, deepPath] = match;
|
|
733
|
+
const suggestedSource = `@mui/${package_}`;
|
|
734
|
+
|
|
735
|
+
// Infer component name from directory (e.g., Button from /Button/...)
|
|
736
|
+
const importName = componentDir;
|
|
737
|
+
const localName = node.specifiers[0]?.local?.name || importName;
|
|
738
|
+
|
|
739
|
+
context.report({
|
|
740
|
+
node,
|
|
741
|
+
messageId: 'deepImport',
|
|
742
|
+
data: {
|
|
743
|
+
source,
|
|
744
|
+
suggestedSource,
|
|
745
|
+
importName,
|
|
746
|
+
},
|
|
747
|
+
fix(fixer) {
|
|
748
|
+
const fixes = [fixer.replaceText(node.source, `"${suggestedSource}"`)];
|
|
749
|
+
|
|
750
|
+
// Convert default import to named import
|
|
751
|
+
if (node.specifiers[0]?.type === 'ImportDefaultSpecifier') {
|
|
752
|
+
// If the local name equals the imported name, use simple syntax
|
|
753
|
+
if (importName === localName) {
|
|
754
|
+
fixes.push(fixer.replaceText(node.specifiers[0], `{ ${importName} }`));
|
|
755
|
+
} else {
|
|
756
|
+
fixes.push(fixer.replaceText(node.specifiers[0], `{ ${importName} as ${localName} }`));
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
return fixes;
|
|
761
|
+
},
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
},
|
|
765
|
+
};
|
|
766
|
+
},
|
|
767
|
+
},
|
|
768
|
+
|
|
769
|
+
'no-grid-legacy': {
|
|
770
|
+
meta: {
|
|
771
|
+
type: 'problem',
|
|
772
|
+
docs: {
|
|
773
|
+
description: 'Old Grid was renamed to GridLegacy in MUI V7',
|
|
774
|
+
category: 'Breaking Changes',
|
|
775
|
+
recommended: true,
|
|
776
|
+
},
|
|
777
|
+
messages: {
|
|
778
|
+
gridLegacyImport: '⚠️ The old Grid was deprecated and renamed to GridLegacy!\n\n' +
|
|
779
|
+
'🔧 You are using:\n' +
|
|
780
|
+
' import Grid from "@mui/material/Grid"\n\n' +
|
|
781
|
+
'✅ Option 1 - Continue using old Grid (temporary):\n' +
|
|
782
|
+
' import { GridLegacy as Grid } from "@mui/material"\n\n' +
|
|
783
|
+
'✅ Option 2 - Migrate to new Grid (recommended):\n' +
|
|
784
|
+
' import { Grid } from "@mui/material"\n' +
|
|
785
|
+
' // Use size={{ "{"}xs: 12{"}"} }} instead of item xs={12}\n\n' +
|
|
786
|
+
'💡 The new Grid is more powerful and uses the `size` prop!\n' +
|
|
787
|
+
' See: https://mui.com/material-ui/migration/upgrade-to-grid-v2/',
|
|
788
|
+
},
|
|
789
|
+
schema: [],
|
|
790
|
+
fixable: 'code',
|
|
791
|
+
},
|
|
792
|
+
create(context) {
|
|
793
|
+
return {
|
|
794
|
+
ImportDeclaration(node) {
|
|
795
|
+
const source = node.source.value;
|
|
796
|
+
|
|
797
|
+
// Detect direct import of old Grid: import Grid from '@mui/material/Grid'
|
|
798
|
+
if (source === '@mui/material/Grid') {
|
|
799
|
+
context.report({
|
|
800
|
+
node,
|
|
801
|
+
messageId: 'gridLegacyImport',
|
|
802
|
+
fix(fixer) {
|
|
803
|
+
// Suggested fix: change to GridLegacy
|
|
804
|
+
const fixes = [fixer.replaceText(node.source, '"@mui/material"')];
|
|
805
|
+
|
|
806
|
+
// Rename default import to GridLegacy as Grid
|
|
807
|
+
if (node.specifiers.length > 0 && node.specifiers[0].type === 'ImportDefaultSpecifier') {
|
|
808
|
+
const localName = node.specifiers[0].local.name;
|
|
809
|
+
if (localName === 'Grid') {
|
|
810
|
+
fixes.push(fixer.replaceText(node.specifiers[0], '{ GridLegacy as Grid }'));
|
|
811
|
+
} else {
|
|
812
|
+
// Keep custom alias
|
|
813
|
+
fixes.push(fixer.replaceText(node.specifiers[0], `{ GridLegacy as ${localName} }`));
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
return fixes;
|
|
818
|
+
},
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
},
|
|
822
|
+
};
|
|
823
|
+
},
|
|
824
|
+
},
|
|
388
825
|
};
|
|
389
826
|
|
|
390
|
-
//
|
|
827
|
+
// Export the plugin (ESM and CommonJS compatible)
|
|
391
828
|
const plugin = {
|
|
829
|
+
meta: {
|
|
830
|
+
name: packageJson.name,
|
|
831
|
+
version: packageJson.version,
|
|
832
|
+
},
|
|
392
833
|
rules: muiV7Rules,
|
|
393
834
|
configs: {
|
|
394
835
|
recommended: {
|
|
395
836
|
plugins: ['mui-v7'],
|
|
396
837
|
rules: {
|
|
397
|
-
// Breaking changes - ERRORS (
|
|
838
|
+
// Breaking changes - ERRORS (code breaks)
|
|
398
839
|
'mui-v7/no-unstable-grid': 'error',
|
|
399
840
|
'mui-v7/no-grid2-import': 'error',
|
|
400
841
|
'mui-v7/no-grid-item-prop': 'error',
|
|
401
842
|
'mui-v7/no-lab-imports': 'error',
|
|
402
843
|
'mui-v7/no-deprecated-props': 'error',
|
|
403
|
-
|
|
844
|
+
'mui-v7/no-deprecated-imports': 'error',
|
|
845
|
+
'mui-v7/no-deep-imports': 'error',
|
|
846
|
+
'mui-v7/no-grid-legacy': 'error',
|
|
847
|
+
// Best practices - WARNINGS (suggestions)
|
|
848
|
+
'mui-v7/prefer-slots-api': 'warn',
|
|
404
849
|
'mui-v7/prefer-theme-vars': 'warn',
|
|
405
850
|
},
|
|
406
851
|
},
|
|
@@ -413,7 +858,11 @@ const plugin = {
|
|
|
413
858
|
'mui-v7/no-grid-item-prop': 'error',
|
|
414
859
|
'mui-v7/no-lab-imports': 'error',
|
|
415
860
|
'mui-v7/no-deprecated-props': 'error',
|
|
416
|
-
|
|
861
|
+
'mui-v7/no-deprecated-imports': 'error',
|
|
862
|
+
'mui-v7/no-deep-imports': 'error',
|
|
863
|
+
'mui-v7/no-grid-legacy': 'error',
|
|
864
|
+
// Best practices - ERRORS also in strict mode
|
|
865
|
+
'mui-v7/prefer-slots-api': 'error',
|
|
417
866
|
'mui-v7/prefer-theme-vars': 'error',
|
|
418
867
|
},
|
|
419
868
|
},
|