From 67ecf06418f927435cec3089007dbaf31e9d4fe3 Mon Sep 17 00:00:00 2001 From: Amirabbas Ghasemi Date: Thu, 20 Feb 2025 00:49:22 +0330 Subject: [PATCH 1/4] chore: sync react 19 replace default props improvments with react codemod --- .../19/replace-default-props/src/index.ts | 111 ++++++++++++++++-- 1 file changed, 102 insertions(+), 9 deletions(-) diff --git a/codemods/react/19/replace-default-props/src/index.ts b/codemods/react/19/replace-default-props/src/index.ts index 99fc3720..1aacafed 100644 --- a/codemods/react/19/replace-default-props/src/index.ts +++ b/codemods/react/19/replace-default-props/src/index.ts @@ -7,16 +7,20 @@ import type { MemberExpression, ObjectProperty, Property, + VariableDeclaration, + RestElement, } from "jscodeshift"; -import { getFunctionName } from "@codemod.com/codemod-utils/dist/jscodeshift/function.js"; -import { getFunctionComponents } from "@codemod.com/codemod-utils/dist/jscodeshift/react.js"; +import { + getFunctionName, + getFunctionComponents, +} from "@codemod.com/codemod-utils"; const getComponentStaticPropValue = ( j: JSCodeshift, root: Collection, componentName: string, - name: string, + name: string ): ASTPath | null => { return ( root @@ -38,9 +42,8 @@ const getComponentStaticPropValue = ( const buildPropertyWithDefaultValue = ( j: JSCodeshift, property: ObjectProperty | Property, - defaultValue: any, + defaultValue: any ) => { - // Special handling for nested destructuring patterns if (property.value.type === "ObjectPattern") { return j.assignmentPattern(property.value, defaultValue); } @@ -55,7 +58,7 @@ const buildPropertyWithDefaultValue = ( export default function transform( file: FileInfo, - api: API, + api: API ): string | undefined { const j = api.jscodeshift; const root = j(file.source); @@ -68,11 +71,21 @@ export default function transform( return; } + const componentFunction = j.functionDeclaration( + j.identifier(componentName), + path.value.params, + path.value.body + ); + + if (componentFunction === null) { + return; + } + const defaultProps = getComponentStaticPropValue( j, root, componentName, - "defaultProps", + "defaultProps" ); const defaultPropsRight = defaultProps?.parent?.value?.right ?? null; @@ -82,17 +95,46 @@ export default function transform( } const defaultPropsMap = new Map(); + const defaultPropsConstants: VariableDeclaration[] = []; defaultPropsRight.properties?.forEach((property) => { if ( (j.Property.check(property) || j.ObjectProperty.check(property)) && j.Identifier.check(property.key) ) { - defaultPropsMap.set(property.key.name, property.value); + if ( + property.value.type === "ObjectExpression" || + property.value.type === "ArrayExpression" || + property.value.type === "ArrowFunctionExpression" + ) { + const constName = `${componentName[0]?.toLowerCase()}${componentName.slice( + 1 + )}DefaultProp${ + property.key.name[0]?.toUpperCase() + property.key.name.slice(1) + }`; + const constNamePath = root + .find(j.Identifier, { + name: constName, + }) + .paths(); + if (constNamePath.length) { + return defaultPropsMap.set(property.key.name, property.value); + } + defaultPropsConstants.push( + j.variableDeclaration("const", [ + j.variableDeclarator(j.identifier(constName), property.value), + ]) + ); + defaultPropsMap.set(property.key.name, j.identifier(constName)); + } else { + defaultPropsMap.set(property.key.name, property.value); + } } }); const propsArg = path.value.params.at(0); + let inlineDefaultProps: { key: string; value: any }[] = []; + let propsArgName: string | undefined; if (j.ObjectPattern.check(propsArg)) { propsArg.properties.forEach((property) => { @@ -105,11 +147,62 @@ export default function transform( property.value = buildPropertyWithDefaultValue( j, property, - defaultPropsMap.get(property.key.name), + defaultPropsMap.get(property.key.name) + ); + defaultPropsMap.delete(property.key.name); + } + } else if (j.RestElement.check(property)) { + const restElement = property as RestElement; + if (j.Identifier.check(restElement.argument)) { + propsArgName = restElement.argument.name; + inlineDefaultProps = Array.from(defaultPropsMap.entries()).map( + ([key, value]) => ({ key, value }) ); } } }); + } else if (j.Identifier.check(propsArg)) { + propsArgName = propsArg.name; + inlineDefaultProps = Array.from(defaultPropsMap.entries()).map( + ([key, value]) => ({ key, value }) + ); + } + + if (propsArgName && inlineDefaultProps.length) { + componentFunction.body.body.unshift( + j.expressionStatement( + j.assignmentExpression( + "=", + j.identifier(propsArgName), + j.objectExpression([ + j.spreadElement(j.identifier(propsArgName)), + ...inlineDefaultProps.map(({ key, value }) => + j.objectProperty( + j.identifier(key), + j.conditionalExpression( + j.binaryExpression( + "===", + j.unaryExpression( + "typeof", + j.identifier(`${propsArgName}.${key}`) + ), + j.literal("undefined") + ), + value, + j.identifier(`${propsArgName}.${key}`) + ) + ) + ), + ]) + ) + ) + ); + } + + if (defaultPropsConstants.length && path.parent) { + for (let defaultPropsConstant of defaultPropsConstants) { + path.parent.parent.insertBefore(defaultPropsConstant); + } } j(defaultProps).closest(j.ExpressionStatement).remove(); From 4e19fb52909784cfc975b4d6b63a422286596656 Mon Sep 17 00:00:00 2001 From: Amirabbas Ghasemi Date: Thu, 20 Feb 2025 00:56:12 +0330 Subject: [PATCH 2/4] chore: bump replace default props codemod version --- codemods/react/19/replace-default-props/.codemodrc.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codemods/react/19/replace-default-props/.codemodrc.json b/codemods/react/19/replace-default-props/.codemodrc.json index 4726df91..778adf34 100644 --- a/codemods/react/19/replace-default-props/.codemodrc.json +++ b/codemods/react/19/replace-default-props/.codemodrc.json @@ -1,7 +1,7 @@ { "$schema": "https://codemod-utils.s3.us-west-1.amazonaws.com/configuration_schema.json", "name": "react/19/replace-default-props", - "version": "1.0.3", + "version": "1.0.5", "engine": "jscodeshift", "private": false, "arguments": [], From 3783df3f47011305cf3a4079807b20fedb222f63 Mon Sep 17 00:00:00 2001 From: Amirabbas Ghasemi Date: Thu, 20 Feb 2025 01:10:45 +0330 Subject: [PATCH 3/4] fix: update text fixtures --- .../__testfixtures__/button-jsx-example-input.jsx | 2 +- .../__testfixtures__/button-jsx-example-output.jsx | 2 +- .../__testfixtures__/nested-destructuring.output.tsx | 6 ++++-- .../__testfixtures__/props-not-destructured.input.tsx | 5 ++++- .../__testfixtures__/props-not-destructured.output.tsx | 8 ++++++++ .../__testfixtures__/with-functions.output.tsx | 4 +++- 6 files changed, 21 insertions(+), 6 deletions(-) diff --git a/codemods/react/19/replace-default-props/__testfixtures__/button-jsx-example-input.jsx b/codemods/react/19/replace-default-props/__testfixtures__/button-jsx-example-input.jsx index 92a9d9b4..e91abad6 100644 --- a/codemods/react/19/replace-default-props/__testfixtures__/button-jsx-example-input.jsx +++ b/codemods/react/19/replace-default-props/__testfixtures__/button-jsx-example-input.jsx @@ -5,4 +5,4 @@ const Button = ({ size, color }) => { Button.defaultProps = { size: "16px", color: "blue", -}; \ No newline at end of file +}; diff --git a/codemods/react/19/replace-default-props/__testfixtures__/button-jsx-example-output.jsx b/codemods/react/19/replace-default-props/__testfixtures__/button-jsx-example-output.jsx index 154516b8..aada3529 100644 --- a/codemods/react/19/replace-default-props/__testfixtures__/button-jsx-example-output.jsx +++ b/codemods/react/19/replace-default-props/__testfixtures__/button-jsx-example-output.jsx @@ -1,3 +1,3 @@ const Button = ({ size = "16px", color = "blue" }) => { return ; -}; \ No newline at end of file +}; diff --git a/codemods/react/19/replace-default-props/__testfixtures__/nested-destructuring.output.tsx b/codemods/react/19/replace-default-props/__testfixtures__/nested-destructuring.output.tsx index 5c669c36..bdb8c58c 100644 --- a/codemods/react/19/replace-default-props/__testfixtures__/nested-destructuring.output.tsx +++ b/codemods/react/19/replace-default-props/__testfixtures__/nested-destructuring.output.tsx @@ -1,7 +1,9 @@ -const Card = ({ user: { name, age } = { +const cardDefaultPropUser = { name: "Unknown", age: 0, -} }) => { +}; + +const Card = ({ user: { name, age } = cardDefaultPropUser }) => { return (

{name}

diff --git a/codemods/react/19/replace-default-props/__testfixtures__/props-not-destructured.input.tsx b/codemods/react/19/replace-default-props/__testfixtures__/props-not-destructured.input.tsx index 1c666b74..a915c772 100644 --- a/codemods/react/19/replace-default-props/__testfixtures__/props-not-destructured.input.tsx +++ b/codemods/react/19/replace-default-props/__testfixtures__/props-not-destructured.input.tsx @@ -1,7 +1,10 @@ const C = (props) => { + console.log(props.helloWorld); return <>{props.text}; }; C.defaultProps = { - text: "test", + text: "Hello", + test: 2, + helloWorld: true, }; diff --git a/codemods/react/19/replace-default-props/__testfixtures__/props-not-destructured.output.tsx b/codemods/react/19/replace-default-props/__testfixtures__/props-not-destructured.output.tsx index 0ffeacc9..ad7f23f4 100644 --- a/codemods/react/19/replace-default-props/__testfixtures__/props-not-destructured.output.tsx +++ b/codemods/react/19/replace-default-props/__testfixtures__/props-not-destructured.output.tsx @@ -1,3 +1,11 @@ const C = (props) => { + props = { + ...props, + text: typeof props.text === "undefined" ? "Hello" : props.text, + test: typeof props.test === "undefined" ? 2 : props.test, + helloWorld: typeof props.helloWorld === "undefined" ? true : props.helloWorld + }; + + console.log(props.helloWorld); return <>{props.text}; }; diff --git a/codemods/react/19/replace-default-props/__testfixtures__/with-functions.output.tsx b/codemods/react/19/replace-default-props/__testfixtures__/with-functions.output.tsx index b90f38f1..6bd65a67 100644 --- a/codemods/react/19/replace-default-props/__testfixtures__/with-functions.output.tsx +++ b/codemods/react/19/replace-default-props/__testfixtures__/with-functions.output.tsx @@ -1,3 +1,5 @@ -const List = ({ items = [], renderItem = (item) =>
  • {item}
  • }) => { +const listDefaultPropItems = []; +const listDefaultPropRenderItem = (item) =>
  • {item}
  • ; +const List = ({ items = listDefaultPropItems, renderItem = listDefaultPropRenderItem }) => { return
      {items.map(renderItem)}
    ; }; From 737ffe4001bd2e2960b11858505e52c67551a6c1 Mon Sep 17 00:00:00 2001 From: Amirabbas Ghasemi Date: Thu, 20 Feb 2025 01:13:03 +0330 Subject: [PATCH 4/4] feat: add rest element example test case --- .../rest-element-example.input.jsx | 5 + .../rest-element-example.output.jsx | 9 ++ .../19/replace-default-props/test/test.ts | 105 +++++++++++------- 3 files changed, 78 insertions(+), 41 deletions(-) create mode 100644 codemods/react/19/replace-default-props/__testfixtures__/rest-element-example.input.jsx create mode 100644 codemods/react/19/replace-default-props/__testfixtures__/rest-element-example.output.jsx diff --git a/codemods/react/19/replace-default-props/__testfixtures__/rest-element-example.input.jsx b/codemods/react/19/replace-default-props/__testfixtures__/rest-element-example.input.jsx new file mode 100644 index 00000000..cfc1f843 --- /dev/null +++ b/codemods/react/19/replace-default-props/__testfixtures__/rest-element-example.input.jsx @@ -0,0 +1,5 @@ +const MyComp = ({foo, ...props}) => { + console.log(props.bar) +} + +MyComp.defaultProps = { foo: "hello", bar: "bye", test: 2 }; diff --git a/codemods/react/19/replace-default-props/__testfixtures__/rest-element-example.output.jsx b/codemods/react/19/replace-default-props/__testfixtures__/rest-element-example.output.jsx new file mode 100644 index 00000000..22d2bad5 --- /dev/null +++ b/codemods/react/19/replace-default-props/__testfixtures__/rest-element-example.output.jsx @@ -0,0 +1,9 @@ +const MyComp = ({foo = "hello", ...props}) => { + props = { + ...props, + bar: typeof props.bar === "undefined" ? "bye" : props.bar, + test: typeof props.test === "undefined" ? 2 : props.test + }; + + console.log(props.bar) +} diff --git a/codemods/react/19/replace-default-props/test/test.ts b/codemods/react/19/replace-default-props/test/test.ts index 29b2e46f..3a449b68 100644 --- a/codemods/react/19/replace-default-props/test/test.ts +++ b/codemods/react/19/replace-default-props/test/test.ts @@ -10,12 +10,12 @@ const buildApi = (parser: string | undefined): API => ({ jscodeshift: parser ? jscodeshift.withParser(parser) : jscodeshift, stats: () => { console.error( - "The stats function was called, which is not supported on purpose", + "The stats function was called, which is not supported on purpose" ); }, report: () => { console.error( - "The report function was called, which is not supported on purpose", + "The report function was called, which is not supported on purpose" ); }, }); @@ -24,11 +24,11 @@ describe("react/19/replace-default-props", () => { it("should correctly transform single default prop", async () => { const INPUT = await readFile( join(__dirname, "..", "__testfixtures__/single-default-prop.input.tsx"), - "utf-8", + "utf-8" ); const OUTPUT = await readFile( join(__dirname, "..", "__testfixtures__/single-default-prop.output.tsx"), - "utf-8", + "utf-8" ); const actualOutput = transform( @@ -36,12 +36,12 @@ describe("react/19/replace-default-props", () => { path: "index.js", source: INPUT, }, - buildApi("tsx"), + buildApi("tsx") ); assert.deepEqual( actualOutput?.replace(/W/gm, ""), - OUTPUT.replace(/W/gm, ""), + OUTPUT.replace(/W/gm, "") ); }); @@ -50,17 +50,17 @@ describe("react/19/replace-default-props", () => { join( __dirname, "..", - "__testfixtures__/multiple-default-props.input.tsx", + "__testfixtures__/multiple-default-props.input.tsx" ), - "utf-8", + "utf-8" ); const OUTPUT = await readFile( join( __dirname, "..", - "__testfixtures__/multiple-default-props.output.tsx", + "__testfixtures__/multiple-default-props.output.tsx" ), - "utf-8", + "utf-8" ); const actualOutput = transform( @@ -68,23 +68,23 @@ describe("react/19/replace-default-props", () => { path: "index.js", source: INPUT, }, - buildApi("tsx"), + buildApi("tsx") ); assert.deepEqual( actualOutput?.replace(/W/gm, ""), - OUTPUT.replace(/W/gm, ""), + OUTPUT.replace(/W/gm, "") ); }); it("should correctly transform nested default props", async () => { const INPUT = await readFile( join(__dirname, "..", "__testfixtures__/nested-destructuring.input.tsx"), - "utf-8", + "utf-8" ); const OUTPUT = await readFile( join(__dirname, "..", "__testfixtures__/nested-destructuring.output.tsx"), - "utf-8", + "utf-8" ); const actualOutput = transform( @@ -92,23 +92,23 @@ describe("react/19/replace-default-props", () => { path: "index.js", source: INPUT, }, - buildApi("tsx"), + buildApi("tsx") ); assert.deepEqual( actualOutput?.replace(/W/gm, ""), - OUTPUT.replace(/W/gm, ""), + OUTPUT.replace(/W/gm, "") ); }); it("should correctly transform default props with functions", async () => { const INPUT = await readFile( join(__dirname, "..", "__testfixtures__/with-functions.input.tsx"), - "utf-8", + "utf-8" ); const OUTPUT = await readFile( join(__dirname, "..", "__testfixtures__/with-functions.output.tsx"), - "utf-8", + "utf-8" ); const actualOutput = transform( @@ -116,23 +116,23 @@ describe("react/19/replace-default-props", () => { path: "index.js", source: INPUT, }, - buildApi("tsx"), + buildApi("tsx") ); assert.deepEqual( actualOutput?.replace(/W/gm, ""), - OUTPUT.replace(/W/gm, ""), + OUTPUT.replace(/W/gm, "") ); }); it("should correctly transform when props have rest prop", async () => { const INPUT = await readFile( join(__dirname, "..", "__testfixtures__/with-rest-props.input.tsx"), - "utf-8", + "utf-8" ); const OUTPUT = await readFile( join(__dirname, "..", "__testfixtures__/with-rest-props.output.tsx"), - "utf-8", + "utf-8" ); const actualOutput = transform( @@ -140,12 +140,12 @@ describe("react/19/replace-default-props", () => { path: "index.js", source: INPUT, }, - buildApi("tsx"), + buildApi("tsx") ); assert.deepEqual( actualOutput?.replace(/W/gm, ""), - OUTPUT.replace(/W/gm, ""), + OUTPUT.replace(/W/gm, "") ); }); @@ -154,17 +154,17 @@ describe("react/19/replace-default-props", () => { join( __dirname, "..", - "__testfixtures__/props-not-destructured.input.tsx", + "__testfixtures__/props-not-destructured.input.tsx" ), - "utf-8", + "utf-8" ); const OUTPUT = await readFile( join( __dirname, "..", - "__testfixtures__/props-not-destructured.output.tsx", + "__testfixtures__/props-not-destructured.output.tsx" ), - "utf-8", + "utf-8" ); const actualOutput = transform( @@ -172,27 +172,27 @@ describe("react/19/replace-default-props", () => { path: "index.js", source: INPUT, }, - buildApi("tsx"), + buildApi("tsx") ); assert.deepEqual( actualOutput?.replace(/W/gm, ""), - OUTPUT.replace(/W/gm, ""), + OUTPUT.replace(/W/gm, "") ); }); it("should correctly transform when some but not all props are defaulted", async () => { const INPUT = await readFile( join(__dirname, "..", "__testfixtures__/partial-default-props.input.tsx"), - "utf-8", + "utf-8" ); const OUTPUT = await readFile( join( __dirname, "..", - "__testfixtures__/partial-default-props.output.tsx", + "__testfixtures__/partial-default-props.output.tsx" ), - "utf-8", + "utf-8" ); const actualOutput = transform( @@ -200,23 +200,23 @@ describe("react/19/replace-default-props", () => { path: "index.js", source: INPUT, }, - buildApi("tsx"), + buildApi("tsx") ); assert.deepEqual( actualOutput?.replace(/W/gm, ""), - OUTPUT.replace(/W/gm, ""), + OUTPUT.replace(/W/gm, "") ); }); it("should correctly transform when props are not destructured", async () => { const INPUT = await readFile( join(__dirname, "..", "__testfixtures__/button-jsx-example-input.jsx"), - "utf-8", + "utf-8" ); const OUTPUT = await readFile( join(__dirname, "..", "__testfixtures__/button-jsx-example-output.jsx"), - "utf-8", + "utf-8" ); const actualOutput = transform( @@ -224,19 +224,42 @@ describe("react/19/replace-default-props", () => { path: "index.js", source: INPUT, }, - buildApi("jsx"), + buildApi("jsx") ); const fs = require("node:fs"); fs.writeFileSync( join(__dirname, "..", "__testfixtures__/button-jsx-example-output.jsx"), - actualOutput, + actualOutput ); assert.deepEqual( actualOutput?.replace(/W/gm, ""), - OUTPUT.replace(/W/gm, ""), + OUTPUT.replace(/W/gm, "") ); }); -}); + it("rest element example", async () => { + const INPUT = await readFile( + join(__dirname, "..", "__testfixtures__/rest-element-example.input.jsx"), + "utf-8" + ); + const OUTPUT = await readFile( + join(__dirname, "..", "__testfixtures__/rest-element-example.output.jsx"), + "utf-8" + ); + + const actualOutput = transform( + { + path: "index.js", + source: INPUT, + }, + buildApi("jsx") + ); + + assert.deepEqual( + actualOutput?.replace(/W/gm, ""), + OUTPUT.replace(/W/gm, "") + ); + }); +});