import { type API, type FileInfo, type JSCodeshift, type Options } from 'jscodeshift'; import { type Collection } from 'jscodeshift/src/Collection'; import { addJSXAttributeToJSXElement } from './utils/add-jsx-attribute-to-jsx-element'; import { dtpPropsToMoveIntoPickerProps, type ToPickerFormula, } from './utils/dtp-props-to-move-into-picker-props'; import { getImportDeclarationCollection } from './utils/get-import-declaration-collection'; import { getImportSpecifierCollection } from './utils/get-import-specifier-collection'; import { getImportSpecifierName } from './utils/get-import-specifier-name'; import { getJSXAttributeByName } from './utils/get-jsx-attribute-by-name'; import { getJSXAttributesByName } from './utils/get-jsx-attributes-by-name'; import { hasImportDeclaration } from './utils/has-import-declaration'; import { removeJSXAttributeByName } from './utils/remove-jsx-attribute-by-name'; import { removeJSXAttributeObjectPropertyByName } from './utils/remove-jsx-attribute-object-property-by-name'; import { type FromPickerFormula, selectPropsToMoveIntoProps, } from './utils/select-props-to-move-into-props'; const importPath = '@atlaskit/datetime-picker'; const dateTimePickerImportName = 'DateTimePicker'; const datePickerImportName = 'DatePicker'; const timePickerImportName = 'TimePicker'; const pickerAndSelectPropNames = [ ['datePickerSelectProps', 'datePickerProps'], ['timePickerSelectProps', 'timePickerProps'], ]; /** * Move *pickerSelectProps into their respectice *pickerProps as a property * named `selectProps`. */ function moveDateTimePickerSelectProps(j: JSCodeshift, collection: Collection) { const importDeclarationCollection = getImportDeclarationCollection(j, collection, importPath); const importSpecifierCollection = getImportSpecifierCollection( j, importDeclarationCollection, dateTimePickerImportName, ); const importSpecifierName = getImportSpecifierName(importSpecifierCollection); // If our component is not present, quit early if (importSpecifierName === null) { return; } collection.findJSXElements(dateTimePickerImportName).forEach((jsxElementPath) => { // For each picker, pickerAndSelectPropNames.forEach((migration) => { const [selectPropsName, pickerPropsName] = migration; const datePickerPropsAttr = getJSXAttributeByName(j, jsxElementPath, pickerPropsName); const datePickerSelectPropsAttr = getJSXAttributeByName(j, jsxElementPath, selectPropsName); // If there are both the *selectProps and the *pickerProps, if (datePickerPropsAttr && datePickerSelectPropsAttr) { // Get the select props value // @ts-ignore -- Property 'expression' does not exist on type 'LiteralKind | JSXElement | JSXExpressionContainer | JSXFragment'. Property 'expression' does not exist on type 'Literal'. let datePickerSelectPropsExpression = datePickerSelectPropsAttr?.value?.expression; let datePickerSelectProps; if (datePickerSelectPropsExpression?.properties) { datePickerSelectProps = j.objectExpression(datePickerSelectPropsExpression.properties); } else if (datePickerSelectPropsExpression) { datePickerSelectProps = datePickerSelectPropsExpression; } else { // This is nothing we can use, so skip return; } // This property definitely exists! Just can't figure out how to type properly. Please forgive me. // @ts-ignore -- Property 'expression' does not exist on type 'LiteralKind | JSXElement | JSXExpressionContainer | JSXFragment'. Property 'expression' does not exist on type 'Literal'. const datePickerProps = datePickerPropsAttr.value?.expression.properties; // Make a new selectProps property that will go inside the *pickerProps const selectPropsProperty = j.property( 'init', j.identifier('selectProps'), datePickerSelectProps, ); // Add this to the component datePickerProps.push(selectPropsProperty); // If there is not a *pickerProps prop } else if (datePickerSelectPropsAttr) { // Create one and put the selectProps inside of it const newAttr = j.jsxAttribute( j.jsxIdentifier(pickerPropsName), j.jsxExpressionContainer( j.objectExpression([ j.property( 'init', j.identifier('selectProps'), // @ts-ignore -- Property 'expression' does not exist on type 'LiteralKind | JSXElement | JSXExpressionContainer | JSXFragment'. Property 'expression' does not exist on type 'Literal'. datePickerSelectPropsAttr.value.expression, ), ]), ), ); addJSXAttributeToJSXElement(j, jsxElementPath, newAttr); } // Remove the *selectProps attribute removeJSXAttributeByName(j, jsxElementPath, selectPropsName); }); }); } /** * Move props out of the *pickerProps into their respective top-level props. */ function moveDateTimePickerSelectPropsToPickerProps( j: JSCodeshift, collection: Collection, formulas: FromPickerFormula[], ) { const importDeclarationCollection = getImportDeclarationCollection(j, collection, importPath); const importSpecifierCollection = getImportSpecifierCollection( j, importDeclarationCollection, dateTimePickerImportName, ); const importSpecifierName = getImportSpecifierName(importSpecifierCollection); // If we can't find our import specifier, exit early if (importSpecifierName === null) { return; } // For each component, collection.findJSXElements(dateTimePickerImportName).forEach((jsxElementPath) => { pickerAndSelectPropNames.forEach((migration) => { const pickerPropsName = migration[1]; // For each picker prop to move into the top level, formulas.forEach((formula) => { const { source, newPropName: newPropertyName } = formula; const [selectPropsName, oldPropertyName] = source; // Get the relevant *picker props where the source prop is found const pickerPropsAttr = getJSXAttributeByName(j, jsxElementPath, pickerPropsName); // If no *picker props, return early since there's nothing to do if (!pickerPropsAttr) { return; } // @ts-ignore -- Property 'expression' does not exist on type 'LiteralKind | JSXElement | JSXExpressionContainer | JSXFragment'. Property 'expression' does not exist on type 'Literal'. const pickerPropsProperties = pickerPropsAttr.value?.expression.properties; const existingNewProperty = pickerPropsProperties.find( (property: any) => (property.key?.name || property.key?.value) === newPropertyName, ); // If the new property we're migrating to exists, leave it alone so that // manual remediation can be done. Both shouldn't exist, anyway. if (existingNewProperty) { return; } // Get the selectProps property const selectPropsProperty = pickerPropsProperties.find( (property: any) => (property.key?.name || property.key?.value) === selectPropsName, ); // If no *picker.selectProps, return early since there's nothing to do if (!selectPropsProperty) { return; } const selectPropsProperties = selectPropsProperty.value?.properties; // If selectProps has no properties, quit early if (!selectPropsProperties) { return; } // Get old property's node const oldProp = selectPropsProperties.find((prop: any) => { return prop.key && (prop.key.name || prop.key.value) === oldPropertyName; }); // If the old property does not exist within the *pickerProps, return early if (!oldProp) { return; } // Add the new attribute to the component const newProp = j.objectProperty(j.jsxIdentifier(`"${newPropertyName}"`), oldProp.value); pickerPropsProperties.push(newProp); // Remove the old object property selectPropsProperties.splice(selectPropsProperties.indexOf(oldProp), 1); if (selectPropsProperties.length === 0) { pickerPropsProperties.splice(pickerPropsProperties.indexOf(selectPropsProperty), 1); } }); }); }); } /** * Move props out of the *pickerProps into their respective top-level props. */ function moveSelectPropsToTop( j: JSCodeshift, collection: Collection, formulas: FromPickerFormula[], ) { const importDeclarationCollection = getImportDeclarationCollection(j, collection, importPath); [datePickerImportName, timePickerImportName].forEach((pickerImportName) => { const importSpecifierCollection = getImportSpecifierCollection( j, importDeclarationCollection, pickerImportName, ); const importSpecifierName = getImportSpecifierName(importSpecifierCollection); // If we can't find our import specifier, exit early if (importSpecifierName === null) { return; } // For each component, collection.findJSXElements(pickerImportName).forEach((jsxElementPath) => { // For each picker prop to move into the top level, formulas.forEach((formula) => { const { source, newPropName } = formula; const [selectPropsName, oldPropertyName] = source; // Get the relevant *picker props where the source prop is found const selectProps = getJSXAttributeByName(j, jsxElementPath, selectPropsName); // If no date picker props, return early since there's nothing to do if (!selectProps) { return; } // If the new prop we're migrating to exists, leave it alone so that // manual remediation can be done. Both shouldn't exist, anyway. const existingNewPropAttr = getJSXAttributeByName(j, jsxElementPath, newPropName); if (existingNewPropAttr) { return; } // @ts-ignore -- Property 'expression' does not exist on type 'LiteralKind | JSXElement | JSXExpressionContainer | JSXFragment'. Property 'expression' does not exist on type 'Literal'. let selectPropProperties = selectProps.value?.expression.properties; // If the new prop does not yet exist and we have *pickerProps, if (selectPropProperties) { // Get old property's node const oldProp = selectPropProperties.find((prop: any) => { return prop.key && (prop.key.name || prop.key.value) === oldPropertyName; }); // If the old property does exist within the *pickerProps, if (oldProp) { const newValue = oldProp.value.value ? j.literal(oldProp.value.value) : j.jsxExpressionContainer(oldProp.value); // Add the new attribute to the component const newProp = j.jsxAttribute(j.jsxIdentifier(newPropName), newValue); addJSXAttributeToJSXElement(j, jsxElementPath, newProp); } } // Remove the old object property removeJSXAttributeObjectPropertyByName(j, jsxElementPath, selectPropsName, oldPropertyName); }); }); }); } function moveDateTimePickerProps( j: JSCodeshift, collection: Collection, formulas: ToPickerFormula[], ) { const importDeclarationCollection = getImportDeclarationCollection(j, collection, importPath); const importSpecifierCollection = getImportSpecifierCollection( j, importDeclarationCollection, dateTimePickerImportName, ); const importSpecifierName = getImportSpecifierName(importSpecifierCollection); // If we can't find our component, quit early if (importSpecifierName === null) { return; } // For each datetime picker import, collection.findJSXElements(dateTimePickerImportName).forEach((jsxElementPath) => { formulas.forEach((formula) => { const { oldPropName, destination } = formula; const [pickerPropsName, propertyName] = destination; const attributes = j(jsxElementPath).find(j.JSXAttribute).nodes(); // Find the old prop const oldPropAttr = attributes?.find( (attr) => (attr.name?.name || attr.name) === oldPropName, ); // Find the *pickerProps prop if it exists const datePickerPickerPropsAttr = attributes?.find( (attr) => attr.name && attr.name.name === pickerPropsName, ); // Boolean attributes have `null` value if (!oldPropAttr || oldPropAttr.value === undefined) { return; } // Create one and put the old value inside of it let newValue = oldPropAttr.value; // Remove surrounding brackets when placed in object if (newValue === null) { newValue = j.booleanLiteral(true); } else if (newValue?.type === 'JSXExpressionContainer') { // @ts-ignore Type 'JSXEmptyExpression | ExpressionKind' is not assignable to type 'LiteralKind | JSXElement | JSXExpressionContainer | JSXFragment'. Type 'Identifier' is not assignable to type 'LiteralKind | JSXElement | JSXExpressionContainer | JSXFragment'. Property 'openingElement' is missing in type 'Identifier' but required in type 'JSXElement'. newValue = newValue.expression; // If boolean attribute without explicit value, make it `true` } // Just type guarding for TS if (!newValue) { return; } if (datePickerPickerPropsAttr) { // @ts-ignore -- Property 'expression' does not exist on type 'LiteralKind | JSXElement | JSXExpressionContainer | JSXFragment'. Property 'expression' does not exist on type 'Literal'. const pickerPropsObj = datePickerPickerPropsAttr.value?.expression.properties; // Create a new *pickerProps property and put the old prop inside of it // as an object property const newPickerProperty = j.property('init', j.identifier(propertyName), newValue); // Add this new property to the existing one pickerPropsObj.push(newPickerProperty); // Remove the old one attributes?.splice(attributes.indexOf(datePickerPickerPropsAttr), 1); // If there is no *pickerProps, } else { const newProp = j.jsxAttribute( j.jsxIdentifier(pickerPropsName), j.jsxExpressionContainer( // Adding `true` for boolean attributes that have no value; // This definitely exists on here. Can't coerce right. May god have mercy on my soul for this ts-ignore. // @ts-ignore Argument of type 'LiteralKind | JSXElement | JSXExpressionContainer | JSXFragment | null | undefined' is not assignable to parameter of type 'Literal | JSXText | StringLiteral | NumericLiteral | BigIntLiteral | NullLiteral | BooleanLiteral | ... 53 more ... | TSParameterProperty'. Type 'undefined' is not assignable to type 'Literal | JSXText | StringLiteral | NumericLiteral | BigIntLiteral | NullLiteral | BooleanLiteral | ... 53 more ... | TSParameterProperty'. j.objectExpression([j.property('init', j.identifier(propertyName), newValue)]), ), ); addJSXAttributeToJSXElement(j, jsxElementPath, newProp); } // Get the old prop we're migrating from getJSXAttributesByName(j, jsxElementPath, oldPropName).forEach((jsxAttribute) => { // Remove the original old prop we're migrating from j(jsxAttribute).remove(); }); }); }); } export default function transformer( fileInfo: FileInfo, { jscodeshift: j }: API, options: Options, ): string { const { source } = fileInfo; const collection = j(source); // If our component is not here, skip the file if (!hasImportDeclaration(j, collection, importPath)) { return source; } // Move specific DTP props into *pickerProps moveDateTimePickerProps(j, collection, dtpPropsToMoveIntoPickerProps); // Move all *selectProps into the related *pickerProps.selectProps moveDateTimePickerSelectProps(j, collection); // Move all relevant *selectProps to *pickerProps moveDateTimePickerSelectPropsToPickerProps(j, collection, selectPropsToMoveIntoProps); // Move specific *pickerProps entries into top level moveSelectPropsToTop(j, collection, selectPropsToMoveIntoProps); return collection.toSource(options.printOptions || { quote: 'single' }); }