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": [],
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__/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/__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
;
};
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();
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, "")
+ );
+ });
+});