@react-spectrum/codemods 0.2.1 → 0.4.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.
@@ -22,6 +22,39 @@ exports.changes = {
22
22
  }
23
23
  ]
24
24
  },
25
+ ActionGroup: {
26
+ changes: [
27
+ {
28
+ description: 'Comment out overflowMode',
29
+ reason: 'It has not been implemented yet',
30
+ function: {
31
+ name: 'commentOutProp',
32
+ args: { propToComment: 'overflowMode' }
33
+ }
34
+ },
35
+ {
36
+ description: 'Comment out buttonLabelBehavior',
37
+ reason: 'It has not been implemented yet',
38
+ function: {
39
+ name: 'commentOutProp',
40
+ args: { propToComment: 'buttonLabelBehavior' }
41
+ }
42
+ },
43
+ {
44
+ description: 'Comment out summaryIcon',
45
+ reason: 'It has not been implemented yet',
46
+ function: {
47
+ name: 'commentOutProp',
48
+ args: { propToComment: 'summaryIcon' }
49
+ }
50
+ },
51
+ {
52
+ description: 'Replace with ActionButtonGroup or ToggleButtonGroup',
53
+ reason: 'The API has changed',
54
+ function: { name: 'updateActionGroup', args: {} }
55
+ }
56
+ ]
57
+ },
25
58
  ActionMenu: {
26
59
  changes: [
27
60
  {
@@ -334,6 +367,30 @@ exports.changes = {
334
367
  name: 'moveRenderPropsToChild',
335
368
  args: { newChildComponent: 'Dialog' }
336
369
  }
370
+ },
371
+ {
372
+ description: 'Rename isDismissable to isDismissible',
373
+ reason: 'Fixed spelling',
374
+ function: { name: 'updatePropName', args: { oldProp: 'isDismissable', newProp: 'isDismissible' } }
375
+ },
376
+ {
377
+ description: 'Update Dialog child to Popover or FullscreenDialog depending on type prop',
378
+ reason: 'Updated API',
379
+ function: { name: 'updateDialogChild', args: {} }
380
+ }
381
+ ]
382
+ },
383
+ DialogContainer: {
384
+ changes: [
385
+ {
386
+ description: 'Rename isDismissable to isDismissible',
387
+ reason: 'Fixed spelling',
388
+ function: { name: 'updatePropName', args: { oldProp: 'isDismissable', newProp: 'isDismissible' } }
389
+ },
390
+ {
391
+ description: 'Update Dialog child to Popover or FullscreenDialog depending on type prop',
392
+ reason: 'Updated API',
393
+ function: { name: 'updateDialogChild', args: {} }
337
394
  }
338
395
  ]
339
396
  },
@@ -1091,5 +1148,113 @@ exports.changes = {
1091
1148
  }
1092
1149
  }
1093
1150
  ]
1151
+ },
1152
+ TableView: {
1153
+ changes: [
1154
+ {
1155
+ description: 'Add columns prop to Row',
1156
+ reason: 'Rows now require a columns prop from TableHeader',
1157
+ function: {
1158
+ name: 'addColumnsPropToRow',
1159
+ args: {}
1160
+ }
1161
+ },
1162
+ {
1163
+ description: 'Leave comment if nested columns are used',
1164
+ reason: 'Nested columns are not supported yet',
1165
+ function: {
1166
+ name: 'commentIfNestedColumns',
1167
+ args: {}
1168
+ }
1169
+ },
1170
+ {
1171
+ description: 'Comment out dragAndDropHooks',
1172
+ reason: 'It has not been implemented yet',
1173
+ function: {
1174
+ name: 'commentOutProp',
1175
+ args: { propToComment: 'dragAndDropHooks' }
1176
+ }
1177
+ },
1178
+ {
1179
+ description: 'Comment out selectionStyle="highlight"',
1180
+ reason: 'It has not been implemented yet',
1181
+ function: {
1182
+ name: 'commentOutProp',
1183
+ args: { propToComment: 'selectionStyle' }
1184
+ }
1185
+ },
1186
+ {
1187
+ description: 'Comment out UNSTABLE_allowsExpandableRows',
1188
+ reason: 'It has not been implemented yet',
1189
+ function: {
1190
+ name: 'commentOutProp',
1191
+ args: { propToComment: 'UNSTABLE_allowsExpandableRows' }
1192
+ }
1193
+ },
1194
+ {
1195
+ description: 'Comment out UNSTABLE_defaultExpandedKeys',
1196
+ reason: 'It has not been implemented yet',
1197
+ function: {
1198
+ name: 'commentOutProp',
1199
+ args: { propToComment: 'UNSTABLE_defaultExpandedKeys' }
1200
+ }
1201
+ },
1202
+ {
1203
+ description: 'Comment out UNSTABLE_expandedKeys',
1204
+ reason: 'It has not been implemented yet',
1205
+ function: {
1206
+ name: 'commentOutProp',
1207
+ args: { propToComment: 'UNSTABLE_expandedKeys' }
1208
+ }
1209
+ },
1210
+ {
1211
+ description: 'Comment out UNSTABLE_onExpandedChange',
1212
+ reason: 'It has not been implemented yet',
1213
+ function: {
1214
+ name: 'commentOutProp',
1215
+ args: { propToComment: 'UNSTABLE_onExpandedChange' }
1216
+ }
1217
+ },
1218
+ {
1219
+ description: 'Add isRowHeader prop to fist Column if one doesn\'t eixst already',
1220
+ reason: 'Updated API',
1221
+ function: {
1222
+ name: 'addRowHeader',
1223
+ args: {}
1224
+ }
1225
+ }
1226
+ ]
1227
+ },
1228
+ Column: {
1229
+ changes: [
1230
+ {
1231
+ description: 'Update key prop to id',
1232
+ reason: 'Updated API',
1233
+ function: {
1234
+ name: 'updateKeyToId',
1235
+ args: {}
1236
+ }
1237
+ }
1238
+ ]
1239
+ },
1240
+ Row: {
1241
+ changes: [
1242
+ {
1243
+ description: 'Update key prop to id',
1244
+ reason: 'Updated API',
1245
+ function: {
1246
+ name: 'updateKeyToId',
1247
+ args: {}
1248
+ }
1249
+ },
1250
+ {
1251
+ description: 'Update child function to receive column object instead of column key',
1252
+ reason: 'Updated API',
1253
+ function: {
1254
+ name: 'updateRowFunctionArg',
1255
+ args: {}
1256
+ }
1257
+ }
1258
+ ]
1094
1259
  }
1095
1260
  };
@@ -49,6 +49,8 @@ availableComponents.add('Item');
49
49
  availableComponents.add('Section');
50
50
  // Don't update v3 Provider
51
51
  availableComponents.delete('Provider');
52
+ // Replaced by ActionButtonGroup and ToggleButtonGroup
53
+ availableComponents.add('ActionGroup');
52
54
  function transformer(file, api, options) {
53
55
  let j = api.jscodeshift.withParser({
54
56
  parse(source) {
@@ -227,7 +229,7 @@ function transformer(file, api, options) {
227
229
  if (importedComponents.size) {
228
230
  // Add imports to existing @react-spectrum/s2 import if it exists, otherwise add a new one.
229
231
  let importSpecifiers = new Set([...importedComponents]
230
- .filter(([c]) => c !== 'Flex' && c !== 'Grid' && c !== 'View' && c !== 'Item' && c !== 'Section')
232
+ .filter(([c]) => c !== 'Flex' && c !== 'Grid' && c !== 'View' && c !== 'Item' && c !== 'Section' && c !== 'ActionGroup')
231
233
  .map(([, specifier]) => specifier));
232
234
  let existingImport = root.find(j.ImportDeclaration, {
233
235
  source: { value: '@react-spectrum/s2' }
@@ -140,7 +140,7 @@ const UNIT_RE = /(%|px|em|rem|vw|vh|auto|cm|mm|in|pt|pc|ex|ch|rem|vmin|vmax|fr)$
140
140
  const FUNC_RE = /^\s*\w+\(/;
141
141
  // const SPECTRUM_VARIABLE_RE = /(static-)?size-\d+|single-line-(height|width)/g;
142
142
  const SIZING_RE = /auto|100vh|min-content|max-content|fit-content/;
143
- function convertDimension(value, toPixels = false) {
143
+ function convertDimension(value, type) {
144
144
  let pixelValue;
145
145
  if (typeof value === 'number') {
146
146
  pixelValue = value;
@@ -170,10 +170,10 @@ function convertDimension(value, toPixels = false) {
170
170
  if (pixelValue == null) {
171
171
  throw new Error('invalid dimension: ' + value);
172
172
  }
173
- if (toPixels) {
173
+ if (type === 'px') {
174
174
  return `${pixelValue}px`;
175
175
  }
176
- if (spacingValues.includes(pixelValue)) {
176
+ if (type === 'size' || spacingValues.includes(pixelValue)) {
177
177
  return pixelValue;
178
178
  }
179
179
  // TODO: Convert to rems? Find nearest value?
@@ -184,16 +184,16 @@ function convertGridTrack(value, toPixels = false) {
184
184
  return value;
185
185
  }
186
186
  else {
187
- return convertDimension(value, toPixels);
187
+ return convertDimension(value, toPixels ? 'px' : 'space');
188
188
  }
189
189
  }
190
- function convertUnsafeDimension(value) {
190
+ function convertUnsafeDimension(value, type) {
191
191
  if (typeof value === 'number') {
192
- return convertDimension(value);
192
+ return convertDimension(value, type);
193
193
  }
194
194
  let m = value.match(/^var\(--spectrum-global-dimension-(static-)?size-(.*)\)$/);
195
195
  if (m) {
196
- return convertDimension(`${m[1] || ''}size-${m[2]}`);
196
+ return convertDimension(`${m[1] || ''}size-${m[2]}`, type);
197
197
  }
198
198
  return null;
199
199
  }
@@ -63,7 +63,21 @@ function getStylePropValue(prop, value, element, colorVersion, condition = '') {
63
63
  case 'maxWidth':
64
64
  case 'height':
65
65
  case 'minHeight':
66
- case 'maxHeight':
66
+ case 'maxHeight': {
67
+ if (value.type === 'StringLiteral' || value.type === 'NumericLiteral') {
68
+ let val = (0, dimensions_1.convertDimension)(value.value, 'size');
69
+ if (val != null) {
70
+ return {
71
+ macroValues: [{ key: mappedProp, value: val }]
72
+ };
73
+ }
74
+ }
75
+ else if (value.type === 'ObjectExpression') {
76
+ return getResponsiveValue(prop, value, element, colorVersion);
77
+ }
78
+ // return [mappedProp, customProp, [[customProp, value]]];
79
+ return null;
80
+ }
67
81
  case 'margin':
68
82
  case 'marginStart':
69
83
  case 'marginEnd':
@@ -79,7 +93,7 @@ function getStylePropValue(prop, value, element, colorVersion, condition = '') {
79
93
  case 'end':
80
94
  case 'flexBasis': {
81
95
  if (value.type === 'StringLiteral' || value.type === 'NumericLiteral') {
82
- let val = (0, dimensions_1.convertDimension)(value.value);
96
+ let val = (0, dimensions_1.convertDimension)(value.value, 'space');
83
97
  if (val != null) {
84
98
  return {
85
99
  macroValues: [{ key: mappedProp, value: val }]
@@ -182,7 +196,7 @@ function getStylePropValue(prop, value, element, colorVersion, condition = '') {
182
196
  case 'rowGap':
183
197
  if (element === 'Flex' || element === 'Grid') {
184
198
  if (value.type === 'StringLiteral' || value.type === 'NumericLiteral') {
185
- let val = (0, dimensions_1.convertDimension)(value.value);
199
+ let val = (0, dimensions_1.convertDimension)(value.value, 'space');
186
200
  if (val != null) {
187
201
  return {
188
202
  macroValues: [{ key: mappedProp, value: val }]
@@ -308,7 +322,7 @@ function getStylePropValue(prop, value, element, colorVersion, condition = '') {
308
322
  case 'paddingBottom':
309
323
  if (element === 'View') {
310
324
  if (value.type === 'StringLiteral' || value.type === 'NumericLiteral') {
311
- let val = (0, dimensions_1.convertDimension)(value.value);
325
+ let val = (0, dimensions_1.convertDimension)(value.value, 'space');
312
326
  if (val != null) {
313
327
  return {
314
328
  macroValues: [{ key: mappedProp, value: val }]
@@ -410,7 +424,7 @@ function getStylePropValue(prop, value, element, colorVersion, condition = '') {
410
424
  // Try to automatically convert size prop to a macro value for components that supported size.
411
425
  if (element === 'ColorArea' || element === 'ColorWheel') {
412
426
  if (value.type === 'StringLiteral' || value.type === 'NumericLiteral') {
413
- let val = (0, dimensions_1.convertDimension)(value.value);
427
+ let val = (0, dimensions_1.convertDimension)(value.value, 'size');
414
428
  if (val != null) {
415
429
  return {
416
430
  macroValues: [{ key: 'size', value: val }]
@@ -24,6 +24,7 @@ var __importStar = (this && this.__importStar) || function (mod) {
24
24
  };
25
25
  Object.defineProperty(exports, "__esModule", { value: true });
26
26
  exports.functionMap = void 0;
27
+ exports.commentIfNestedColumns = commentIfNestedColumns;
27
28
  const utils_1 = require("./utils");
28
29
  const dimensions_1 = require("./dimensions");
29
30
  const getComponents_1 = require("../getComponents");
@@ -285,7 +286,16 @@ function moveRenderPropsToChild(path, options) {
285
286
  t.isArrowFunctionExpression(renderFunction.expression) &&
286
287
  t.isJSXElement(renderFunction.expression.body)) {
287
288
  const dialogElement = renderFunction.expression.body;
288
- const newRenderFunction = t.jsxExpressionContainer(t.arrowFunctionExpression(renderFunction.expression.params, t.jsxFragment(t.jsxOpeningFragment(), t.jsxClosingFragment(), dialogElement.children)));
289
+ const originalParam = renderFunction.expression.params[0];
290
+ if (!t.isIdentifier(originalParam)) {
291
+ (0, utils_1.addComment)(path.node.children[renderFunctionIndex], ' TODO(S2-upgrade): Could not automatically move the render props. You\'ll need to update this manually.');
292
+ return;
293
+ }
294
+ const paramName = originalParam.name;
295
+ const objectPattern = t.objectPattern([
296
+ t.objectProperty(t.identifier(paramName), t.identifier(paramName), false, true)
297
+ ]);
298
+ const newRenderFunction = t.jsxExpressionContainer(t.arrowFunctionExpression([objectPattern], t.jsxFragment(t.jsxOpeningFragment(), t.jsxClosingFragment(), dialogElement.children)));
289
299
  let removedOnDismiss = false;
290
300
  const attributes = dialogElement.openingElement.attributes.filter((attr) => {
291
301
  if (t.isJSXAttribute(attr) && attr.name.name === 'onDismiss') {
@@ -322,32 +332,13 @@ function updateComponentWithinCollection(path, options) {
322
332
  t.isJSXIdentifier(closestParentCollection.node.openingElement.name) &&
323
333
  (0, utils_1.getName)(path, closestParentCollection.node.openingElement.name) === parentComponent) {
324
334
  // If closest parent collection component matches parentComponent, replace with newComponent
325
- let attributes = path.node.openingElement.attributes;
326
- let keyProp = attributes.find((attr) => t.isJSXAttribute(attr) && attr.name.name === 'key');
327
- if (keyProp &&
328
- t.isJSXAttribute(keyProp)) {
329
- // Update key prop to be id
330
- keyProp.name = t.jsxIdentifier('id');
331
- }
332
- if (t.isArrowFunctionExpression(path.parentPath.node) &&
333
- path.parentPath.parentPath &&
334
- t.isCallExpression(path.parentPath.parentPath.node) &&
335
- path.parentPath.parentPath.node.callee.type === 'MemberExpression' &&
336
- path.parentPath.parentPath.node.callee.property.type === 'Identifier' &&
337
- path.parentPath.parentPath.node.callee.property.name === 'map') {
338
- // If Array.map is used, keep the key prop
339
- if (keyProp &&
340
- t.isJSXAttribute(keyProp)) {
341
- let newKeyProp = t.jsxAttribute(t.jsxIdentifier('key'), keyProp.value);
342
- attributes.push(newKeyProp);
343
- }
344
- }
335
+ updateKeyToId(path);
345
336
  let localName = newComponent;
346
337
  if (availableComponents.has(newComponent)) {
347
338
  let program = path.findParent((p) => t.isProgram(p.node));
348
339
  localName = (0, utils_1.addComponentImport)(program, newComponent);
349
340
  }
350
- let newNode = t.jsxElement(t.jsxOpeningElement(t.jsxIdentifier(localName), attributes), t.jsxClosingElement(t.jsxIdentifier(localName)), path.node.children);
341
+ let newNode = t.jsxElement(t.jsxOpeningElement(t.jsxIdentifier(localName), path.node.openingElement.attributes), t.jsxClosingElement(t.jsxIdentifier(localName)), path.node.children);
351
342
  path.replaceWith(newNode);
352
343
  }
353
344
  }
@@ -358,7 +349,7 @@ function updateComponentWithinCollection(path, options) {
358
349
  * Example: If they're declaring declaring Items somewhere above the collection.
359
350
  */
360
351
  function commentIfParentCollectionNotDetected(path) {
361
- const collectionItemParents = new Set(['Menu', 'ActionMenu', 'TagGroup', 'Breadcrumbs', 'Picker', 'ComboBox', 'ListBox', 'TabList', 'TabPanels', 'ActionGroup', 'ListBox', 'ListView', 'Collection', 'SearchAutocomplete', 'Accordion', 'ActionBar', 'StepList']);
352
+ const collectionItemParents = new Set(['Menu', 'ActionMenu', 'TagGroup', 'Breadcrumbs', 'Picker', 'ComboBox', 'ListBox', 'TabList', 'TabPanels', 'ActionGroup', 'ActionButtonGroup', 'ToggleButtonGroup', 'ListBox', 'ListView', 'Collection', 'SearchAutocomplete', 'Accordion', 'ActionBar', 'StepList']);
362
353
  if (t.isJSXElement(path.node)) {
363
354
  // Find closest parent collection component
364
355
  let closestParentCollection = path.findParent((p) => t.isJSXElement(p.node) &&
@@ -532,7 +523,7 @@ function convertDimensionValueToPx(path, options) {
532
523
  if (attrPath && t.isJSXAttribute(attrPath.node) && attrPath.node.name.name === propToConvertValue) {
533
524
  if (t.isStringLiteral(attrPath.node.value)) {
534
525
  try {
535
- let value = (0, dimensions_1.convertDimension)(attrPath.node.value.value);
526
+ let value = (0, dimensions_1.convertDimension)(attrPath.node.value.value, 'size');
536
527
  if (value && typeof value === 'number') {
537
528
  attrPath.node.value = t.jsxExpressionContainer(t.numericLiteral(value));
538
529
  }
@@ -685,6 +676,343 @@ function updateLegacyLink(path) {
685
676
  }
686
677
  }
687
678
  }
679
+ /**
680
+ * Copies the columns prop from the TableHeader to the Row component.
681
+ */
682
+ function addColumnsPropToRow(path) {
683
+ const tableHeaderPath = path.get('children').find((child) => t.isJSXElement(child.node) &&
684
+ t.isJSXIdentifier(child.node.openingElement.name) &&
685
+ (0, utils_1.getName)(child, child.node.openingElement.name) === 'TableHeader');
686
+ if (!tableHeaderPath) {
687
+ (0, utils_1.addComment)(path.node, ' TODO(S2-upgrade): Could not find TableHeader within Table to retrieve columns prop.');
688
+ return;
689
+ }
690
+ const columnsProp = tableHeaderPath
691
+ .get('openingElement')
692
+ .get('attributes')
693
+ .find((attr) => t.isJSXAttribute(attr.node) && attr.node.name.name === 'columns');
694
+ if (columnsProp) {
695
+ path.traverse({
696
+ JSXElement(innerPath) {
697
+ if (t.isJSXElement(innerPath.node) &&
698
+ t.isJSXIdentifier(innerPath.node.openingElement.name) &&
699
+ (0, utils_1.getName)(innerPath, innerPath.node.openingElement.name) === 'Row') {
700
+ let rowPath = innerPath;
701
+ rowPath.node.openingElement.attributes.push(columnsProp.node);
702
+ // If Row doesn't contain id prop, leave a comment for the user to check manually
703
+ let idProp = rowPath.get('openingElement').get('attributes').find((attr) => t.isJSXAttribute(attr.node) && attr.node.name.name === 'id');
704
+ if (!idProp) {
705
+ (0, utils_1.addComment)(rowPath.node, ' TODO(S2-upgrade): If the items do not have id properties, you\'ll need to add an id prop to the Row.');
706
+ }
707
+ }
708
+ }
709
+ });
710
+ }
711
+ }
712
+ /**
713
+ * Updates the function signature of the Row component.
714
+ */
715
+ function updateRowFunctionArg(path) {
716
+ // Find the function passed as a child
717
+ let functionChild = path.get('children').find(childPath => childPath.isJSXExpressionContainer() &&
718
+ childPath.get('expression').isArrowFunctionExpression());
719
+ let tablePath = path.findParent((p) => t.isJSXElement(p.node) &&
720
+ t.isJSXIdentifier(p.node.openingElement.name) &&
721
+ (0, utils_1.getName)(path, p.node.openingElement.name) === 'TableView');
722
+ let tableHeaderPath = tablePath?.get('children').find((child) => t.isJSXElement(child.node) &&
723
+ t.isJSXIdentifier(child.node.openingElement.name) &&
724
+ (0, utils_1.getName)(child, child.node.openingElement.name) === 'TableHeader');
725
+ function findColumnKeyProp(path) {
726
+ let columnKeyProp = 'id';
727
+ path.traverse({
728
+ JSXElement(columnPath) {
729
+ if (t.isArrowFunctionExpression(columnPath.parentPath.node) &&
730
+ t.isJSXElement(columnPath.node) &&
731
+ t.isJSXIdentifier(columnPath.node.openingElement.name) &&
732
+ (0, utils_1.getName)(columnPath, columnPath.node.openingElement.name) === 'Column') {
733
+ let openingElement = columnPath.get('openingElement');
734
+ let keyPropPath = openingElement.get('attributes').find(attr => t.isJSXAttribute(attr.node) &&
735
+ (attr.node.name.name === 'key' || attr.node.name.name === 'id'));
736
+ keyPropPath?.traverse({
737
+ Identifier(innerPath) {
738
+ if (innerPath.node.name === 'column' &&
739
+ innerPath.parentPath.node.type === 'MemberExpression' &&
740
+ t.isIdentifier(innerPath.parentPath.node.property)) {
741
+ columnKeyProp = innerPath.parentPath.node.property.name;
742
+ }
743
+ }
744
+ });
745
+ }
746
+ }
747
+ });
748
+ return columnKeyProp || 'id';
749
+ }
750
+ let columnKey = findColumnKeyProp(tableHeaderPath);
751
+ if (functionChild && functionChild.isJSXExpressionContainer()) {
752
+ let arrowFuncPath = functionChild.get('expression');
753
+ if (arrowFuncPath.isArrowFunctionExpression()) {
754
+ let params = arrowFuncPath.node.params;
755
+ if (params.length === 1 && t.isIdentifier(params[0])) {
756
+ let paramName = params[0].name;
757
+ // Rename parameter to 'column'
758
+ params[0].name = 'column';
759
+ // Replace references to the old parameter name inside the function body
760
+ arrowFuncPath.get('body').traverse({
761
+ Identifier(innerPath) {
762
+ if (innerPath.node.name === paramName &&
763
+ // Ensure we're not replacing the parameter declaration
764
+ innerPath.node !== params[0]) {
765
+ // Replace with column key
766
+ innerPath.replaceWith(t.memberExpression(t.identifier('column'), t.identifier(columnKey ?? 'id')));
767
+ }
768
+ }
769
+ });
770
+ }
771
+ }
772
+ }
773
+ }
774
+ /**
775
+ * Updates the key prop to id. Keeps the key prop if it's used in an array.map function.
776
+ */
777
+ function updateKeyToId(path) {
778
+ let attributes = path.node.openingElement.attributes;
779
+ let keyProp = attributes.find((attr) => t.isJSXAttribute(attr) && attr.name.name === 'key');
780
+ if (keyProp &&
781
+ t.isJSXAttribute(keyProp)) {
782
+ // Update key prop to be id
783
+ keyProp.name = t.jsxIdentifier('id');
784
+ }
785
+ if (t.isArrowFunctionExpression(path.parentPath.node) &&
786
+ path.parentPath.parentPath &&
787
+ t.isCallExpression(path.parentPath.parentPath.node) &&
788
+ path.parentPath.parentPath.node.callee.type === 'MemberExpression' &&
789
+ path.parentPath.parentPath.node.callee.property.type === 'Identifier' &&
790
+ path.parentPath.parentPath.node.callee.property.name === 'map') {
791
+ // If Array.map is used, keep the key prop
792
+ if (keyProp &&
793
+ t.isJSXAttribute(keyProp)) {
794
+ let newKeyProp = t.jsxAttribute(t.jsxIdentifier('key'), keyProp.value);
795
+ attributes.push(newKeyProp);
796
+ }
797
+ }
798
+ }
799
+ function commentIfNestedColumns(path) {
800
+ const headerPath = path.get('children').find((child) => t.isJSXElement(child.node) &&
801
+ t.isJSXIdentifier(child.node.openingElement.name) &&
802
+ (0, utils_1.getName)(child, child.node.openingElement.name) === 'TableHeader');
803
+ const columns = headerPath?.get('children') || [];
804
+ let hasNestedColumns = false;
805
+ columns.forEach(column => {
806
+ let columnChildren = column.get('children');
807
+ if (columnChildren.find(child => t.isJSXElement(child.node) &&
808
+ t.isJSXIdentifier(child.node.openingElement.name) &&
809
+ (0, utils_1.getName)(child, child.node.openingElement.name) === 'Column')) {
810
+ hasNestedColumns = true;
811
+ }
812
+ });
813
+ if (hasNestedColumns) {
814
+ (0, utils_1.addComment)(path.node, ' TODO(S2-upgrade): Nested Column components are not supported yet.');
815
+ }
816
+ }
817
+ /**
818
+ * Updates DialogTrigger and DialogContainer to the new API.
819
+ *
820
+ * Example:
821
+ * - When `type="popover"`, replaces Dialog with `<Popover>`.
822
+ * - When `type="fullscreen"`, replaces Dialog with `<FullscreenDialog>`.
823
+ * - When `type="fullscreenTakeover"`, replaces Dialog with `<FullscreenDialog variant="fullscreenTakeover">`.
824
+ */
825
+ function updateDialogChild(path) {
826
+ let typePath = path.get('openingElement').get('attributes').find((attr) => t.isJSXAttribute(attr.node) && attr.node.name.name === 'type');
827
+ let type = typePath?.node.value?.type === 'StringLiteral' ? typePath.node.value?.value : 'modal';
828
+ let newComponent = 'Dialog';
829
+ let props = [];
830
+ if (type === 'popover') {
831
+ newComponent = 'Popover';
832
+ }
833
+ else if (type === 'fullscreen' || type === 'fullscreenTakeover') {
834
+ newComponent = 'FullscreenDialog';
835
+ if (type === 'fullscreenTakeover') {
836
+ props.push(t.jsxAttribute(t.jsxIdentifier('variant'), t.stringLiteral(type)));
837
+ }
838
+ }
839
+ for (let prop of ['isDismissible', 'mobileType', 'hideArrow', 'placement', 'shouldFlip', 'isKeyboardDismissDisabled', 'containerPadding', 'offset', 'crossOffset']) {
840
+ let attr = path.get('openingElement').get('attributes').find(attr => attr.isJSXAttribute() && attr.node.name.name === prop);
841
+ if (attr) {
842
+ props.push(attr.node);
843
+ attr.remove();
844
+ }
845
+ }
846
+ typePath?.remove();
847
+ let localName = newComponent;
848
+ if (newComponent !== 'Dialog' && availableComponents.has(newComponent)) {
849
+ let program = path.findParent((p) => t.isProgram(p.node));
850
+ localName = (0, utils_1.addComponentImport)(program, newComponent);
851
+ }
852
+ path.traverse({
853
+ JSXElement(dialog) {
854
+ if (!t.isJSXIdentifier(dialog.node.openingElement.name) || (0, utils_1.getName)(dialog, dialog.node.openingElement.name) !== 'Dialog') {
855
+ return;
856
+ }
857
+ dialog.node.openingElement.name = t.jsxIdentifier(localName);
858
+ if (dialog.node.closingElement) {
859
+ dialog.node.closingElement.name = t.jsxIdentifier(localName);
860
+ }
861
+ dialog.node.openingElement.attributes.push(...props);
862
+ }
863
+ });
864
+ }
865
+ function updateActionGroup(path) {
866
+ let selectionModePath = path.get('openingElement').get('attributes').find((attr) => t.isJSXAttribute(attr.node) && attr.node.name.name === 'selectionMode');
867
+ let selectionMode = t.isStringLiteral(selectionModePath?.node.value) ? selectionModePath.node.value.value : 'none';
868
+ let newComponent, childComponent;
869
+ if (selectionMode === 'none') {
870
+ newComponent = 'ActionButtonGroup';
871
+ childComponent = 'ActionButton';
872
+ selectionModePath?.remove();
873
+ }
874
+ else {
875
+ newComponent = 'ToggleButtonGroup';
876
+ childComponent = 'ToggleButton';
877
+ }
878
+ let localName = newComponent;
879
+ if (availableComponents.has(newComponent)) {
880
+ let program = path.findParent((p) => t.isProgram(p.node));
881
+ localName = (0, utils_1.addComponentImport)(program, newComponent);
882
+ }
883
+ let localChildName = childComponent;
884
+ if (availableComponents.has(childComponent)) {
885
+ let program = path.findParent((p) => t.isProgram(p.node));
886
+ localChildName = (0, utils_1.addComponentImport)(program, childComponent);
887
+ }
888
+ // Convert dynamic collection to an array.map.
889
+ let items = path.get('openingElement').get('attributes').find((attr) => t.isJSXAttribute(attr.node) && attr.node.name.name === 'items');
890
+ let itemArg;
891
+ if (items && t.isJSXExpressionContainer(items.node.value) && t.isExpression(items.node.value.expression)) {
892
+ let child = path.get('children').find(c => c.isJSXExpressionContainer());
893
+ if (child && child.isJSXExpressionContainer() && t.isFunction(child.node.expression)) {
894
+ let arg = child.node.expression.params[0];
895
+ if (t.isIdentifier(arg)) {
896
+ itemArg = arg;
897
+ }
898
+ child.replaceWith(t.jsxExpressionContainer(t.callExpression(t.memberExpression(items.node.value.expression, t.identifier('map')), [child.node.expression])));
899
+ }
900
+ }
901
+ items?.remove();
902
+ let onAction = path.get('openingElement').get('attributes').find((attr) => t.isJSXAttribute(attr.node) && attr.node.name.name === 'onAction');
903
+ // Pull disabledKeys prop out into a variable, converted to a Set.
904
+ // Then we can check it in the isDisabled prop of each item.
905
+ let disabledKeysPath = path.get('openingElement').get('attributes').find((attr) => t.isJSXAttribute(attr.node) && attr.node.name.name === 'disabledKeys');
906
+ let disabledKeys;
907
+ if (disabledKeysPath && t.isJSXExpressionContainer(disabledKeysPath.node.value) && t.isExpression(disabledKeysPath.node.value.expression)) {
908
+ disabledKeys = path.scope.generateUidIdentifier('disabledKeys');
909
+ path.scope.push({
910
+ id: disabledKeys,
911
+ init: t.newExpression(t.identifier('Set'), [disabledKeysPath.node.value.expression]),
912
+ kind: 'let'
913
+ });
914
+ disabledKeysPath.remove();
915
+ }
916
+ path.traverse({
917
+ JSXElement(child) {
918
+ if (t.isJSXIdentifier(child.node.openingElement.name) && child.node.openingElement.name.name === 'Item') {
919
+ // Replace Item with ActionButton or ToggleButton.
920
+ let childNode = t.cloneNode(child.node);
921
+ childNode.openingElement.name = t.jsxIdentifier(localChildName);
922
+ if (childNode.closingElement) {
923
+ childNode.closingElement.name = t.jsxIdentifier(localChildName);
924
+ }
925
+ // If there is no key prop and we are using dynamic collections, add a default computed from item.key ?? item.id.
926
+ let key = childNode.openingElement.attributes.find(attr => t.isJSXAttribute(attr) && attr.name.name === 'key');
927
+ if (!key && itemArg) {
928
+ let id = t.jsxExpressionContainer(t.logicalExpression('??', t.memberExpression(itemArg, t.identifier('key')), t.memberExpression(itemArg, t.identifier('id'))));
929
+ key = t.jsxAttribute(t.jsxIdentifier('key'), id);
930
+ childNode.openingElement.attributes.push(key);
931
+ }
932
+ // If this is a ToggleButtonGroup, add an id prop in addition to key when needed.
933
+ if (key && newComponent === 'ToggleButtonGroup') {
934
+ // If we are in an array.map we need both key and id. Otherwise, we only need id.
935
+ if (itemArg) {
936
+ childNode.openingElement.attributes.push(t.jsxAttribute(t.jsxIdentifier('id'), key.value));
937
+ }
938
+ else {
939
+ key.name.name = 'id';
940
+ }
941
+ }
942
+ let keyValue = undefined;
943
+ if (key && t.isJSXExpressionContainer(key.value) && t.isExpression(key.value.expression)) {
944
+ keyValue = key.value.expression;
945
+ }
946
+ else if (key && t.isStringLiteral(key.value)) {
947
+ keyValue = key.value;
948
+ }
949
+ // Add an onPress to each item that calls the previous onAction, passing in the key.
950
+ if (onAction && t.isJSXExpressionContainer(onAction.node.value) && t.isExpression(onAction.node.value.expression)) {
951
+ childNode.openingElement.attributes.push(t.jsxAttribute(t.jsxIdentifier('onPress'), t.jsxExpressionContainer(keyValue
952
+ ? t.arrowFunctionExpression([], t.callExpression(onAction.node.value.expression, [keyValue]))
953
+ : onAction.node.value.expression)));
954
+ }
955
+ // Add an isDisabled prop to each item, testing whether it is in disabledKeys.
956
+ if (disabledKeys && keyValue) {
957
+ childNode.openingElement.attributes.push(t.jsxAttribute(t.jsxIdentifier('isDisabled'), t.jsxExpressionContainer(t.callExpression(t.memberExpression(disabledKeys, t.identifier('has')), [keyValue]))));
958
+ }
959
+ child.replaceWith(childNode);
960
+ }
961
+ }
962
+ });
963
+ onAction?.remove();
964
+ path.node.openingElement.name = t.jsxIdentifier(localName);
965
+ if (path.node.closingElement) {
966
+ path.node.closingElement.name = t.jsxIdentifier(localName);
967
+ }
968
+ }
969
+ /**
970
+ * Adds isRowHeader to the first Column in a table if there isn't already a row header.
971
+ * @param path
972
+ */
973
+ function addRowHeader(path) {
974
+ let tableHeaderPath = path.get('children').find((child) => t.isJSXElement(child.node) &&
975
+ t.isJSXIdentifier(child.node.openingElement.name) &&
976
+ (0, utils_1.getName)(child, child.node.openingElement.name) === 'TableHeader');
977
+ // Check if isRowHeader is already set on a Column
978
+ let hasRowHeader = false;
979
+ tableHeaderPath?.get('children').forEach((child) => {
980
+ if (t.isJSXElement(child.node) &&
981
+ t.isJSXIdentifier(child.node.openingElement.name) &&
982
+ (0, utils_1.getName)(child, child.node.openingElement.name) === 'Column') {
983
+ let isRowHeaderProp = child.get('openingElement').get('attributes').find((attr) => t.isJSXAttribute(attr.node) && attr.node.name.name === 'isRowHeader');
984
+ if (isRowHeaderProp) {
985
+ hasRowHeader = true;
986
+ }
987
+ }
988
+ });
989
+ // If there isn't already a row header, add one to the first Column if possible
990
+ if (!hasRowHeader) {
991
+ tableHeaderPath?.get('children').forEach((child) => {
992
+ // Add to first Column if static
993
+ if (!hasRowHeader &&
994
+ t.isJSXElement(child.node) &&
995
+ t.isJSXIdentifier(child.node.openingElement.name) &&
996
+ (0, utils_1.getName)(child, child.node.openingElement.name) === 'Column') {
997
+ child.node.openingElement.attributes.push(t.jsxAttribute(t.jsxIdentifier('isRowHeader'), t.jsxExpressionContainer(t.booleanLiteral(true))));
998
+ hasRowHeader = true;
999
+ }
1000
+ // If render function is used, leave a comment to update manually
1001
+ if (t.isJSXExpressionContainer(child.node) &&
1002
+ t.isArrowFunctionExpression(child.node.expression)) {
1003
+ (0, utils_1.addComment)(child.node, ' TODO(S2-upgrade): You\'ll need to add isRowHeader to one of the columns manually.');
1004
+ }
1005
+ // If array.map is used, leave a comment to update manually
1006
+ if (t.isJSXExpressionContainer(child.node) &&
1007
+ t.isCallExpression(child.node.expression) &&
1008
+ t.isMemberExpression(child.node.expression.callee) &&
1009
+ t.isIdentifier(child.node.expression.callee.property) &&
1010
+ child.node.expression.callee.property.name === 'map') {
1011
+ (0, utils_1.addComment)(child.node, ' TODO(S2-upgrade): You\'ll need to add isRowHeader to one of the columns manually.');
1012
+ }
1013
+ });
1014
+ }
1015
+ }
688
1016
  exports.functionMap = {
689
1017
  updatePropNameAndValue,
690
1018
  updatePropValueAndAddNewProp,
@@ -704,5 +1032,12 @@ exports.functionMap = {
704
1032
  updatePlacementToSingleValue,
705
1033
  removeComponentIfWithinParent,
706
1034
  updateAvatarSize,
707
- updateLegacyLink
1035
+ updateLegacyLink,
1036
+ addColumnsPropToRow,
1037
+ updateRowFunctionArg,
1038
+ updateDialogChild,
1039
+ updateActionGroup,
1040
+ updateKeyToId,
1041
+ commentIfNestedColumns,
1042
+ addRowHeader
708
1043
  };
@@ -160,7 +160,12 @@ function handleProperty(element, property, value) {
160
160
  case 'maxWidth':
161
161
  case 'height':
162
162
  case 'minHeight':
163
- case 'maxHeight':
163
+ case 'maxHeight': {
164
+ if (value.type === 'NumericLiteral' || value.type === 'StringLiteral') {
165
+ return (0, dimensions_1.convertUnsafeDimension)(value.value, 'size');
166
+ }
167
+ break;
168
+ }
164
169
  case 'margin':
165
170
  case 'marginInlineStart':
166
171
  case 'marginInlineEnd':
@@ -178,7 +183,7 @@ function handleProperty(element, property, value) {
178
183
  case 'insetInlineEnd':
179
184
  case 'flexBasis': {
180
185
  if (value.type === 'NumericLiteral' || value.type === 'StringLiteral') {
181
- return (0, dimensions_1.convertUnsafeDimension)(value.value);
186
+ return (0, dimensions_1.convertUnsafeDimension)(value.value, 'space');
182
187
  }
183
188
  break;
184
189
  }
@@ -196,7 +201,7 @@ function handleProperty(element, property, value) {
196
201
  case 'columnGap': {
197
202
  if (element === 'View' || element === 'Flex' || element === 'Grid') {
198
203
  if (value.type === 'NumericLiteral' || value.type === 'StringLiteral') {
199
- return (0, dimensions_1.convertUnsafeDimension)(value.value);
204
+ return (0, dimensions_1.convertUnsafeDimension)(value.value, 'space');
200
205
  }
201
206
  }
202
207
  break;
@@ -14,12 +14,7 @@ const skipped = new Set([
14
14
  'Accordion',
15
15
  'Card',
16
16
  'CardView',
17
- 'TableView',
18
- 'TableHeader',
19
- 'Column',
20
- 'TableBody',
21
- 'Row',
22
- 'Cell'
17
+ 'ActionBar'
23
18
  ]);
24
19
  function getComponents() {
25
20
  // Determine list of available components in S2 from index.ts
@@ -155,10 +155,6 @@ exports.iconMap = new Map([
155
155
  'Channel',
156
156
  'Channel'
157
157
  ],
158
- [
159
- 'ChatAdd',
160
- 'Channel'
161
- ],
162
158
  [
163
159
  'Checkmark',
164
160
  'CheckmarkSize300'
@@ -711,10 +707,22 @@ exports.iconMap = new Map([
711
707
  'Pause',
712
708
  'Pause'
713
709
  ],
710
+ [
711
+ 'PauseCircle',
712
+ 'PauseCircle'
713
+ ],
714
714
  [
715
715
  'Pending',
716
716
  'ClockPending'
717
717
  ],
718
+ [
719
+ 'PinOff',
720
+ 'PinOff'
721
+ ],
722
+ [
723
+ 'PinOn',
724
+ 'PinOn'
725
+ ],
718
726
  [
719
727
  'Play',
720
728
  'Play'
@@ -991,6 +999,10 @@ exports.iconMap = new Map([
991
999
  'TextBulleted',
992
1000
  'ListBulleted'
993
1001
  ],
1002
+ [
1003
+ 'TextIncrease',
1004
+ 'TextIncrease'
1005
+ ],
994
1006
  [
995
1007
  'TextItalic',
996
1008
  'TextItalic'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@react-spectrum/codemods",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
4
4
  "main": "dist/index.js",
5
5
  "source": "src/index.ts",
6
6
  "bin": "dist/index.js",
@@ -24,29 +24,31 @@
24
24
  "@babel/parser": "^7.24.5",
25
25
  "@babel/traverse": "^7.24.5",
26
26
  "@babel/types": "^7.24.5",
27
- "@react-spectrum/s2": "^0.4.1",
28
- "@react-types/shared": "^3.25.0",
27
+ "@react-spectrum/s2": "^0.6.0",
28
+ "@react-types/shared": "^3.27.0",
29
29
  "@types/node": "^20",
30
30
  "boxen": "^5.1.2",
31
31
  "build": "^0.1.4",
32
32
  "chalk": "^4.0.0",
33
33
  "execa": "^5.1.1",
34
34
  "jscodeshift": "^0.15.2",
35
- "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0",
36
35
  "recast": "^0.23.9",
37
36
  "ts-node": "^10.9.2",
38
37
  "uuid": "^9.0.1"
39
38
  },
40
39
  "devDependencies": {
41
40
  "@types/jscodeshift": "^0.11.11",
42
- "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0",
43
41
  "typescript": "^5.5.0"
44
42
  },
43
+ "peerDependencies": {
44
+ "react": "^18.0.0 || ^19.0.0-rc.1",
45
+ "react-dom": "^18.0.0 || ^19.0.0-rc.1"
46
+ },
45
47
  "rsp": {
46
48
  "type": "cli"
47
49
  },
48
50
  "publishConfig": {
49
51
  "access": "public"
50
52
  },
51
- "gitHead": "8e0a28d188cdbdbd2b32296fa034b1b02ddde229"
53
+ "gitHead": "09e7f44bebdc9d89122926b2b439a0a38a2814ea"
52
54
  }