Skip to content

Instantly share code, notes, and snippets.

@alangpierce
Created August 31, 2016 16:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save alangpierce/8cad274de23ad9086a6b61dd21f2c22a to your computer and use it in GitHub Desktop.
Save alangpierce/8cad274de23ad9086a6b61dd21f2c22a to your computer and use it in GitHub Desktop.
Benchling jscodeshift script to convert code to use React.createElement
/**
* jscodeshift script to convert our way of creating React elements in CoffeeScript to
* React.createElement. The create-element-to-jsx script then converts the result to JSX.
*
* For example, this code:
* rd.div(myProps, child1, child2)
*
* becomes:
* React.createElement('div', myProps, child1, child2)
*
* and this code:
* const Glyphicon = React.createFactory(require('react-components/glyphicon'));
* ...
* Glyphicon(myProps, child1, child2)
*
* becomes
* import Glyphicon from 'react-components/glyphicon'
* ...
* React.createElement(Glyphicon, myProps, child1, child2)
*
* Test case:
* http://astexplorer.net/#/Pq226Pd7fS/1
*/
export default (fileInfo, api) => {
const j = api.jscodeshift;
const root = j(fileInfo.source);
const customComponents = transformComponentImports(j, root);
transformComponentUsages(j, root, customComponents);
return root.toSource();
};
// The result of this command from the coffee directory:
// grep -oh -R '\srd\.[^ ()]*\s' * | sort -u | sed 's/ rd\.\(.*\) /\1/'
const HTML_TAGS = [
'a', 'address', 'audio', 'b', 'br', 'button', 'col', 'colgroup', 'dd', 'div', 'dl', 'dt', 'em',
'fieldset', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'iframe', 'img', 'input',
'kbd', 'label', 'legend', 'li', 'ol', 'option', 'p', 'polygon', 'select', 'small', 'source',
'span', 'strong', 'sub', 'svg', 'table', 'tbody', 'td', 'textarea', 'tfoot', 'th', 'thead', 'tr',
'u', 'ul',
];
// Matchers to make jscodeshift matching a little more concise.
const call = (callee, args) => ({type: 'CallExpression', callee, arguments: args});
const access = (object, property) => ({type: 'MemberExpression', object, property});
const id = (name) => ({type: 'Identifier', name});
/**
* This function finds all usages of createFactory in imports, rewrites them to
* just import the component directly, and returns an array of the names of these
* components. That way, our later rewriting code can know to include these in the
* names of all valid tags.
*/
const transformComponentImports = (j, root) => {
const extractValues = (path) => ({
// The name of the import, e.g. "Glyphicon".
importId: path.node.declarations[0].id.name,
// The file path of the import, e.g. "react-components/glyphicon".
importPath: path.node.declarations[0].init.arguments[0].arguments[0].value,
});
const resultImportIds = [];
root.find(j.VariableDeclaration, {
declarations: [{
id: {type: 'Identifier'},
init: call(
access(id('React'), id('createFactory')),
[call(id('require'), [{type: 'Literal'}])]),
}],
}).filter((path) => {
// Make sure that the identifier being imported isn't used in some unexpected way. It should be
// used once for the import itself and aside from that, only in call expressions.
const {importId} = extractValues(path);
const numUsages = root.find(j.Identifier, id(importId)).size();
const numCalls = root.find(j.CallExpression, call(id(importId), {})).size();
return numCalls === numUsages - 1;
}).forEach((path) => {
const {importId} = extractValues(path);
resultImportIds.push(importId);
}).replaceWith((path) => {
const {importId, importPath} = extractValues(path);
const result = j.importDeclaration(
[j.importDefaultSpecifier(j.identifier(importId))],
j.literal(importPath)
);
result.comments = getAllCommentsUnderNode(path.node, j);
return result;
});
return resultImportIds;
};
const transformComponentUsages = (j, root, customComponents) => {
// Turn appropriate function calls into React.createElement.
root.find(
j.CallExpression
).forEach((path) => {
const replaceTag = getReplaceTag(j, path.node, customComponents);
if (replaceTag == null) {
return;
}
const {node} = path;
const calleeComments = getAllCommentsUnderNode(node.callee, j);
node.callee = j.memberExpression(j.identifier('React'), j.identifier('createElement'));
node.callee.comments = calleeComments;
node.arguments.unshift(replaceTag);
path.replace(node);
});
};
const getReplaceTag = (j, callNode, customComponents) => {
const {callee} = callNode;
if (callee.type === 'MemberExpression'
&& callee.object.type === 'Identifier'
&& callee.object.name === 'rd'
&& callee.property.type === 'Identifier'
&& includes(HTML_TAGS, callee.property.name)) {
return j.literal(callee.property.name);
} else if (callee.type === 'Identifier'
&& includes(customComponents, callee.name)) {
return callee.name;
}
return null;
};
// Little helper since we don't have Array.prototype.includes.
const includes = (arr, elem) => {
return arr.indexOf(elem) > -1;
};
const getAllCommentsUnderNode = (node, j) => {
const comments = [];
j.types.visit(node, {
visitNode(path) {
if (path.node.comments) {
for (const comment of path.node.comments) {
comment.leading = false;
comment.trailing = true;
comments.push(comment);
}
}
this.traverse(path);
},
});
return comments;
};
The MIT License (MIT)
Copyright (c) 2016 Benchling, 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment