diff --git a/packages/dev/codemods/src/s1-to-s2/README.md b/packages/dev/codemods/src/s1-to-s2/README.md index c2db857dd15..0751b1c1a80 100644 --- a/packages/dev/codemods/src/s1-to-s2/README.md +++ b/packages/dev/codemods/src/s1-to-s2/README.md @@ -13,3 +13,11 @@ Run `npx @react-spectrum/codemods s1-to-s2` from the directory you want to upgra ## How it works The upgrade assistant use codemods written with [jscodeshift](https://github.com/facebook/jscodeshift). + +## Adding a new codemod + +To add a new codemod for `Button`, for example, you would: + +1. Create a new transform function in `src/codemods/components/Button/transform.ts` and export it as `default` +2. Implement the transform logic +3. Add tests for the transform in `__tests__/button.test.ts` diff --git a/packages/dev/codemods/src/s1-to-s2/src/codemods/button.ts b/packages/dev/codemods/src/s1-to-s2/src/codemods/button.ts deleted file mode 100644 index 62bbbebb0f7..00000000000 --- a/packages/dev/codemods/src/s1-to-s2/src/codemods/button.ts +++ /dev/null @@ -1,31 +0,0 @@ -import {getPropValue} from './utils'; -import {NodePath} from '@babel/traverse'; -import * as t from '@babel/types'; - -export function transformButton(path: NodePath) { - path.traverse({ - JSXAttribute(path) { - let value = getPropValue(path.node.value); - if (path.node.name.type !== 'JSXIdentifier' || !value) { - return; - } - - switch (path.node.name.name) { - case 'variant': { - if (value.type === 'StringLiteral') { - if (value.value === 'cta') { - value.value = 'accent'; - } else if (value.value === 'overBackground') { - value.value = 'primary'; - path.insertAfter(t.jsxAttribute( - t.jsxIdentifier('staticColor'), - t.stringLiteral('white') - )); - } - } - break; - } - } - } - }); -} diff --git a/packages/dev/codemods/src/s1-to-s2/src/codemods/changes.ts b/packages/dev/codemods/src/s1-to-s2/src/codemods/changes.ts deleted file mode 100644 index 4f23d59e83d..00000000000 --- a/packages/dev/codemods/src/s1-to-s2/src/codemods/changes.ts +++ /dev/null @@ -1,1386 +0,0 @@ -import { - AddCommentToElementOptions, - CommentOutPropOptions, - ConvertDimensionValueToPxOptions, - MovePropToNewChildComponentOptions, - MovePropToParentComponentOptions, - MoveRenderPropsOptions, - RemoveComponentIfWithinParentOptions, - RemovePropOptions, - UpdateComponentIfPropPresentOptions, - UpdateComponentWithinCollectionOptions, - UpdatePlacementToSingleValueProps, - UpdatePropNameAndValueOptions, - UpdatePropNameOptions, - UpdatePropValueAndAddNewPropOptions, - UpdateToNewComponentOptions -} from './transforms'; - -type FunctionInfo = - | { name: 'commentOutProp', args: CommentOutPropOptions } - | { name: 'removeProp', args: RemovePropOptions } - | { name: 'updatePropNameAndValue', args: UpdatePropNameAndValueOptions } - | { - name: 'updatePropValueAndAddNewProp', - args: UpdatePropValueAndAddNewPropOptions - } - | { name: 'updatePropName', args: UpdatePropNameOptions } - | { - name: 'updateComponentIfPropPresent', - args: UpdateComponentIfPropPresentOptions - } - | { name: 'updateToNewComponent', args: UpdateToNewComponentOptions } - | { name: 'moveRenderPropsToChild', args: MoveRenderPropsOptions } - | { name: 'removeProp', args: RemovePropOptions } - | { - name: 'updateComponentWithinCollection', - args: UpdateComponentWithinCollectionOptions - } - | { - name: 'updateTabs', - args: {} - } - | { - name: 'movePropToNewChildComponent', - args: MovePropToNewChildComponentOptions - } - | { - name: 'movePropToParentComponent', - args: MovePropToParentComponentOptions - } - | { - name: 'convertDimensionValueToPx', - args: ConvertDimensionValueToPxOptions - } - | { - name: 'updatePlacementToSingleValue', - args: UpdatePlacementToSingleValueProps - } - | { - name: 'removeComponentIfWithinParent', - args: RemoveComponentIfWithinParentOptions - } - | { - name: 'addCommentToElement', - args: AddCommentToElementOptions - } - | { - name: 'commentIfParentCollectionNotDetected', - args: {} - } - | { - name: 'updateAvatarSize', - args: {} - } - | { - name: 'updateLegacyLink', - args: {} - } - | { - name: 'addColumnsPropToRow', - args: {} - } - | { - name: 'updateRowFunctionArg', - args: {} - } - | { - name: 'updateKeyToId', - args: {} - } - | { - name: 'updateDialogChild', - args: {} - } - | { - name: 'updateActionGroup', - args: {} - } | { - name: 'commentIfNestedColumns', - args: {} - } | { - name: 'addRowHeader', - args: {} - } - -type Change = { - description: string, - reason: string, - function: FunctionInfo -}; - -type ComponentChanges = { - changes: Change[] -}; - -type ChangesJSON = { - [component: string]: ComponentChanges -}; - -export const changes: ChangesJSON = { - Avatar: { - changes: [ - { - description: 'Comment out isDisabled', - reason: 'It has not been implemented yet', - function: { - name: 'commentOutProp', - args: {propToComment: 'isDisabled'} - } - }, - { - description: 'Update size prop', - reason: 'Updated naming convention', - function: { - name: 'updateAvatarSize', - args: {} - } - } - ] - }, - ActionGroup: { - changes: [ - { - description: 'Comment out overflowMode', - reason: 'It has not been implemented yet', - function: { - name: 'commentOutProp', - args: {propToComment: 'overflowMode'} - } - }, - { - description: 'Comment out buttonLabelBehavior', - reason: 'It has not been implemented yet', - function: { - name: 'commentOutProp', - args: {propToComment: 'buttonLabelBehavior'} - } - }, - { - description: 'Comment out summaryIcon', - reason: 'It has not been implemented yet', - function: { - name: 'commentOutProp', - args: {propToComment: 'summaryIcon'} - } - }, - { - description: 'Replace with ActionButtonGroup or ToggleButtonGroup', - reason: 'The API has changed', - function: {name: 'updateActionGroup', args: {}} - } - ] - }, - ActionMenu: { - changes: [ - { - description: 'Comment out closeOnSelect', - reason: 'It has not been implemented yet', - function: { - name: 'commentOutProp', - args: {propToComment: 'closeOnSelect'} - } - }, - { - description: 'Comment out trigger', - reason: 'It has not been implemented yet', - function: { - name: 'commentOutProp', - args: {propToComment: 'trigger'} - } - } - ] - }, - Badge: { - changes: [ - { - description: "Change variant='info' to variant='informative'", - reason: 'Updated naming convention', - function: { - name: 'updatePropNameAndValue', - args: { - oldProp: 'variant', - oldValue: 'info', - newProp: 'variant', - newValue: 'informative' - } - } - } - ] - }, - Breadcrumbs: { - changes: [ - { - description: 'Comment out showRoot', - reason: 'It has not been implemented yet', - function: { - name: 'commentOutProp', - args: {propToComment: 'showRoot'} - } - }, - { - description: 'Comment out isMultiline', - reason: 'It has not been implemented yet', - function: { - name: 'commentOutProp', - args: {propToComment: 'isMultiline'} - } - }, - { - description: 'Comment out autoFocusCurrent', - reason: 'It has not been implemented yet', - function: { - name: 'commentOutProp', - args: {propToComment: 'autoFocusCurrent'} - } - }, - { - description: 'Remove size="S"', - reason: 'Small is no longer a supported size in Spectrum 2', - function: { - name: 'removeProp', - args: {propToRemove: 'size', propValue: 'S'} - } - }, - { - description: 'Add comment to wrap in nav element if needed', - reason: 'A nav element is no longer included inside Breadcrumbs by default. You can wrap the Breadcrumbs component in a nav element if needed.', - function: { - name: 'addCommentToElement', - args: {comment: 'S2 Breadcrumbs no longer includes a nav element by default. You can wrap the Breadcrumbs component in a nav element if needed.'} - } - } - ] - }, - Button: { - changes: [ - { - description: 'Change variant="cta" to variant="accent"', - reason: 'Call-to-action was deprecated', - function: { - name: 'updatePropNameAndValue', - args: { - oldProp: 'variant', - oldValue: 'cta', - newProp: 'variant', - newValue: 'accent' - } - } - }, - { - description: - 'Change variant="overBackground" to variant="primary" staticColor="white"', - reason: 'Updated design guidelines', - function: { - name: 'updatePropValueAndAddNewProp', - args: { - oldProp: 'variant', - oldValue: 'overBackground', - newProp: 'variant', - newValue: 'primary', - additionalProp: 'staticColor', - additionalValue: 'white' - } - } - }, - { - description: 'Change style to fillStyle', - reason: 'To avoid confusion with HTMLElement style attribute', - function: { - name: 'updatePropName', - args: {oldProp: 'style', newProp: 'fillStyle'} - } - }, - { - description: 'Comment out isPending', - reason: 'It has not been implemented yet', - function: { - name: 'commentOutProp', - args: {propToComment: 'isPending'} - } - }, - { - description: 'Remove isQuiet', - reason: 'It is no longer supported', - function: {name: 'removeProp', args: {propToRemove: 'isQuiet'}} - }, - { - description: - 'If href is present, Button should be converted to a LinkButton', - reason: 'Improved API and behavior for links', - function: { - name: 'updateComponentIfPropPresent', - args: { - newComponent: 'LinkButton', - propToCheck: 'href' - } - } - }, - { - description: 'Remove elementType', - reason: 'It is no longer supported', - function: {name: 'removeProp', args: {propToRemove: 'elementType'}} - } - ] - }, - CheckboxGroup: { - changes: [ - { - description: 'Remove showErrorIcon', - reason: 'It has been removed for accessibility reasons', - function: { - name: 'removeProp', - args: {propToRemove: 'showErrorIcon'} - } - } - ] - }, - ColorArea: { - changes: [] - }, - ColorWheel: { - changes: [] - }, - ColorSlider: { - changes: [ - { - description: 'Remove showValueLabel', - reason: 'It has been removed for accessibility reasons', - function: { - name: 'removeProp', - args: {propToRemove: 'showValueLabel'} - } - } - ] - }, - ColorField: { - changes: [ - { - description: 'Remove isQuiet', - reason: 'It is no longer supported', - function: { - name: 'removeProp', - args: {propToRemove: 'isQuiet'} - } - }, - { - description: 'Remove placeholder', - reason: 'It has been removed for accessibility reasons', - function: { - name: 'removeProp', - args: {propToRemove: 'placeholder'} - } - }, - { - description: "Change validationState='invalid' to isInvalid", - reason: 'Updated API', - function: { - name: 'updatePropNameAndValue', - args: { - oldProp: 'validationState', - oldValue: 'invalid', - newProp: 'isInvalid', - newValue: true - } - } - }, - { - description: "Remove validationState='valid'", - reason: 'It is no longer supported', - function: { - name: 'removeProp', - args: {propToRemove: 'validationState', propValue: 'valid'} - } - } - ] - }, - ComboBox: { - changes: [ - { - description: - 'Change menuWidth value from a DimensionValue to a pixel value', - reason: 'Updated design guidelines', - function: { - name: 'convertDimensionValueToPx', - args: { - propToConvertValue: 'menuWidth' - } - } - }, - { - description: 'Remove isQuiet', - reason: 'It is no longer supported', - function: {name: 'removeProp', args: {propToRemove: 'isQuiet'}} - }, - { - description: 'Comment out loadingState', - reason: 'It has not been implemented yet', - function: { - name: 'commentOutProp', - args: {propToComment: 'loadingState'} - } - }, - { - description: 'Remove placeholder', - reason: 'It is no longer supported', - function: {name: 'removeProp', args: {propToRemove: 'placeholder'}} - }, - { - description: "Change validationState='invalid' to isInvalid", - reason: 'Updated API', - function: { - name: 'updatePropNameAndValue', - args: { - oldProp: 'validationState', - oldValue: 'invalid', - newProp: 'isInvalid', - newValue: true - } - } - }, - { - description: "Remove validationState='valid'", - reason: 'It is no longer supported', - function: { - name: 'removeProp', - args: {propToRemove: 'validationState', propValue: 'valid'} - } - }, - { - description: 'Comment out onLoadMore', - reason: 'It has not been implemented yet', - function: { - name: 'commentOutProp', - args: {propToComment: 'onLoadMore'} - } - } - ] - }, - Dialog: { - changes: [] - }, - DialogTrigger: { - changes: [ - { - description: "Comment out type='tray'", - reason: 'Tray has not been implemented yet', - function: {name: 'commentOutProp', args: {propToComment: 'type', propValue: 'tray'}} - }, - { - description: "Comment out mobileType='tray'", - reason: 'mobileType has not been implemented yet', - function: { - name: 'commentOutProp', - args: {propToComment: 'mobileType'} - } - }, - { - description: 'Remove targetRef', - reason: 'It is no longer supported', - function: {name: 'removeProp', args: {propToRemove: 'targetRef'}} - }, - { - description: - 'Update children to move render props from being the second child of DialogTrigger to being a child of Dialog', - reason: 'Updated API', - function: { - name: 'moveRenderPropsToChild', - args: {newChildComponent: 'Dialog'} - } - }, - { - description: 'Rename isDismissable to isDismissible', - reason: 'Fixed spelling', - function: {name: 'updatePropName', args: {oldProp: 'isDismissable', newProp: 'isDismissible'}} - }, - { - description: 'Update Dialog child to Popover or FullscreenDialog depending on type prop', - reason: 'Updated API', - function: {name: 'updateDialogChild', args: {}} - } - ] - }, - DialogContainer: { - changes: [ - { - description: 'Rename isDismissable to isDismissible', - reason: 'Fixed spelling', - function: {name: 'updatePropName', args: {oldProp: 'isDismissable', newProp: 'isDismissible'}} - }, - { - description: 'Update Dialog child to Popover or FullscreenDialog depending on type prop', - reason: 'Updated API', - function: {name: 'updateDialogChild', args: {}} - } - ] - }, - Divider: { - changes: [ - { - description: 'Remove Divider if within a Dialog', - reason: 'Updated design', - function: { - name: 'removeComponentIfWithinParent', - args: {parentComponent: 'Dialog'} - } - } - ] - }, - Form: { - changes: [ - { - description: 'Remove isQuiet', - reason: 'It is no longer supported', - function: {name: 'removeProp', args: {propToRemove: 'isQuiet'}} - }, - { - description: 'Remove isReadOnly', - reason: 'It is no longer supported', - function: {name: 'removeProp', args: {propToRemove: 'isReadOnly'}} - }, - { - description: 'Remove validationState', - reason: 'It is no longer supported', - function: { - name: 'removeProp', - args: {propToRemove: 'validationState'} - } - } - ] - }, - InlineAlert: { - changes: [ - { - description: "Change variant='info' to variant='informative'", - reason: 'Updated naming convention', - function: { - name: 'updatePropNameAndValue', - args: { - oldProp: 'variant', - oldValue: 'info', - newProp: 'variant', - newValue: 'informative' - } - } - } - ] - }, - Item: { - changes: [ - { - description: 'If within Menu, update Item to be a MenuItem', - reason: 'Updated collections API', - function: { - name: 'updateComponentWithinCollection', - args: { - parentComponent: 'Menu', - newComponent: 'MenuItem' - } - } - }, - { - description: 'If within ActionMenu, update Item to be a MenuItem', - reason: 'Updated collections API', - function: { - name: 'updateComponentWithinCollection', - args: { - parentComponent: 'ActionMenu', - newComponent: 'MenuItem' - } - } - }, - { - description: 'If within TagGroup, update Item to be a Tag', - reason: 'Updated collections API', - function: { - name: 'updateComponentWithinCollection', - args: { - parentComponent: 'TagGroup', - newComponent: 'Tag' - } - } - }, - { - description: 'If within Breadcrumbs, update Item to be a Breadcrumb', - reason: 'Updated collections API', - function: { - name: 'updateComponentWithinCollection', - args: { - parentComponent: 'Breadcrumbs', - newComponent: 'Breadcrumb' - } - } - }, - { - description: 'If within Picker, update Item to be a PickerItem', - reason: 'Updated collections API', - function: { - name: 'updateComponentWithinCollection', - args: { - parentComponent: 'Picker', - newComponent: 'PickerItem' - } - } - }, - { - description: 'If within ComboBox, update Item to be a ComboBoxItem', - reason: 'Updated collections API', - function: { - name: 'updateComponentWithinCollection', - args: { - parentComponent: 'ComboBox', - newComponent: 'ComboBoxItem' - } - } - }, - { - description: 'Leave comment if we cannot determine parent collection component', - reason: 'No collection parent detected', - function: { - name: 'commentIfParentCollectionNotDetected', - args: {} - } - } - // TODO: Not yet implemented in S2 - // { - // description: 'If within ListBox, update Item to be a ListBoxItem', - // reason: 'Updated collections API', - // function: { - // name: 'updateComponentWithinCollection', - // args: { - // parentComponent: 'ListBox', - // newComponent: 'ListBoxItem' - // } - // } - // }, - ] - }, - Link: { - changes: [ - { - description: "Change variant='overBackground' to staticColor='white'", - reason: 'Updated design guidelines', - function: { - name: 'updatePropNameAndValue', - args: { - oldProp: 'variant', - oldValue: 'overBackground', - newProp: 'staticColor', - newValue: 'white' - } - } - }, - { - description: 'Remove inner anchor element if used (legacy API)', - reason: 'Updated API', - function: { - name: 'updateLegacyLink', - args: {} - } - } - ] - }, - MenuTrigger: { - changes: [ - { - description: 'Comment out closeOnSelect', - reason: 'It has not been implemented yet', - function: { - name: 'commentOutProp', - args: {propToComment: 'closeOnSelect'} - } - } - ] - }, - SubmenuTrigger: { - changes: [ - { - description: 'Remove targetKey', - reason: 'Potential v3 bug or API differ bug', - function: {name: 'removeProp', args: {propToRemove: 'targetKey'}} - } - ] - }, - NumberField: { - changes: [ - { - description: 'Remove isQuiet', - reason: 'It is no longer supported', - function: {name: 'removeProp', args: {propToRemove: 'isQuiet'}} - }, - { - description: "Change validationState='invalid' to isInvalid", - reason: 'Updated API', - function: { - name: 'updatePropNameAndValue', - args: { - oldProp: 'validationState', - oldValue: 'invalid', - newProp: 'isInvalid', - newValue: true - } - } - }, - { - description: "Remove validationState='valid'", - reason: 'It is no longer supported', - function: { - name: 'removeProp', - args: {propToRemove: 'validationState', propValue: 'valid'} - } - } - ] - }, - Picker: { - changes: [ - { - description: - 'Change menuWidth value from a DimensionValue to a pixel value', - reason: 'Updated design guidelines', - function: { - name: 'convertDimensionValueToPx', - args: { - propToConvertValue: 'menuWidth' - } - } - }, - { - description: 'Remove isQuiet', - reason: 'It is no longer supported', - function: {name: 'removeProp', args: {propToRemove: 'isQuiet'}} - }, - { - description: "Change validationState='invalid' to isInvalid", - reason: 'Updated API', - function: { - name: 'updatePropNameAndValue', - args: { - oldProp: 'validationState', - oldValue: 'invalid', - newProp: 'isInvalid', - newValue: true - } - } - }, - { - description: "Remove validationState='valid'", - reason: 'It is no longer supported', - function: { - name: 'removeProp', - args: {propToRemove: 'validationState', propValue: 'valid'} - } - }, - { - description: 'Comment out isLoading', - reason: 'It has not been implemented yet', - function: { - name: 'commentOutProp', - args: {propToComment: 'isLoading'} - } - }, - { - description: 'Comment out onLoadMore', - reason: 'It has not been implemented yet', - function: { - name: 'commentOutProp', - args: {propToComment: 'onLoadMore'} - } - } - ] - }, - ProgressBar: { - changes: [ - { - description: "Change variant='overBackground' to staticColor='white'", - reason: 'Updated design guidelines', - function: { - name: 'updatePropNameAndValue', - args: { - oldProp: 'variant', - oldValue: 'overBackground', - newProp: 'staticColor', - newValue: 'white' - } - } - }, - { - description: 'Comment out labelPosition', - reason: 'It has not been implemented yet', - function: { - name: 'commentOutProp', - args: {propToComment: 'labelPosition'} - } - }, - { - description: 'Remove showValueLabel', - reason: 'It was removed for accessibility reasons', - function: {name: 'removeProp', args: {propToRemove: 'showValueLabel'}} - } - ] - }, - ProgressCircle: { - changes: [ - { - description: "Change variant='overBackground' to staticColor='white'", - reason: 'Updated design guidelines', - function: { - name: 'updatePropNameAndValue', - args: { - oldProp: 'variant', - oldValue: 'overBackground', - newProp: 'staticColor', - newValue: 'white' - } - } - } - ] - }, - RadioGroup: { - changes: [ - { - description: "Change validationState='invalid' to isInvalid", - reason: 'Updated API', - function: { - name: 'updatePropNameAndValue', - args: { - oldProp: 'validationState', - oldValue: 'invalid', - newProp: 'isInvalid', - newValue: true - } - } - }, - { - description: "Remove validationState='valid'", - reason: 'It is no longer supported', - function: { - name: 'removeProp', - args: {propToRemove: 'validationState', propValue: 'valid'} - } - }, - { - description: 'Remove showErrorIcon', - reason: 'It has been removed for accessibility reasons', - function: { - name: 'removeProp', - args: {propToRemove: 'showErrorIcon'} - } - } - ] - }, - RangeSlider: { - changes: [ - { - description: 'Remove showValueLabel', - reason: 'It was removed for accessibility reasons', - function: {name: 'removeProp', args: {propToRemove: 'showValueLabel'}} - }, - { - description: 'Comment out getValueLabel', - reason: 'It has not been implemented yet', - function: { - name: 'commentOutProp', - args: {propToComment: 'getValueLabel'} - } - }, - { - description: 'Comment out orientation', - reason: 'It has not been implemented yet', - function: { - name: 'commentOutProp', - args: {propToComment: 'orientation'} - } - } - ] - }, - SearchField: { - changes: [ - { - description: 'Remove placeholder', - reason: 'It has been removed for accessibility reasons', - function: {name: 'removeProp', args: {propToRemove: 'placeholder'}} - }, - { - description: 'Comment out icon', - reason: 'It has not been implemented yet', - function: {name: 'commentOutProp', args: {propToComment: 'icon'}} - }, - { - description: 'Remove isQuiet', - reason: 'It is no longer supported', - function: {name: 'removeProp', args: {propToRemove: 'isQuiet'}} - }, - { - description: "Change validationState='invalid' to isInvalid", - reason: 'Updated API', - function: { - name: 'updatePropNameAndValue', - args: { - oldProp: 'validationState', - oldValue: 'invalid', - newProp: 'isInvalid', - newValue: true - } - } - }, - { - description: "Remove validationState='valid'", - reason: 'It is no longer supported', - function: { - name: 'removeProp', - args: {propToRemove: 'validationState', propValue: 'valid'} - } - } - ] - }, - Section: { - changes: [ - { - description: 'If within Menu, update Section to be a MenuSection', - reason: 'Updated component structure', - function: { - name: 'updateComponentWithinCollection', - args: { - parentComponent: 'Menu', - newComponent: 'MenuSection' - } - } - }, - { - description: 'If within Picker, update Section to be a PickerSection', - reason: 'Updated component structure', - function: { - name: 'updateComponentWithinCollection', - args: { - parentComponent: 'Picker', - newComponent: 'PickerSection' - } - } - }, - { - description: 'If within ComboBox, update Section to be a ComboBoxSection', - reason: 'Updated component structure', - function: { - name: 'updateComponentWithinCollection', - args: { - parentComponent: 'ComboBox', - newComponent: 'ComboBoxSection' - } - } - }, - { - description: - 'Move title prop string to be a child of new Heading within a Header', - reason: 'Updated API', - function: { - name: 'movePropToNewChildComponent', - args: { - parentComponent: 'Menu', - childComponent: 'MenuSection', - propToMove: 'title', - newChildComponent: 'Header' - } - } - }, - { - description: - 'Move title prop string to be a child of new Heading within a Header', - reason: 'Updated API', - function: { - name: 'movePropToNewChildComponent', - args: { - parentComponent: 'Picker', - childComponent: 'PickerSection', - propToMove: 'title', - newChildComponent: 'Header' - } - } - }, - { - description: - 'Move title prop string to be a child of new Heading within a Header', - reason: 'Updated API', - function: { - name: 'movePropToNewChildComponent', - args: { - parentComponent: 'ComboBox', - childComponent: 'ComboBoxSection', - propToMove: 'title', - newChildComponent: 'Header' - } - } - }, - { - description: 'Leave comment if we cannot determine parent collection component', - reason: 'No collection parent detected', - function: { - name: 'commentIfParentCollectionNotDetected', - args: {} - } - } - ] - }, - Slider: { - changes: [ - { - description: 'Remove isFilled', - reason: 'Slider is always filled in Spectrum 2', - function: {name: 'removeProp', args: {propToRemove: 'isFilled'}} - }, - { - description: 'Remove trackGradient', - reason: 'It is no longer supported', - function: {name: 'removeProp', args: {propToRemove: 'trackGradient'}} - }, - { - description: 'Remove showValueLabel', - reason: 'It was removed for accessibility reasons', - function: {name: 'removeProp', args: {propToRemove: 'showValueLabel'}} - }, - { - description: 'Comment out getValueLabel', - reason: 'It has not been implemented yet', - function: { - name: 'commentOutProp', - args: {propToComment: 'getValueLabel'} - } - }, - { - description: 'Comment out orientation', - reason: 'It has not been implemented yet', - function: { - name: 'commentOutProp', - args: {propToComment: 'orientation'} - } - } - ] - }, - StatusLight: { - changes: [ - { - description: 'Remove isDisabled', - reason: 'It is no longer supported', - function: {name: 'removeProp', args: {propToRemove: 'isDisabled'}} - }, - { - description: "Change variant='info' to variant='informative'", - reason: 'Updated naming convention', - function: { - name: 'updatePropNameAndValue', - args: { - oldProp: 'variant', - oldValue: 'info', - newProp: 'variant', - newValue: 'informative' - } - } - } - ] - }, - Tabs: { - changes: [ - { - description: 'Remove TabPanels components and keep individual TabPanel components inside.', - reason: 'Updated collections API', - function: { - name: 'updateTabs', - args: {} - } - }, - { - description: 'Remove isEmphasized', - reason: 'It is no longer supported', - function: {name: 'removeProp', args: {propToRemove: 'isEmphasized'}} - }, - { - description: 'Remove isQuiet', - reason: 'It is no longer supported', - function: {name: 'removeProp', args: {propToRemove: 'isQuiet'}} - } - ] - }, - TagGroup: { - changes: [ - { - description: 'Change actionLabel to groupActionLabel', - reason: 'To match new onGroupAction prop', - function: { - name: 'updatePropName', - args: {oldProp: 'actionLabel', newProp: 'groupActionLabel'} - } - }, - { - description: 'Change onAction to onGroupAction', - reason: 'To avoid confusion with existing onAction prop on other collection components', - function: { - name: 'updatePropName', - args: {oldProp: 'onAction', newProp: 'onGroupAction'} - } - }, - { - description: "Change validationState='invalid' to isInvalid", - reason: 'Updated API', - function: { - name: 'updatePropNameAndValue', - args: { - oldProp: 'validationState', - oldValue: 'invalid', - newProp: 'isInvalid', - newValue: true - } - } - }, - { - description: "Remove validationState='valid'", - reason: 'It is no longer supported', - function: { - name: 'removeProp', - args: {propToRemove: 'validationState', propValue: 'valid'} - } - } - ] - }, - TextArea: { - changes: [ - { - description: 'Comment out icon', - reason: 'It has not been implemented yet', - function: {name: 'commentOutProp', args: {propToComment: 'icon'}} - }, - { - description: 'Remove isQuiet', - reason: 'It is no longer supported', - function: {name: 'removeProp', args: {propToRemove: 'isQuiet'}} - }, - { - description: 'Remove placeholder', - reason: 'It has been removed for accessibility reasons', - function: {name: 'removeProp', args: {propToRemove: 'placeholder'}} - }, - { - description: "Change validationState='invalid' to isInvalid", - reason: 'Updated API', - function: { - name: 'updatePropNameAndValue', - args: { - oldProp: 'validationState', - oldValue: 'invalid', - newProp: 'isInvalid', - newValue: true - } - } - }, - { - description: "Remove validationState='valid'", - reason: 'It is no longer supported', - function: { - name: 'removeProp', - args: {propToRemove: 'validationState', propValue: 'valid'} - } - } - ] - }, - TextField: { - changes: [ - { - description: 'Comment out icon', - reason: 'It has not been implemented yet', - function: {name: 'commentOutProp', args: {propToComment: 'icon'}} - }, - { - description: 'Remove isQuiet', - reason: 'It is no longer supported', - function: {name: 'removeProp', args: {propToRemove: 'isQuiet'}} - }, - { - description: 'Remove placeholder', - reason: 'It has been removed for accessibility reasons', - function: {name: 'removeProp', args: {propToRemove: 'placeholder'}} - }, - { - description: "Change validationState='invalid' to isInvalid", - reason: 'Updated API', - function: { - name: 'updatePropNameAndValue', - args: { - oldProp: 'validationState', - oldValue: 'invalid', - newProp: 'isInvalid', - newValue: true - } - } - }, - { - description: "Remove validationState='valid'", - reason: 'It is no longer supported', - function: { - name: 'removeProp', - args: {propToRemove: 'validationState', propValue: 'valid'} - } - } - ] - }, - Tooltip: { - changes: [ - { - description: 'Remove variant', - reason: 'It is no longer supported', - function: {name: 'removeProp', args: {propToRemove: 'variant'}} - }, - { - description: - 'Remove placement and add to the parent TooltipTrigger instead', - reason: 'Updated API', - function: { - name: 'movePropToParentComponent', - args: { - parentComponent: 'TooltipTrigger', - childComponent: 'Tooltip', - propToMove: 'placement' - } - } - }, - { - description: 'Remove showIcon', - reason: 'It is no longer supported', - function: {name: 'removeProp', args: {propToRemove: 'showIcon'}} - }, - { - description: - 'Remove isOpen and add to the parent TooltipTrigger instead', - reason: 'Updated API', - function: { - name: 'movePropToParentComponent', - args: { - parentComponent: 'TooltipTrigger', - childComponent: 'Tooltip', - propToMove: 'isOpen' - } - } - } - ] - }, - TooltipTrigger: { - changes: [ - { - description: 'Update placement to use single value', - reason: 'Updated API', - function: { - name: 'updatePlacementToSingleValue', - args: { - propToUpdate: 'placement', - childComponent: 'Tooltip' - } - } - } - ] - }, - TableView: { - changes: [ - { - description: 'Add columns prop to Row', - reason: 'Rows now require a columns prop from TableHeader', - function: { - name: 'addColumnsPropToRow', - args: {} - } - }, - { - description: 'Leave comment if nested columns are used', - reason: 'Nested columns are not supported yet', - function: { - name: 'commentIfNestedColumns', - args: {} - } - }, - { - description: 'Comment out dragAndDropHooks', - reason: 'It has not been implemented yet', - function: { - name: 'commentOutProp', - args: {propToComment: 'dragAndDropHooks'} - } - }, - { - description: 'Comment out selectionStyle="highlight"', - reason: 'It has not been implemented yet', - function: { - name: 'commentOutProp', - args: {propToComment: 'selectionStyle'} - } - }, - { - description: 'Comment out UNSTABLE_allowsExpandableRows', - reason: 'It has not been implemented yet', - function: { - name: 'commentOutProp', - args: {propToComment: 'UNSTABLE_allowsExpandableRows'} - } - }, - { - description: 'Comment out UNSTABLE_defaultExpandedKeys', - reason: 'It has not been implemented yet', - function: { - name: 'commentOutProp', - args: {propToComment: 'UNSTABLE_defaultExpandedKeys'} - } - }, - { - description: 'Comment out UNSTABLE_expandedKeys', - reason: 'It has not been implemented yet', - function: { - name: 'commentOutProp', - args: {propToComment: 'UNSTABLE_expandedKeys'} - } - }, - { - description: 'Comment out UNSTABLE_onExpandedChange', - reason: 'It has not been implemented yet', - function: { - name: 'commentOutProp', - args: {propToComment: 'UNSTABLE_onExpandedChange'} - } - }, - { - description: 'Add isRowHeader prop to fist Column if one doesn\'t eixst already', - reason: 'Updated API', - function: { - name: 'addRowHeader', - args: {} - } - } - ] - }, - Column: { - changes: [ - { - description: 'Update key prop to id', - reason: 'Updated API', - function: { - name: 'updateKeyToId', - args: {} - } - } - ] - }, - Row: { - changes: [ - { - description: 'Update key prop to id', - reason: 'Updated API', - function: { - name: 'updateKeyToId', - args: {} - } - }, - { - description: 'Update child function to receive column object instead of column key', - reason: 'Updated API', - function: { - name: 'updateRowFunctionArg', - args: {} - } - } - ] - } -}; diff --git a/packages/dev/codemods/src/s1-to-s2/src/codemods/codemod.ts b/packages/dev/codemods/src/s1-to-s2/src/codemods/codemod.ts index 1ed7c570c1c..157c583df41 100644 --- a/packages/dev/codemods/src/s1-to-s2/src/codemods/codemod.ts +++ b/packages/dev/codemods/src/s1-to-s2/src/codemods/codemod.ts @@ -1,13 +1,11 @@ /* eslint-disable max-depth */ -import {addComment} from './utils'; +import {addComment} from './shared/utils'; import {API, FileInfo} from 'jscodeshift'; -import {changes as changesJSON} from './changes'; -import {functionMap} from './transforms'; import {getComponents} from '../getComponents'; -import {iconMap} from '../iconMap'; +import {iconMap} from './icons/iconMap'; import {parse as recastParse} from 'recast'; import * as t from '@babel/types'; -import {transformStyleProps} from './styleProps'; +import transformStyleProps from './shared/styleProps'; import traverse, {Binding, NodePath} from '@babel/traverse'; // Determine list of available components in S2 from index.ts @@ -192,20 +190,18 @@ export default function transformer(file: FileInfo, api: API, options: Options) addComment(path.node, ' TODO(S2-upgrade): Could not transform style prop automatically: ' + error); } - const componentInfo = changesJSON[elementName]; - if (!componentInfo) { - return; - } - const {changes} = componentInfo; - - changes.forEach((change) => { - const {function: functionInfo} = change; - let {name: functionName, args: functionArgs} = functionInfo; - // Call the respective transformation function - if (functionMap[functionName]) { - functionMap[functionName](path, functionArgs as any); + // Try to find a specific transform + try { + // Dynamically import the transform based on elementName + const transformPath = `./components/${elementName}/transform`; + // Use require for dynamic import in CommonJS context + const componentTransform = require(transformPath); + if (componentTransform && typeof componentTransform.default === 'function') { + componentTransform.default(path); } - }); + } catch { + // Do nothing if the transform doesn't exist + } }); if (hasMacros) { diff --git a/packages/dev/codemods/src/s1-to-s2/src/codemods/components/ActionGroup/transform.ts b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/ActionGroup/transform.ts new file mode 100644 index 00000000000..91f2fd15cfa --- /dev/null +++ b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/ActionGroup/transform.ts @@ -0,0 +1,188 @@ +import {addComponentImport} from '../../shared/utils'; +import {commentOutProp} from '../../shared/transforms'; +import {getComponents} from '../../../getComponents'; +import {NodePath} from '@babel/traverse'; +import * as t from '@babel/types'; + +let availableComponents = getComponents(); + +/** + * Transforms ActionGroup: + * - Comment out overflowMode (it has not been implemented yet). + * - Comment out buttonLabelBehavior (it has not been implemented yet). + * - Comment out summaryIcon (it has not been implemented yet). + * - Use ActionButtonGroup if no selection. + * - Use ToggleButtonGroup if selection is used. + * - Update root level onAction to onPress on each ActionButton. + * - Apply isDisabled directly on each ActionButton/ToggleButton instead of disabledKeys. + * - Convert dynamic collections render function to items.map. + */ +export default function transformActionGroup(path: NodePath) { + // Comment out overflowMode + commentOutProp(path, {propName: 'overflowMode'}); + + // Comment out buttonLabelBehavior + commentOutProp(path, {propName: 'buttonLabelBehavior'}); + + // Comment out summaryIcon + commentOutProp(path, {propName: 'summaryIcon'}); + + let selectionModePath = path.get('openingElement').get('attributes').find((attr) => t.isJSXAttribute(attr.node) && attr.node.name.name === 'selectionMode') as NodePath | undefined; + let selectionMode = t.isStringLiteral(selectionModePath?.node.value) ? selectionModePath.node.value.value : 'none'; + let newComponentName, childComponentName; + if (selectionMode === 'none') { + // Use ActionButtonGroup if no selection + newComponentName = 'ActionButtonGroup'; + childComponentName = 'ActionButton'; + selectionModePath?.remove(); + } else { + // Use ToggleButtonGroup if selection is used + newComponentName = 'ToggleButtonGroup'; + childComponentName = 'ToggleButton'; + } + + let localName = newComponentName; + if (availableComponents.has(newComponentName)) { + let program = path.findParent((p) => t.isProgram(p.node)) as NodePath; + localName = addComponentImport(program, newComponentName); + } + + let localChildName = childComponentName; + if (availableComponents.has(childComponentName)) { + let program = path.findParent((p) => t.isProgram(p.node)) as NodePath; + localChildName = addComponentImport(program, childComponentName); + } + + + // Convert dynamic collection to an array.map. + let items = path.get('openingElement').get('attributes').find((attr) => t.isJSXAttribute(attr.node) && attr.node.name.name === 'items') as NodePath | undefined; + let itemArg: t.Identifier | undefined; + if (items && t.isJSXExpressionContainer(items.node.value) && t.isExpression(items.node.value.expression)) { + let child = path.get('children').find(c => c.isJSXExpressionContainer()); + if (child && child.isJSXExpressionContainer() && t.isFunction(child.node.expression)) { + let arg = child.node.expression.params[0]; + if (t.isIdentifier(arg)) { + itemArg = arg; + } + + child.replaceWith( + t.jsxExpressionContainer( + t.callExpression( + t.memberExpression( + items.node.value.expression, + t.identifier('map') + ), + [child.node.expression] + ) + ) + ); + } + } + items?.remove(); + + let onAction = path.get('openingElement').get('attributes').find((attr) => t.isJSXAttribute(attr.node) && attr.node.name.name === 'onAction') as NodePath | undefined; + + // Pull disabledKeys prop out into a variable, converted to a Set. + // Then we can check it in the isDisabled prop of each item. + let disabledKeysPath = path.get('openingElement').get('attributes').find((attr) => t.isJSXAttribute(attr.node) && attr.node.name.name === 'disabledKeys') as NodePath | undefined; + let disabledKeys: t.Identifier | undefined; + if (disabledKeysPath && t.isJSXExpressionContainer(disabledKeysPath.node.value) && t.isExpression(disabledKeysPath.node.value.expression)) { + disabledKeys = path.scope.generateUidIdentifier('disabledKeys'); + path.scope.push({ + id: disabledKeys, + init: t.newExpression(t.identifier('Set'), [disabledKeysPath.node.value.expression]), + kind: 'let' + }); + disabledKeysPath.remove(); + } + + path.traverse({ + JSXElement(child) { + if (t.isJSXIdentifier(child.node.openingElement.name) && child.node.openingElement.name.name === 'Item') { + // Replace Item with ActionButton or ToggleButton. + let childNode = t.cloneNode(child.node); + childNode.openingElement.name = t.jsxIdentifier(localChildName); + if (childNode.closingElement) { + childNode.closingElement.name = t.jsxIdentifier(localChildName); + } + + // If there is no key prop and we are using dynamic collections, add a default computed from item.key ?? item.id. + let key = childNode.openingElement.attributes.find(attr => t.isJSXAttribute(attr) && attr.name.name === 'key') as t.JSXAttribute | undefined; + if (!key && itemArg) { + let id = t.jsxExpressionContainer( + t.logicalExpression( + '??', + t.memberExpression(itemArg, t.identifier('key')), + t.memberExpression(itemArg, t.identifier('id')) + ) + ); + + key = t.jsxAttribute( + t.jsxIdentifier('key'), + id + ); + + childNode.openingElement.attributes.push(key); + } + + // If this is a ToggleButtonGroup, add an id prop in addition to key when needed. + if (key && newComponentName === 'ToggleButtonGroup') { + // If we are in an array.map we need both key and id. Otherwise, we only need id. + if (itemArg) { + childNode.openingElement.attributes.push(t.jsxAttribute(t.jsxIdentifier('id'), key.value)); + } else { + key.name.name = 'id'; + } + } + + let keyValue: t.Expression | undefined = undefined; + if (key && t.isJSXExpressionContainer(key.value) && t.isExpression(key.value.expression)) { + keyValue = key.value.expression; + } else if (key && t.isStringLiteral(key.value)) { + keyValue = key.value; + } + + // Add an onPress to each item that calls the previous onAction, passing in the key. + if (onAction && t.isJSXExpressionContainer(onAction.node.value) && t.isExpression(onAction.node.value.expression)) { + childNode.openingElement.attributes.push( + t.jsxAttribute( + t.jsxIdentifier('onPress'), + t.jsxExpressionContainer( + keyValue + ? t.arrowFunctionExpression([], t.callExpression(onAction.node.value.expression, [keyValue])) + : onAction.node.value.expression + ) + ) + ); + } + + // Add an isDisabled prop to each item, testing whether it is in disabledKeys. + if (disabledKeys && keyValue) { + childNode.openingElement.attributes.push( + t.jsxAttribute( + t.jsxIdentifier('isDisabled'), + t.jsxExpressionContainer( + t.callExpression( + t.memberExpression( + disabledKeys, + t.identifier('has') + ), + [keyValue] + ) + ) + ) + ); + } + + child.replaceWith(childNode); + } + } + }); + + onAction?.remove(); + + path.node.openingElement.name = t.jsxIdentifier(localName); + if (path.node.closingElement) { + path.node.closingElement.name = t.jsxIdentifier(localName); + } +} diff --git a/packages/dev/codemods/src/s1-to-s2/src/codemods/components/ActionMenu/transform.ts b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/ActionMenu/transform.ts new file mode 100644 index 00000000000..aab0489787c --- /dev/null +++ b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/ActionMenu/transform.ts @@ -0,0 +1,16 @@ +import {commentOutProp} from '../../shared/transforms'; +import {NodePath} from '@babel/traverse'; +import * as t from '@babel/types'; + +/** + * Transforms ActionMenu: + * - Comment out closeOnSelect (it has not been implemented yet). + * - Comment out trigger (it has not been implemented yet). + */ +export default function transformActionMenu(path: NodePath) { + // Comment out closeOnSelect + commentOutProp(path, {propName: 'closeOnSelect'}); + + // Comment out trigger + commentOutProp(path, {propName: 'trigger'}); +} diff --git a/packages/dev/codemods/src/s1-to-s2/src/codemods/components/Avatar/transform.ts b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/Avatar/transform.ts new file mode 100644 index 00000000000..4b4041517ae --- /dev/null +++ b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/Avatar/transform.ts @@ -0,0 +1,49 @@ +import {commentOutProp} from '../../shared/transforms'; +import {getName} from '../../shared/utils'; +import {NodePath} from '@babel/traverse'; +import * as t from '@babel/types'; + +function updateAvatarSize( + path: NodePath +) { + if ( + t.isJSXElement(path.node) && + t.isJSXIdentifier(path.node.openingElement.name) && + getName(path, path.node.openingElement.name) === 'Avatar' + ) { + let sizeAttrPath = path.get('openingElement').get('attributes').find((attr) => t.isJSXAttribute(attr.node) && attr.node.name.name === 'size') as NodePath; + if (sizeAttrPath) { + let value = sizeAttrPath.node.value; + if (value?.type === 'StringLiteral') { + const avatarDimensions = { + 'avatar-size-50': 16, + 'avatar-size-75': 18, + 'avatar-size-100': 20, + 'avatar-size-200': 22, + 'avatar-size-300': 26, + 'avatar-size-400': 28, + 'avatar-size-500': 32, + 'avatar-size-600': 36, + 'avatar-size-700': 40 + }; + let val = avatarDimensions[value.value as keyof typeof avatarDimensions]; + if (val != null) { + sizeAttrPath.node.value = t.jsxExpressionContainer(t.numericLiteral(val)); + } + } + } + } +} + +/** + * Transforms Avatar: + * - Comment out isDisabled (it has not been implemented yet). + * - Update size to be a pixel value if it currently matches 'avatar-size-*'. + */ +export default function transformAvatar(path: NodePath) { + // Comment out isDisabled + commentOutProp(path, {propName: 'isDisabled'}); + + // Update size to be a pixel value if it currently matches 'avatar-size-*' + updateAvatarSize(path); +} diff --git a/packages/dev/codemods/src/s1-to-s2/src/codemods/components/Badge/transform.ts b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/Badge/transform.ts new file mode 100644 index 00000000000..604c857ac78 --- /dev/null +++ b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/Badge/transform.ts @@ -0,0 +1,17 @@ +import {NodePath} from '@babel/traverse'; +import * as t from '@babel/types'; +import {updatePropNameAndValue} from '../../shared/transforms'; + +/** + * Transforms Badge: + * - Change variant="info" to variant="informative". + */ +export default function transformBadge(path: NodePath) { + // Change variant="info" to variant="informative" + updatePropNameAndValue(path, { + oldPropName: 'variant', + oldPropValue: 'info', + newPropName: 'variant', + newPropValue: 'informative' + }); +} diff --git a/packages/dev/codemods/src/s1-to-s2/src/codemods/components/Breadcrumbs/transform.ts b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/Breadcrumbs/transform.ts new file mode 100644 index 00000000000..1766a2e093b --- /dev/null +++ b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/Breadcrumbs/transform.ts @@ -0,0 +1,34 @@ +import { + addCommentToElement, + commentOutProp, + removeProp +} from '../../shared/transforms'; +import {NodePath} from '@babel/traverse'; +import * as t from '@babel/types'; + +/** + * Transforms Breadcrumbs: + * - Comment out showRoot (it has not been implemented yet). + * - Comment out isMultiline (it has not been implemented yet). + * - Comment out autoFocusCurrent (it has not been implemented yet). + * - Remove size="S" (Small is no longer a supported size in Spectrum 2). + * - Add comment to wrap in nav element if needed. + */ +export default function transformBreadcrumbs(path: NodePath) { + // Comment out showRoot + commentOutProp(path, {propName: 'showRoot'}); + + // Comment out isMultiline + commentOutProp(path, {propName: 'isMultiline'}); + + // Comment out autoFocusCurrent + commentOutProp(path, {propName: 'autoFocusCurrent'}); + + // Remove size="S" + removeProp(path, {propName: 'size', propValue: 'S'}); + + // Add comment to wrap in nav element if needed + addCommentToElement(path, { + comment: 'S2 Breadcrumbs no longer includes a nav element by default. You can wrap the Breadcrumbs component in a nav element if needed.' + }); +} diff --git a/packages/dev/codemods/src/s1-to-s2/src/codemods/components/Button/transform.ts b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/Button/transform.ts new file mode 100644 index 00000000000..2b7aea5409f --- /dev/null +++ b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/Button/transform.ts @@ -0,0 +1,61 @@ +import { + commentOutProp, + removeProp, + updateComponentIfPropPresent, + updatePropName, + updatePropNameAndValue, + updatePropValueAndAddNewPropName +} from '../../shared/transforms'; +import {NodePath} from '@babel/traverse'; +import * as t from '@babel/types'; + +/** + * Transforms Button: + * - Change variant="cta" to variant="accent" + * - Change variant="overBackground" to variant="primary" staticColor="white" + * - Change style to fillStyle + * - Comment out isPending (it has not been implemented yet) + * - Remove isQuiet (it is no longer supported in Spectrum 2) + * - If href is present, the Button should be converted to a LinkButton + * - Remove elementType (it is no longer supported in Spectrum 2). + */ +export default function transformButton(path: NodePath) { + // Change variant="cta" to variant="accent" + updatePropNameAndValue(path, { + oldPropName: 'variant', + oldPropValue: 'cta', + newPropName: 'variant', + newPropValue: 'accent' + }); + + // Change variant="overBackground" to variant="primary" staticColor="white" + updatePropValueAndAddNewPropName(path, { + oldPropName: 'variant', + oldPropValue: 'overBackground', + newPropName: 'variant', + newPropValue: 'primary', + additionalPropName: 'staticColor', + additionalPropValue: 'white' + }); + + // Change style to fillStyle + updatePropName(path, { + oldPropName: 'style', + newPropName: 'fillStyle' + }); + + // Comment out isPending + commentOutProp(path, {propName: 'isPending'}); + + // Remove isQuiet + removeProp(path, {propName: 'isQuiet'}); + + // If href is present, the Button should be converted to a LinkButton + updateComponentIfPropPresent(path, { + propName: 'href', + newComponentName: 'LinkButton' + }); + + // Remove elementType + removeProp(path, {propName: 'elementType'}); +} diff --git a/packages/dev/codemods/src/s1-to-s2/src/codemods/components/CheckboxGroup/transform.ts b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/CheckboxGroup/transform.ts new file mode 100644 index 00000000000..8e660423b90 --- /dev/null +++ b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/CheckboxGroup/transform.ts @@ -0,0 +1,12 @@ +import {NodePath} from '@babel/traverse'; +import {removeProp} from '../../shared/transforms'; +import * as t from '@babel/types'; + +/** + * Transforms CheckboxGroup: + * - Remove showErrorIcon (it has been removed due to accessibility issues). + */ +export default function transformCheckboxGroup(path: NodePath) { + // Remove showErrorIcon + removeProp(path, {propName: 'showErrorIcon'}); +} diff --git a/packages/dev/codemods/src/s1-to-s2/src/codemods/components/ColorField/transform.ts b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/ColorField/transform.ts new file mode 100644 index 00000000000..716f3e47625 --- /dev/null +++ b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/ColorField/transform.ts @@ -0,0 +1,29 @@ +import {NodePath} from '@babel/traverse'; +import {removeProp, updatePropNameAndValue} from '../../shared/transforms'; +import * as t from '@babel/types'; + +/** + * Transforms ColorField: + * - Remove isQuiet (it is no longer supported in Spectrum 2). + * - Remove placeholder (it has been removed due to accessibility issues). + * - Change validationState="invalid" to isInvalid. + * - Remove validationState="valid" (it is no longer supported in Spectrum 2). + */ +export default function transformColorField(path: NodePath) { + // Remove isQuiet + removeProp(path, {propName: 'isQuiet'}); + + // Remove placeholder + removeProp(path, {propName: 'placeholder'}); + + // Change validationState="invalid" to isInvalid + updatePropNameAndValue(path, { + oldPropName: 'validationState', + oldPropValue: 'invalid', + newPropName: 'isInvalid', + newPropValue: true + }); + + // Remove validationState="valid" + removeProp(path, {propName: 'validationState', propValue: 'valid'}); +} diff --git a/packages/dev/codemods/src/s1-to-s2/src/codemods/components/ColorSlider/transform.ts b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/ColorSlider/transform.ts new file mode 100644 index 00000000000..04de2a6a01a --- /dev/null +++ b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/ColorSlider/transform.ts @@ -0,0 +1,12 @@ +import {NodePath} from '@babel/traverse'; +import {removeProp} from '../../shared/transforms'; +import * as t from '@babel/types'; + +/** + * Transforms ColorSlider: + * - Remove showValueLabel (it has been removed due to accessibility issues). + */ +export default function transformColorSlider(path: NodePath) { + // Remove showValueLabel + removeProp(path, {propName: 'showValueLabel'}); +} diff --git a/packages/dev/codemods/src/s1-to-s2/src/codemods/components/Column/transform.ts b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/Column/transform.ts new file mode 100644 index 00000000000..c9a3d987dd2 --- /dev/null +++ b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/Column/transform.ts @@ -0,0 +1,12 @@ +import {NodePath} from '@babel/traverse'; +import * as t from '@babel/types'; +import {updateKeyToId} from '../../shared/transforms'; + +/** + * Transforms Column: + * - Update key to id. + */ +export default function transformColumn(path: NodePath) { + // Update key to id + updateKeyToId(path); +} diff --git a/packages/dev/codemods/src/s1-to-s2/src/codemods/components/ComboBox/transform.ts b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/ComboBox/transform.ts new file mode 100644 index 00000000000..7dcdaa3e228 --- /dev/null +++ b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/ComboBox/transform.ts @@ -0,0 +1,46 @@ +import { + commentOutProp, + convertDimensionValueToPx, + removeProp, + updatePropNameAndValue +} from '../../shared/transforms'; +import {NodePath} from '@babel/traverse'; +import * as t from '@babel/types'; + +/** + * Transforms ComboBox: + * - Change menuWidth value from a DimensionValue to a pixel value. + * - Remove isQuiet (it is no longer supported in Spectrum 2). + * - Comment out loadingState (it has not been implemented yet). + * - Remove placeholder (it is no longer supported in Spectrum 2). + * - Change validationState="invalid" to isInvalid. + * - Remove validationState="valid" (it is no longer supported in Spectrum 2). + * - Comment out onLoadMore (it has not been implemented yet). + */ +export default function transformComboBox(path: NodePath) { + // Change menuWidth value from a DimensionValue to a pixel value + convertDimensionValueToPx(path, {propName: 'menuWidth'}); + + // Remove isQuiet + removeProp(path, {propName: 'isQuiet'}); + + // Comment out loadingState + commentOutProp(path, {propName: 'loadingState'}); + + // Remove placeholder + removeProp(path, {propName: 'placeholder'}); + + // Change validationState="invalid" to isInvalid + updatePropNameAndValue(path, { + oldPropName: 'validationState', + oldPropValue: 'invalid', + newPropName: 'isInvalid', + newPropValue: true + }); + + // Remove validationState="valid" + removeProp(path, {propName: 'validationState', propValue: 'valid'}); + + // Comment out onLoadMore + commentOutProp(path, {propName: 'onLoadMore'}); +} diff --git a/packages/dev/codemods/src/s1-to-s2/src/codemods/components/ContextualHelp/transform.ts b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/ContextualHelp/transform.ts new file mode 100644 index 00000000000..a7f4bcb2081 --- /dev/null +++ b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/ContextualHelp/transform.ts @@ -0,0 +1,16 @@ +import {commentOutProp, updatePlacementToSingleValue} from '../../shared/transforms'; +import {NodePath} from '@babel/traverse'; +import * as t from '@babel/types'; + +/** + * Transforms ContextualHelp: + * - Comment out variant="info" (informative variant is the only one supported). + * - Update placement prop to have only one value (e.g., "bottom left" becomes "bottom"). + */ +export default function transformContextualHelp(path: NodePath) { + // Comment out variant="info" + commentOutProp(path, {propName: 'variant', propValue: 'info'}); + + // Update placement prop to have only one value + updatePlacementToSingleValue(path, {propToUpdateName: 'placement'}); +} diff --git a/packages/dev/codemods/src/s1-to-s2/src/codemods/components/DialogContainer/transform.ts b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/DialogContainer/transform.ts new file mode 100644 index 00000000000..de58d3ae0f9 --- /dev/null +++ b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/DialogContainer/transform.ts @@ -0,0 +1,17 @@ +import {NodePath} from '@babel/traverse'; +import * as t from '@babel/types'; +import {updateDialogChild} from '../DialogTrigger/transform'; +import {updatePropName} from '../../shared/transforms'; + +/** + * Transforms DialogContainer: + * - Remove type (dependent on the dialog level child used, e.g., Dialog, FullscreenDialog, Popover). + * - Move isDismissable (as isDismissible) to the dialog level component. + * - Move isKeyboardDismissDisabled to the dialog level component. + */ +export default function transformDialogContainer(path: NodePath) { + // Move isDismissable (as isDismissible) to the dialog level component + updatePropName(path, {oldPropName: 'isDismissable', newPropName: 'isDismissible'}); + + updateDialogChild(path); +} diff --git a/packages/dev/codemods/src/s1-to-s2/src/codemods/components/DialogTrigger/transform.ts b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/DialogTrigger/transform.ts new file mode 100644 index 00000000000..217c0ec0c5b --- /dev/null +++ b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/DialogTrigger/transform.ts @@ -0,0 +1,95 @@ +import {addComponentImport, getName} from '../../shared/utils'; +import { + commentOutProp, + moveRenderPropsToChild, + removeProp, + updatePropName +} from '../../shared/transforms'; +import {getComponents} from '../../../getComponents'; +import {NodePath} from '@babel/traverse'; +import * as t from '@babel/types'; + +let availableComponents = getComponents(); + +/** + * Updates DialogTrigger and DialogContainer to the new API. + * + * Example: + * - When `type="popover"`, replaces Dialog with ``. + * - When `type="fullscreen"`, replaces Dialog with ``. + * - When `type="fullscreenTakeover"`, replaces Dialog with ``. + */ +export function updateDialogChild( + path: NodePath +) { + let typePath = path.get('openingElement').get('attributes').find((attr) => t.isJSXAttribute(attr.node) && attr.node.name.name === 'type') as NodePath | undefined; + let type = typePath?.node.value?.type === 'StringLiteral' ? typePath.node.value?.value : 'modal'; + let newComponentName = 'Dialog'; + let props: t.JSXAttribute[] = []; + if (type === 'popover') { + newComponentName = 'Popover'; + } else if (type === 'fullscreen' || type === 'fullscreenTakeover') { + newComponentName = 'FullscreenDialog'; + if (type === 'fullscreenTakeover') { + props.push(t.jsxAttribute(t.jsxIdentifier('variant'), t.stringLiteral(type))); + } + } + + for (let prop of ['isDismissible', 'mobileType', 'hideArrow', 'placement', 'shouldFlip', 'isKeyboardDismissDisabled', 'containerPadding', 'offset', 'crossOffset']) { + let attr = path.get('openingElement').get('attributes').find(attr => attr.isJSXAttribute() && attr.node.name.name === prop) as NodePath | undefined; + if (attr) { + props.push(attr.node); + attr.remove(); + } + } + + typePath?.remove(); + + let localName = newComponentName; + if (newComponentName !== 'Dialog' && availableComponents.has(newComponentName)) { + let program = path.findParent((p) => t.isProgram(p.node)) as NodePath; + localName = addComponentImport(program, newComponentName); + } + + path.traverse({ + JSXElement(dialog) { + if (!t.isJSXIdentifier(dialog.node.openingElement.name) || getName(dialog, dialog.node.openingElement.name) !== 'Dialog') { + return; + } + + dialog.node.openingElement.name = t.jsxIdentifier(localName); + if (dialog.node.closingElement) { + dialog.node.closingElement.name = t.jsxIdentifier(localName); + } + + dialog.node.openingElement.attributes.push(...props); + } + }); +} + +/** + * Transforms DialogTrigger: + * - Comment out type="tray" (it has not been implemented yet). + * - Comment out mobileType (it has not been implemented yet). + * - Remove targetRef (it is no longer supported). + * - Move render props to the child component (updated API). + */ +export default function transformDialogTrigger(path: NodePath) { + // Comment out type="tray" + commentOutProp(path, {propName: 'type', propValue: 'tray'}); + + // Comment out mobileType + commentOutProp(path, {propName: 'mobileType'}); + + // Remove targetRef + removeProp(path, {propName: 'targetRef'}); + + // Move render props to the child component + moveRenderPropsToChild(path, {newChildComponentName: 'Dialog'}); + + // Update isDismissable to isDismissible + updatePropName(path, {oldPropName: 'isDismissable', newPropName: 'isDismissible'}); + + // Update DialogTrigger to the new API + updateDialogChild(path); +} diff --git a/packages/dev/codemods/src/s1-to-s2/src/codemods/components/Divider/transform.ts b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/Divider/transform.ts new file mode 100644 index 00000000000..62af597fad3 --- /dev/null +++ b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/Divider/transform.ts @@ -0,0 +1,12 @@ +import {NodePath} from '@babel/traverse'; +import {removeComponentIfWithinParent} from '../../shared/transforms'; +import * as t from '@babel/types'; + +/** + * Transforms Divider: + * - Remove Divider component if within a Dialog (Updated design for Dialog in Spectrum 2). + */ +export default function transformDivider(path: NodePath) { + // Remove Divider component if within a Dialog + removeComponentIfWithinParent(path, {parentComponentName: 'Dialog'}); +} diff --git a/packages/dev/codemods/src/s1-to-s2/src/codemods/components/Form/transform.ts b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/Form/transform.ts new file mode 100644 index 00000000000..7b45947aaa8 --- /dev/null +++ b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/Form/transform.ts @@ -0,0 +1,24 @@ +import {NodePath} from '@babel/traverse'; +import {removeProp} from '../../shared/transforms'; +import * as t from '@babel/types'; + +/** + * Transforms Form: + * - Remove isQuiet (it is no longer supported in Spectrum 2). + * - Remove isReadOnly (it is no longer supported in Spectrum 2). + * - Remove validationState (it is no longer supported in Spectrum 2). + * - Remove validationBehavior (it is no longer supported in Spectrum 2). + */ +export default function transformForm(path: NodePath) { + // Remove isQuiet + removeProp(path, {propName: 'isQuiet'}); + + // Remove isReadOnly + removeProp(path, {propName: 'isReadOnly'}); + + // Remove validationState + removeProp(path, {propName: 'validationState'}); + + // Remove validationBehavior + removeProp(path, {propName: 'validationBehavior'}); +} diff --git a/packages/dev/codemods/src/s1-to-s2/src/codemods/components/InlineAlert/transform.ts b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/InlineAlert/transform.ts new file mode 100644 index 00000000000..797a63379c2 --- /dev/null +++ b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/InlineAlert/transform.ts @@ -0,0 +1,17 @@ +import {NodePath} from '@babel/traverse'; +import * as t from '@babel/types'; +import {updatePropNameAndValue} from '../../shared/transforms'; + +/** + * Transforms InlineAlert: + * - Change variant="info" to variant="informative". + */ +export default function transformInlineAlert(path: NodePath) { + // Change variant="info" to variant="informative" + updatePropNameAndValue(path, { + oldPropName: 'variant', + oldPropValue: 'info', + newPropName: 'variant', + newPropValue: 'informative' + }); +} diff --git a/packages/dev/codemods/src/s1-to-s2/src/codemods/components/Item/transform.ts b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/Item/transform.ts new file mode 100644 index 00000000000..52a91535563 --- /dev/null +++ b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/Item/transform.ts @@ -0,0 +1,26 @@ +import {commentIfParentCollectionNotDetected, updateComponentWithinCollection} from '../../shared/transforms'; +import {NodePath} from '@babel/traverse'; +import * as t from '@babel/types'; + +/** + * Transforms Item: + * - If within Menu: Update Item to be a MenuItem. + * - If within ActionMenu: Update Item to be a MenuItem. + * - If within TagGroup: Update Item to be a Tag. + * - If within Breadcrumbs: Update Item to be a Breadcrumb. + * - If within Picker: Update Item to be a PickerItem. + * - If within ComboBox: Update Item to be a ComboBoxItem. + * - Update key to id (and keep key if rendered inside array.map). + */ +export default function transformItem(path: NodePath) { + // Update Items based on parent collection component + updateComponentWithinCollection(path, {parentComponentName: 'Menu', newComponentName: 'MenuItem'}); + updateComponentWithinCollection(path, {parentComponentName: 'ActionMenu', newComponentName: 'MenuItem'}); + updateComponentWithinCollection(path, {parentComponentName: 'TagGroup', newComponentName: 'Tag'}); + updateComponentWithinCollection(path, {parentComponentName: 'Breadcrumbs', newComponentName: 'Breadcrumb'}); + updateComponentWithinCollection(path, {parentComponentName: 'Picker', newComponentName: 'PickerItem'}); + updateComponentWithinCollection(path, {parentComponentName: 'ComboBox', newComponentName: 'ComboBoxItem'}); + + // Comment if parent collection not detected + commentIfParentCollectionNotDetected(path); +} diff --git a/packages/dev/codemods/src/s1-to-s2/src/codemods/components/Link/transform.ts b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/Link/transform.ts new file mode 100644 index 00000000000..8912afb005b --- /dev/null +++ b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/Link/transform.ts @@ -0,0 +1,49 @@ +import {addComment} from '../../shared/utils'; +import {NodePath} from '@babel/traverse'; +import * as t from '@babel/types'; +import {updatePropNameAndValue} from '../../shared/transforms'; + +/** + * If was used inside Link (legacy API), remove the and apply props directly to Link. + */ +function updateLegacyLink( + path: NodePath +) { + let missingOuterHref = t.isJSXElement(path.node) && !path.node.openingElement.attributes.some((attr) => t.isJSXAttribute(attr) && attr.name.name === 'href'); + if (missingOuterHref) { + let innerLink = path.node.children.find((child) => t.isJSXElement(child) && t.isJSXIdentifier(child.openingElement.name)); + if (innerLink && t.isJSXElement(innerLink)) { + let innerAttributes = innerLink.openingElement.attributes; + let outerAttributes = path.node.openingElement.attributes; + innerAttributes.forEach((attr) => { + outerAttributes.push(attr); + }); + + if ( + t.isJSXIdentifier(innerLink.openingElement.name) && + innerLink.openingElement.name.name !== 'a' + ) { + addComment(path.node, ' TODO(S2-upgrade): You may have been using a custom link component here. You\'ll need to update this manually.'); + } + path.node.children = innerLink.children; + } + } +} + +/** + * Transforms Link: + * - Change variant="overBackground" to staticColor="white". + * - If was used inside Link (legacy API), remove the and apply props directly to Link. + */ +export default function transformLink(path: NodePath) { + // Change variant="overBackground" to staticColor="white" + updatePropNameAndValue(path, { + oldPropName: 'variant', + oldPropValue: 'overBackground', + newPropName: 'staticColor', + newPropValue: 'white' + }); + + // Update legacy Link + updateLegacyLink(path); +} diff --git a/packages/dev/codemods/src/s1-to-s2/src/codemods/components/NumberField/transform.ts b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/NumberField/transform.ts new file mode 100644 index 00000000000..36dcab89a0b --- /dev/null +++ b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/NumberField/transform.ts @@ -0,0 +1,25 @@ +import {NodePath} from '@babel/traverse'; +import {removeProp, updatePropNameAndValue} from '../../shared/transforms'; +import * as t from '@babel/types'; + +/** + * Transforms NumberField: + * - Remove isQuiet (it is no longer supported in Spectrum 2). + * - Change validationState="invalid" to isInvalid. + * - Remove validationState="valid" (it is no longer supported in Spectrum 2). + */ +export default function transformNumberField(path: NodePath) { + // Remove isQuiet + removeProp(path, {propName: 'isQuiet'}); + + // Change validationState="invalid" to isInvalid + updatePropNameAndValue(path, { + oldPropName: 'validationState', + oldPropValue: 'invalid', + newPropName: 'isInvalid', + newPropValue: true + }); + + // Remove validationState="valid" + removeProp(path, {propName: 'validationState', propValue: 'valid'}); +} diff --git a/packages/dev/codemods/src/s1-to-s2/src/codemods/components/Picker/transform.ts b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/Picker/transform.ts new file mode 100644 index 00000000000..0705f4f5011 --- /dev/null +++ b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/Picker/transform.ts @@ -0,0 +1,42 @@ +import { + commentOutProp, + convertDimensionValueToPx, + removeProp, + updatePropNameAndValue +} from '../../shared/transforms'; +import {NodePath} from '@babel/traverse'; +import * as t from '@babel/types'; + +/** + * Transforms Picker: + * - Change menuWidth value from a DimensionValue to a pixel value. + * - Remove isQuiet (it is no longer supported in Spectrum 2). + * - Change validationState="invalid" to isInvalid. + * - Remove validationState="valid" (it is no longer supported in Spectrum 2). + * - Comment out isLoading (it has not been implemented yet). + * - Comment out onLoadMore (it has not been implemented yet). + */ +export default function transformPicker(path: NodePath) { + // Change menuWidth value from a DimensionValue to a pixel value + convertDimensionValueToPx(path, {propName: 'menuWidth'}); + + // Remove isQuiet + removeProp(path, {propName: 'isQuiet'}); + + // Change validationState="invalid" to isInvalid + updatePropNameAndValue(path, { + oldPropName: 'validationState', + oldPropValue: 'invalid', + newPropName: 'isInvalid', + newPropValue: true + }); + + // Remove validationState="valid" + removeProp(path, {propName: 'validationState', propValue: 'valid'}); + + // Comment out isLoading + commentOutProp(path, {propName: 'isLoading'}); + + // Comment out onLoadMore + commentOutProp(path, {propName: 'onLoadMore'}); +} diff --git a/packages/dev/codemods/src/s1-to-s2/src/codemods/components/ProgressBar/transform.ts b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/ProgressBar/transform.ts new file mode 100644 index 00000000000..f9f6b1313c5 --- /dev/null +++ b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/ProgressBar/transform.ts @@ -0,0 +1,25 @@ +import {commentOutProp, removeProp, updatePropNameAndValue} from '../../shared/transforms'; +import {NodePath} from '@babel/traverse'; +import * as t from '@babel/types'; + +/** + * Transforms ProgressBar: + * - Change variant="overBackground" to staticColor="white". + * - Comment out labelPosition (it has not been implemented yet). + * - Comment out showValueLabel (it has not been implemented yet). + */ +export default function transformProgressBar(path: NodePath) { + // Change variant="overBackground" to staticColor="white" + updatePropNameAndValue(path, { + oldPropName: 'variant', + oldPropValue: 'overBackground', + newPropName: 'staticColor', + newPropValue: 'white' + }); + + // Comment out labelPosition + commentOutProp(path, {propName: 'labelPosition'}); + + // Comment out showValueLabel + removeProp(path, {propName: 'showValueLabel'}); +} diff --git a/packages/dev/codemods/src/s1-to-s2/src/codemods/components/ProgressCircle/transform.ts b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/ProgressCircle/transform.ts new file mode 100644 index 00000000000..a0099b22b5a --- /dev/null +++ b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/ProgressCircle/transform.ts @@ -0,0 +1,17 @@ +import {NodePath} from '@babel/traverse'; +import * as t from '@babel/types'; +import {updatePropNameAndValue} from '../../shared/transforms'; + +/** + * Transforms ProgressCircle: + * - Change variant="overBackground" to staticColor="white". + */ +export default function transformProgressCircle(path: NodePath) { + // Change variant="overBackground" to staticColor="white" + updatePropNameAndValue(path, { + oldPropName: 'variant', + oldPropValue: 'overBackground', + newPropName: 'staticColor', + newPropValue: 'white' + }); +} diff --git a/packages/dev/codemods/src/s1-to-s2/src/codemods/components/RadioGroup/transform.ts b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/RadioGroup/transform.ts new file mode 100644 index 00000000000..0fedb73054c --- /dev/null +++ b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/RadioGroup/transform.ts @@ -0,0 +1,25 @@ +import {NodePath} from '@babel/traverse'; +import {removeProp, updatePropNameAndValue} from '../../shared/transforms'; +import * as t from '@babel/types'; + +/** + * Transforms RadioGroup: + * - Change validationState="invalid" to isInvalid. + * - Remove validationState="valid" (it is no longer supported in Spectrum 2). + * - Remove showErrorIcon (it has been removed due to accessibility issues). + */ +export default function transformRadioGroup(path: NodePath) { + // Change validationState="invalid" to isInvalid + updatePropNameAndValue(path, { + oldPropName: 'validationState', + oldPropValue: 'invalid', + newPropName: 'isInvalid', + newPropValue: true + }); + + // Remove validationState="valid" + removeProp(path, {propName: 'validationState', propValue: 'valid'}); + + // Remove showErrorIcon + removeProp(path, {propName: 'showErrorIcon'}); +} diff --git a/packages/dev/codemods/src/s1-to-s2/src/codemods/components/RangeSlider/transform.ts b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/RangeSlider/transform.ts new file mode 100644 index 00000000000..f5214765878 --- /dev/null +++ b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/RangeSlider/transform.ts @@ -0,0 +1,20 @@ +import {commentOutProp, removeProp} from '../../shared/transforms'; +import {NodePath} from '@babel/traverse'; +import * as t from '@babel/types'; + +/** + * Transforms RangeSlider: + * - Remove showValueLabel (it has been removed due to accessibility issues). + * - Comment out getValueLabel (it has not been implemented yet). + * - Comment out orientation (it has not been implemented yet). + */ +export default function transformRangeSlider(path: NodePath) { + // Remove showValueLabel + removeProp(path, {propName: 'showValueLabel'}); + + // Comment out getValueLabel + commentOutProp(path, {propName: 'getValueLabel'}); + + // Comment out orientation + commentOutProp(path, {propName: 'orientation'}); +} diff --git a/packages/dev/codemods/src/s1-to-s2/src/codemods/components/Row/transform.ts b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/Row/transform.ts new file mode 100644 index 00000000000..0626424121e --- /dev/null +++ b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/Row/transform.ts @@ -0,0 +1,107 @@ +import {getName} from '../../shared/utils'; +import {NodePath} from '@babel/traverse'; +import * as t from '@babel/types'; +import {updateKeyToId} from '../../shared/transforms'; + +/** + * Updates the function signature of the Row component. + */ +function updateRowFunctionArg( + path: NodePath +) { + // Find the export function passed as a child + let functionChild = path.get('children').find(childPath => + childPath.isJSXExpressionContainer() && + childPath.get('expression').isArrowFunctionExpression() + ); + + let tablePath = path.findParent((p) => + t.isJSXElement(p.node) && + t.isJSXIdentifier(p.node.openingElement.name) && + getName(path, p.node.openingElement.name) === 'TableView' + ); + + let tableHeaderPath = tablePath?.get('children').find((child) => + t.isJSXElement(child.node) && + t.isJSXIdentifier(child.node.openingElement.name) && + getName(child as NodePath, child.node.openingElement.name) === 'TableHeader' + ) as NodePath | undefined; + + function findColumnKeyProp(path: NodePath) { + let columnKeyProp = 'id'; + path.traverse({ + JSXElement(columnPath) { + if ( + t.isArrowFunctionExpression(columnPath.parentPath.node) && + t.isJSXElement(columnPath.node) && + t.isJSXIdentifier(columnPath.node.openingElement.name) && + getName(columnPath as NodePath, columnPath.node.openingElement.name) === 'Column' + ) { + let openingElement = columnPath.get('openingElement'); + let keyPropPath = openingElement.get('attributes').find(attr => + t.isJSXAttribute(attr.node) && + (attr.node.name.name === 'key' || attr.node.name.name === 'id') + ); + keyPropPath?.traverse({ + Identifier(innerPath) { + if ( + innerPath.node.name === 'column' && + innerPath.parentPath.node.type === 'MemberExpression' && + t.isIdentifier(innerPath.parentPath.node.property) + ) { + columnKeyProp = innerPath.parentPath.node.property.name; + } + } + }); + } + } + }); + return columnKeyProp || 'id'; + } + + let columnKey = findColumnKeyProp(tableHeaderPath as NodePath); + + if (functionChild && functionChild.isJSXExpressionContainer()) { + let arrowFuncPath = functionChild.get('expression'); + if (arrowFuncPath.isArrowFunctionExpression()) { + let params = arrowFuncPath.node.params; + if (params.length === 1 && t.isIdentifier(params[0])) { + let paramName = params[0].name; + + // Rename parameter to 'column' + params[0].name = 'column'; + + // Replace references to the old parameter name inside the export function body + arrowFuncPath.get('body').traverse({ + Identifier(innerPath) { + if ( + innerPath.node.name === paramName && + // Ensure we're not replacing the parameter declaration + innerPath.node !== params[0] + ) { + // Replace with column key + innerPath.replaceWith( + t.memberExpression( + t.identifier('column'), + t.identifier(columnKey ?? 'id') + ) + ); + } + } + }); + } + } + } +} + +/** + * Transforms Row: + * - Update key to id. + * - Update function signature. + */ +export default function transformRow(path: NodePath) { + // Update key to id + updateKeyToId(path); + + updateRowFunctionArg(path); +} diff --git a/packages/dev/codemods/src/s1-to-s2/src/codemods/components/SearchField/transform.ts b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/SearchField/transform.ts new file mode 100644 index 00000000000..8dc81df674f --- /dev/null +++ b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/SearchField/transform.ts @@ -0,0 +1,33 @@ +import {commentOutProp, removeProp, updatePropNameAndValue} from '../../shared/transforms'; +import {NodePath} from '@babel/traverse'; +import * as t from '@babel/types'; + +/** + * Transforms SearchField: + * - Remove placeholder (it has been removed due to accessibility issues). + * - Comment out icon (it has not been implemented yet). + * - Remove isQuiet (it is no longer supported in Spectrum 2). + * - Change validationState="invalid" to isInvalid. + * - Remove validationState="valid" (it is no longer supported in Spectrum 2). + */ +export default function transformSearchField(path: NodePath) { + // Remove placeholder + removeProp(path, {propName: 'placeholder'}); + + // Comment out icon + commentOutProp(path, {propName: 'icon'}); + + // Remove isQuiet + removeProp(path, {propName: 'isQuiet'}); + + // Change validationState="invalid" to isInvalid + updatePropNameAndValue(path, { + oldPropName: 'validationState', + oldPropValue: 'invalid', + newPropName: 'isInvalid', + newPropValue: true + }); + + // Remove validationState="valid" + removeProp(path, {propName: 'validationState', propValue: 'valid'}); +} diff --git a/packages/dev/codemods/src/s1-to-s2/src/codemods/components/Section/transform.ts b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/Section/transform.ts new file mode 100644 index 00000000000..84b0695bce2 --- /dev/null +++ b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/Section/transform.ts @@ -0,0 +1,39 @@ +import {commentIfParentCollectionNotDetected, movePropToNewChildComponentName, updateComponentWithinCollection} from '../../shared/transforms'; +import {NodePath} from '@babel/traverse'; +import * as t from '@babel/types'; + +/** + * Transforms Section: + * - If within Menu: Update Section to be a MenuSection. + * - If within Picker: Update Section to be a PickerSection. + * - If within ComboBox: Update Section to be a ComboBoxSection. + */ +export default function transformSection(path: NodePath) { + // Update Sections based on parent collection component + updateComponentWithinCollection(path, {parentComponentName: 'Menu', newComponentName: 'MenuSection'}); + updateComponentWithinCollection(path, {parentComponentName: 'Picker', newComponentName: 'PickerSection'}); + updateComponentWithinCollection(path, {parentComponentName: 'ComboBox', newComponentName: 'ComboBoxSection'}); + + // Move title prop to Header component + movePropToNewChildComponentName(path, { + parentComponentName: 'Menu', + childComponentName: 'MenuSection', + propName: 'title', + newChildComponentName: 'Header' + }); + movePropToNewChildComponentName(path, { + parentComponentName: 'Picker', + childComponentName: 'PickerSection', + propName: 'title', + newChildComponentName: 'Header' + }); + movePropToNewChildComponentName(path, { + parentComponentName: 'ComboBox', + childComponentName: 'ComboBoxSection', + propName: 'title', + newChildComponentName: 'Header' + }); + + // Comment if parent collection not detected + commentIfParentCollectionNotDetected(path); +} diff --git a/packages/dev/codemods/src/s1-to-s2/src/codemods/components/Slider/transform.ts b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/Slider/transform.ts new file mode 100644 index 00000000000..3a69877db88 --- /dev/null +++ b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/Slider/transform.ts @@ -0,0 +1,28 @@ +import {commentOutProp, removeProp} from '../../shared/transforms'; +import {NodePath} from '@babel/traverse'; +import * as t from '@babel/types'; + +/** + * Transforms Slider: + * - Remove isFilled (Slider is always filled in Spectrum 2). + * - Remove trackGradient (Not supported in S2 design). + * - Remove showValueLabel (it has been removed due to accessibility issues). + * - Comment out getValueLabel (it has not been implemented yet). + * - Comment out orientation (it has not been implemented yet). + */ +export default function transformSlider(path: NodePath) { + // Remove isFilled + removeProp(path, {propName: 'isFilled'}); + + // Remove trackGradient + removeProp(path, {propName: 'trackGradient'}); + + // Remove showValueLabel + removeProp(path, {propName: 'showValueLabel'}); + + // Comment out getValueLabel + commentOutProp(path, {propName: 'getValueLabel'}); + + // Comment out orientation + commentOutProp(path, {propName: 'orientation'}); +} diff --git a/packages/dev/codemods/src/s1-to-s2/src/codemods/components/StatusLight/transform.ts b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/StatusLight/transform.ts new file mode 100644 index 00000000000..e61ccdef010 --- /dev/null +++ b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/StatusLight/transform.ts @@ -0,0 +1,21 @@ +import {NodePath} from '@babel/traverse'; +import {removeProp, updatePropNameAndValue} from '../../shared/transforms'; +import * as t from '@babel/types'; + +/** + * Transforms StatusLight: + * - Remove isDisabled (it is no longer supported in Spectrum 2). + * - Change variant="info" to variant="informative". + */ +export default function transformStatusLight(path: NodePath) { + // Remove isDisabled + removeProp(path, {propName: 'isDisabled'}); + + // Change variant="info" to variant="informative" + updatePropNameAndValue(path, { + oldPropName: 'variant', + oldPropValue: 'info', + newPropName: 'variant', + newPropValue: 'informative' + }); +} diff --git a/packages/dev/codemods/src/s1-to-s2/src/codemods/components/TableView/transform.ts b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/TableView/transform.ts new file mode 100644 index 00000000000..9984fea9fab --- /dev/null +++ b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/TableView/transform.ts @@ -0,0 +1,175 @@ +import {addComment, getName} from '../../shared/utils'; +import {commentOutProp} from '../../shared/transforms'; +import {NodePath} from '@babel/traverse'; +import * as t from '@babel/types'; + +/** + * Copies the columns prop from the TableHeader to the Row component. + */ +function addColumnsPropToRow( + path: NodePath +) { + const tableHeaderPath = path.get('children').find((child) => + t.isJSXElement(child.node) && + t.isJSXIdentifier(child.node.openingElement.name) && + getName(child as NodePath, child.node.openingElement.name) === 'TableHeader' + ) as NodePath | undefined; + + if (!tableHeaderPath) { + addComment(path.node, ' TODO(S2-upgrade): Could not find TableHeader within Table to retrieve columns prop.'); + return; + } + + const columnsProp = tableHeaderPath + .get('openingElement') + .get('attributes') + .find((attr) => t.isJSXAttribute(attr.node) && attr.node.name.name === 'columns') as NodePath | undefined; + + if (columnsProp) { + path.traverse({ + JSXElement(innerPath) { + if ( + t.isJSXElement(innerPath.node) && + t.isJSXIdentifier(innerPath.node.openingElement.name) && + getName(innerPath as NodePath, innerPath.node.openingElement.name) === 'Row' + ) { + let rowPath = innerPath as NodePath; + rowPath.node.openingElement.attributes.push(columnsProp.node); + + // If Row doesn't contain id prop, leave a comment for the user to check manually + let idProp = rowPath.get('openingElement').get('attributes').find((attr) => t.isJSXAttribute(attr.node) && attr.node.name.name === 'id'); + if (!idProp) { + 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.'); + } + } + } + }); + } +} + +function commentIfNestedColumns( + path: NodePath +) { + const headerPath = path.get('children').find((child) => + t.isJSXElement(child.node) && + t.isJSXIdentifier(child.node.openingElement.name) && + getName(child as NodePath, child.node.openingElement.name) === 'TableHeader' + ) as NodePath | undefined; + const columns = headerPath?.get('children') || []; + + let hasNestedColumns = false; + + columns.forEach(column => { + let columnChildren = column.get('children'); + if ( + columnChildren.find(child => + t.isJSXElement(child.node) && + t.isJSXIdentifier(child.node.openingElement.name) && + getName(child as NodePath, child.node.openingElement.name) === 'Column' + ) + ) { + hasNestedColumns = true; + } + }); + + if (hasNestedColumns) { + addComment(path.node, ' TODO(S2-upgrade): Nested Column components are not supported yet.'); + } +} + +/** + * Adds isRowHeader to the first Column in a table if there isn't already a row header. + * @param path + */ +function addRowHeader( + path: NodePath +) { + let tableHeaderPath = path.get('children').find((child) => + t.isJSXElement(child.node) && + t.isJSXIdentifier(child.node.openingElement.name) && + getName(child as NodePath, child.node.openingElement.name) === 'TableHeader' + ) as NodePath | undefined; + + + // Check if isRowHeader is already set on a Column + let hasRowHeader = false; + tableHeaderPath?.get('children').forEach((child) => { + if ( + t.isJSXElement(child.node) && + t.isJSXIdentifier(child.node.openingElement.name) && + getName(child as NodePath, child.node.openingElement.name) === 'Column' + ) { + let isRowHeaderProp = (child.get('openingElement') as NodePath).get('attributes').find((attr) => t.isJSXAttribute(attr.node) && attr.node.name.name === 'isRowHeader') as NodePath | undefined; + if (isRowHeaderProp) { + hasRowHeader = true; + } + } + }); + + // If there isn't already a row header, add one to the first Column if possible + if (!hasRowHeader) { + tableHeaderPath?.get('children').forEach((child) => { + // Add to first Column if static + if ( + !hasRowHeader && + t.isJSXElement(child.node) && + t.isJSXIdentifier(child.node.openingElement.name) && + getName(child as NodePath, child.node.openingElement.name) === 'Column' + ) { + child.node.openingElement.attributes.push(t.jsxAttribute(t.jsxIdentifier('isRowHeader'), t.jsxExpressionContainer(t.booleanLiteral(true)))); + hasRowHeader = true; + } + + // If render function is used, leave a comment to update manually + if ( + t.isJSXExpressionContainer(child.node) && + t.isArrowFunctionExpression(child.node.expression) + ) { + addComment(child.node, ' TODO(S2-upgrade): You\'ll need to add isRowHeader to one of the columns manually.'); + } + + // If array.map is used, leave a comment to update manually + if ( + t.isJSXExpressionContainer(child.node) && + t.isCallExpression(child.node.expression) && + t.isMemberExpression(child.node.expression.callee) && + t.isIdentifier(child.node.expression.callee.property) && + child.node.expression.callee.property.name === 'map' + ) { + addComment(child.node, ' TODO(S2-upgrade): You\'ll need to add isRowHeader to one of the columns manually.'); + } + }); + } +} + +/** + * Transforms TableView: + * - For Column and Row: Update key to be id (and keep key if rendered inside array.map). + * - For dynamic tables, pass a columns prop into Row. + * - For Row: Update dynamic render function to pass in column instead of columnKey. + * - Comment out UNSTABLE_allowsExpandableRows (it has not been implemented yet). + * - Comment out UNSTABLE_onExpandedChange (it has not been implemented yet). + * - Comment out UNSTABLE_expandedKeys (it has not been implemented yet). + * - Comment out UNSTABLE_defaultExpandedKeys (it has not been implemented yet). + */ +export default function transformTable(path: NodePath) { + // Add columns prop to Row for dynamic tables + addColumnsPropToRow(path); + + // Comment out nested columns + commentIfNestedColumns(path); + + // Comment out dragAndDropHooks + commentOutProp(path, {propName: 'dragAndDropHooks'}); + + // Comment out selectionStyle="highlight" + commentOutProp(path, {propName: 'selectionStyle'}); + + // Comment out unstable expandable rows props + commentOutProp(path, {propName: 'UNSTABLE_allowsExpandableRows'}); + commentOutProp(path, {propName: 'UNSTABLE_onExpandedChange'}); + commentOutProp(path, {propName: 'UNSTABLE_expandedKeys'}); + commentOutProp(path, {propName: 'UNSTABLE_defaultExpandedKeys'}); + + addRowHeader(path); +} diff --git a/packages/dev/codemods/src/s1-to-s2/src/codemods/components/Tabs/transform.ts b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/Tabs/transform.ts new file mode 100644 index 00000000000..dceba75b327 --- /dev/null +++ b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/Tabs/transform.ts @@ -0,0 +1,99 @@ +import {getName, removeComponentImport} from '../../shared/utils'; +import {NodePath} from '@babel/traverse'; +import {removeProp, updateComponentWithinCollection, updateToNewComponentName} from '../../shared/transforms'; +import * as t from '@babel/types'; + +function transformTabList(tabListPath: NodePath): t.JSXElement { + tabListPath.get('children').forEach(itemPath => { + if ( + t.isJSXElement(itemPath.node) && + t.isJSXIdentifier(itemPath.node.openingElement.name) && + getName(itemPath as NodePath, itemPath.node.openingElement.name) === 'Item' + ) { + updateComponentWithinCollection(itemPath as NodePath, {parentComponentName: 'TabList', newComponentName: 'Tab'}); + } + }); + return tabListPath.node; +} + +function transformTabPanels(tabPanelsPath: NodePath, itemsProp: t.JSXAttribute | null): t.JSXElement[] { + // Dynamic case + let dynamicRender = tabPanelsPath.get('children').find(path => t.isJSXExpressionContainer(path.node)); + if (dynamicRender) { + updateToNewComponentName(tabPanelsPath, {newComponentName: 'Collection'}); + let itemPath = (dynamicRender.get('expression') as NodePath).get('body'); + updateComponentWithinCollection(itemPath as NodePath, {parentComponentName: 'Collection', newComponentName: 'TabPanel'}); + if (itemsProp) { + tabPanelsPath.node.openingElement.attributes.push(t.jsxAttribute(t.jsxIdentifier('items'), itemsProp.value)); + } + return [tabPanelsPath.node]; + } + + // Static case + return tabPanelsPath.get('children').map(itemPath => { + if ( + t.isJSXElement(itemPath.node) && + t.isJSXIdentifier(itemPath.node.openingElement.name) && + getName(itemPath as NodePath, itemPath.node.openingElement.name) === 'Item' + ) { + updateComponentWithinCollection(itemPath as NodePath, {parentComponentName: 'TabPanels', newComponentName: 'TabPanel'}); + return itemPath.node; + } + return null; + }).filter(Boolean) as t.JSXElement[]; +} + +/** + * Transforms Tabs props and structure: + * - Inside TabList: Update Item to be Tab. + * - Update items on Tabs to be on TabList. + * - Inside TabPanels: Update Item to be a TabPanel and remove the surrounding TabPanels. + * - Remove isEmphasized (it is no longer supported in Spectrum 2). + * - Remove isQuiet (it is no longer supported in Spectrum 2). + */ +export default function transformTabs(path: NodePath) { + + let program = path.findParent((p) => t.isProgram(p.node)) as NodePath; + removeComponentImport(program, 'TabPanels'); + + let tabListNode: t.JSXElement | null = null; + let tabPanelsNodes: t.JSXElement[] = []; + let itemsProp: t.JSXAttribute | null = null; + + path.node.openingElement.attributes = path.node.openingElement.attributes.filter(attr => { + if (t.isJSXAttribute(attr) && attr.name.name === 'items') { + itemsProp = attr; + return false; + } + return true; + }); + + path.get('children').forEach(childPath => { + if (t.isJSXElement(childPath.node)) { + if ( + t.isJSXIdentifier(childPath.node.openingElement.name) && + getName(childPath as NodePath, childPath.node.openingElement.name) === 'TabList' + ) { + tabListNode = transformTabList(childPath as NodePath); + if (itemsProp) { + tabListNode.openingElement.attributes.push(itemsProp); + } + } else if ( + t.isJSXIdentifier(childPath.node.openingElement.name) && + getName(childPath as NodePath, childPath.node.openingElement.name) === 'TabPanels' + ) { + tabPanelsNodes = transformTabPanels(childPath as NodePath, itemsProp); + } + } + }); + + if (tabListNode) { + path.node.children = [tabListNode, ...tabPanelsNodes]; + } + + // Remove isEmphasized + removeProp(path, {propName: 'isEmphasized'}); + + // Remove isQuiet + removeProp(path, {propName: 'isQuiet'}); +} diff --git a/packages/dev/codemods/src/s1-to-s2/src/codemods/components/TagGroup/transform.ts b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/TagGroup/transform.ts new file mode 100644 index 00000000000..1bbe3c13119 --- /dev/null +++ b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/TagGroup/transform.ts @@ -0,0 +1,30 @@ +import {NodePath} from '@babel/traverse'; +import {removeProp, updatePropName, updatePropNameAndValue} from '../../shared/transforms'; +import * as t from '@babel/types'; + +/** + * Transforms TagGroup: + * - Rename actionLabel to groupActionLabel. + * - Rename onAction to onGroupAction. + * - Change validationState="invalid" to isInvalid. + * - Update Item to be Tag. + * - Remove validationState="valid" (it is no longer supported in Spectrum 2). + */ +export default function transformTagGroup(path: NodePath) { + // Rename actionLabel to groupActionLabel + updatePropName(path, {oldPropName: 'actionLabel', newPropName: 'groupActionLabel'}); + + // Rename onAction to onGroupAction + updatePropName(path, {oldPropName: 'onAction', newPropName: 'onGroupAction'}); + + // Change validationState="invalid" to isInvalid + updatePropNameAndValue(path, { + oldPropName: 'validationState', + oldPropValue: 'invalid', + newPropName: 'isInvalid', + newPropValue: true + }); + + // Remove validationState="valid" + removeProp(path, {propName: 'validationState', propValue: 'valid'}); +} diff --git a/packages/dev/codemods/src/s1-to-s2/src/codemods/components/TextArea/transform.ts b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/TextArea/transform.ts new file mode 100644 index 00000000000..585660efefa --- /dev/null +++ b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/TextArea/transform.ts @@ -0,0 +1,33 @@ +import {commentOutProp, removeProp, updatePropNameAndValue} from '../../shared/transforms'; +import {NodePath} from '@babel/traverse'; +import * as t from '@babel/types'; + +/** + * Transforms TextArea: + * - Comment out icon (it has not been implemented yet). + * - Remove isQuiet (it is no longer supported in Spectrum 2). + * - Remove placeholder (it has been removed due to accessibility issues). + * - Change validationState="invalid" to isInvalid. + * - Remove validationState="valid" (it is no longer supported in Spectrum 2). + */ +export default function transformTextArea(path: NodePath) { + // Comment out icon + commentOutProp(path, {propName: 'icon'}); + + // Remove isQuiet + removeProp(path, {propName: 'isQuiet'}); + + // Remove placeholder + removeProp(path, {propName: 'placeholder'}); + + // Change validationState="invalid" to isInvalid + updatePropNameAndValue(path, { + oldPropName: 'validationState', + oldPropValue: 'invalid', + newPropName: 'isInvalid', + newPropValue: true + }); + + // Remove validationState="valid" + removeProp(path, {propName: 'validationState', propValue: 'valid'}); +} diff --git a/packages/dev/codemods/src/s1-to-s2/src/codemods/components/TextField/transform.ts b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/TextField/transform.ts new file mode 100644 index 00000000000..1de2a30e21c --- /dev/null +++ b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/TextField/transform.ts @@ -0,0 +1,33 @@ +import {commentOutProp, removeProp, updatePropNameAndValue} from '../../shared/transforms'; +import {NodePath} from '@babel/traverse'; +import * as t from '@babel/types'; + +/** + * Transforms TextField: + * - Comment out icon (it has not been implemented yet). + * - Remove isQuiet (it is no longer supported in Spectrum 2). + * - Remove placeholder (it has been removed due to accessibility issues). + * - Change validationState="invalid" to isInvalid. + * - Remove validationState="valid" (it is no longer supported in Spectrum 2). + */ +export default function transformTextField(path: NodePath) { + // Comment out icon + commentOutProp(path, {propName: 'icon'}); + + // Remove isQuiet + removeProp(path, {propName: 'isQuiet'}); + + // Remove placeholder + removeProp(path, {propName: 'placeholder'}); + + // Change validationState="invalid" to isInvalid + updatePropNameAndValue(path, { + oldPropName: 'validationState', + oldPropValue: 'invalid', + newPropName: 'isInvalid', + newPropValue: true + }); + + // Remove validationState="valid" + removeProp(path, {propName: 'validationState', propValue: 'valid'}); +} diff --git a/packages/dev/codemods/src/s1-to-s2/src/codemods/components/Tooltip/transform.ts b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/Tooltip/transform.ts new file mode 100644 index 00000000000..87f61303398 --- /dev/null +++ b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/Tooltip/transform.ts @@ -0,0 +1,32 @@ +import {movePropToParentComponent, removeProp} from '../../shared/transforms'; +import {NodePath} from '@babel/traverse'; +import * as t from '@babel/types'; + +/** + * Transforms Tooltip: + * - Remove variant (it is no longer supported in Spectrum 2). + * - Move placement prop to the parent TooltipTrigger. + * - Remove showIcon (it is no longer supported in Spectrum 2). + * - Move isOpen prop to the parent TooltipTrigger. + */ +export default function transformTooltip(path: NodePath) { + // Remove variant + removeProp(path, {propName: 'variant'}); + + // Move placement prop to the parent TooltipTrigger + movePropToParentComponent(path, { + parentComponentName: 'TooltipTrigger', + childComponentName: 'Tooltip', + propName: 'placement' + }); + + // Remove showIcon + removeProp(path, {propName: 'showIcon'}); + + // Move isOpen prop to the parent TooltipTrigger + movePropToParentComponent(path, { + parentComponentName: 'TooltipTrigger', + childComponentName: 'Tooltip', + propName: 'isOpen' + }); +} diff --git a/packages/dev/codemods/src/s1-to-s2/src/codemods/components/TooltipTrigger/transform.ts b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/TooltipTrigger/transform.ts new file mode 100644 index 00000000000..fd1be7f9c9d --- /dev/null +++ b/packages/dev/codemods/src/s1-to-s2/src/codemods/components/TooltipTrigger/transform.ts @@ -0,0 +1,15 @@ +import {NodePath} from '@babel/traverse'; +import * as t from '@babel/types'; +import {updatePlacementToSingleValue} from '../../shared/transforms'; + +/** + * Transforms TooltipTrigger: + * - Updates placement prop to single value. + */ +export default function transformTooltipTrigger(path: NodePath) { + // Update placement prop to single value + updatePlacementToSingleValue(path, { + propToUpdateName: 'placement', + childComponentName: 'Tooltip' + }); +} diff --git a/packages/dev/codemods/src/s1-to-s2/src/codemods/dialog.ts b/packages/dev/codemods/src/s1-to-s2/src/codemods/dialog.ts deleted file mode 100644 index 8b6fedb2443..00000000000 --- a/packages/dev/codemods/src/s1-to-s2/src/codemods/dialog.ts +++ /dev/null @@ -1,47 +0,0 @@ -import {addComment} from './utils'; -import {NodePath} from '@babel/traverse'; -import * as t from '@babel/types'; - -export function transformDialog(path: NodePath) { - path.get('children').forEach(path => { - // S2 dialogs don't have a divider anymore. - if (path.isJSXElement()) { - let name = path.get('openingElement').get('name'); - if (name.referencesImport('@adobe/react-spectrum', 'Divider') || name.referencesImport('@react-spectrum/divider', 'Divider')) { - path.remove(); - } - } - }); -} - -export function transformDialogTrigger(path: NodePath) { - path.get('children').forEach(path => { - // Move close function inside dialog. - // TODO: handle other types of functions too? - if (!path.isJSXExpressionContainer()) { - return; - } - - let expression = path.get('expression'); - if (!expression.isArrowFunctionExpression()) { - return; - } - - let body = expression.get('body'); - if (body.isJSXElement()) { - let name = body.get('openingElement').get('name'); - if ((name.referencesImport('@adobe/react-spectrum', 'Dialog') || name.referencesImport('@react-spectrum/dialog', 'Dialog'))) { - body.node.children = [t.jsxExpressionContainer( - t.arrowFunctionExpression( - expression.node.params, - t.jsxFragment(t.jsxOpeningFragment(), t.jsxClosingFragment(), body.node.children) - ) - )]; - path.replaceWith(body.node); - return; - } - } - - addComment(body.node, ' TODO(S2-upgrade): update this dialog to move the close function inside'); - }); -} diff --git a/packages/dev/codemods/src/s1-to-s2/src/iconMap.ts b/packages/dev/codemods/src/s1-to-s2/src/codemods/icons/iconMap.ts similarity index 100% rename from packages/dev/codemods/src/s1-to-s2/src/iconMap.ts rename to packages/dev/codemods/src/s1-to-s2/src/codemods/icons/iconMap.ts diff --git a/packages/dev/codemods/src/s1-to-s2/src/codemods/colors.ts b/packages/dev/codemods/src/s1-to-s2/src/codemods/shared/colors.ts similarity index 100% rename from packages/dev/codemods/src/s1-to-s2/src/codemods/colors.ts rename to packages/dev/codemods/src/s1-to-s2/src/codemods/shared/colors.ts diff --git a/packages/dev/codemods/src/s1-to-s2/src/codemods/dimensions.ts b/packages/dev/codemods/src/s1-to-s2/src/codemods/shared/dimensions.ts similarity index 100% rename from packages/dev/codemods/src/s1-to-s2/src/codemods/dimensions.ts rename to packages/dev/codemods/src/s1-to-s2/src/codemods/shared/dimensions.ts diff --git a/packages/dev/codemods/src/s1-to-s2/src/codemods/styleProps.ts b/packages/dev/codemods/src/s1-to-s2/src/codemods/shared/styleProps.ts similarity index 99% rename from packages/dev/codemods/src/s1-to-s2/src/codemods/styleProps.ts rename to packages/dev/codemods/src/s1-to-s2/src/codemods/shared/styleProps.ts index 0752350b768..cbfaa937139 100644 --- a/packages/dev/codemods/src/s1-to-s2/src/codemods/styleProps.ts +++ b/packages/dev/codemods/src/s1-to-s2/src/codemods/shared/styleProps.ts @@ -3,7 +3,7 @@ import {convertColor} from './colors'; import {convertDimension, convertGridTrack} from './dimensions'; import {NodePath} from '@babel/traverse'; import * as t from '@babel/types'; -import {transformUnsafeStyle} from './unsafeStyle'; +import transformUnsafeStyle from './unsafeStyle'; export const borderWidths = { none: 0, @@ -591,7 +591,7 @@ function expandSpaceShorthand( } } -export function transformStyleProps(path: NodePath, element: string) { +export default function transformStyleProps(path: NodePath, element: string) { let macroValues = new Map; let dynamicValues = new Map; let conditions = new Map; diff --git a/packages/dev/codemods/src/s1-to-s2/src/codemods/shared/transforms.ts b/packages/dev/codemods/src/s1-to-s2/src/codemods/shared/transforms.ts new file mode 100644 index 00000000000..d9a07471eb9 --- /dev/null +++ b/packages/dev/codemods/src/s1-to-s2/src/codemods/shared/transforms.ts @@ -0,0 +1,794 @@ +import {addComment, addComponentImport, getName} from './utils'; +import {convertDimension} from './dimensions'; +import {getComponents} from '../../getComponents'; +import {NodePath} from '@babel/traverse'; +import type {ReactNode} from 'react'; +import * as t from '@babel/types'; + +let availableComponents = getComponents(); + +/** + * Update prop name and value to new prop name and value. + * + * Example: + * - Button: Change variant="cta" to variant="accent". + * - Link: Change `variant="overBackground"` to `staticColor="white"`. + */ +export function updatePropNameAndValue( + path: NodePath, + options: { + /** Prop name to replace. */ + oldPropName: string, + /** Prop value to replace. */ + oldPropValue: ReactNode, + /** Updated prop name. */ + newPropName: string, + /** Updated prop value. */ + newPropValue: ReactNode + } +) { + const {oldPropName, oldPropValue, newPropName, newPropValue} = options; + + let attrPath = path.get('openingElement').get('attributes').find((attr) => t.isJSXAttribute(attr.node) && attr.node.name.name === oldPropName) as NodePath; + if (attrPath && t.isJSXAttribute(attrPath.node) && attrPath.node.name.name === oldPropName) { + if ( + t.isStringLiteral(attrPath.node.value) && + attrPath.node.value.value === oldPropValue + ) { + // Update old prop name to new prop name + attrPath.node.name.name = newPropName; + + // If prop value is a string and matches the old value, replace it with the new value + if (typeof newPropValue === 'string') { + attrPath.node.value = t.stringLiteral(newPropValue); + } else if (typeof newPropValue === 'boolean') { + if (!newPropValue) { + attrPath.node.value = t.jsxExpressionContainer(t.booleanLiteral(newPropValue)); + } else { + attrPath.node.value = null; + } + } + } else if (t.isJSXExpressionContainer(attrPath.node.value)) { + if (t.isIdentifier(attrPath.node.value.expression)) { + // @ts-ignore + if (attrPath.node.comments && [...attrPath.node.comments].some((comment) => comment.value.includes('could not be automatically'))) { + return; + } + addComment(attrPath.node, ` TODO(S2-upgrade): Prop ${oldPropName} could not be automatically updated because ${attrPath.node.value.expression.name} could not be followed.`); + } else { + // If prop value is an expression, traverse to find a string literal that matches the old and replace it with the new value + attrPath.traverse({ + StringLiteral(stringPath) { + if ( + t.isStringLiteral(stringPath.node) && + stringPath.node.value === oldPropValue + ) { + // Update old prop name to new prop name + attrPath.node.name.name = newPropName; + + if (typeof newPropValue === 'string') { + stringPath.replaceWith(t.stringLiteral(newPropValue)); + } else if (typeof newPropValue === 'boolean') { + if (!newPropValue) { + stringPath.replaceWith(t.booleanLiteral(newPropValue)); + } else { + attrPath.node.value = null; + } + } + } + } + }); + } + } + } +} + +/** + * Updates a prop name and value to new prop name and value, and adds an additional prop. + * + * Example: + * - Button: Change `variant="overBackground"` to `variant="primary" staticColor="white"`. + */ +export function updatePropValueAndAddNewPropName( + path: NodePath, + options: { + /** Prop name to replace. */ + oldPropName: string, + /** Prop value to replace. */ + oldPropValue: ReactNode, + /** Updated prop name. */ + newPropName: string, + /** Updated prop value. */ + newPropValue: ReactNode, + /** Additional new prop name to add. */ + additionalPropName: string, + /** Additional new prop value to use. */ + additionalPropValue: string + } +) { + const { + oldPropName, + oldPropValue, + newPropName, + newPropValue, + additionalPropName, + additionalPropValue + } = options; + + let attrPath = path.get('openingElement').get('attributes').find((attr) => t.isJSXAttribute(attr.node) && attr.node.name.name === oldPropName) as NodePath; + if (attrPath && t.isStringLiteral(attrPath.node.value) && attrPath.node.value.value === oldPropValue) { + // Update old prop name to new prop name + attrPath.node.name.name = newPropName; + + // If prop value is a string and matches the old value, replace it with the new value + if (typeof newPropValue === 'string') { + attrPath.node.value = t.stringLiteral(newPropValue); + } else if (typeof newPropValue === 'boolean') { + attrPath.node.value = t.jsxExpressionContainer(t.booleanLiteral(newPropValue)); + } + + if (additionalPropName && additionalPropValue) { + attrPath.insertAfter( + t.jsxAttribute(t.jsxIdentifier(additionalPropName), t.stringLiteral(additionalPropValue as string)) + ); + } + } else if (attrPath && t.isJSXExpressionContainer(attrPath.node.value)) { + // If prop value is an expression, traverse to find a string literal that matches the old and replace it with the new value + attrPath.traverse({ + StringLiteral(stringPath) { + if ( + t.isStringLiteral(stringPath.node) && + stringPath.node.value === oldPropValue + ) { + // Update old prop name to new prop name + attrPath.node.name.name = newPropName; + + if (typeof newPropValue === 'string') { + stringPath.replaceWith(t.stringLiteral(newPropValue)); + } else if (typeof newPropValue === 'boolean') { + stringPath.replaceWith(t.booleanLiteral(newPropValue)); + } + + if (additionalPropName && additionalPropValue) { + attrPath.insertAfter( + t.jsxAttribute(t.jsxIdentifier(additionalPropName), t.stringLiteral(additionalPropValue as string)) + ); + } + } + } + }); + } +} + +/** + * Updates a prop name to new prop name. + * + * Example: + * - Button: Change style to fillStyle. + */ +export function updatePropName( + path: NodePath, + options: { + /** Prop name to replace. */ + oldPropName: string, + /** Updated prop name. */ + newPropName: string + } +) { + const {oldPropName, newPropName} = options; + + let attrPath = path.get('openingElement').get('attributes').find((attr) => t.isJSXAttribute(attr.node) && attr.node.name.name === oldPropName) as NodePath; + if (attrPath && t.isJSXAttribute(attrPath.node) && attrPath.node.name.name === oldPropName) { + attrPath.node.name.name = newPropName; + } +} + +/** + * Removes a prop. + * + * Example: + * - Button: Remove isQuiet (it is no longer supported). + */ +export function removeProp( + path: NodePath, + options: { + /** Prop name to remove. */ + propName: string, + /** If provided, prop will only be removed if set to this value. */ + propValue?: string + } +) { + const {propName, propValue} = options; + + let attrPath = path.get('openingElement').get('attributes').find((attr) => t.isJSXAttribute(attr.node) && attr.node.name.name === propName) as NodePath; + if (attrPath && t.isJSXAttribute(attrPath.node) && attrPath.node.name.name === propName) { + if (propValue) { + // If prop value is provided, remove prop only if it matches the value + if (t.isStringLiteral(attrPath.node.value) && attrPath.node.value.value === propValue) { + attrPath.remove(); + } else if ( + t.isJSXExpressionContainer(attrPath.node.value) + ) { + if (t.isIdentifier(attrPath.node.value.expression)) { + // @ts-ignore + // eslint-disable-next-line max-depth + if (attrPath.node.comments && [...attrPath.node.comments].some((comment) => comment.value.includes('could not be automatically'))) { + return; + } + addComment(attrPath.node, ` TODO(S2-upgrade): Prop ${propName} could not be automatically removed because ${attrPath.node.value.expression.name} could not be followed.`); + } else { + attrPath.traverse({ + StringLiteral(stringPath) { + if ( + t.isStringLiteral(stringPath.node) && + stringPath.node.value === propValue + ) { + // Invalid prop value was found inside expression. + addComment(attrPath.node, ` TODO(S2-upgrade): ${propName}="${propValue}" is no longer supported. You'll need to update this manually.`); + } + } + }); + } + } + } else { + // No prop value provided, so remove prop regardless of value + attrPath.remove(); + } + } +} + +/** + * Comments out a prop. + * + * Example: + * - Button: Comment out isPending (it has not been implemented yet). + */ +export function commentOutProp( + path: NodePath, + options: { + /** Prop to comment out. */ + propName: string, + /** If provided, prop will only be commented out if set to this value. */ + propValue?: string + } +) { + const {propName, propValue} = options; + + let attrPath = path.get('openingElement').get('attributes').find((attr) => t.isJSXAttribute(attr.node) && attr.node.name.name === propName) as NodePath; + if (attrPath && t.isJSXAttribute(attrPath.node) && attrPath.node.name.name === propName) { + if (propValue) { + // If prop value is provided, comment out prop only if it matches the value + if (t.isStringLiteral(attrPath.node.value) && attrPath.node.value.value === propValue) { + addComment(attrPath.parentPath.node, ` TODO(S2-upgrade): ${propName}="${propValue}" has not been implemented yet.`); + attrPath.remove(); + } else { + attrPath.traverse({ + StringLiteral(stringPath) { + if ( + t.isStringLiteral(stringPath.node) && + stringPath.node.value === propValue + ) { + addComment(attrPath.parentPath.node, ` TODO(S2-upgrade): ${propName}="${propValue}" has not been implemented yet.`); + attrPath.remove(); + } + } + }); + } + } else { + addComment(attrPath.parentPath.node, ` TODO(S2-upgrade): ${propName} has not been implemented yet.`); + attrPath.remove(); + } + } +} + +/** + * Add a comment above an element. + * + * Example: + * - Breadcrumbs: Check if nav needs to be added around Bre. + */ +export function addCommentToElement( + path: NodePath, + options: { + /** Comment to leave. */ + comment: string + } +) { + const {comment} = options; + addComment(path.node, ` TODO(S2-upgrade): ${comment}`); +} + +/** + * If a prop is present, updates to use a new component. + * + * Example: + * - Button: If `href` is present, Button should be converted to `LinkButton`. + */ +export function updateComponentIfPropPresent( + path: NodePath, + options: { + /** Updated component to use. */ + newComponentName: string, + /** Will update component if this prop is present. */ + propName: string + } +) { + const {newComponentName, propName} = options; + + let attrPath = path.get('openingElement').get('attributes').find((attr) => t.isJSXAttribute(attr.node) && attr.node.name.name === propName) as NodePath; + if (attrPath && t.isJSXAttribute(attrPath.node) && attrPath.node.name.name === propName) { + let node = attrPath.findParent((p) => t.isJSXElement(p.node))?.node; + if (node && t.isJSXElement(node)) { + let localName = newComponentName; + if (availableComponents.has(newComponentName)) { + let program = path.findParent((p) => t.isProgram(p.node)) as NodePath; + localName = addComponentImport(program, newComponentName); + } + node.openingElement.name = t.jsxIdentifier(localName); + if (node.closingElement) { + node.closingElement.name = t.jsxIdentifier(localName); + } + } + } +} + + +/** + * Remove render props, and move usage to a child component. + * + * Example: + * - DialogTrigger: Update children to remove render props usage, and note that `close` export function moved from `DialogTrigger` to `Dialog`. + */ +export function moveRenderPropsToChild( + path: NodePath, + options: { + newChildComponentName: string + } +) { + const {newChildComponentName} = options; + + const renderFunctionIndex = path.node.children.findIndex( + (child) => + t.isJSXExpressionContainer(child) && + t.isArrowFunctionExpression(child.expression) && + t.isJSXElement(child.expression.body) && + t.isJSXIdentifier(child.expression.body.openingElement.name)); + + const renderFunction = path.node.children[renderFunctionIndex] as t.JSXExpressionContainer; + + if ( + t.isJSXExpressionContainer(renderFunction) && + t.isArrowFunctionExpression(renderFunction.expression) && + t.isJSXElement(renderFunction.expression.body) && + t.isJSXIdentifier(renderFunction.expression.body.openingElement.name) && + getName(path, renderFunction.expression.body.openingElement.name) !== newChildComponentName + ) { + addComment(renderFunction, ' TODO(S2-upgrade): update this dialog to move the close function inside'); + return; + } + + if ( + renderFunction && + t.isArrowFunctionExpression(renderFunction.expression) && + t.isJSXElement(renderFunction.expression.body) + ) { + const dialogElement = renderFunction.expression.body; + + const originalParam = renderFunction.expression.params[0]; + if (!t.isIdentifier(originalParam)) { + addComment(path.node.children[renderFunctionIndex], ' TODO(S2-upgrade): Could not automatically move the render props. You\'ll need to update this manually.'); + return; + } + const paramName = originalParam.name; + const objectPattern = t.objectPattern([ + t.objectProperty(t.identifier(paramName), + t.identifier(paramName), + false, + true + ) + ]); + + const newRenderFunction = t.jsxExpressionContainer( + t.arrowFunctionExpression( + [objectPattern], + t.jsxFragment( + t.jsxOpeningFragment(), + t.jsxClosingFragment(), + dialogElement.children + ) + ) + ); + + let removedOnDismiss = false; + const attributes = dialogElement.openingElement.attributes.filter((attr) => { + if (t.isJSXAttribute(attr) && attr.name.name === 'onDismiss') { + removedOnDismiss = true; + return false; + } + return true; + }); + + path.node.children[renderFunctionIndex] = t.jsxElement( + t.jsxOpeningElement(t.jsxIdentifier(newChildComponentName), attributes), + t.jsxClosingElement(t.jsxIdentifier(newChildComponentName)), + [newRenderFunction] + ); + + if (removedOnDismiss) { + addComment(path.node.children[renderFunctionIndex], ' onDismiss was removed from Dialog. Use onOpenChange on the DialogTrigger, or onDismiss on the DialogContainer instead'); + } + } +} + + +/** + * If within a collection component, updates to use a new component. + * + * Example: + * - Item: If within `Menu`, update name from `Item` to `MenuItem`. + */ +export function updateComponentWithinCollection( + path: NodePath, + options: { + parentComponentName: string, + newComponentName: string + } +) { + const {parentComponentName, newComponentName} = options; + + // Collections currently implemented + // TODO: Add 'ActionGroup', 'ListBox', 'ListView' once implemented + const collectionItemParents = new Set(['Menu', 'ActionMenu', 'TagGroup', 'Breadcrumbs', 'Picker', 'ComboBox', 'ListBox', 'TabList', 'TabPanels', 'Collection']); + + if ( + t.isJSXElement(path.node) && + t.isJSXIdentifier(path.node.openingElement.name) + ) { + // Find closest parent collection component + let closestParentCollection = path.findParent((p) => + t.isJSXElement(p.node) && + t.isJSXIdentifier(p.node.openingElement.name) && + collectionItemParents.has(getName(path, p.node.openingElement.name)) + ); + if ( + closestParentCollection && + t.isJSXElement(closestParentCollection.node) && + t.isJSXIdentifier(closestParentCollection.node.openingElement.name) && + getName(path, closestParentCollection.node.openingElement.name) === parentComponentName + ) { + // If closest parent collection component matches parentComponentName, replace with newComponentName + + updateKeyToId(path); + + let localName = newComponentName; + if (availableComponents.has(newComponentName)) { + let program = path.findParent((p) => t.isProgram(p.node)) as NodePath; + localName = addComponentImport(program, newComponentName); + } + + let newNode = t.jsxElement( + t.jsxOpeningElement(t.jsxIdentifier(localName), path.node.openingElement.attributes), + t.jsxClosingElement(t.jsxIdentifier(localName)), + path.node.children + ); + path.replaceWith(newNode); + } + } +} + +/** + * If no parent collection detected, leave a comment for the user to check manually. + * + * Example: If they're declaring declaring Items somewhere above the collection. + */ +export function commentIfParentCollectionNotDetected( + path: NodePath +) { + const collectionItemParents = new Set(['Menu', 'ActionMenu', 'TagGroup', 'Breadcrumbs', 'Picker', 'ComboBox', 'ListBox', 'TabList', 'TabPanels', 'ActionGroup', 'ActionButtonGroup', 'ToggleButtonGroup', 'ListBox', 'ListView', 'Collection', 'SearchAutocomplete', 'Accordion', 'ActionBar', 'StepList']); + if ( + t.isJSXElement(path.node) + ) { + // Find closest parent collection component + let closestParentCollection = path.findParent((p) => + t.isJSXElement(p.node) && + t.isJSXIdentifier(p.node.openingElement.name) && + collectionItemParents.has(getName(path, p.node.openingElement.name)) + ); + if (!closestParentCollection) { + // If we couldn't find a parent collection parent, leave a comment for them to check manually + addComment(path.node, ' TODO(S2-upgrade): Couldn\'t automatically detect what type of collection component this is rendered in. You\'ll need to update this manually.'); + } + } +} + +/** + * If within a component, moves prop to new child component. + * + * Example: + * - Section: If within `Menu`, move `title` prop string to be a child of new `Heading` within a `Header`. + */ +export function movePropToNewChildComponentName( + path: NodePath, + options: { + parentComponentName: string, + childComponentName: string, + propName: string, + newChildComponentName: string + } +) { + const {parentComponentName, childComponentName, propName, newChildComponentName} = + options; + + if ( + t.isJSXElement(path.node) && + t.isJSXElement(path.parentPath.node) && + t.isJSXIdentifier(path.node.openingElement.name) && + t.isJSXIdentifier(path.parentPath.node.openingElement.name) && + getName(path, path.node.openingElement.name) === childComponentName && + getName(path, path.parentPath.node.openingElement.name) === parentComponentName + ) { + let propValue: t.JSXAttribute['value'] | void; + path.node.openingElement.attributes = + path.node.openingElement.attributes.filter((attr) => { + if (t.isJSXAttribute(attr) && attr.name.name === propName) { + propValue = attr.value; + return false; + } + return true; + }); + + if (propValue) { + path.node.children.unshift( + t.jsxElement( + t.jsxOpeningElement(t.jsxIdentifier(newChildComponentName), []), + t.jsxClosingElement(t.jsxIdentifier(newChildComponentName)), + [t.isStringLiteral(propValue) ? t.jsxText(propValue.value) : propValue] + ) + ); + // TODO: handle dynamic collections. Need to wrap export function child in and move `items` prop down. + } + } +} + +/** + * Updates prop to be on parent component. + * + * Example: + * - Tooltip: Remove `placement` and add to the parent `TooltipTrigger` instead. + */ +export function movePropToParentComponent( + path: NodePath, + options: { + parentComponentName: string, + childComponentName: string, + propName: string + } +) { + const {parentComponentName, childComponentName, propName} = options; + + path.traverse({ + JSXAttribute(attributePath) { + if ( + t.isJSXElement(path.parentPath.node) && + t.isJSXIdentifier(path.node.openingElement.name) && + t.isJSXIdentifier(path.parentPath.node.openingElement.name) && + attributePath.node.name.name === propName && + getName(path, path.node.openingElement.name) === childComponentName && + getName(path, path.parentPath.node.openingElement.name) === parentComponentName + ) { + path.parentPath.node.openingElement.attributes.push( + t.jsxAttribute(t.jsxIdentifier(propName), attributePath.node.value) + ); + attributePath.remove(); + } + } + }); +} + +/** + * Update to use a new component. + * + * Example: + * - Flex: Update `Flex` to be a `div` and apply flex styles using the style macro. + */ +export function updateToNewComponentName( + path: NodePath, + options: { + newComponentName: string + } +) { + const {newComponentName} = options; + + let localName = newComponentName; + if (availableComponents.has(newComponentName)) { + let program = path.findParent((p) => t.isProgram(p.node)) as NodePath; + localName = addComponentImport(program, newComponentName); + } + + path.node.openingElement.name = t.jsxIdentifier(localName); + if (path.node.closingElement) { + path.node.closingElement.name = t.jsxIdentifier(localName); + } +} + +const conversions = { + 'cm': 37.8, + 'mm': 3.78, + 'in': 96, + 'pt': 1.33, + 'pc': 16 +}; + +/** + * Updates prop to use pixel value instead. + * + * Example: + * - ComboBox: Update `menuWidth` to a pixel value. + */ +export function convertDimensionValueToPx( + path: NodePath, + options: { + propName: string + } +) { + const {propName} = options; + + let attrPath = path.get('openingElement').get('attributes').find((attr) => t.isJSXAttribute(attr.node) && attr.node.name.name === propName) as NodePath; + if (attrPath && t.isJSXAttribute(attrPath.node) && attrPath.node.name.name === propName) { + if (t.isStringLiteral(attrPath.node.value)) { + try { + let value = convertDimension(attrPath.node.value.value, 'size'); + if (value && typeof value === 'number') { + attrPath.node.value = t.jsxExpressionContainer(t.numericLiteral(value)); + } else if (value && typeof value === 'string') { + // eslint-disable-next-line max-depth + if ((/%|vw|vh|auto|ex|ch|rem|vmin|vmax/).test(value)) { + addComment(attrPath.node, ' TODO(S2-upgrade): Unable to convert CSS unit to a pixel value'); + } else if ((/cm|mm|in|pt|pc/).test(value)) { + let unit = value.replace(/\[|\]|\d+/g, ''); + let conversion = conversions[unit as keyof typeof conversions]; + value = Number(value.replace(/\[|\]|cm|mm|in|pt|pc/g, '')); + // eslint-disable-next-line max-depth + if (!isNaN(value)) { + let pixelValue = Math.round(conversion * value); + attrPath.node.value = t.jsxExpressionContainer(t.numericLiteral(pixelValue)); + } + } else if ((/px/).test(value)) { + let pixelValue = Number(value.replace(/\[|\]|px/g, '')); + // eslint-disable-next-line max-depth + if (!isNaN(pixelValue)) { + attrPath.node.value = t.jsxExpressionContainer(t.numericLiteral(pixelValue)); + } + } + } + } catch (error) { + addComment(attrPath.node, ` TODO(S2-upgrade): Prop ${propName} could not be automatically updated due to error: ${error}`); + } + } else if (t.isJSXExpressionContainer(attrPath.node.value)) { + if (t.isIdentifier(attrPath.node.value.expression)) { + addComment(attrPath.node, ` TODO(S2-upgrade): Prop ${propName} could not be automatically updated because ${attrPath.node.value.expression.name} could not be followed.`); + } + } + } +} + +/** + * Updates double placement values to a single value. + * + * Example: + * - TooltipTrigger: Update `placement="bottom left"` to `placement="bottom"`. + */ +export function updatePlacementToSingleValue( + path: NodePath, + options: { + propToUpdateName: string, + /* If provided, updates the prop on the specified child component */ + childComponentName?: string + } +) { + const {propToUpdateName, childComponentName} = options; + + const doublePlacementValues = new Set([ + 'bottom left', + 'bottom right', + 'bottom start', + 'bottom end', + 'top left', + 'top right', + 'top start', + 'top end', + 'left top', + 'left bottom', + 'start top', + 'start bottom', + 'right top', + 'right bottom', + 'end top', + 'end bottom' + ]); + + let elementPath = childComponentName ? + path.get('children').find( + (child) => t.isJSXElement(child.node) && + t.isJSXIdentifier(child.node.openingElement.name) && + getName(path, child.node.openingElement.name) === childComponentName + ) as NodePath : path; + let attrPath = elementPath.get('openingElement').get('attributes').find((attr) => t.isJSXAttribute(attr.node) && attr.node.name.name === propToUpdateName) as NodePath; + if (attrPath && t.isJSXAttribute(attrPath.node) && attrPath.node.name.name === propToUpdateName) { + if (t.isStringLiteral(attrPath.node.value) && doublePlacementValues.has(attrPath.node.value.value)) { + attrPath.replaceWith(t.jsxAttribute(t.jsxIdentifier(propToUpdateName), t.stringLiteral(attrPath.node.value.value.split(' ')[0]))); + return; + } else if (t.isJSXExpressionContainer(attrPath.node.value)) { + attrPath.traverse({ + StringLiteral(stringPath) { + if ( + t.isStringLiteral(stringPath.node) && + doublePlacementValues.has(stringPath.node.value) + ) { + stringPath.replaceWith(t.stringLiteral(stringPath.node.value.split(' ')[0])); + return; + } + } + }); + } + } +} + +/** + * Remove component if within a parent component. + * + * Example: + * - Divider: Remove `Divider` if used within a `Dialog`. + */ +export function removeComponentIfWithinParent( + path: NodePath, + options: { + parentComponentName: string + } +) { + const {parentComponentName} = options; + if ( + t.isJSXElement(path.node) && + t.isJSXElement(path.parentPath.node) && + t.isJSXIdentifier(path.node.openingElement.name) && + t.isJSXIdentifier(path.parentPath.node.openingElement.name) && + getName(path, path.parentPath.node.openingElement.name) === parentComponentName + ) { + path.remove(); + } +} + +/** + * Updates the key prop to id. Keeps the key prop if it's used in an array.map function. + */ +export function updateKeyToId( + path: NodePath +) { + let attributes = path.node.openingElement.attributes; + let keyProp = attributes.find((attr) => t.isJSXAttribute(attr) && attr.name.name === 'key'); + if ( + keyProp && + t.isJSXAttribute(keyProp) + ) { + // Update key prop to be id + keyProp.name = t.jsxIdentifier('id'); + } + + if ( + t.isArrowFunctionExpression(path.parentPath.node) && + path.parentPath.parentPath && + t.isCallExpression(path.parentPath.parentPath.node) && + path.parentPath.parentPath.node.callee.type === 'MemberExpression' && + path.parentPath.parentPath.node.callee.property.type === 'Identifier' && + path.parentPath.parentPath.node.callee.property.name === 'map' + ) { + // If Array.map is used, keep the key prop + if ( + keyProp && + t.isJSXAttribute(keyProp) + ) { + let newKeyProp = t.jsxAttribute(t.jsxIdentifier('key'), keyProp.value); + attributes.push(newKeyProp); + } + } +} diff --git a/packages/dev/codemods/src/s1-to-s2/src/codemods/unsafeStyle.ts b/packages/dev/codemods/src/s1-to-s2/src/codemods/shared/unsafeStyle.ts similarity index 99% rename from packages/dev/codemods/src/s1-to-s2/src/codemods/unsafeStyle.ts rename to packages/dev/codemods/src/s1-to-s2/src/codemods/shared/unsafeStyle.ts index 83202fa37a2..8be657d42b0 100644 --- a/packages/dev/codemods/src/s1-to-s2/src/codemods/unsafeStyle.ts +++ b/packages/dev/codemods/src/s1-to-s2/src/codemods/shared/unsafeStyle.ts @@ -4,7 +4,7 @@ import {convertUnsafeDimension} from './dimensions'; import {convertUnsafeStyleColor} from './colors'; import * as t from '@babel/types'; -export function transformUnsafeStyle( +export default function transformUnsafeStyle( value: t.ObjectExpression, element: string ): StylePropValue | null { diff --git a/packages/dev/codemods/src/s1-to-s2/src/codemods/utils.ts b/packages/dev/codemods/src/s1-to-s2/src/codemods/shared/utils.ts similarity index 91% rename from packages/dev/codemods/src/s1-to-s2/src/codemods/utils.ts rename to packages/dev/codemods/src/s1-to-s2/src/codemods/shared/utils.ts index 88365a5eefe..f3425a7f715 100644 --- a/packages/dev/codemods/src/s1-to-s2/src/codemods/utils.ts +++ b/packages/dev/codemods/src/s1-to-s2/src/codemods/shared/utils.ts @@ -73,15 +73,15 @@ export function addComment(node: any, comment: string) { }); } -export function addComponentImport(path: NodePath, newComponent: string) { - // If newComponent variable already exists in scope, alias new import to avoid conflict. - let existingBinding = path.scope.getBinding(newComponent); - let localName = newComponent; +export function addComponentImport(path: NodePath, newComponentName: string) { + // If newComponentName variable already exists in scope, alias new import to avoid conflict. + let existingBinding = path.scope.getBinding(newComponentName); + let localName = newComponentName; if (existingBinding) { - let newName = newComponent; + let newName = newComponentName; let i = 1; while (path.scope.hasBinding(newName)) { - newName = newComponent + i; + newName = newComponentName + i; i++; } localName = newName; @@ -93,7 +93,7 @@ export function addComponentImport(path: NodePath, newComponent: stri return ( t.isImportSpecifier(specifier) && specifier.imported.type === 'Identifier' && - specifier.imported.name === newComponent + specifier.imported.name === newComponentName ); }); if (specifier) { @@ -101,14 +101,14 @@ export function addComponentImport(path: NodePath, newComponent: stri return localName; } existingImport.specifiers.push( - t.importSpecifier(t.identifier(localName), t.identifier(newComponent)) + t.importSpecifier(t.identifier(localName), t.identifier(newComponentName)) ); } else { let importDeclaration = t.importDeclaration( [ t.importSpecifier( t.identifier(localName), - t.identifier(newComponent) + t.identifier(newComponentName) ) ], t.stringLiteral('@react-spectrum/s2') diff --git a/packages/dev/codemods/src/s1-to-s2/src/codemods/transforms.ts b/packages/dev/codemods/src/s1-to-s2/src/codemods/transforms.ts deleted file mode 100644 index a8cd465bb0a..00000000000 --- a/packages/dev/codemods/src/s1-to-s2/src/codemods/transforms.ts +++ /dev/null @@ -1,1446 +0,0 @@ -import {addComment, addComponentImport, getName, removeComponentImport} from './utils'; -import {convertDimension} from './dimensions'; -import {getComponents} from '../getComponents'; -import {NodePath} from '@babel/traverse'; -import type {ReactNode} from 'react'; -import * as t from '@babel/types'; - -let availableComponents = getComponents(); - -export interface UpdatePropNameAndValueOptions { - /** Prop name to replace. */ - oldProp: string, - /** Prop value to replace. */ - oldValue: ReactNode, - /** Updated prop name. */ - newProp: string, - /** Updated prop value. */ - newValue: ReactNode -} - -/** - * Update prop name and value to new prop name and value. - * - * Example: - * - Button: Change variant="cta" to variant="accent". - * - Link: Change `variant="overBackground"` to `staticColor="white"`. - */ -function updatePropNameAndValue( - path: NodePath, - options: UpdatePropNameAndValueOptions -) { - const {oldProp, oldValue, newProp, newValue} = options; - - let attrPath = path.get('openingElement').get('attributes').find((attr) => t.isJSXAttribute(attr.node) && attr.node.name.name === oldProp) as NodePath; - if (attrPath && t.isJSXAttribute(attrPath.node) && attrPath.node.name.name === oldProp) { - if ( - t.isStringLiteral(attrPath.node.value) && - attrPath.node.value.value === oldValue - ) { - // Update old prop name to new prop name - attrPath.node.name.name = newProp; - - // If prop value is a string and matches the old value, replace it with the new value - if (typeof newValue === 'string') { - attrPath.node.value = t.stringLiteral(newValue); - } else if (typeof newValue === 'boolean') { - if (!newValue) { - attrPath.node.value = t.jsxExpressionContainer(t.booleanLiteral(newValue)); - } else { - attrPath.node.value = null; - } - } - } else if (t.isJSXExpressionContainer(attrPath.node.value)) { - if (t.isIdentifier(attrPath.node.value.expression)) { - // @ts-ignore - if (attrPath.node.comments && [...attrPath.node.comments].some((comment) => comment.value.includes('could not be automatically'))) { - return; - } - addComment(attrPath.node, ` TODO(S2-upgrade): Prop ${oldProp} could not be automatically updated because ${attrPath.node.value.expression.name} could not be followed.`); - } else { - // If prop value is an expression, traverse to find a string literal that matches the old and replace it with the new value - attrPath.traverse({ - StringLiteral(stringPath) { - if ( - t.isStringLiteral(stringPath.node) && - stringPath.node.value === oldValue - ) { - // Update old prop name to new prop name - attrPath.node.name.name = newProp; - - if (typeof newValue === 'string') { - stringPath.replaceWith(t.stringLiteral(newValue)); - } else if (typeof newValue === 'boolean') { - if (!newValue) { - stringPath.replaceWith(t.booleanLiteral(newValue)); - } else { - attrPath.node.value = null; - } - } - } - } - }); - } - } - } -} - -export interface UpdatePropValueAndAddNewPropOptions { - /** Prop name to replace. */ - oldProp: string, - /** Prop value to replace. */ - oldValue: ReactNode, - /** Updated prop name. */ - newProp: string, - /** Updated prop value. */ - newValue: ReactNode, - /** Additional new prop name to add. */ - additionalProp: string, - /** Additional new prop value to use. */ - additionalValue: string -} - -/** - * Updates a prop name and value to new prop name and value, and adds an additional prop. - * - * Example: - * - Button: Change `variant="overBackground"` to `variant="primary" staticColor="white"`. - */ -function updatePropValueAndAddNewProp( - path: NodePath, - options: UpdatePropValueAndAddNewPropOptions -) { - const { - oldProp, - oldValue, - newProp, - newValue, - additionalProp, - additionalValue - } = options; - - let attrPath = path.get('openingElement').get('attributes').find((attr) => t.isJSXAttribute(attr.node) && attr.node.name.name === oldProp) as NodePath; - if (attrPath && t.isStringLiteral(attrPath.node.value) && attrPath.node.value.value === oldValue) { - // Update old prop name to new prop name - attrPath.node.name.name = newProp; - - // If prop value is a string and matches the old value, replace it with the new value - if (typeof newValue === 'string') { - attrPath.node.value = t.stringLiteral(newValue); - } else if (typeof newValue === 'boolean') { - attrPath.node.value = t.jsxExpressionContainer(t.booleanLiteral(newValue)); - } - - if (additionalProp && additionalValue) { - attrPath.insertAfter( - t.jsxAttribute(t.jsxIdentifier(additionalProp), t.stringLiteral(additionalValue as string)) - ); - } - } else if (attrPath && t.isJSXExpressionContainer(attrPath.node.value)) { - // If prop value is an expression, traverse to find a string literal that matches the old and replace it with the new value - attrPath.traverse({ - StringLiteral(stringPath) { - if ( - t.isStringLiteral(stringPath.node) && - stringPath.node.value === oldValue - ) { - // Update old prop name to new prop name - attrPath.node.name.name = newProp; - - if (typeof newValue === 'string') { - stringPath.replaceWith(t.stringLiteral(newValue)); - } else if (typeof newValue === 'boolean') { - stringPath.replaceWith(t.booleanLiteral(newValue)); - } - - if (additionalProp && additionalValue) { - attrPath.insertAfter( - t.jsxAttribute(t.jsxIdentifier(additionalProp), t.stringLiteral(additionalValue as string)) - ); - } - } - } - }); - } -} - -export interface UpdatePropNameOptions { - /** Prop name to replace. */ - oldProp: string, - /** Updated prop name. */ - newProp: string -} - -/** - * Updates a prop name to new prop name. - * - * Example: - * - Button: Change style to fillStyle. - */ -function updatePropName( - path: NodePath, - options: UpdatePropNameOptions -) { - const {oldProp, newProp} = options; - - let attrPath = path.get('openingElement').get('attributes').find((attr) => t.isJSXAttribute(attr.node) && attr.node.name.name === oldProp) as NodePath; - if (attrPath && t.isJSXAttribute(attrPath.node) && attrPath.node.name.name === oldProp) { - attrPath.node.name.name = newProp; - } -} - -export interface RemovePropOptions { - /** Prop name to remove. */ - propToRemove: string, - /** If provided, prop will only be removed if set to this value. */ - propValue?: string -} - -/** - * Removes a prop. - * - * Example: - * - Button: Remove isQuiet (it is no longer supported). - */ -function removeProp(path: NodePath, options: RemovePropOptions) { - const {propToRemove, propValue} = options; - - let attrPath = path.get('openingElement').get('attributes').find((attr) => t.isJSXAttribute(attr.node) && attr.node.name.name === propToRemove) as NodePath; - if (attrPath && t.isJSXAttribute(attrPath.node) && attrPath.node.name.name === propToRemove) { - if (propValue) { - // If prop value is provided, remove prop only if it matches the value - if (t.isStringLiteral(attrPath.node.value) && attrPath.node.value.value === propValue) { - attrPath.remove(); - } else if ( - t.isJSXExpressionContainer(attrPath.node.value) - ) { - if (t.isIdentifier(attrPath.node.value.expression)) { - // @ts-ignore - // eslint-disable-next-line max-depth - if (attrPath.node.comments && [...attrPath.node.comments].some((comment) => comment.value.includes('could not be automatically'))) { - return; - } - addComment(attrPath.node, ` TODO(S2-upgrade): Prop ${propToRemove} could not be automatically removed because ${attrPath.node.value.expression.name} could not be followed.`); - } else { - attrPath.traverse({ - StringLiteral(stringPath) { - if ( - t.isStringLiteral(stringPath.node) && - stringPath.node.value === propValue - ) { - // Invalid prop value was found inside expression. - addComment(attrPath.node, ` TODO(S2-upgrade): ${propToRemove}="${propValue}" is no longer supported. You'll need to update this manually.`); - } - } - }); - } - } - } else { - // No prop value provided, so remove prop regardless of value - attrPath.remove(); - } - } -} - -export interface CommentOutPropOptions { - /** Prop to comment out. */ - propToComment: string, - /** If provided, prop will only be commented out if set to this value. */ - propValue?: string -} - -/** - * Comments out a prop. - * - * Example: - * - Button: Comment out isPending (it has not been implemented yet). - */ -function commentOutProp( - path: NodePath, - options: CommentOutPropOptions -) { - const {propToComment, propValue} = options; - - let attrPath = path.get('openingElement').get('attributes').find((attr) => t.isJSXAttribute(attr.node) && attr.node.name.name === propToComment) as NodePath; - if (attrPath && t.isJSXAttribute(attrPath.node) && attrPath.node.name.name === propToComment) { - if (propValue) { - // If prop value is provided, comment out prop only if it matches the value - if (t.isStringLiteral(attrPath.node.value) && attrPath.node.value.value === propValue) { - addComment(attrPath.parentPath.node, ` TODO(S2-upgrade): ${propToComment}="${propValue}" has not been implemented yet.`); - attrPath.remove(); - } else { - attrPath.traverse({ - StringLiteral(stringPath) { - if ( - t.isStringLiteral(stringPath.node) && - stringPath.node.value === propValue - ) { - addComment(attrPath.parentPath.node, ` TODO(S2-upgrade): ${propToComment}="${propValue}" has not been implemented yet.`); - attrPath.remove(); - } - } - }); - } - } else { - addComment(attrPath.parentPath.node, ` TODO(S2-upgrade): ${propToComment} has not been implemented yet.`); - attrPath.remove(); - } - } -} - -export interface AddCommentToElementOptions { - /** Comment to leave. */ - comment: string -} - -/** - * Add a comment above an element. - * - * Example: - * - Breadcrumbs: Check if nav needs to be added around Bre. - */ -function addCommentToElement( - path: NodePath, - options: AddCommentToElementOptions -) { - const {comment} = options; - addComment(path.node, ` TODO(S2-upgrade): ${comment}`); -} - -export interface UpdateComponentIfPropPresentOptions { - /** Updated component to use. */ - newComponent: string, - /** Will update component if this prop is present. */ - propToCheck: string -} - -/** - * If a prop is present, updates to use a new component. - * - * Example: - * - Button: If `href` is present, Button should be converted to `LinkButton`. - */ -function updateComponentIfPropPresent( - path: NodePath, - options: UpdateComponentIfPropPresentOptions -) { - const {newComponent, propToCheck} = options; - - let attrPath = path.get('openingElement').get('attributes').find((attr) => t.isJSXAttribute(attr.node) && attr.node.name.name === propToCheck) as NodePath; - if (attrPath && t.isJSXAttribute(attrPath.node) && attrPath.node.name.name === propToCheck) { - let node = attrPath.findParent((p) => t.isJSXElement(p.node))?.node; - if (node && t.isJSXElement(node)) { - let localName = newComponent; - if (availableComponents.has(newComponent)) { - let program = path.findParent((p) => t.isProgram(p.node)) as NodePath; - localName = addComponentImport(program, newComponent); - } - node.openingElement.name = t.jsxIdentifier(localName); - if (node.closingElement) { - node.closingElement.name = t.jsxIdentifier(localName); - } - } - } -} - -export interface MoveRenderPropsOptions { - newChildComponent: string -} - -/** - * Remove render props, and move usage to a child component. - * - * Example: - * - DialogTrigger: Update children to remove render props usage, and note that `close` function moved from `DialogTrigger` to `Dialog`. - */ -function moveRenderPropsToChild( - path: NodePath, - options: MoveRenderPropsOptions -) { - const {newChildComponent} = options; - - const renderFunctionIndex = path.node.children.findIndex( - (child) => - t.isJSXExpressionContainer(child) && - t.isArrowFunctionExpression(child.expression) && - t.isJSXElement(child.expression.body) && - t.isJSXIdentifier(child.expression.body.openingElement.name)); - - const renderFunction = path.node.children[renderFunctionIndex] as t.JSXExpressionContainer; - - if ( - t.isJSXExpressionContainer(renderFunction) && - t.isArrowFunctionExpression(renderFunction.expression) && - t.isJSXElement(renderFunction.expression.body) && - t.isJSXIdentifier(renderFunction.expression.body.openingElement.name) && - getName(path, renderFunction.expression.body.openingElement.name) !== newChildComponent - ) { - addComment(renderFunction, ' TODO(S2-upgrade): update this dialog to move the close function inside'); - return; - } - - if ( - renderFunction && - t.isArrowFunctionExpression(renderFunction.expression) && - t.isJSXElement(renderFunction.expression.body) - ) { - const dialogElement = renderFunction.expression.body; - - const originalParam = renderFunction.expression.params[0]; - if (!t.isIdentifier(originalParam)) { - addComment(path.node.children[renderFunctionIndex], ' TODO(S2-upgrade): Could not automatically move the render props. You\'ll need to update this manually.'); - return; - } - const paramName = originalParam.name; - const objectPattern = t.objectPattern([ - t.objectProperty(t.identifier(paramName), - t.identifier(paramName), - false, - true - ) - ]); - - const newRenderFunction = t.jsxExpressionContainer( - t.arrowFunctionExpression( - [objectPattern], - t.jsxFragment( - t.jsxOpeningFragment(), - t.jsxClosingFragment(), - dialogElement.children - ) - ) - ); - - let removedOnDismiss = false; - const attributes = dialogElement.openingElement.attributes.filter((attr) => { - if (t.isJSXAttribute(attr) && attr.name.name === 'onDismiss') { - removedOnDismiss = true; - return false; - } - return true; - }); - - path.node.children[renderFunctionIndex] = t.jsxElement( - t.jsxOpeningElement(t.jsxIdentifier(newChildComponent), attributes), - t.jsxClosingElement(t.jsxIdentifier(newChildComponent)), - [newRenderFunction] - ); - - if (removedOnDismiss) { - addComment(path.node.children[renderFunctionIndex], ' onDismiss was removed from Dialog. Use onOpenChange on the DialogTrigger, or onDismiss on the DialogContainer instead'); - } - } -} - -export interface UpdateComponentWithinCollectionOptions { - parentComponent: string, - newComponent: string -} - -/** - * If within a collection component, updates to use a new component. - * - * Example: - * - Item: If within `Menu`, update name from `Item` to `MenuItem`. - */ -function updateComponentWithinCollection( - path: NodePath, - options: UpdateComponentWithinCollectionOptions -) { - const {parentComponent, newComponent} = options; - - // Collections currently implemented - // TODO: Add 'ActionGroup', 'ListBox', 'ListView' once implemented - const collectionItemParents = new Set(['Menu', 'ActionMenu', 'TagGroup', 'Breadcrumbs', 'Picker', 'ComboBox', 'ListBox', 'TabList', 'TabPanels', 'Collection']); - - if ( - t.isJSXElement(path.node) && - t.isJSXIdentifier(path.node.openingElement.name) - ) { - // Find closest parent collection component - let closestParentCollection = path.findParent((p) => - t.isJSXElement(p.node) && - t.isJSXIdentifier(p.node.openingElement.name) && - collectionItemParents.has(getName(path, p.node.openingElement.name)) - ); - if ( - closestParentCollection && - t.isJSXElement(closestParentCollection.node) && - t.isJSXIdentifier(closestParentCollection.node.openingElement.name) && - getName(path, closestParentCollection.node.openingElement.name) === parentComponent - ) { - // If closest parent collection component matches parentComponent, replace with newComponent - - updateKeyToId(path); - - let localName = newComponent; - if (availableComponents.has(newComponent)) { - let program = path.findParent((p) => t.isProgram(p.node)) as NodePath; - localName = addComponentImport(program, newComponent); - } - - let newNode = t.jsxElement( - t.jsxOpeningElement(t.jsxIdentifier(localName), path.node.openingElement.attributes), - t.jsxClosingElement(t.jsxIdentifier(localName)), - path.node.children - ); - path.replaceWith(newNode); - } - } -} - -/** - * If no parent collection detected, leave a comment for the user to check manually. - * - * Example: If they're declaring declaring Items somewhere above the collection. - */ -function commentIfParentCollectionNotDetected( - path: NodePath -) { - const collectionItemParents = new Set(['Menu', 'ActionMenu', 'TagGroup', 'Breadcrumbs', 'Picker', 'ComboBox', 'ListBox', 'TabList', 'TabPanels', 'ActionGroup', 'ActionButtonGroup', 'ToggleButtonGroup', 'ListBox', 'ListView', 'Collection', 'SearchAutocomplete', 'Accordion', 'ActionBar', 'StepList']); - if ( - t.isJSXElement(path.node) - ) { - // Find closest parent collection component - let closestParentCollection = path.findParent((p) => - t.isJSXElement(p.node) && - t.isJSXIdentifier(p.node.openingElement.name) && - collectionItemParents.has(getName(path, p.node.openingElement.name)) - ); - if (!closestParentCollection) { - // If we couldn't find a parent collection parent, leave a comment for them to check manually - addComment(path.node, ' TODO(S2-upgrade): Couldn\'t automatically detect what type of collection component this is rendered in. You\'ll need to update this manually.'); - } - } -} - -/** - * Updates Tabs to the new API. - * - * Example: - * - Tabs: Remove TabPanels components and keep individual TabPanel components inside. - */ -function updateTabs( - path: NodePath -) { - function transformTabs(path: NodePath) { - let tabListNode: t.JSXElement | null = null; - let tabPanelsNodes: t.JSXElement[] = []; - let itemsProp: t.JSXAttribute | null = null; - - path.node.openingElement.attributes = path.node.openingElement.attributes.filter(attr => { - if (t.isJSXAttribute(attr) && attr.name.name === 'items') { - itemsProp = attr; - return false; - } - return true; - }); - - path.get('children').forEach(childPath => { - if (t.isJSXElement(childPath.node)) { - if ( - t.isJSXIdentifier(childPath.node.openingElement.name) && - getName(childPath as NodePath, childPath.node.openingElement.name) === 'TabList' - ) { - tabListNode = transformTabList(childPath as NodePath); - if (itemsProp) { - tabListNode.openingElement.attributes.push(itemsProp); - } - } else if ( - t.isJSXIdentifier(childPath.node.openingElement.name) && - getName(childPath as NodePath, childPath.node.openingElement.name) === 'TabPanels' - ) { - tabPanelsNodes = transformTabPanels(childPath as NodePath, itemsProp); - } - } - }); - - if (tabListNode) { - path.node.children = [tabListNode, ...tabPanelsNodes]; - } - } - - function transformTabList(tabListPath: NodePath): t.JSXElement { - tabListPath.get('children').forEach(itemPath => { - if ( - t.isJSXElement(itemPath.node) && - t.isJSXIdentifier(itemPath.node.openingElement.name) && - getName(itemPath as NodePath, itemPath.node.openingElement.name) === 'Item' - ) { - updateComponentWithinCollection(itemPath as NodePath, {parentComponent: 'TabList', newComponent: 'Tab'}); - } - }); - return tabListPath.node; - } - - function transformTabPanels(tabPanelsPath: NodePath, itemsProp: t.JSXAttribute | null): t.JSXElement[] { - // Dynamic case - let dynamicRender = tabPanelsPath.get('children').find(path => t.isJSXExpressionContainer(path.node)); - if (dynamicRender) { - updateToNewComponent(tabPanelsPath, {newComponent: 'Collection'}); - let itemPath = (dynamicRender.get('expression') as NodePath).get('body'); - updateComponentWithinCollection(itemPath as NodePath, {parentComponent: 'Collection', newComponent: 'TabPanel'}); - if (itemsProp) { - tabPanelsPath.node.openingElement.attributes.push(t.jsxAttribute(t.jsxIdentifier('items'), itemsProp.value)); - } - return [tabPanelsPath.node]; - } - - // Static case - return tabPanelsPath.get('children').map(itemPath => { - if ( - t.isJSXElement(itemPath.node) && - t.isJSXIdentifier(itemPath.node.openingElement.name) && - getName(itemPath as NodePath, itemPath.node.openingElement.name) === 'Item' - ) { - updateComponentWithinCollection(itemPath as NodePath, {parentComponent: 'TabPanels', newComponent: 'TabPanel'}); - return itemPath.node; - } - return null; - }).filter(Boolean) as t.JSXElement[]; - } - - let program = path.findParent((p) => t.isProgram(p.node)) as NodePath; - removeComponentImport(program, 'TabPanels'); - transformTabs(path); -} - -export interface MovePropToNewChildComponentOptions { - parentComponent: string, - childComponent: string, - propToMove: string, - newChildComponent: string -} - -/** - * If within a component, moves prop to new child component. - * - * Example: - * - Section: If within `Menu`, move `title` prop string to be a child of new `Heading` within a `Header`. - */ -function movePropToNewChildComponent( - path: NodePath, - options: MovePropToNewChildComponentOptions -) { - const {parentComponent, childComponent, propToMove, newChildComponent} = - options; - - if ( - t.isJSXElement(path.node) && - t.isJSXElement(path.parentPath.node) && - t.isJSXIdentifier(path.node.openingElement.name) && - t.isJSXIdentifier(path.parentPath.node.openingElement.name) && - getName(path, path.node.openingElement.name) === childComponent && - getName(path, path.parentPath.node.openingElement.name) === parentComponent - ) { - let propValue: t.JSXAttribute['value'] | void; - path.node.openingElement.attributes = - path.node.openingElement.attributes.filter((attr) => { - if (t.isJSXAttribute(attr) && attr.name.name === propToMove) { - propValue = attr.value; - return false; - } - return true; - }); - - if (propValue) { - path.node.children.unshift( - t.jsxElement( - t.jsxOpeningElement(t.jsxIdentifier(newChildComponent), []), - t.jsxClosingElement(t.jsxIdentifier(newChildComponent)), - [t.isStringLiteral(propValue) ? t.jsxText(propValue.value) : propValue] - ) - ); - // TODO: handle dynamic collections. Need to wrap function child in and move `items` prop down. - } - } -} - -export interface MovePropToParentComponentOptions { - parentComponent: string, - childComponent: string, - propToMove: string -} - -/** - * Updates prop to be on parent component. - * - * Example: - * - Tooltip: Remove `placement` and add to the parent `TooltipTrigger` instead. - */ -function movePropToParentComponent( - path: NodePath, - options: MovePropToParentComponentOptions -) { - const {parentComponent, childComponent, propToMove} = options; - - path.traverse({ - JSXAttribute(attributePath) { - if ( - t.isJSXElement(path.parentPath.node) && - t.isJSXIdentifier(path.node.openingElement.name) && - t.isJSXIdentifier(path.parentPath.node.openingElement.name) && - attributePath.node.name.name === propToMove && - getName(path, path.node.openingElement.name) === childComponent && - getName(path, path.parentPath.node.openingElement.name) === parentComponent - ) { - path.parentPath.node.openingElement.attributes.push( - t.jsxAttribute(t.jsxIdentifier(propToMove), attributePath.node.value) - ); - attributePath.remove(); - } - } - }); -} - -export interface UpdateToNewComponentOptions { - newComponent: string -} - -/** - * Update to use a new component. - * - * Example: - * - Flex: Update `Flex` to be a `div` and apply flex styles using the style macro. - */ -function updateToNewComponent( - path: NodePath, - options: UpdateToNewComponentOptions -) { - const {newComponent} = options; - - let localName = newComponent; - if (availableComponents.has(newComponent)) { - let program = path.findParent((p) => t.isProgram(p.node)) as NodePath; - localName = addComponentImport(program, newComponent); - } - - path.node.openingElement.name = t.jsxIdentifier(localName); - if (path.node.closingElement) { - path.node.closingElement.name = t.jsxIdentifier(localName); - } -} - -const conversions = { - 'cm': 37.8, - 'mm': 3.78, - 'in': 96, - 'pt': 1.33, - 'pc': 16 -}; - -export interface ConvertDimensionValueToPxOptions { - propToConvertValue: string -} - -/** - * Updates prop to use pixel value instead. - * - * Example: - * - ComboBox: Update `menuWidth` to a pixel value. - */ -function convertDimensionValueToPx( - path: NodePath, - options: ConvertDimensionValueToPxOptions -) { - const {propToConvertValue} = options; - - let attrPath = path.get('openingElement').get('attributes').find((attr) => t.isJSXAttribute(attr.node) && attr.node.name.name === propToConvertValue) as NodePath; - if (attrPath && t.isJSXAttribute(attrPath.node) && attrPath.node.name.name === propToConvertValue) { - if (t.isStringLiteral(attrPath.node.value)) { - try { - let value = convertDimension(attrPath.node.value.value, 'size'); - if (value && typeof value === 'number') { - attrPath.node.value = t.jsxExpressionContainer(t.numericLiteral(value)); - } else if (value && typeof value === 'string') { - // eslint-disable-next-line max-depth - if ((/%|vw|vh|auto|ex|ch|rem|vmin|vmax/).test(value)) { - addComment(attrPath.node, ' TODO(S2-upgrade): Unable to convert CSS unit to a pixel value'); - } else if ((/cm|mm|in|pt|pc/).test(value)) { - let unit = value.replace(/\[|\]|\d+/g, ''); - let conversion = conversions[unit as keyof typeof conversions]; - value = Number(value.replace(/\[|\]|cm|mm|in|pt|pc/g, '')); - // eslint-disable-next-line max-depth - if (!isNaN(value)) { - let pixelValue = Math.round(conversion * value); - attrPath.node.value = t.jsxExpressionContainer(t.numericLiteral(pixelValue)); - } - } else if ((/px/).test(value)) { - let pixelValue = Number(value.replace(/\[|\]|px/g, '')); - // eslint-disable-next-line max-depth - if (!isNaN(pixelValue)) { - attrPath.node.value = t.jsxExpressionContainer(t.numericLiteral(pixelValue)); - } - } - } - } catch (error) { - addComment(attrPath.node, ` TODO(S2-upgrade): Prop ${propToConvertValue} could not be automatically updated due to error: ${error}`); - } - } else if (t.isJSXExpressionContainer(attrPath.node.value)) { - if (t.isIdentifier(attrPath.node.value.expression)) { - addComment(attrPath.node, ` TODO(S2-upgrade): Prop ${propToConvertValue} could not be automatically updated because ${attrPath.node.value.expression.name} could not be followed.`); - } - } - } -} - -export interface UpdatePlacementToSingleValueProps { - propToUpdate: string, - /* If provided, updates the prop on the specified child component */ - childComponent?: string -} - -/** - * Updates double placement values to a single value. - * - * Example: - * - TooltipTrigger: Update `placement="bottom left"` to `placement="bottom"`. - */ -function updatePlacementToSingleValue( - path: NodePath, - options: UpdatePlacementToSingleValueProps -) { - const {propToUpdate, childComponent} = options; - - const doublePlacementValues = new Set([ - 'bottom left', - 'bottom right', - 'bottom start', - 'bottom end', - 'top left', - 'top right', - 'top start', - 'top end', - 'left top', - 'left bottom', - 'start top', - 'start bottom', - 'right top', - 'right bottom', - 'end top', - 'end bottom' - ]); - - let elementPath = childComponent ? - path.get('children').find( - (child) => t.isJSXElement(child.node) && - t.isJSXIdentifier(child.node.openingElement.name) && - getName(path, child.node.openingElement.name) === childComponent - ) as NodePath : path; - let attrPath = elementPath.get('openingElement').get('attributes').find((attr) => t.isJSXAttribute(attr.node) && attr.node.name.name === propToUpdate) as NodePath; - if (attrPath && t.isJSXAttribute(attrPath.node) && attrPath.node.name.name === propToUpdate) { - if (t.isStringLiteral(attrPath.node.value) && doublePlacementValues.has(attrPath.node.value.value)) { - attrPath.replaceWith(t.jsxAttribute(t.jsxIdentifier(propToUpdate), t.stringLiteral(attrPath.node.value.value.split(' ')[0]))); - return; - } else if (t.isJSXExpressionContainer(attrPath.node.value)) { - attrPath.traverse({ - StringLiteral(stringPath) { - if ( - t.isStringLiteral(stringPath.node) && - doublePlacementValues.has(stringPath.node.value) - ) { - stringPath.replaceWith(t.stringLiteral(stringPath.node.value.split(' ')[0])); - return; - } - } - }); - } - } -} - -export interface RemoveComponentIfWithinParentOptions { - parentComponent: string -} - -/** - * Remove component if within a parent component. - * - * Example: - * - Divider: Remove `Divider` if used within a `Dialog`. - */ -function removeComponentIfWithinParent( - path: NodePath, - options: RemoveComponentIfWithinParentOptions -) { - const {parentComponent} = options; - if ( - t.isJSXElement(path.node) && - t.isJSXElement(path.parentPath.node) && - t.isJSXIdentifier(path.node.openingElement.name) && - t.isJSXIdentifier(path.parentPath.node.openingElement.name) && - getName(path, path.parentPath.node.openingElement.name) === parentComponent - ) { - path.remove(); - } -} - -function updateAvatarSize( - path: NodePath -) { - if ( - t.isJSXElement(path.node) && - t.isJSXIdentifier(path.node.openingElement.name) && - getName(path, path.node.openingElement.name) === 'Avatar' - ) { - let sizeAttrPath = path.get('openingElement').get('attributes').find((attr) => t.isJSXAttribute(attr.node) && attr.node.name.name === 'size') as NodePath; - if (sizeAttrPath) { - let value = sizeAttrPath.node.value; - if (value?.type === 'StringLiteral') { - const avatarDimensions = { - 'avatar-size-50': 16, - 'avatar-size-75': 18, - 'avatar-size-100': 20, - 'avatar-size-200': 22, - 'avatar-size-300': 26, - 'avatar-size-400': 28, - 'avatar-size-500': 32, - 'avatar-size-600': 36, - 'avatar-size-700': 40 - }; - let val = avatarDimensions[value.value as keyof typeof avatarDimensions]; - if (val != null) { - sizeAttrPath.node.value = t.jsxExpressionContainer(t.numericLiteral(val)); - } - } - } - } -} - -/** - * Handles the legacy `Link` API where an `a` tag or custom router component could be used within a `Link` component. - * Removes the inner component and moves its attributes to the `Link` component. - */ -function updateLegacyLink( - path: NodePath -) { - let missingOuterHref = t.isJSXElement(path.node) && !path.node.openingElement.attributes.some((attr) => t.isJSXAttribute(attr) && attr.name.name === 'href'); - if (missingOuterHref) { - let innerLink = path.node.children.find((child) => t.isJSXElement(child) && t.isJSXIdentifier(child.openingElement.name)); - if (innerLink && t.isJSXElement(innerLink)) { - let innerAttributes = innerLink.openingElement.attributes; - let outerAttributes = path.node.openingElement.attributes; - innerAttributes.forEach((attr) => { - outerAttributes.push(attr); - }); - - if ( - t.isJSXIdentifier(innerLink.openingElement.name) && - innerLink.openingElement.name.name !== 'a' - ) { - addComment(path.node, ' TODO(S2-upgrade): You may have been using a custom link component here. You\'ll need to update this manually.'); - } - path.node.children = innerLink.children; - } - } -} - -/** - * Copies the columns prop from the TableHeader to the Row component. - */ -function addColumnsPropToRow( - path: NodePath -) { - const tableHeaderPath = path.get('children').find((child) => - t.isJSXElement(child.node) && - t.isJSXIdentifier(child.node.openingElement.name) && - getName(child as NodePath, child.node.openingElement.name) === 'TableHeader' - ) as NodePath | undefined; - - if (!tableHeaderPath) { - addComment(path.node, ' TODO(S2-upgrade): Could not find TableHeader within Table to retrieve columns prop.'); - return; - } - - const columnsProp = tableHeaderPath - .get('openingElement') - .get('attributes') - .find((attr) => t.isJSXAttribute(attr.node) && attr.node.name.name === 'columns') as NodePath | undefined; - - if (columnsProp) { - path.traverse({ - JSXElement(innerPath) { - if ( - t.isJSXElement(innerPath.node) && - t.isJSXIdentifier(innerPath.node.openingElement.name) && - getName(innerPath as NodePath, innerPath.node.openingElement.name) === 'Row' - ) { - let rowPath = innerPath as NodePath; - rowPath.node.openingElement.attributes.push(columnsProp.node); - - // If Row doesn't contain id prop, leave a comment for the user to check manually - let idProp = rowPath.get('openingElement').get('attributes').find((attr) => t.isJSXAttribute(attr.node) && attr.node.name.name === 'id'); - if (!idProp) { - 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.'); - } - } - } - }); - } -} - -/** - * Updates the function signature of the Row component. - */ -function updateRowFunctionArg( - path: NodePath -) { - // Find the function passed as a child - let functionChild = path.get('children').find(childPath => - childPath.isJSXExpressionContainer() && - childPath.get('expression').isArrowFunctionExpression() - ); - - let tablePath = path.findParent((p) => - t.isJSXElement(p.node) && - t.isJSXIdentifier(p.node.openingElement.name) && - getName(path, p.node.openingElement.name) === 'TableView' - ); - - let tableHeaderPath = tablePath?.get('children').find((child) => - t.isJSXElement(child.node) && - t.isJSXIdentifier(child.node.openingElement.name) && - getName(child as NodePath, child.node.openingElement.name) === 'TableHeader' - ) as NodePath | undefined; - - function findColumnKeyProp(path: NodePath) { - let columnKeyProp = 'id'; - path.traverse({ - JSXElement(columnPath) { - if ( - t.isArrowFunctionExpression(columnPath.parentPath.node) && - t.isJSXElement(columnPath.node) && - t.isJSXIdentifier(columnPath.node.openingElement.name) && - getName(columnPath as NodePath, columnPath.node.openingElement.name) === 'Column' - ) { - let openingElement = columnPath.get('openingElement'); - let keyPropPath = openingElement.get('attributes').find(attr => - t.isJSXAttribute(attr.node) && - (attr.node.name.name === 'key' || attr.node.name.name === 'id') - ); - keyPropPath?.traverse({ - Identifier(innerPath) { - if ( - innerPath.node.name === 'column' && - innerPath.parentPath.node.type === 'MemberExpression' && - t.isIdentifier(innerPath.parentPath.node.property) - ) { - columnKeyProp = innerPath.parentPath.node.property.name; - } - } - }); - } - } - }); - return columnKeyProp || 'id'; - } - - let columnKey = findColumnKeyProp(tableHeaderPath as NodePath); - - if (functionChild && functionChild.isJSXExpressionContainer()) { - let arrowFuncPath = functionChild.get('expression'); - if (arrowFuncPath.isArrowFunctionExpression()) { - let params = arrowFuncPath.node.params; - if (params.length === 1 && t.isIdentifier(params[0])) { - let paramName = params[0].name; - - // Rename parameter to 'column' - params[0].name = 'column'; - - // Replace references to the old parameter name inside the function body - arrowFuncPath.get('body').traverse({ - Identifier(innerPath) { - if ( - innerPath.node.name === paramName && - // Ensure we're not replacing the parameter declaration - innerPath.node !== params[0] - ) { - // Replace with column key - innerPath.replaceWith( - t.memberExpression( - t.identifier('column'), - t.identifier(columnKey ?? 'id') - ) - ); - } - } - }); - } - } - } -} - -/** - * Updates the key prop to id. Keeps the key prop if it's used in an array.map function. - */ -function updateKeyToId( - path: NodePath -) { - let attributes = path.node.openingElement.attributes; - let keyProp = attributes.find((attr) => t.isJSXAttribute(attr) && attr.name.name === 'key'); - if ( - keyProp && - t.isJSXAttribute(keyProp) - ) { - // Update key prop to be id - keyProp.name = t.jsxIdentifier('id'); - } - - if ( - t.isArrowFunctionExpression(path.parentPath.node) && - path.parentPath.parentPath && - t.isCallExpression(path.parentPath.parentPath.node) && - path.parentPath.parentPath.node.callee.type === 'MemberExpression' && - path.parentPath.parentPath.node.callee.property.type === 'Identifier' && - path.parentPath.parentPath.node.callee.property.name === 'map' - ) { - // If Array.map is used, keep the key prop - if ( - keyProp && - t.isJSXAttribute(keyProp) - ) { - let newKeyProp = t.jsxAttribute(t.jsxIdentifier('key'), keyProp.value); - attributes.push(newKeyProp); - } - } -} - -export function commentIfNestedColumns( - path: NodePath -) { - const headerPath = path.get('children').find((child) => - t.isJSXElement(child.node) && - t.isJSXIdentifier(child.node.openingElement.name) && - getName(child as NodePath, child.node.openingElement.name) === 'TableHeader' - ) as NodePath | undefined; - const columns = headerPath?.get('children') || []; - - let hasNestedColumns = false; - - columns.forEach(column => { - let columnChildren = column.get('children'); - if ( - columnChildren.find(child => - t.isJSXElement(child.node) && - t.isJSXIdentifier(child.node.openingElement.name) && - getName(child as NodePath, child.node.openingElement.name) === 'Column' - ) - ) { - hasNestedColumns = true; - } - }); - - if (hasNestedColumns) { - addComment(path.node, ' TODO(S2-upgrade): Nested Column components are not supported yet.'); - } -} - -/** - * Updates DialogTrigger and DialogContainer to the new API. - * - * Example: - * - When `type="popover"`, replaces Dialog with ``. - * - When `type="fullscreen"`, replaces Dialog with ``. - * - When `type="fullscreenTakeover"`, replaces Dialog with ``. - */ -function updateDialogChild( - path: NodePath -) { - let typePath = path.get('openingElement').get('attributes').find((attr) => t.isJSXAttribute(attr.node) && attr.node.name.name === 'type') as NodePath | undefined; - let type = typePath?.node.value?.type === 'StringLiteral' ? typePath.node.value?.value : 'modal'; - let newComponent = 'Dialog'; - let props: t.JSXAttribute[] = []; - if (type === 'popover') { - newComponent = 'Popover'; - } else if (type === 'fullscreen' || type === 'fullscreenTakeover') { - newComponent = 'FullscreenDialog'; - if (type === 'fullscreenTakeover') { - props.push(t.jsxAttribute(t.jsxIdentifier('variant'), t.stringLiteral(type))); - } - } - - for (let prop of ['isDismissible', 'mobileType', 'hideArrow', 'placement', 'shouldFlip', 'isKeyboardDismissDisabled', 'containerPadding', 'offset', 'crossOffset']) { - let attr = path.get('openingElement').get('attributes').find(attr => attr.isJSXAttribute() && attr.node.name.name === prop) as NodePath | undefined; - if (attr) { - props.push(attr.node); - attr.remove(); - } - } - - typePath?.remove(); - - let localName = newComponent; - if (newComponent !== 'Dialog' && availableComponents.has(newComponent)) { - let program = path.findParent((p) => t.isProgram(p.node)) as NodePath; - localName = addComponentImport(program, newComponent); - } - - path.traverse({ - JSXElement(dialog) { - if (!t.isJSXIdentifier(dialog.node.openingElement.name) || getName(dialog, dialog.node.openingElement.name) !== 'Dialog') { - return; - } - - dialog.node.openingElement.name = t.jsxIdentifier(localName); - if (dialog.node.closingElement) { - dialog.node.closingElement.name = t.jsxIdentifier(localName); - } - - dialog.node.openingElement.attributes.push(...props); - } - }); -} - -function updateActionGroup( - path: NodePath -) { - let selectionModePath = path.get('openingElement').get('attributes').find((attr) => t.isJSXAttribute(attr.node) && attr.node.name.name === 'selectionMode') as NodePath | undefined; - let selectionMode = t.isStringLiteral(selectionModePath?.node.value) ? selectionModePath.node.value.value : 'none'; - let newComponent, childComponent; - if (selectionMode === 'none') { - newComponent = 'ActionButtonGroup'; - childComponent = 'ActionButton'; - selectionModePath?.remove(); - } else { - newComponent = 'ToggleButtonGroup'; - childComponent = 'ToggleButton'; - } - - let localName = newComponent; - if (availableComponents.has(newComponent)) { - let program = path.findParent((p) => t.isProgram(p.node)) as NodePath; - localName = addComponentImport(program, newComponent); - } - - let localChildName = childComponent; - if (availableComponents.has(childComponent)) { - let program = path.findParent((p) => t.isProgram(p.node)) as NodePath; - localChildName = addComponentImport(program, childComponent); - } - - - // Convert dynamic collection to an array.map. - let items = path.get('openingElement').get('attributes').find((attr) => t.isJSXAttribute(attr.node) && attr.node.name.name === 'items') as NodePath | undefined; - let itemArg: t.Identifier | undefined; - if (items && t.isJSXExpressionContainer(items.node.value) && t.isExpression(items.node.value.expression)) { - let child = path.get('children').find(c => c.isJSXExpressionContainer()); - if (child && child.isJSXExpressionContainer() && t.isFunction(child.node.expression)) { - let arg = child.node.expression.params[0]; - if (t.isIdentifier(arg)) { - itemArg = arg; - } - - child.replaceWith( - t.jsxExpressionContainer( - t.callExpression( - t.memberExpression( - items.node.value.expression, - t.identifier('map') - ), - [child.node.expression] - ) - ) - ); - } - } - items?.remove(); - - let onAction = path.get('openingElement').get('attributes').find((attr) => t.isJSXAttribute(attr.node) && attr.node.name.name === 'onAction') as NodePath | undefined; - - // Pull disabledKeys prop out into a variable, converted to a Set. - // Then we can check it in the isDisabled prop of each item. - let disabledKeysPath = path.get('openingElement').get('attributes').find((attr) => t.isJSXAttribute(attr.node) && attr.node.name.name === 'disabledKeys') as NodePath | undefined; - let disabledKeys: t.Identifier | undefined; - if (disabledKeysPath && t.isJSXExpressionContainer(disabledKeysPath.node.value) && t.isExpression(disabledKeysPath.node.value.expression)) { - disabledKeys = path.scope.generateUidIdentifier('disabledKeys'); - path.scope.push({ - id: disabledKeys, - init: t.newExpression(t.identifier('Set'), [disabledKeysPath.node.value.expression]), - kind: 'let' - }); - disabledKeysPath.remove(); - } - - path.traverse({ - JSXElement(child) { - if (t.isJSXIdentifier(child.node.openingElement.name) && child.node.openingElement.name.name === 'Item') { - // Replace Item with ActionButton or ToggleButton. - let childNode = t.cloneNode(child.node); - childNode.openingElement.name = t.jsxIdentifier(localChildName); - if (childNode.closingElement) { - childNode.closingElement.name = t.jsxIdentifier(localChildName); - } - - // If there is no key prop and we are using dynamic collections, add a default computed from item.key ?? item.id. - let key = childNode.openingElement.attributes.find(attr => t.isJSXAttribute(attr) && attr.name.name === 'key') as t.JSXAttribute | undefined; - if (!key && itemArg) { - let id = t.jsxExpressionContainer( - t.logicalExpression( - '??', - t.memberExpression(itemArg, t.identifier('key')), - t.memberExpression(itemArg, t.identifier('id')) - ) - ); - - key = t.jsxAttribute( - t.jsxIdentifier('key'), - id - ); - - childNode.openingElement.attributes.push(key); - } - - // If this is a ToggleButtonGroup, add an id prop in addition to key when needed. - if (key && newComponent === 'ToggleButtonGroup') { - // If we are in an array.map we need both key and id. Otherwise, we only need id. - if (itemArg) { - childNode.openingElement.attributes.push(t.jsxAttribute(t.jsxIdentifier('id'), key.value)); - } else { - key.name.name = 'id'; - } - } - - let keyValue: t.Expression | undefined = undefined; - if (key && t.isJSXExpressionContainer(key.value) && t.isExpression(key.value.expression)) { - keyValue = key.value.expression; - } else if (key && t.isStringLiteral(key.value)) { - keyValue = key.value; - } - - // Add an onPress to each item that calls the previous onAction, passing in the key. - if (onAction && t.isJSXExpressionContainer(onAction.node.value) && t.isExpression(onAction.node.value.expression)) { - childNode.openingElement.attributes.push( - t.jsxAttribute( - t.jsxIdentifier('onPress'), - t.jsxExpressionContainer( - keyValue - ? t.arrowFunctionExpression([], t.callExpression(onAction.node.value.expression, [keyValue])) - : onAction.node.value.expression - ) - ) - ); - } - - // Add an isDisabled prop to each item, testing whether it is in disabledKeys. - if (disabledKeys && keyValue) { - childNode.openingElement.attributes.push( - t.jsxAttribute( - t.jsxIdentifier('isDisabled'), - t.jsxExpressionContainer( - t.callExpression( - t.memberExpression( - disabledKeys, - t.identifier('has') - ), - [keyValue] - ) - ) - ) - ); - } - - child.replaceWith(childNode); - } - } - }); - - onAction?.remove(); - - path.node.openingElement.name = t.jsxIdentifier(localName); - if (path.node.closingElement) { - path.node.closingElement.name = t.jsxIdentifier(localName); - } -} - -/** - * Adds isRowHeader to the first Column in a table if there isn't already a row header. - * @param path - */ -function addRowHeader( - path: NodePath -) { - let tableHeaderPath = path.get('children').find((child) => - t.isJSXElement(child.node) && - t.isJSXIdentifier(child.node.openingElement.name) && - getName(child as NodePath, child.node.openingElement.name) === 'TableHeader' - ) as NodePath | undefined; - - - // Check if isRowHeader is already set on a Column - let hasRowHeader = false; - tableHeaderPath?.get('children').forEach((child) => { - if ( - t.isJSXElement(child.node) && - t.isJSXIdentifier(child.node.openingElement.name) && - getName(child as NodePath, child.node.openingElement.name) === 'Column' - ) { - let isRowHeaderProp = (child.get('openingElement') as NodePath).get('attributes').find((attr) => t.isJSXAttribute(attr.node) && attr.node.name.name === 'isRowHeader') as NodePath | undefined; - if (isRowHeaderProp) { - hasRowHeader = true; - } - } - }); - - // If there isn't already a row header, add one to the first Column if possible - if (!hasRowHeader) { - tableHeaderPath?.get('children').forEach((child) => { - // Add to first Column if static - if ( - !hasRowHeader && - t.isJSXElement(child.node) && - t.isJSXIdentifier(child.node.openingElement.name) && - getName(child as NodePath, child.node.openingElement.name) === 'Column' - ) { - child.node.openingElement.attributes.push(t.jsxAttribute(t.jsxIdentifier('isRowHeader'), t.jsxExpressionContainer(t.booleanLiteral(true)))); - hasRowHeader = true; - } - - // If render function is used, leave a comment to update manually - if ( - t.isJSXExpressionContainer(child.node) && - t.isArrowFunctionExpression(child.node.expression) - ) { - addComment(child.node, ' TODO(S2-upgrade): You\'ll need to add isRowHeader to one of the columns manually.'); - } - - // If array.map is used, leave a comment to update manually - if ( - t.isJSXExpressionContainer(child.node) && - t.isCallExpression(child.node.expression) && - t.isMemberExpression(child.node.expression.callee) && - t.isIdentifier(child.node.expression.callee.property) && - child.node.expression.callee.property.name === 'map' - ) { - addComment(child.node, ' TODO(S2-upgrade): You\'ll need to add isRowHeader to one of the columns manually.'); - } - }); - } -} - -export const functionMap = { - updatePropNameAndValue, - updatePropValueAndAddNewProp, - updatePropName, - removeProp, - commentOutProp, - addCommentToElement, - updateComponentIfPropPresent, - moveRenderPropsToChild, - updateComponentWithinCollection, - commentIfParentCollectionNotDetected, - updateTabs, - movePropToNewChildComponent, - movePropToParentComponent, - updateToNewComponent, - convertDimensionValueToPx, - updatePlacementToSingleValue, - removeComponentIfWithinParent, - updateAvatarSize, - updateLegacyLink, - addColumnsPropToRow, - updateRowFunctionArg, - updateDialogChild, - updateActionGroup, - updateKeyToId, - commentIfNestedColumns, - addRowHeader -};