diff --git a/community/react/remove-default-props/README.md b/community/react/remove-default-props/README.md index 8112fbfa4..1147237f2 100644 --- a/community/react/remove-default-props/README.md +++ b/community/react/remove-default-props/README.md @@ -6,13 +6,26 @@ _Credit_: [https://github.com/reactjs/react-codemod](https://github.com/reactjs/ ```jsx /* INPUT */ -import React from 'react' +import React from 'react'; -export const Greet = ({ name }) => Hi {name} -Greet.defaultProps = { name: 'Stranger' } +export const Greet = ({ name }) => Hi {name}; +Greet.defaultProps = { text: 'Stranger' }; /* OUTPUT */ -import React from 'react' +import React from 'react'; -export const Greet = ({ name }) => Hi {name} +export const Greet = ({ name, text = 'Stranger' }) => Hi {name}; ``` + +```jsx +/* INPUT */ +import React from 'react'; + +export const Greet = (props) => Hi {name}; +Greet.defaultProps = { text: 'Stranger' }; + +/* OUTPUT */ +import React from 'react'; + +export const Greet = ({ ...props, text = 'Stranger' }) => Hi {name}; +``` \ No newline at end of file diff --git a/community/react/remove-default-props/motions/getNewParams.ts b/community/react/remove-default-props/motions/getNewParams.ts new file mode 100644 index 000000000..033deb332 --- /dev/null +++ b/community/react/remove-default-props/motions/getNewParams.ts @@ -0,0 +1,73 @@ +import { + JSCodeshift, + FunctionDeclaration, + ASTPath, + ArrowFunctionExpression, +} from 'jscodeshift'; + +export function getNewParams( + j: JSCodeshift, + component: ASTPath, + defaultParams: any, +) { + const params = component.node.params; + + const existingSingleProp = params.find(param => param.type === 'Identifier'); + + const destructuredProps = params.find( + param => param.type === 'ObjectPattern', + ); + + const noExistingProps = params.length === 0; + + let newParams: FunctionDeclaration['params'] = []; + + if (noExistingProps) { + newParams = [j.objectPattern(defaultParams)]; + } else if (existingSingleProp) { + newParams = [ + j.objectPattern([ + // @ts-ignore + j.spreadProperty(existingSingleProp), + ...defaultParams, + ]), + ]; + } else { + newParams = getNewDestructuredParams(destructuredProps, j, defaultParams); + } + return newParams; +} +function getNewDestructuredParams( + existingPropsParam: FunctionDeclaration['params'][number] | undefined, + j: JSCodeshift, + defaultParams: any, +) { + if (existingPropsParam && 'properties' in existingPropsParam) { + const restProp = existingPropsParam.properties.find( + // @ts-expect-error for some reason it does not exist + prop => prop.type === 'RestElement', + ); + + const existingPropsDestructuredProps = existingPropsParam.properties + .filter(prop => prop.type !== restProp?.type) + .map(prop => j.property('init', (prop as any).key, (prop as any).value)) + .filter(Boolean); + + const restPropArg = + restProp && 'argument' in restProp + ? restProp.argument + : j.identifier('rest'); + + const newParams = [ + j.objectPattern([ + ...existingPropsDestructuredProps, + ...defaultParams, + // @ts-expect-error RestElement is not assignable as above + ...(restProp ? [j.restProperty(restPropArg)] : []), + ]), + ]; + return newParams; + } + + return []; +} diff --git a/community/react/remove-default-props/motions/moveDefaultPropsToArrowFunctionExpression.ts b/community/react/remove-default-props/motions/moveDefaultPropsToArrowFunctionExpression.ts new file mode 100644 index 000000000..791288ac1 --- /dev/null +++ b/community/react/remove-default-props/motions/moveDefaultPropsToArrowFunctionExpression.ts @@ -0,0 +1,43 @@ +import { Collection, JSCodeshift } from 'jscodeshift'; +import { getNewParams } from './getNewParams'; + +export function moveDefaultPropsToArrowFunctionExpression( + j: JSCodeshift, + source: Collection, +) { + source.find(j.VariableDeclarator).forEach(component => { + const defaultProps = source.find(j.AssignmentExpression, { + left: { + // @ts-ignore + object: { name: component.node.id?.name }, + property: { name: 'defaultProps' }, + }, + }); + + const defaultValues = defaultProps.get('right').value.properties; + + if (defaultProps.length === 0) return; + + // Generate a new function parameter for each default prop + const defaultParams = defaultValues.map((prop: any) => { + const key = prop.key.name; + const value = prop.value.value; + return j.objectProperty( + j.identifier(key), + j.assignmentPattern(j.identifier(key), j.literal(value)), + ); + }); + + const arrowFunction = component.get('init'); + + const newParams = getNewParams(j, arrowFunction, defaultParams); + + j(component).replaceWith( + j.variableDeclarator( + // @ts-ignore + j.identifier(component.node.id.name), + j.arrowFunctionExpression(newParams!, j.blockStatement([])), + ), + ); + }); +} diff --git a/community/react/remove-default-props/motions/moveDefaultPropsToFunctionDeclaration.ts b/community/react/remove-default-props/motions/moveDefaultPropsToFunctionDeclaration.ts new file mode 100644 index 000000000..a6d4bdcb5 --- /dev/null +++ b/community/react/remove-default-props/motions/moveDefaultPropsToFunctionDeclaration.ts @@ -0,0 +1,53 @@ +import { JSCodeshift, Collection } from 'jscodeshift'; +import { getNewParams } from './getNewParams'; + +export function moveDefaultPropsToFunctionDeclaration( + j: JSCodeshift, + source: Collection, +) { + source + .find(j.FunctionDeclaration) + .forEach(component => { + const defaultProps = source.find(j.AssignmentExpression, { + left: { + object: { name: component.node.id?.name }, + property: { name: 'defaultProps' }, + }, + }); + + if (defaultProps.length === 0) return; + + // Extract the default props object + const defaultValues = defaultProps.get('right').value.properties; + + // Generate a new function parameter for each default prop + const defaultParams = defaultValues.map((prop: any) => { + const key = prop.key.name; + const value = prop.value.value; + // return j.objectPattern(`${key}=${JSON.stringify(value)}`); + return j.objectProperty( + j.identifier(key), + j.assignmentPattern(j.identifier(key), j.literal(value)), + ); + }); + // Find the defaultProps assignment expression + const newParams = getNewParams(j, component, defaultParams); + // Replace the original function declaration with a new one + j(component).replaceWith(nodePath => + j.functionDeclaration( + nodePath.node.id, + newParams!, + nodePath.node.body, + nodePath.node.generator, + nodePath.node.async, + ), + ); + }) + .find(j.AssignmentExpression, { + left: { + object: { type: 'Identifier' }, + property: { name: 'defaultProps' }, + }, + }) + .toSource(); +} diff --git a/community/react/remove-default-props/motions/removeDefaultPropsAssignment.ts b/community/react/remove-default-props/motions/removeDefaultPropsAssignment.ts new file mode 100644 index 000000000..b95a8f5bb --- /dev/null +++ b/community/react/remove-default-props/motions/removeDefaultPropsAssignment.ts @@ -0,0 +1,18 @@ +import { JSCodeshift } from 'jscodeshift'; +import { Collection } from 'jscodeshift/src/Collection'; + +export function removeDefaultPropsAssignment( + j: JSCodeshift, + source: Collection, +) { + const removePath = (path: any) => j(path).remove(); + const isAssigningDefaultProps = (e: any) => + e.node.left && + e.node.left.property && + e.node.left.property.name === 'defaultProps'; + + return source + .find(j.AssignmentExpression) + .filter(isAssigningDefaultProps) + .forEach(removePath); +} diff --git a/community/react/remove-default-props/transform.spec.ts b/community/react/remove-default-props/transform.spec.ts index ef098f3cd..675e8d224 100644 --- a/community/react/remove-default-props/transform.spec.ts +++ b/community/react/remove-default-props/transform.spec.ts @@ -2,21 +2,236 @@ import { applyTransform } from '@codeshift/test-utils'; import * as transformer from './transform'; describe('react#remove-default-props transform', () => { - it('should remove default props', async () => { - const result = await applyTransform( - transformer, - ` -import React from 'react'; - -export const Greet = ({ name }) => Hi {name}; -Greet.defaultProps = { name: 'Stranger' }; - `, - { parser: 'tsx' }, - ); - - expect(result).toMatchInlineSnapshot(` - import React from 'react'; export const Greet = ({ name }) => - Hi {name}; - `); + describe('components with function declaration', () => { + it('should move default props when no existing props', async () => { + const result = await applyTransform( + transformer, + ` + import React from 'react'; + export function Greet(){ Hi {name}; } + Greet.defaultProps = { name: 'Stranger' }; + `, + { parser: 'tsx' }, + ); + expect(result).toMatchInlineSnapshot(` + import React from 'react'; export function Greet( { name: name = "Stranger" + } ) { + Hi {name}; } + `); + }); + + it('when there are other props', async () => { + const result = await applyTransform( + transformer, + ` + import React from 'react'; + export function Greet({text}){ Hi {name} {text}; } + Greet.defaultProps = { name: 'Stranger' }; + `, + { parser: 'tsx' }, + ); + expect(result).toMatchInlineSnapshot(` + import React from 'react'; export function Greet( { text: text, name: + name = "Stranger" } ) { + Hi {name} {text}; } + `); + }); + + it('when there are other destructured renamed props', async () => { + const result = await applyTransform( + transformer, + ` + import React from 'react'; + export function Greet({text:myText}){ Hi {name} {text}; } + Greet.defaultProps = { name: 'Stranger' }; + `, + { parser: 'tsx' }, + ); + expect(result).toMatchInlineSnapshot(` + import React from 'react'; export function Greet( { text: myText, name: + name = "Stranger" } ) { + Hi {name} {text}; } + `); + }); + + it('preserves default values for destructured components', async () => { + const result = await applyTransform( + transformer, + ` + import React from 'react'; + export function Greet({text:myText, props='amazingText'}){ Hi {name} {text}; } + Greet.defaultProps = { name: 'Stranger' }; + `, + { parser: 'tsx' }, + ); + expect(result).toMatchInlineSnapshot(` + import React from 'react'; export function Greet( { text: myText, props: + props='amazingText', name: name = "Stranger" } ) { + Hi {name} {text}; } + `); + }); + + it('when there are rest parameters', async () => { + const result = await applyTransform( + transformer, + ` + import React from 'react'; + export function Greet({prop1,...someRest}){ Hi {name} {text}; } + Greet.defaultProps = { name: 'Stranger' }; + `, + { parser: 'tsx' }, + ); + expect(result).toMatchInlineSnapshot(` + import React from 'react'; export function Greet( { prop1: prop1, name: + name = "Stranger", ...someRest } ) { + Hi {name} {text}; } + `); + }); + + it('works with any props parameter passed', async () => { + const result = await applyTransform( + transformer, + ` + import React from 'react'; + export function Greet(props){ Hi {name} {text}; } + Greet.defaultProps = { name: 'Stranger' }; + `, + { parser: 'tsx' }, + ); + expect(result).toMatchInlineSnapshot(` + import React from 'react'; export function Greet( { ...props, name: name + = "Stranger" } ) { + Hi {name} {text}; } + `); + }); + }); + + describe('components with as arrow functions', () => { + it('should move default props when no existing props', async () => { + const result = await applyTransform( + transformer, + ` + import React from 'react'; + export const Greet = () => { Hi {name}; } + Greet.defaultProps = { name: 'Stranger' }; + `, + { parser: 'tsx' }, + ); + expect(result).toMatchInlineSnapshot(` + "import React from 'react'; + export const Greet = ( + { + name: name = \\"Stranger\\" + } + ) => {}" + `); + }); + + it('when there are other props', async () => { + const result = await applyTransform( + transformer, + ` + import React from 'react'; + export const Greet = ({text}) => { Hi {name} {text}; } + Greet.defaultProps = { name: 'Stranger' }; + `, + { parser: 'tsx' }, + ); + expect(result).toMatchInlineSnapshot(` + "import React from 'react'; + export const Greet = ( + { + text: text, + name: name = \\"Stranger\\" + } + ) => {}" + `); + }); + + it('when there are other destructured renamed props', async () => { + const result = await applyTransform( + transformer, + ` + import React from 'react'; + export const Greet = ({text:myText}) => { Hi {name} {text}; } + Greet.defaultProps = { name: 'Stranger' }; + `, + { parser: 'tsx' }, + ); + expect(result).toMatchInlineSnapshot(` + "import React from 'react'; + export const Greet = ( + { + text: myText, + name: name = \\"Stranger\\" + } + ) => {}" + `); + }); + + it('preserves default values for destructured components', async () => { + const result = await applyTransform( + transformer, + ` + import React from 'react'; + export const Greet = ({text:myText, props='amazingText'}) => { Hi {name} {text}; } + Greet.defaultProps = { name: 'Stranger' }; + `, + { parser: 'tsx' }, + ); + expect(result).toMatchInlineSnapshot(` + "import React from 'react'; + export const Greet = ( + { + text: myText, + props: props='amazingText', + name: name = \\"Stranger\\" + } + ) => {}" + `); + }); + + it('when there are rest parameters', async () => { + const result = await applyTransform( + transformer, + ` + import React from 'react'; + export const Greet = ({prop1,...someRest}) =>{ Hi {name} {text}; } + Greet.defaultProps = { name: 'Stranger' }; + `, + { parser: 'tsx' }, + ); + expect(result).toMatchInlineSnapshot(` + "import React from 'react'; + export const Greet = ( + { + prop1: prop1, + name: name = \\"Stranger\\", + ...someRest + } + ) => {}" + `); + }); + + it('works with any props parameter passed', async () => { + const result = await applyTransform( + transformer, + ` + import React from 'react'; + export const Greet = (props) => { Hi {name} {text}; } + Greet.defaultProps = { name: 'Stranger' }; + `, + { parser: 'tsx' }, + ); + expect(result).toMatchInlineSnapshot(` + "import React from 'react'; + export const Greet = ( + { + ...props, + name: name = \\"Stranger\\" + } + ) => {}" + `); + }); }); }); diff --git a/community/react/remove-default-props/transform.ts b/community/react/remove-default-props/transform.ts index eea4b99d3..7db75d6ca 100644 --- a/community/react/remove-default-props/transform.ts +++ b/community/react/remove-default-props/transform.ts @@ -1,19 +1,19 @@ -import { API, FileInfo, Options } from 'jscodeshift'; +import { FileInfo, API } from 'jscodeshift'; +import { applyMotions } from '../../../packages/utils/src'; -export default function transformer( - file: FileInfo, - { jscodeshift: j }: API, - options: Options, -) { - const removePath = (path: any) => j(path).remove(); - const isAssigningDefaultProps = (e: any) => - e.node.left && - e.node.left.property && - e.node.left.property.name === 'defaultProps'; +import { moveDefaultPropsToArrowFunctionExpression } from './motions/moveDefaultPropsToArrowFunctionExpression'; +import { moveDefaultPropsToFunctionDeclaration } from './motions/moveDefaultPropsToFunctionDeclaration'; +import { removeDefaultPropsAssignment } from './motions/removeDefaultPropsAssignment'; - return j(file.source) - .find(j.AssignmentExpression) - .filter(isAssigningDefaultProps) - .forEach(removePath) - .toSource(options.printOptions); +export default function transformer(file: FileInfo, api: API) { + const j = api.jscodeshift; + const source = j(file.source); + + applyMotions(j, source, [ + moveDefaultPropsToFunctionDeclaration, + moveDefaultPropsToArrowFunctionExpression, + removeDefaultPropsAssignment, + ]); + + return source.toSource(); }