Skip to content

Instantly share code, notes, and snippets.

@ZachGawlik
Last active January 3, 2018 02:50
Show Gist options
  • Save ZachGawlik/effabb207e37a1bc3c02593e0b751996 to your computer and use it in GitHub Desktop.
Save ZachGawlik/effabb207e37a1bc3c02593e0b751996 to your computer and use it in GitHub Desktop.
factory-to-create-element codemod v0.1

Converts factory-style React to React.createElement style. This can then be fed to react-codemod's create-element-to-jsx. Along the way, it'll also add a React import when needed and remove react-dom-factories imports.

Run using jscodeshift like so:

jscodeshift -t /path/to/factory-to-create-element.js /directory/to/transform/**/*.js

Example

Input:

MyComponent({ prop1: val1 }, div({}, "Hello World!"));

After factory-to-create-element:

React.createElement(
  MyComponent,
  { prop1: val1 },
  React.createElement("div", {}, "Hello World!")
);

After create-element-to-jsx:

<MyComponent prop1={val1}>
  <div>Hello World!</div>
</MyComponent>

Further steps

Following using this codemod, you can uninstall react-dom-factories. You should also remove any components/imports wrapped in React.createFactory.

Known errors:

  • Will botch non-factory PascalCase functions
"use strict";
const isFuncIdentifier = (() => {
const isCallExpression = path => path.parent.node.type === "CallExpression"
const isCallee = path => path.parent.node.callee &&
path.parent.node.callee.name === path.node.name;
return (path) => isCallExpression(path) && isCallee(path);
})();
const addReactImport = (() => {
function useImportSyntax(j, root) {
return root
.find(j.ImportDeclaration, {
importKind: 'value'
})
.length > 0;
}
function hasReactImport(j, root) {
return root
.find(j.ImportDeclaration, {
source: {value: 'react'}
})
.length > 0;
}
function hasReactRequire(j, root) {
return root.find(j.CallExpression, {
callee: {name: 'require'},
arguments: {0: {value: 'react'}}
}).length > 0;
}
function findFirstRequire(j, root) {
return root
.find(j.CallExpression, {callee: {name: 'require'}})
.at(0)
.paths()[0];
}
function findFirstImport(j, root) {
return root.find(j.ImportDeclaration).at(0).paths()[0];
}
function retainLeadingComment(j, root, newFirstLine) {
const firstNode = root.find(j.Program).get('body', 0).node;
const {comments} = firstNode;
if (comments) {
delete firstNode.comments;
newFirstLine.comments = comments;
}
}
return (j, root) => {
if (useImportSyntax(j, root)) {
if (hasReactImport(j, root)) {
return;
}
const path = findFirstImport(j, root);
if (path) {
const importStatement = j.importDeclaration(
[j.importDefaultSpecifier(j.identifier('React'))],
j.literal('react')
);
retainLeadingComment(j, root, newFirstLine);
j(path).insertBefore(importStatement);
}
} else {
if (hasReactRequire(j, root)) {
return;
}
const path = findFirstRequire(j, root);
if (path) {
const requireStatement = j.template.statement([
"const React = require('react');\n"
]);
j(path.parent.parent).insertAfter(requireStatement);
}
}
};
})();
const deleteReactDomImport = (() => {
return (j, root) => {
const reactDomFactoriesRequire = root.find(j.VariableDeclarator, {
id: {
type: "ObjectPattern"
},
init: {
callee: {name: 'require'},
arguments: [
{
value: 'react-dom-factories'
}
]
}
});
if (reactDomFactoriesRequire.length === 0) {
return false;
}
const requireLine = reactDomFactoriesRequire.paths()[0];
const requiredDomElements = requireLine.node.id.properties.map(
property => property.key.name
);
reactDomFactoriesRequire.remove();
return true;
}
})();
module.exports = function(file, api) {
const j = api.jscodeshift;
const root = j(file.source);
// matches dom elements of form `div(props, children)` etc
const isDomIdentifier = path => isFuncIdentifier(path) &&
importedDomElements.includes(path.node.name)
// Matches factory components. e.g. MyTooltip(props, children);
const isCustomFactoryIdentifier = path => isFuncIdentifier(path) &&
path.node.name[0] === path.node.name[0].toUpperCase();
const isFactoryIdentifier = path => isDomIdentifier(path) ||
isCustomFactoryIdentifier(path)
function replaceNonDomFactory(path) {
const args = path.parent.node.arguments;
const factoryPath = path.parent.node;
const factoryName = factoryPath.callee.name;
changedFactoryNames.push(factoryName);
if (isDomIdentifier(path)) {
// Replace div(props, children) => React.createElement('div', props, children)
args.unshift(j.literal(factoryName));
} else {
// Replace MyTooltip(props, children) => React.createElement(MyTooltip, props, children)
args.unshift(factoryName);
}
path.node.name = "React.createElement";
}
function replaceFactoriesWithCreateElement(j, root) {
const factoryMethods = root.find(j.Identifier).filter(isFactoryIdentifier)
factoryMethods.forEach(replaceNonDomFactory);
return factoryMethods.length > 0;
}
function changeFromFactoryImports(j, root) {
[...new Set(changedFactoryNames)].map(factoryName => {
const requires = root.find(j.VariableDeclarator, {
id: {
name: factoryName
},
init: {
callee: {name: 'require'}
}
}).length;
});
}
function deleteReactDomImport(j, root) {
const reactDomFactoriesRequire = root.find(j.VariableDeclarator, {
id: {
type: "ObjectPattern"
},
init: {
callee: {name: 'require'},
arguments: [
{
value: 'react-dom-factories'
}
]
}
});
if (reactDomFactoriesRequire.length === 0) {
return false;
}
const requireLine = reactDomFactoriesRequire.paths()[0];
importedDomElements = requireLine.node.id.properties.map(
property => property.key.name
);
reactDomFactoriesRequire.remove();
return true;
}
const changedFactoryNames = []
let importedDomElements = [];
deleteReactDomImport(j, root);
const hasModifications = replaceFactoriesWithCreateElement(j, root);
if (hasModifications) {
addReactImport(j, root);
changeFromFactoryImports(j, root);
}
return hasModifications ? root.toSource({ quote: "single" }) : null;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment