@mui/x-codemod 9.0.0-rc.0 → 9.0.4

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 CHANGED
@@ -69,7 +69,7 @@ To run codemods for a specific package, refer to the respective section.
69
69
  <!-- #npm-tag-reference -->
70
70
 
71
71
  ```bash
72
- npx @mui/x-codemod@next v9.0.0/preset-safe <path|folder>
72
+ npx @mui/x-codemod@latest v9.0.0/preset-safe <path|folder>
73
73
  ```
74
74
 
75
75
  The corresponding sub-sections are listed below
@@ -90,7 +90,7 @@ If `charts` is the only property, the entire `experimentalFeatures` prop is remo
90
90
  <!-- #npm-tag-reference -->
91
91
 
92
92
  ```bash
93
- npx @mui/x-codemod@next v9.0.0/data-grid/remove-stabilized-experimentalFeatures <path|folder>
93
+ npx @mui/x-codemod@latest v9.0.0/data-grid/remove-stabilized-experimentalFeatures <path|folder>
94
94
  ```
95
95
 
96
96
  ```diff
@@ -109,7 +109,7 @@ The `preset-safe` codemods for Charts.
109
109
  <!-- #npm-tag-reference -->
110
110
 
111
111
  ```bash
112
- npx @mui/x-codemod@next v9.0.0/charts/preset-safe <path|folder>
112
+ npx @mui/x-codemod@latest v9.0.0/charts/preset-safe <path|folder>
113
113
  ```
114
114
 
115
115
  The list includes these transformers
@@ -320,7 +320,7 @@ The `preset-safe` codemods for Pickers.
320
320
  <!-- #npm-tag-reference -->
321
321
 
322
322
  ```bash
323
- npx @mui/x-codemod@next v9.0.0/pickers/preset-safe <path|folder>
323
+ npx @mui/x-codemod@latest v9.0.0/pickers/preset-safe <path|folder>
324
324
  ```
325
325
 
326
326
  The list includes these transformers
@@ -332,6 +332,7 @@ The list includes these transformers
332
332
  - [`rename-pickers-day`](#rename-pickers-day)
333
333
  - [`rename-picker-classes`](#rename-picker-classes)
334
334
  - [`remove-disable-margin`](#remove-disable-margin)
335
+ - [`migrate-text-field-props`](#migrate-text-field-props)
335
336
 
336
337
  #### `rename-field-ref`
337
338
 
@@ -351,7 +352,7 @@ Renames the `unstableFieldRef` prop to `fieldRef` on all Picker and Field compon
351
352
  <!-- #npm-tag-reference -->
352
353
 
353
354
  ```bash
354
- npx @mui/x-codemod@next v9.0.0/pickers/rename-field-ref <path|folder>
355
+ npx @mui/x-codemod@latest v9.0.0/pickers/rename-field-ref <path|folder>
355
356
  ```
356
357
 
357
358
  #### `remove-enable-accessible-field-dom-structure`
@@ -373,7 +374,7 @@ The accessible DOM structure is now the only supported option and this prop has
373
374
  <!-- #npm-tag-reference -->
374
375
 
375
376
  ```bash
376
- npx @mui/x-codemod@next v9.0.0/pickers/remove-enable-accessible-field-dom-structure <path|folder>
377
+ npx @mui/x-codemod@latest v9.0.0/pickers/remove-enable-accessible-field-dom-structure <path|folder>
377
378
  ```
378
379
 
379
380
  #### `remove-picker-day-2`
@@ -392,7 +393,7 @@ Also handles objects passed through variables (for example `const slots = { day:
392
393
  <!-- #npm-tag-reference -->
393
394
 
394
395
  ```bash
395
- npx @mui/x-codemod@next v9.0.0/pickers/remove-picker-day-2 <path>
396
+ npx @mui/x-codemod@latest v9.0.0/pickers/remove-picker-day-2 <path>
396
397
  ```
397
398
 
398
399
  #### `rename-picker-day-2`
@@ -419,7 +420,7 @@ Renames `PickerDay2` and `DateRangePickerDay2` components and their related type
419
420
  <!-- #npm-tag-reference -->
420
421
 
421
422
  ```bash
422
- npx @mui/x-codemod@next v9.0.0/pickers/rename-picker-day-2 <path>
423
+ npx @mui/x-codemod@latest v9.0.0/pickers/rename-picker-day-2 <path>
423
424
  ```
424
425
 
425
426
  #### `rename-pickers-day`
@@ -443,7 +444,7 @@ Renames `PickersDay` to `PickerDay` and all related types, classes, and theme co
443
444
  <!-- #npm-tag-reference -->
444
445
 
445
446
  ```bash
446
- npx @mui/x-codemod@next v9.0.0/pickers/rename-pickers-day <path>
447
+ npx @mui/x-codemod@latest v9.0.0/pickers/rename-pickers-day <path>
447
448
  ```
448
449
 
449
450
  #### `rename-picker-classes`
@@ -464,7 +465,7 @@ Renames `PickerDay` and `DateRangePickerDay` CSS class keys to their new equival
464
465
  <!-- #npm-tag-reference -->
465
466
 
466
467
  ```bash
467
- npx @mui/x-codemod@next v9.0.0/pickers/rename-picker-classes <path>
468
+ npx @mui/x-codemod@latest v9.0.0/pickers/rename-picker-classes <path>
468
469
  ```
469
470
 
470
471
  #### `remove-disable-margin`
@@ -484,7 +485,37 @@ When `disableMargin={false}`, the prop is simply removed without adding the CSS
484
485
  <!-- #npm-tag-reference -->
485
486
 
486
487
  ```bash
487
- npx @mui/x-codemod@next v9.0.0/pickers/remove-disable-margin <path>
488
+ npx @mui/x-codemod@latest v9.0.0/pickers/remove-disable-margin <path>
489
+ ```
490
+
491
+ #### `migrate-text-field-props`
492
+
493
+ Rewrites the legacy `InputProps`, `inputProps`, `InputLabelProps` and `FormHelperTextProps` props on Picker, Field and `PickersTextField` components into the new `slotProps.{input,htmlInput,inputLabel,formHelperText}` shape. On Picker and Field components the new keys are nested inside `slotProps.textField.slotProps`; on `PickersTextField` they live directly under `slotProps`.
494
+
495
+ ```diff
496
+ -<DateField
497
+ - InputProps={{ name: 'birthday' }}
498
+ - inputProps={{ 'data-testid': 'html-input' }}
499
+ -/>
500
+ +<DateField
501
+ + slotProps={{
502
+ + textField: {
503
+ + slotProps: {
504
+ + input: { name: 'birthday' },
505
+ + htmlInput: { 'data-testid': 'html-input' },
506
+ + },
507
+ + },
508
+ + }}
509
+ +/>
510
+
511
+ -<DatePicker slotProps={{ textField: { InputProps: { name: 'date' } } }} />
512
+ +<DatePicker slotProps={{ textField: { slotProps: { input: { name: 'date' } } } }} />
513
+ ```
514
+
515
+ <!-- #npm-tag-reference -->
516
+
517
+ ```bash
518
+ npx @mui/x-codemod@next v9.0.0/pickers/migrate-text-field-props <path>
488
519
  ```
489
520
 
490
521
  ## v8.0.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mui/x-codemod",
3
- "version": "9.0.0-rc.0",
3
+ "version": "9.0.4",
4
4
  "author": "MUI Team",
5
5
  "description": "Codemod scripts for MUI X.",
6
6
  "license": "MIT",
@@ -24,11 +24,11 @@
24
24
  },
25
25
  "dependencies": {
26
26
  "@babel/core": "^7.29.0",
27
- "@babel/runtime": "^7.28.6",
27
+ "@babel/runtime": "^7.29.2",
28
28
  "@babel/traverse": "^7.29.0",
29
29
  "jscodeshift": "17.3.0",
30
30
  "yargs": "^18.0.0",
31
- "@mui/x-internals": "9.0.0-rc.0"
31
+ "@mui/x-internals": "^9.0.4"
32
32
  },
33
33
  "sideEffects": false,
34
34
  "publishConfig": {
@@ -15,11 +15,27 @@ const addItemToObject = (path, value, object, j) => {
15
15
 
16
16
  // Final case where we have to add the property to the object.
17
17
  if (splittedPath.length === 1) {
18
- const propertyToAdd = j.objectProperty(j.identifier(path), value);
19
18
  if (object === null) {
19
+ const propertyToAdd = j.objectProperty(j.identifier(path), value);
20
20
  return j.objectExpression([propertyToAdd]);
21
21
  }
22
- return j.objectExpression([...(object.properties ?? []).filter(property => property.key.name !== path), propertyToAdd]);
22
+
23
+ // When both the existing and new values are ObjectExpressions, merge their properties
24
+ // (new properties win on key conflicts) instead of replacing the entire object.
25
+ const existingProperty = (object.properties ?? []).find(property => property.key?.name === path || property.key?.value === path);
26
+ if (existingProperty && existingProperty.value.type === 'ObjectExpression' && value.type === 'ObjectExpression') {
27
+ // Spread elements (e.g. `{ ...rest }`) have no `key`, so guard against `undefined`
28
+ // sneaking into the dedup set — otherwise existing spreads would be filtered out.
29
+ const newKeys = new Set(value.properties.map(p => p.key?.name ?? p.key?.value).filter(key => key !== undefined));
30
+ const mergedValue = j.objectExpression([...existingProperty.value.properties.filter(p => {
31
+ const key = p.key?.name ?? p.key?.value;
32
+ return key === undefined || !newKeys.has(key);
33
+ }), ...value.properties]);
34
+ const mergedProperty = j.objectProperty(j.identifier(path), mergedValue);
35
+ return j.objectExpression([...(object.properties ?? []).filter(property => (property.key?.name ?? property.key?.value) !== path), mergedProperty]);
36
+ }
37
+ const propertyToAdd = j.objectProperty(j.identifier(path), value);
38
+ return j.objectExpression([...(object.properties ?? []).filter(property => (property.key?.name ?? property.key?.value) !== path), propertyToAdd]);
23
39
  }
24
40
  const remainingPath = splittedPath.slice(1).join('.');
25
41
  const targetKey = splittedPath[0];
@@ -30,11 +46,12 @@ const addItemToObject = (path, value, object, j) => {
30
46
  }
31
47
 
32
48
  // Look if the object we got already contains the property we have to use.
33
- const correspondingObject = (object.properties ?? []).find(property => property.key.name === targetKey);
49
+ // `property.key` is missing on spread / rest elements, so guard the access.
50
+ const correspondingObject = (object.properties ?? []).find(property => (property.key?.name ?? property.key?.value) === targetKey);
34
51
  const propertyToAdd = j.objectProperty(j.identifier(targetKey),
35
52
  // Here we use recursion to mix the new value with the current one
36
53
  addItemToObject(remainingPath, value, correspondingObject?.value ?? null, j));
37
- return j.objectExpression([...(object.properties ?? []).filter(property => property.key.name !== targetKey), propertyToAdd]);
54
+ return j.objectExpression([...(object.properties ?? []).filter(property => (property.key?.name ?? property.key?.value) !== targetKey), propertyToAdd]);
38
55
  };
39
56
 
40
57
  /**
@@ -0,0 +1,192 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
4
+ Object.defineProperty(exports, "__esModule", {
5
+ value: true
6
+ });
7
+ exports.default = transformer;
8
+ exports.testConfig = void 0;
9
+ var _path = _interopRequireDefault(require("path"));
10
+ var _readFile = _interopRequireDefault(require("../../../util/readFile"));
11
+ var _addComponentsSlots = require("../../../util/addComponentsSlots");
12
+ var _removeProps = _interopRequireDefault(require("../../../util/removeProps"));
13
+ // @ts-ignore - JS file without types
14
+
15
+ /**
16
+ * Maps a legacy text-field prop name to the corresponding key inside `slotProps`.
17
+ */
18
+ const PROP_TO_SLOT = {
19
+ InputProps: 'input',
20
+ inputProps: 'htmlInput',
21
+ InputLabelProps: 'inputLabel',
22
+ FormHelperTextProps: 'formHelperText'
23
+ };
24
+ const LEGACY_PROP_NAMES = Object.keys(PROP_TO_SLOT);
25
+ const FIELD_AND_PICKER_NAMES = [
26
+ // Fields
27
+ 'DateField', 'DateTimeField', 'TimeField', 'DateRangeField', 'DateTimeRangeField', 'TimeRangeField', 'MultiInputDateRangeField', 'MultiInputDateTimeRangeField', 'MultiInputTimeRangeField', 'SingleInputDateRangeField', 'SingleInputDateTimeRangeField', 'SingleInputTimeRangeField',
28
+ // Pickers
29
+ 'DatePicker', 'DesktopDatePicker', 'MobileDatePicker', 'StaticDatePicker', 'DateTimePicker', 'DesktopDateTimePicker', 'MobileDateTimePicker', 'StaticDateTimePicker', 'TimePicker', 'DesktopTimePicker', 'MobileTimePicker', 'StaticTimePicker', 'DateRangePicker', 'DesktopDateRangePicker', 'MobileDateRangePicker', 'StaticDateRangePicker', 'DateTimeRangePicker', 'DesktopDateTimeRangePicker', 'MobileDateTimeRangePicker', 'TimeRangePicker', 'DesktopTimeRangePicker', 'MobileTimeRangePicker'];
30
+ const ALL_TARGET_NAMES = [...FIELD_AND_PICKER_NAMES, 'PickersTextField'];
31
+ const getKeyName = key => {
32
+ if (!key) {
33
+ return undefined;
34
+ }
35
+ if (key.type === 'Identifier') {
36
+ return key.name;
37
+ }
38
+ if (key.type === 'Literal' || key.type === 'StringLiteral') {
39
+ return String(key.value);
40
+ }
41
+ return undefined;
42
+ };
43
+ function transformer(file, api, options) {
44
+ const j = api.jscodeshift;
45
+ const root = j(file.source);
46
+ const printOptions = options.printOptions || {
47
+ quote: 'single',
48
+ trailingComma: true
49
+ };
50
+
51
+ // 1. Rewrite legacy props passed directly as JSX attributes on field / picker components.
52
+ root.find(j.JSXElement).filter(elementPath => {
53
+ const nameNode = elementPath.value.openingElement.name;
54
+ return nameNode && nameNode.type === 'JSXIdentifier' && ALL_TARGET_NAMES.includes(nameNode.name);
55
+ }).forEach(elementPath => {
56
+ const nameNode = elementPath.value.openingElement.name;
57
+ // For PickersTextField the new keys live directly on `slotProps`.
58
+ // For every other component they live on a nested `textField.slotProps` object.
59
+ const pathPrefix = nameNode.name === 'PickersTextField' ? '' : 'textField.slotProps.';
60
+ const attributesToTransform = j(elementPath).find(j.JSXAttribute).filter(attribute => {
61
+ const attributeParent = attribute.parentPath.parentPath;
62
+ if (attributeParent.value.type !== 'JSXOpeningElement' || attributeParent.value.name.name !== nameNode.name) {
63
+ return false;
64
+ }
65
+ return LEGACY_PROP_NAMES.includes(attribute.value.name.name);
66
+ });
67
+ attributesToTransform.forEach(attribute => {
68
+ const attributeName = attribute.value.name.name;
69
+ const value = attribute.value.value?.type === 'JSXExpressionContainer' ? attribute.value.value.expression : attribute.value.value || j.booleanLiteral(true);
70
+ (0, _addComponentsSlots.transformNestedProp)(elementPath, 'slotProps', `${pathPrefix}${PROP_TO_SLOT[attributeName]}`, value, j);
71
+ });
72
+ });
73
+
74
+ // Drop the now-orphaned legacy attributes from the targeted components.
75
+ (0, _removeProps.default)({
76
+ root,
77
+ componentNames: ALL_TARGET_NAMES,
78
+ props: LEGACY_PROP_NAMES,
79
+ j
80
+ });
81
+
82
+ // 2. Rewrite legacy props found inside `slotProps={{ field: { ... } }}` and
83
+ // `slotProps={{ textField: { ... } }}` regardless of which component they appear on.
84
+ root.find(j.JSXAttribute, {
85
+ name: {
86
+ name: 'slotProps'
87
+ }
88
+ }).forEach(attrPath => {
89
+ const openingElement = attrPath.parentPath?.parentPath?.value;
90
+ if (!openingElement || openingElement.type !== 'JSXOpeningElement' || openingElement.name?.type !== 'JSXIdentifier' || !FIELD_AND_PICKER_NAMES.includes(openingElement.name.name)) {
91
+ return;
92
+ }
93
+ const attrValue = attrPath.value.value;
94
+ if (!attrValue || attrValue.type !== 'JSXExpressionContainer') {
95
+ return;
96
+ }
97
+ const expression = attrValue.expression;
98
+ if (expression.type !== 'ObjectExpression') {
99
+ return;
100
+ }
101
+ // Collect legacy props found inside `field` / `textField` slot objects.
102
+ // - `textField` legacy props are migrated in-place under `textField.slotProps.<newKey>`.
103
+ // - `field` legacy props cannot be nested under `field.slotProps.textField.slotProps.<newKey>`
104
+ // because the `field` slotProps type does not allow it. Hoist them to the sibling
105
+ // `textField.slotProps.<newKey>` instead.
106
+ const fieldCollected = [];
107
+ expression.properties.forEach(prop => {
108
+ if (prop.type === 'SpreadElement' || prop.type === 'ExperimentalSpreadProperty') {
109
+ console.warn(`[migrate-text-field-props] ${file.path}: encountered a spread inside slotProps; ` + `cannot inspect for legacy text field props. Migrate manually if needed.`);
110
+ return;
111
+ }
112
+ if (prop.type !== 'Property' && prop.type !== 'ObjectProperty') {
113
+ return;
114
+ }
115
+ const keyName = getKeyName(prop.key);
116
+ if (keyName !== 'field' && keyName !== 'textField') {
117
+ return;
118
+ }
119
+ if (prop.value.type !== 'ObjectExpression') {
120
+ console.warn(`[migrate-text-field-props] ${file.path}: \`slotProps.${keyName}\` is set to a ` + `non-literal value (e.g. a variable or a function); cannot migrate legacy ` + `text field props automatically. Migrate manually if needed.`);
121
+ return;
122
+ }
123
+ let target = prop.value;
124
+ const remaining = [];
125
+ const collected = [];
126
+ target.properties.forEach(innerProp => {
127
+ if (innerProp.type !== 'Property' && innerProp.type !== 'ObjectProperty') {
128
+ remaining.push(innerProp);
129
+ return;
130
+ }
131
+ const innerKey = getKeyName(innerProp.key);
132
+ if (innerKey && LEGACY_PROP_NAMES.includes(innerKey)) {
133
+ collected.push({
134
+ newKey: PROP_TO_SLOT[innerKey],
135
+ value: innerProp.value
136
+ });
137
+ return;
138
+ }
139
+ remaining.push(innerProp);
140
+ });
141
+ if (collected.length === 0) {
142
+ return;
143
+ }
144
+ if (keyName === 'field') {
145
+ // Defer: hoist these to the sibling `textField` slot below.
146
+ target.properties = remaining;
147
+ fieldCollected.push(...collected);
148
+ return;
149
+ }
150
+ target.properties = remaining;
151
+ // Use the same recursive merge helper used by `transformNestedProp`.
152
+ collected.forEach(({
153
+ newKey,
154
+ value
155
+ }) => {
156
+ target = (0, _addComponentsSlots.addItemToObject)(`slotProps.${newKey}`, value, target, j);
157
+ });
158
+ prop.value = target;
159
+ });
160
+ if (fieldCollected.length > 0) {
161
+ // Drop `field` if it became empty.
162
+ expression.properties = expression.properties.filter(prop => {
163
+ if (prop.type !== 'Property' && prop.type !== 'ObjectProperty') {
164
+ return true;
165
+ }
166
+ if (getKeyName(prop.key) !== 'field') {
167
+ return true;
168
+ }
169
+ return prop.value.type !== 'ObjectExpression' || prop.value.properties.length > 0;
170
+ });
171
+ // Hoist the collected legacy props to `textField.slotProps.<newKey>`.
172
+ let merged = expression;
173
+ fieldCollected.forEach(({
174
+ newKey,
175
+ value
176
+ }) => {
177
+ merged = (0, _addComponentsSlots.addItemToObject)(`textField.slotProps.${newKey}`, value, merged, j);
178
+ });
179
+ expression.properties = merged.properties;
180
+ }
181
+ });
182
+ return root.toSource(printOptions);
183
+ }
184
+ const testConfig = () => ({
185
+ name: 'migrate-text-field-props',
186
+ specFiles: [{
187
+ name: 'migrate legacy text field props to slotProps',
188
+ actual: (0, _readFile.default)(_path.default.join(__dirname, 'actual.spec.tsx')),
189
+ expected: (0, _readFile.default)(_path.default.join(__dirname, 'expected.spec.tsx'))
190
+ }]
191
+ });
192
+ exports.testConfig = testConfig;
@@ -13,11 +13,12 @@ var _renamePickersDay = _interopRequireDefault(require("../rename-pickers-day"))
13
13
  var _renamePickerClasses = _interopRequireDefault(require("../rename-picker-classes"));
14
14
  var _removeDisableMargin = _interopRequireDefault(require("../remove-disable-margin"));
15
15
  var _removeEnableAccessibleFieldDomStructure = _interopRequireDefault(require("../remove-enable-accessible-field-dom-structure"));
16
+ var _migrateTextFieldProps = _interopRequireDefault(require("../migrate-text-field-props"));
16
17
  // Order matters: removePickerDay2 must run before renamePickerDay2
17
18
  // because it looks for `PickerDay2` identifiers in slot objects.
18
19
  // If renamePickerDay2 ran first, those identifiers would already be
19
20
  // renamed to `PickerDay` and removePickerDay2 would not find them.
20
- const allModules = [_renameFieldRef.default, _removePickerDay.default, _renamePickerDay.default, _renamePickersDay.default, _renamePickerClasses.default, _removeDisableMargin.default, _removeEnableAccessibleFieldDomStructure.default];
21
+ const allModules = [_renameFieldRef.default, _removePickerDay.default, _renamePickerDay.default, _renamePickersDay.default, _renamePickerClasses.default, _removeDisableMargin.default, _removeEnableAccessibleFieldDomStructure.default, _migrateTextFieldProps.default];
21
22
  function transformer(file, api, options) {
22
23
  allModules.forEach(transform => {
23
24
  file.source = transform(file, api, options);