diff --git a/codemods/react/create-element-to-jsx/.codemodrc.json b/codemods/react/create-element-to-jsx/.codemodrc.json
new file mode 100644
index 00000000..3aaea8cd
--- /dev/null
+++ b/codemods/react/create-element-to-jsx/.codemodrc.json
@@ -0,0 +1,13 @@
+{
+ "$schema": "https://codemod-utils.s3.us-west-1.amazonaws.com/configuration_schema.json",
+ "name": "react/create-element-to-jsx",
+ "description": "Transform React.createElement to JSX",
+ "version": "1.0.0",
+ "engine": "jscodeshift",
+ "private": false,
+ "arguments": [],
+ "meta": {
+ "tags": ["react", "migration"],
+ "git": "https://github.com/reactjs/react-codemod/tree/master/transforms"
+ }
+}
diff --git a/codemods/react/create-element-to-jsx/.gitignore b/codemods/react/create-element-to-jsx/.gitignore
new file mode 100644
index 00000000..76add878
--- /dev/null
+++ b/codemods/react/create-element-to-jsx/.gitignore
@@ -0,0 +1,2 @@
+node_modules
+dist
\ No newline at end of file
diff --git a/codemods/react/create-element-to-jsx/LICENSE b/codemods/react/create-element-to-jsx/LICENSE
new file mode 100644
index 00000000..9e051010
--- /dev/null
+++ b/codemods/react/create-element-to-jsx/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2015-present, Facebook, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/codemods/react/create-element-to-jsx/README.md b/codemods/react/create-element-to-jsx/README.md
new file mode 100644
index 00000000..26c25b03
--- /dev/null
+++ b/codemods/react/create-element-to-jsx/README.md
@@ -0,0 +1,25 @@
+This codemod transforms React.createElement calls into JSX syntax, making your code more readable and maintainable.
+
+## Example
+
+### Before
+
+```tsx
+return React.createElement(
+ "div",
+ { className: "container" },
+ React.createElement("h1", null, "Hello World"),
+ React.createElement("p", { style: { color: "blue" } }, "Welcome to React")
+);
+```
+
+### After
+
+```tsx
+return (
+
+
Hello World
+
Welcome to React
+
+);
+```
diff --git a/codemods/react/create-element-to-jsx/__testfixtures__/fixture1.input.jsx b/codemods/react/create-element-to-jsx/__testfixtures__/fixture1.input.jsx
new file mode 100644
index 00000000..bc07fd30
--- /dev/null
+++ b/codemods/react/create-element-to-jsx/__testfixtures__/fixture1.input.jsx
@@ -0,0 +1,7 @@
+import React from 'react';
+
+function App() {
+ return React.createElement('div', null, 'Hello World');
+}
+
+export default App;
\ No newline at end of file
diff --git a/codemods/react/create-element-to-jsx/__testfixtures__/fixture1.output.jsx b/codemods/react/create-element-to-jsx/__testfixtures__/fixture1.output.jsx
new file mode 100644
index 00000000..91003aa1
--- /dev/null
+++ b/codemods/react/create-element-to-jsx/__testfixtures__/fixture1.output.jsx
@@ -0,0 +1,11 @@
+import React from 'react';
+
+function App() {
+ return (
+
+ Hello World
+
+ );
+}
+
+export default App;
\ No newline at end of file
diff --git a/codemods/react/create-element-to-jsx/__testfixtures__/fixture2.input.jsx b/codemods/react/create-element-to-jsx/__testfixtures__/fixture2.input.jsx
new file mode 100644
index 00000000..6f3caba1
--- /dev/null
+++ b/codemods/react/create-element-to-jsx/__testfixtures__/fixture2.input.jsx
@@ -0,0 +1,15 @@
+import React from 'react';
+
+function Button({ disabled }) {
+ return React.createElement(
+ 'button',
+ {
+ className: 'btn',
+ onClick: () => console.log('clicked'),
+ disabled
+ },
+ 'Click me'
+ );
+}
+
+export default Button;
\ No newline at end of file
diff --git a/codemods/react/create-element-to-jsx/__testfixtures__/fixture2.output.jsx b/codemods/react/create-element-to-jsx/__testfixtures__/fixture2.output.jsx
new file mode 100644
index 00000000..00b93033
--- /dev/null
+++ b/codemods/react/create-element-to-jsx/__testfixtures__/fixture2.output.jsx
@@ -0,0 +1,14 @@
+import React from 'react';
+
+function Button({ disabled }) {
+ return (
+ console.log('clicked')}
+ disabled={disabled}>
+ Click me
+
+ );
+}
+
+export default Button;
\ No newline at end of file
diff --git a/codemods/react/create-element-to-jsx/__testfixtures__/fixture3.input.jsx b/codemods/react/create-element-to-jsx/__testfixtures__/fixture3.input.jsx
new file mode 100644
index 00000000..5e17c747
--- /dev/null
+++ b/codemods/react/create-element-to-jsx/__testfixtures__/fixture3.input.jsx
@@ -0,0 +1,26 @@
+import React from 'react';
+
+const Button = ({ onClick, children }) => React.createElement(
+ 'button',
+ { onClick },
+ children
+);
+
+function App() {
+ return React.createElement(
+ 'div',
+ { className: 'container', id: 'main' },
+ React.createElement(
+ 'h1',
+ { style: { color: 'blue' } },
+ 'Hello World'
+ ),
+ React.createElement(
+ Button,
+ { onClick: () => console.log('clicked') },
+ 'Click me'
+ )
+ );
+}
+
+export default App;
\ No newline at end of file
diff --git a/codemods/react/create-element-to-jsx/__testfixtures__/fixture3.output.jsx b/codemods/react/create-element-to-jsx/__testfixtures__/fixture3.output.jsx
new file mode 100644
index 00000000..c2f9290f
--- /dev/null
+++ b/codemods/react/create-element-to-jsx/__testfixtures__/fixture3.output.jsx
@@ -0,0 +1,20 @@
+import React from 'react';
+
+const Button = ({ onClick, children }) =>
+ {children}
+ ;
+
+function App() {
+ return (
+
+
+ Hello World
+
+ console.log('clicked')}>
+ Click me
+
+
+ );
+}
+
+export default App;
\ No newline at end of file
diff --git a/codemods/react/create-element-to-jsx/cdmd_dist/index.cjs b/codemods/react/create-element-to-jsx/cdmd_dist/index.cjs
new file mode 100644
index 00000000..13ee3baf
--- /dev/null
+++ b/codemods/react/create-element-to-jsx/cdmd_dist/index.cjs
@@ -0,0 +1,251 @@
+module.exports = function transform(file, api, options) {
+ const j = api.jscodeshift;
+ const printOptions = options.printOptions || {};
+ const root = j(file.source);
+ const ReactUtils = require("@react-codemods/utils")(j);
+ const encodeJSXTextValue = (value) =>
+ value.replace(//g, ">");
+
+ const canLiteralBePropString = (node) =>
+ node.raw.indexOf("\\") === -1 && node.value.indexOf('"') === -1;
+
+ const convertExpressionToJSXAttributes = (expression) => {
+ if (!expression) {
+ return {
+ attributes: [],
+ extraComments: [],
+ };
+ }
+
+ const isReactSpread =
+ expression.type === "CallExpression" &&
+ expression.callee.type === "MemberExpression" &&
+ expression.callee.object.name === "React" &&
+ expression.callee.property.name === "__spread";
+
+ const isObjectAssign =
+ expression.type === "CallExpression" &&
+ expression.callee.type === "MemberExpression" &&
+ expression.callee.object.name === "Object" &&
+ expression.callee.property.name === "assign";
+
+ const validSpreadTypes = [
+ "Identifier",
+ "MemberExpression",
+ "CallExpression",
+ ];
+
+ if (isReactSpread || isObjectAssign) {
+ const resultAttributes = [];
+ const resultExtraComments = expression.comments || [];
+ const { callee } = expression;
+ for (const node of [callee, callee.object, callee.property]) {
+ resultExtraComments.push(...(node.comments || []));
+ }
+ expression.arguments.forEach((expression) => {
+ const { attributes, extraComments } =
+ convertExpressionToJSXAttributes(expression);
+ resultAttributes.push(...attributes);
+ resultExtraComments.push(...extraComments);
+ });
+
+ return {
+ attributes: resultAttributes,
+ extraComments: resultExtraComments,
+ };
+ } else if (validSpreadTypes.indexOf(expression.type) != -1) {
+ return {
+ attributes: [j.jsxSpreadAttribute(expression)],
+ extraComments: [],
+ };
+ } else if (expression.type === "ObjectExpression") {
+ const attributes = expression.properties.map((property) => {
+ if (property.type === "SpreadProperty") {
+ const spreadAttribute = j.jsxSpreadAttribute(property.argument);
+ spreadAttribute.comments = property.comments;
+ return spreadAttribute;
+ } else if (property.type === "Property") {
+ const propertyValueType = property.value.type;
+
+ let value;
+ if (
+ propertyValueType === "Literal" &&
+ typeof property.value.value === "string" &&
+ canLiteralBePropString(property.value)
+ ) {
+ value = j.literal(property.value.value);
+ value.comments = property.value.comments;
+ } else {
+ value = j.jsxExpressionContainer(property.value);
+ }
+
+ let jsxIdentifier;
+ if (property.key.type === "Literal") {
+ jsxIdentifier = j.jsxIdentifier(property.key.value);
+ } else {
+ jsxIdentifier = j.jsxIdentifier(property.key.name);
+ }
+ jsxIdentifier.comments = property.key.comments;
+
+ const jsxAttribute = j.jsxAttribute(jsxIdentifier, value);
+ jsxAttribute.comments = property.comments;
+ return jsxAttribute;
+ }
+ return null;
+ });
+
+ return {
+ attributes,
+ extraComments: expression.comments || [],
+ };
+ } else if (expression.type === "Literal" && expression.value === null) {
+ return {
+ attributes: [],
+ extraComments: expression.comments || [],
+ };
+ } else {
+ throw new Error(`Unexpected attribute of type "${expression.type}"`);
+ }
+ };
+
+ const canConvertToJSXIdentifier = (node) =>
+ (node.type === "Literal" && typeof node.value === "string") ||
+ node.type === "Identifier" ||
+ (node.type === "MemberExpression" &&
+ !node.computed &&
+ canConvertToJSXIdentifier(node.object) &&
+ canConvertToJSXIdentifier(node.property));
+
+ const jsxIdentifierFor = (node) => {
+ let identifier;
+ let comments = node.comments || [];
+ if (node.type === "Literal") {
+ identifier = j.jsxIdentifier(node.value);
+ } else if (node.type === "MemberExpression") {
+ let { identifier: objectIdentifier, comments: objectComments } =
+ jsxIdentifierFor(node.object);
+ let { identifier: propertyIdentifier, comments: propertyComments } =
+ jsxIdentifierFor(node.property);
+ identifier = j.jsxMemberExpression(objectIdentifier, propertyIdentifier);
+ comments.push(...objectComments, ...propertyComments);
+ } else {
+ identifier = j.jsxIdentifier(node.name);
+ }
+ return { identifier, comments };
+ };
+
+ const isCapitalizationInvalid = (node) =>
+ (node.type === "Literal" && !/^[a-z]/.test(node.value)) ||
+ (node.type === "Identifier" && /^[a-z]/.test(node.name));
+
+ const convertNodeToJSX = (node) => {
+ const comments = node.value.comments || [];
+ const { callee } = node.value;
+ for (const calleeNode of [callee, callee.object, callee.property]) {
+ for (const comment of calleeNode.comments || []) {
+ comment.leading = true;
+ comment.trailing = false;
+ comments.push(comment);
+ }
+ }
+
+ const args = node.value.arguments;
+
+ if (
+ isCapitalizationInvalid(args[0]) ||
+ !canConvertToJSXIdentifier(args[0])
+ ) {
+ return node.value;
+ }
+
+ const { identifier: jsxIdentifier, comments: identifierComments } =
+ jsxIdentifierFor(args[0]);
+ const props = args[1];
+
+ const { attributes, extraComments } =
+ convertExpressionToJSXAttributes(props);
+
+ for (const comment of [...identifierComments, ...extraComments]) {
+ comment.leading = false;
+ comment.trailing = true;
+ comments.push(comment);
+ }
+
+ const children = args.slice(2).map((child, index) => {
+ if (
+ child.type === "Literal" &&
+ typeof child.value === "string" &&
+ !child.comments &&
+ child.value !== "" &&
+ child.value.trim() === child.value
+ ) {
+ return j.jsxText(encodeJSXTextValue(child.value));
+ } else if (
+ child.type === "CallExpression" &&
+ child.callee.object &&
+ child.callee.object.name === "React" &&
+ child.callee.property.name === "createElement"
+ ) {
+ const jsxChild = convertNodeToJSX(node.get("arguments", index + 2));
+ if (
+ jsxChild.type !== "JSXElement" ||
+ (jsxChild.comments || []).length > 0
+ ) {
+ return j.jsxExpressionContainer(jsxChild);
+ } else {
+ return jsxChild;
+ }
+ } else if (child.type === "SpreadElement") {
+ return j.jsxExpressionContainer(child.argument);
+ } else {
+ return j.jsxExpressionContainer(child);
+ }
+ });
+
+ const openingElement = j.jsxOpeningElement(jsxIdentifier, attributes);
+
+ if (children.length) {
+ const endIdentifier = Object.assign({}, jsxIdentifier, { comments: [] });
+ // Add text newline nodes between elements so recast formats one child per
+ // line instead of all children on one line.
+ const paddedChildren = [j.jsxText("\n")];
+ for (const child of children) {
+ paddedChildren.push(child, j.jsxText("\n"));
+ }
+ const element = j.jsxElement(
+ openingElement,
+ j.jsxClosingElement(endIdentifier),
+ paddedChildren,
+ );
+ element.comments = comments;
+ return element;
+ } else {
+ openingElement.selfClosing = true;
+ const element = j.jsxElement(openingElement);
+ element.comments = comments;
+ return element;
+ }
+ };
+
+ if (options["explicit-require"] === false || ReactUtils.hasReact(root)) {
+ const mutations = root
+ .find(j.CallExpression, {
+ callee: {
+ object: {
+ name: "React",
+ },
+ property: {
+ name: "createElement",
+ },
+ },
+ })
+ .replaceWith(convertNodeToJSX)
+ .size();
+
+ if (mutations) {
+ return root.toSource(printOptions);
+ }
+ }
+
+ return null;
+};
diff --git a/codemods/react/create-element-to-jsx/package.json b/codemods/react/create-element-to-jsx/package.json
new file mode 100644
index 00000000..ba26015f
--- /dev/null
+++ b/codemods/react/create-element-to-jsx/package.json
@@ -0,0 +1,27 @@
+{
+ "name": "create-element-to-jsx",
+ "license": "MIT",
+ "devDependencies": {
+ "@codemod.com/codemod-utils": "*",
+ "@types/jscodeshift": "^0.11.10",
+ "@types/node": "20.9.0",
+ "jscodeshift": "^0.15.1",
+ "typescript": "^5.2.2",
+ "vitest": "^1.0.1",
+ "@react-codemods/utils": "workspace:*"
+ },
+ "scripts": {
+ "test": "vitest run",
+ "test:watch": "vitest watch"
+ },
+ "files": [
+ "README.md",
+ ".codemodrc.json",
+ "/dist/index.cjs"
+ ],
+ "type": "module",
+ "author": "amirabbas-gh",
+ "dependencies": {
+ "ast-types": "^0.14.2"
+ }
+}
diff --git a/codemods/react/create-element-to-jsx/src/index.cjs b/codemods/react/create-element-to-jsx/src/index.cjs
new file mode 100644
index 00000000..13ee3baf
--- /dev/null
+++ b/codemods/react/create-element-to-jsx/src/index.cjs
@@ -0,0 +1,251 @@
+module.exports = function transform(file, api, options) {
+ const j = api.jscodeshift;
+ const printOptions = options.printOptions || {};
+ const root = j(file.source);
+ const ReactUtils = require("@react-codemods/utils")(j);
+ const encodeJSXTextValue = (value) =>
+ value.replace(//g, ">");
+
+ const canLiteralBePropString = (node) =>
+ node.raw.indexOf("\\") === -1 && node.value.indexOf('"') === -1;
+
+ const convertExpressionToJSXAttributes = (expression) => {
+ if (!expression) {
+ return {
+ attributes: [],
+ extraComments: [],
+ };
+ }
+
+ const isReactSpread =
+ expression.type === "CallExpression" &&
+ expression.callee.type === "MemberExpression" &&
+ expression.callee.object.name === "React" &&
+ expression.callee.property.name === "__spread";
+
+ const isObjectAssign =
+ expression.type === "CallExpression" &&
+ expression.callee.type === "MemberExpression" &&
+ expression.callee.object.name === "Object" &&
+ expression.callee.property.name === "assign";
+
+ const validSpreadTypes = [
+ "Identifier",
+ "MemberExpression",
+ "CallExpression",
+ ];
+
+ if (isReactSpread || isObjectAssign) {
+ const resultAttributes = [];
+ const resultExtraComments = expression.comments || [];
+ const { callee } = expression;
+ for (const node of [callee, callee.object, callee.property]) {
+ resultExtraComments.push(...(node.comments || []));
+ }
+ expression.arguments.forEach((expression) => {
+ const { attributes, extraComments } =
+ convertExpressionToJSXAttributes(expression);
+ resultAttributes.push(...attributes);
+ resultExtraComments.push(...extraComments);
+ });
+
+ return {
+ attributes: resultAttributes,
+ extraComments: resultExtraComments,
+ };
+ } else if (validSpreadTypes.indexOf(expression.type) != -1) {
+ return {
+ attributes: [j.jsxSpreadAttribute(expression)],
+ extraComments: [],
+ };
+ } else if (expression.type === "ObjectExpression") {
+ const attributes = expression.properties.map((property) => {
+ if (property.type === "SpreadProperty") {
+ const spreadAttribute = j.jsxSpreadAttribute(property.argument);
+ spreadAttribute.comments = property.comments;
+ return spreadAttribute;
+ } else if (property.type === "Property") {
+ const propertyValueType = property.value.type;
+
+ let value;
+ if (
+ propertyValueType === "Literal" &&
+ typeof property.value.value === "string" &&
+ canLiteralBePropString(property.value)
+ ) {
+ value = j.literal(property.value.value);
+ value.comments = property.value.comments;
+ } else {
+ value = j.jsxExpressionContainer(property.value);
+ }
+
+ let jsxIdentifier;
+ if (property.key.type === "Literal") {
+ jsxIdentifier = j.jsxIdentifier(property.key.value);
+ } else {
+ jsxIdentifier = j.jsxIdentifier(property.key.name);
+ }
+ jsxIdentifier.comments = property.key.comments;
+
+ const jsxAttribute = j.jsxAttribute(jsxIdentifier, value);
+ jsxAttribute.comments = property.comments;
+ return jsxAttribute;
+ }
+ return null;
+ });
+
+ return {
+ attributes,
+ extraComments: expression.comments || [],
+ };
+ } else if (expression.type === "Literal" && expression.value === null) {
+ return {
+ attributes: [],
+ extraComments: expression.comments || [],
+ };
+ } else {
+ throw new Error(`Unexpected attribute of type "${expression.type}"`);
+ }
+ };
+
+ const canConvertToJSXIdentifier = (node) =>
+ (node.type === "Literal" && typeof node.value === "string") ||
+ node.type === "Identifier" ||
+ (node.type === "MemberExpression" &&
+ !node.computed &&
+ canConvertToJSXIdentifier(node.object) &&
+ canConvertToJSXIdentifier(node.property));
+
+ const jsxIdentifierFor = (node) => {
+ let identifier;
+ let comments = node.comments || [];
+ if (node.type === "Literal") {
+ identifier = j.jsxIdentifier(node.value);
+ } else if (node.type === "MemberExpression") {
+ let { identifier: objectIdentifier, comments: objectComments } =
+ jsxIdentifierFor(node.object);
+ let { identifier: propertyIdentifier, comments: propertyComments } =
+ jsxIdentifierFor(node.property);
+ identifier = j.jsxMemberExpression(objectIdentifier, propertyIdentifier);
+ comments.push(...objectComments, ...propertyComments);
+ } else {
+ identifier = j.jsxIdentifier(node.name);
+ }
+ return { identifier, comments };
+ };
+
+ const isCapitalizationInvalid = (node) =>
+ (node.type === "Literal" && !/^[a-z]/.test(node.value)) ||
+ (node.type === "Identifier" && /^[a-z]/.test(node.name));
+
+ const convertNodeToJSX = (node) => {
+ const comments = node.value.comments || [];
+ const { callee } = node.value;
+ for (const calleeNode of [callee, callee.object, callee.property]) {
+ for (const comment of calleeNode.comments || []) {
+ comment.leading = true;
+ comment.trailing = false;
+ comments.push(comment);
+ }
+ }
+
+ const args = node.value.arguments;
+
+ if (
+ isCapitalizationInvalid(args[0]) ||
+ !canConvertToJSXIdentifier(args[0])
+ ) {
+ return node.value;
+ }
+
+ const { identifier: jsxIdentifier, comments: identifierComments } =
+ jsxIdentifierFor(args[0]);
+ const props = args[1];
+
+ const { attributes, extraComments } =
+ convertExpressionToJSXAttributes(props);
+
+ for (const comment of [...identifierComments, ...extraComments]) {
+ comment.leading = false;
+ comment.trailing = true;
+ comments.push(comment);
+ }
+
+ const children = args.slice(2).map((child, index) => {
+ if (
+ child.type === "Literal" &&
+ typeof child.value === "string" &&
+ !child.comments &&
+ child.value !== "" &&
+ child.value.trim() === child.value
+ ) {
+ return j.jsxText(encodeJSXTextValue(child.value));
+ } else if (
+ child.type === "CallExpression" &&
+ child.callee.object &&
+ child.callee.object.name === "React" &&
+ child.callee.property.name === "createElement"
+ ) {
+ const jsxChild = convertNodeToJSX(node.get("arguments", index + 2));
+ if (
+ jsxChild.type !== "JSXElement" ||
+ (jsxChild.comments || []).length > 0
+ ) {
+ return j.jsxExpressionContainer(jsxChild);
+ } else {
+ return jsxChild;
+ }
+ } else if (child.type === "SpreadElement") {
+ return j.jsxExpressionContainer(child.argument);
+ } else {
+ return j.jsxExpressionContainer(child);
+ }
+ });
+
+ const openingElement = j.jsxOpeningElement(jsxIdentifier, attributes);
+
+ if (children.length) {
+ const endIdentifier = Object.assign({}, jsxIdentifier, { comments: [] });
+ // Add text newline nodes between elements so recast formats one child per
+ // line instead of all children on one line.
+ const paddedChildren = [j.jsxText("\n")];
+ for (const child of children) {
+ paddedChildren.push(child, j.jsxText("\n"));
+ }
+ const element = j.jsxElement(
+ openingElement,
+ j.jsxClosingElement(endIdentifier),
+ paddedChildren,
+ );
+ element.comments = comments;
+ return element;
+ } else {
+ openingElement.selfClosing = true;
+ const element = j.jsxElement(openingElement);
+ element.comments = comments;
+ return element;
+ }
+ };
+
+ if (options["explicit-require"] === false || ReactUtils.hasReact(root)) {
+ const mutations = root
+ .find(j.CallExpression, {
+ callee: {
+ object: {
+ name: "React",
+ },
+ property: {
+ name: "createElement",
+ },
+ },
+ })
+ .replaceWith(convertNodeToJSX)
+ .size();
+
+ if (mutations) {
+ return root.toSource(printOptions);
+ }
+ }
+
+ return null;
+};
diff --git a/codemods/react/create-element-to-jsx/test/test.ts b/codemods/react/create-element-to-jsx/test/test.ts
new file mode 100644
index 00000000..3c4770e6
--- /dev/null
+++ b/codemods/react/create-element-to-jsx/test/test.ts
@@ -0,0 +1,89 @@
+import { describe, it } from "vitest";
+import jscodeshift, { type API } from "jscodeshift";
+import transform from "../src/index.cjs";
+import assert from "node:assert";
+import { readFile } from "node:fs/promises";
+import { join } from "node:path";
+
+const buildApi = (parser: string | undefined): API => ({
+ j: parser ? jscodeshift.withParser(parser) : jscodeshift,
+ jscodeshift: parser ? jscodeshift.withParser(parser) : jscodeshift,
+ stats: () => {
+ console.error(
+ "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",
+ );
+ },
+});
+
+describe("create-element-to-jsx", () => {
+ it("test #1", async () => {
+ const INPUT = await readFile(
+ join(__dirname, "..", "__testfixtures__/fixture1.input.jsx"),
+ "utf-8",
+ );
+ const OUTPUT = await readFile(
+ join(__dirname, "..", "__testfixtures__/fixture1.output.jsx"),
+ "utf-8",
+ );
+
+ const actualOutput = transform(
+ {
+ path: "index.jsx",
+ source: INPUT,
+ },
+ buildApi("jsx"),
+ {},
+ );
+
+ assert.strictEqual(actualOutput?.trim(), OUTPUT.trim());
+ });
+
+ it("test #2", async () => {
+ const INPUT = await readFile(
+ join(__dirname, "..", "__testfixtures__/fixture2.input.jsx"),
+ "utf-8",
+ );
+ const OUTPUT = await readFile(
+ join(__dirname, "..", "__testfixtures__/fixture2.output.jsx"),
+ "utf-8",
+ );
+
+ const actualOutput = transform(
+ {
+ path: "index.jsx",
+ source: INPUT,
+ },
+ buildApi("jsx"),
+ {},
+ );
+
+ assert.strictEqual(actualOutput?.trim(), OUTPUT.trim());
+ });
+
+ it("handles spread props", async () => {
+ const INPUT = await readFile(
+ join(__dirname, "..", "__testfixtures__/fixture3.input.jsx"),
+ "utf-8",
+ );
+ const OUTPUT = await readFile(
+ join(__dirname, "..", "__testfixtures__/fixture3.output.jsx"),
+ "utf-8",
+ );
+
+ const actualOutput = transform(
+ {
+ path: "index.jsx",
+ source: INPUT,
+ },
+ buildApi("jsx"),
+ {},
+ );
+
+ assert.strictEqual(actualOutput?.trim(), OUTPUT.trim());
+ });
+});
diff --git a/codemods/react/create-element-to-jsx/tsconfig.json b/codemods/react/create-element-to-jsx/tsconfig.json
new file mode 100644
index 00000000..03c15498
--- /dev/null
+++ b/codemods/react/create-element-to-jsx/tsconfig.json
@@ -0,0 +1,36 @@
+{
+ "compilerOptions": {
+ "module": "NodeNext",
+ "target": "ESNext",
+ "moduleResolution": "NodeNext",
+ "lib": ["ESNext", "DOM"],
+ "skipLibCheck": true,
+ "esModuleInterop": true,
+ "resolveJsonModule": true,
+ "allowSyntheticDefaultImports": true,
+ "isolatedModules": true,
+ "jsx": "react-jsx",
+ "useDefineForClassFields": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUnusedLocals": false,
+ "noUnusedParameters": false,
+ "preserveWatchOutput": true,
+ "strict": true,
+ "strictNullChecks": true,
+ "incremental": true,
+ "noUncheckedIndexedAccess": true,
+ "noPropertyAccessFromIndexSignature": false,
+ "allowJs": true
+ },
+ "include": [
+ "./src/**/*.ts",
+ "./src/**/*.js",
+ "./test/**/*.ts",
+ "./test/**/*.js",
+ "src/index.cjs"
+ ],
+ "exclude": ["node_modules", "./dist/**/*"],
+ "ts-node": {
+ "transpileOnly": true
+ }
+}
diff --git a/codemods/react/create-element-to-jsx/vitest.config.ts b/codemods/react/create-element-to-jsx/vitest.config.ts
new file mode 100644
index 00000000..772ad4c3
--- /dev/null
+++ b/codemods/react/create-element-to-jsx/vitest.config.ts
@@ -0,0 +1,7 @@
+import { configDefaults, defineConfig } from "vitest/config";
+
+export default defineConfig({
+ test: {
+ include: [...configDefaults.include, "**/test/*.ts"],
+ },
+});
diff --git a/tooling/react-utils/index.js b/tooling/react-utils/index.js
new file mode 100644
index 00000000..3bc680c5
--- /dev/null
+++ b/tooling/react-utils/index.js
@@ -0,0 +1,308 @@
+/**
+ * Copyright 2015-present, Facebook, Inc.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+
+'use strict';
+
+module.exports = function(j) {
+ const REACT_CREATE_CLASS_MEMBER_EXPRESSION = {
+ type: 'MemberExpression',
+ object: {
+ name: 'React',
+ },
+ property: {
+ name: 'createClass',
+ },
+ };
+
+ // ---------------------------------------------------------------------------
+ // Checks if the file requires a certain module
+ const hasModule = (path, module) =>
+ path
+ .findVariableDeclarators()
+ .filter(j.filters.VariableDeclarator.requiresModule(module))
+ .size() === 1 ||
+ path
+ .find(j.ImportDeclaration, {
+ type: 'ImportDeclaration',
+ source: {
+ type: 'Literal',
+ },
+ })
+ .filter(declarator => declarator.value.source.value === module)
+ .size() === 1;
+
+ const hasReact = path => (
+ hasModule(path, 'React') ||
+ hasModule(path, 'react') ||
+ hasModule(path, 'react/addons') ||
+ hasModule(path, 'react-native')
+ );
+
+ // ---------------------------------------------------------------------------
+ // Finds all variable declarations that call React.createClass
+ const findReactCreateClassCallExpression = path =>
+ j(path).find(j.CallExpression, {
+ callee: REACT_CREATE_CLASS_MEMBER_EXPRESSION,
+ });
+
+ const findReactCreateClass = path =>
+ path
+ .findVariableDeclarators()
+ .filter(decl => findReactCreateClassCallExpression(decl).size() > 0);
+
+ const findReactCreateClassExportDefault = path =>
+ path.find(j.ExportDeclaration, {
+ default: true,
+ declaration: {
+ type: 'CallExpression',
+ callee: REACT_CREATE_CLASS_MEMBER_EXPRESSION,
+ },
+ });
+
+ const findReactCreateClassModuleExports = path =>
+ path
+ .find(j.AssignmentExpression, {
+ left: {
+ type: 'MemberExpression',
+ object: {
+ type: 'Identifier',
+ name: 'module',
+ },
+ property: {
+ type: 'Identifier',
+ name: 'exports',
+ },
+ },
+ right: {
+ type: 'CallExpression',
+ callee: REACT_CREATE_CLASS_MEMBER_EXPRESSION,
+ },
+ });
+
+ const getReactCreateClassSpec = classPath => {
+ const {value} = classPath;
+ const args = (value.init || value.right || value.declaration).arguments;
+ if (args && args.length) {
+ const spec = args[0];
+ if (spec.type === 'ObjectExpression' && Array.isArray(spec.properties)) {
+ return spec;
+ }
+ }
+ return null;
+ };
+
+ // ---------------------------------------------------------------------------
+ // Finds alias for React.Component if used as named import.
+ const findReactComponentNameByParent = (path, parentClassName) => {
+ const reactImportDeclaration = path
+ .find(j.ImportDeclaration, {
+ type: 'ImportDeclaration',
+ source: {
+ type: 'Literal',
+ },
+ })
+ .filter(importDeclaration => hasReact(path));
+
+ const componentImportSpecifier = reactImportDeclaration
+ .find(j.ImportSpecifier, {
+ type: 'ImportSpecifier',
+ imported: {
+ type: 'Identifier',
+ name: parentClassName,
+ },
+ }).at(0);
+
+ const paths = componentImportSpecifier.paths();
+ return paths.length
+ ? paths[0].value.local.name
+ : undefined;
+ };
+
+ const removeUnusedSuperClassImport = (path, file, superClassName) => {
+ if (path.find(j.Identifier, {
+ type: 'Identifier',
+ name: superClassName
+ }).length === 0) {
+ file.find(j.ImportSpecifier, {
+ type: 'ImportSpecifier',
+ imported: {
+ type: 'Identifier',
+ name: superClassName,
+ }
+ }).remove();
+ }
+ };
+
+ const findReactES6ClassDeclarationByParent = (path, parentClassName) => {
+ const componentImport = findReactComponentNameByParent(path, parentClassName);
+
+ const selector = componentImport
+ ? {
+ superClass: {
+ type: 'Identifier',
+ name: componentImport,
+ },
+ }
+ : {
+ superClass: {
+ type: 'MemberExpression',
+ object: {
+ type: 'Identifier',
+ name: 'React',
+ },
+ property: {
+ type: 'Identifier',
+ name: parentClassName,
+ },
+ },
+ };
+
+ return path
+ .find(j.ClassDeclaration, selector);
+ };
+
+ // Finds all classes that extend React.Component
+ const findReactES6ClassDeclaration = path => {
+ let classDeclarations = findReactES6ClassDeclarationByParent(path, 'Component');
+ if (classDeclarations.size() === 0) {
+ classDeclarations = findReactES6ClassDeclarationByParent(path, 'PureComponent');
+ }
+ return classDeclarations;
+ };
+
+ // ---------------------------------------------------------------------------
+ // Checks if the React class has mixins
+ const isMixinProperty = property => {
+ const key = property.key;
+ const value = property.value;
+ return (
+ key.name === 'mixins' &&
+ value.type === 'ArrayExpression' &&
+ Array.isArray(value.elements) &&
+ value.elements.length
+ );
+ };
+
+ const hasMixins = classPath => {
+ const spec = getReactCreateClassSpec(classPath);
+ return spec && spec.properties.some(isMixinProperty);
+ };
+
+ // ---------------------------------------------------------------------------
+ // Others
+ const getClassExtendReactSpec = classPath => classPath.value.body;
+
+ const createCreateReactClassCallExpression = properties =>
+ j.callExpression(
+ j.memberExpression(
+ j.identifier('React'),
+ j.identifier('createClass'),
+ false
+ ),
+ [j.objectExpression(properties)]
+ );
+
+ const getComponentName =
+ classPath => classPath.node.id && classPath.node.id.name;
+
+ // ---------------------------------------------------------------------------
+ // Direct methods! (see explanation below)
+ const findAllReactCreateClassCalls = path =>
+ path.find(j.CallExpression, {
+ callee: REACT_CREATE_CLASS_MEMBER_EXPRESSION,
+ });
+
+ // Mixin Stuff
+ const containSameElements = (ls1, ls2) => {
+ if (ls1.length !== ls2.length) {
+ return false;
+ }
+
+ return (
+ ls1.reduce((res, x) => res && ls2.indexOf(x) !== -1, true) &&
+ ls2.reduce((res, x) => res && ls1.indexOf(x) !== -1, true)
+ );
+ };
+
+ const keyNameIsMixins = property => property.key.name === 'mixins';
+
+ const isSpecificMixinsProperty = (property, mixinIdentifierNames) => {
+ const key = property.key;
+ const value = property.value;
+
+ return (
+ key.name === 'mixins' &&
+ value.type === 'ArrayExpression' &&
+ Array.isArray(value.elements) &&
+ value.elements.every(elem => elem.type === 'Identifier') &&
+ containSameElements(value.elements.map(elem => elem.name), mixinIdentifierNames)
+ );
+ };
+
+ // These following methods assume that the argument is
+ // a `React.createClass` call expression. In other words,
+ // they should only be used with `findAllReactCreateClassCalls`.
+ const directlyGetCreateClassSpec = classPath => {
+ if (!classPath || !classPath.value) {
+ return null;
+ }
+ const args = classPath.value.arguments;
+ if (args && args.length) {
+ const spec = args[0];
+ if (spec.type === 'ObjectExpression' && Array.isArray(spec.properties)) {
+ return spec;
+ }
+ }
+ return null;
+ };
+
+ const directlyGetComponentName = classPath => {
+ let result = '';
+ if (
+ classPath.parentPath.value &&
+ classPath.parentPath.value.type === 'VariableDeclarator'
+ ) {
+ result = classPath.parentPath.value.id.name;
+ }
+ return result;
+ };
+
+ const directlyHasMixinsField = classPath => {
+ const spec = directlyGetCreateClassSpec(classPath);
+ return spec && spec.properties.some(keyNameIsMixins);
+ };
+
+ const directlyHasSpecificMixins = (classPath, mixinIdentifierNames) => {
+ const spec = directlyGetCreateClassSpec(classPath);
+ return spec && spec.properties.some(prop => isSpecificMixinsProperty(prop, mixinIdentifierNames));
+ };
+
+ return {
+ createCreateReactClassCallExpression,
+ findReactES6ClassDeclaration,
+ findReactCreateClass,
+ findReactCreateClassCallExpression,
+ findReactCreateClassModuleExports,
+ findReactCreateClassExportDefault,
+ getComponentName,
+ getReactCreateClassSpec,
+ getClassExtendReactSpec,
+ hasMixins,
+ hasModule,
+ hasReact,
+ isMixinProperty,
+ removeUnusedSuperClassImport,
+
+ // "direct" methods
+ findAllReactCreateClassCalls,
+ directlyGetComponentName,
+ directlyGetCreateClassSpec,
+ directlyHasMixinsField,
+ directlyHasSpecificMixins,
+ };
+};
\ No newline at end of file
diff --git a/tooling/react-utils/package.json b/tooling/react-utils/package.json
new file mode 100644
index 00000000..8a1f6a27
--- /dev/null
+++ b/tooling/react-utils/package.json
@@ -0,0 +1,8 @@
+{
+ "name": "@react-codemods/utils",
+ "version": "1.0.0",
+ "private": true,
+ "files": [
+ "*.js"
+ ]
+}