Merge pull request #4767 from spicyj/com

Add react-to-react-dom codemod
This commit is contained in:
Ben Alpert
2015-09-04 13:55:04 -07:00
2 changed files with 330 additions and 1 deletions
+10 -1
View File
@@ -45,10 +45,19 @@ are using the master version (>0.13.1) of React as it is using
if you are using or planning to use [Flow](http://flowtype.org/). Also make
sure you are not calling `setState` anywhere outside of your component.
All scripts take an option `--no-explicit-require=true` if you don't have a
These three scripts take an option `--no-explicit-require=true` if you don't have a
`require('React')` statement in your code files and if you access React as a
global.
`react-to-react-dom` updates code for the split of the `react` and `react-dom`
packages (e.g., `React.render` to `ReactDOM.render`). It looks for
`require('react')` and replaces the appropriate property accesses using
`require('react-dom')`. It does not support ES6 modules or other non-CommonJS
systems.
* `jscodeshift -t react/packages/react-codemod/transforms/react-to-react-dom.js <file>`
* After running the automated codemod, you may want to run a regex-based find-and-replace to remove extra whitespace between the added requires, such as `codemod.py -m -d src --extensions js '(var React\s*=\s*require\(.react.\);)\n\n(\s*var ReactDOM)' '\1\n\2'` using https://github.com/facebook/codemod.
### Explanation of the ES2015 class transform
* Ignore components with calls to deprecated APIs. This is very defensive, if
+320
View File
@@ -0,0 +1,320 @@
/**
* Copyright 2013-2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
'use strict';
var CORE_PROPERTIES = [
'Children',
'Component',
'createElement',
'cloneElement',
'isValidElement',
'PropTypes',
'createClass',
'createFactory',
'createMixin',
'DOM',
'__spread',
];
var DOM_PROPERTIES = [
'findDOMNode',
'render',
'unmountComponentAtNode',
'unstable_batchedUpdates',
'unstable_renderSubtreeIntoContainer',
];
var DOM_SERVER_PROPERTIES = [
'renderToString',
'renderToStaticMarkup',
];
function reportError(node, error) {
throw new Error(
`At ${node.loc.start.line}:${node.loc.start.column}: ${error}`
);
}
function isRequire(path, moduleName) {
return (
path.value.type === 'CallExpression' &&
path.value.callee.type === 'Identifier' &&
path.value.callee.name === 'require' &&
path.value.arguments.length === 1 &&
path.value.arguments[0].type === 'Literal' &&
path.value.arguments[0].value === moduleName
);
}
module.exports = function(file, api) {
var j = api.jscodeshift;
var root = j(file.source);
[
['React', 'ReactDOM', 'ReactDOMServer'],
['react', 'react-dom', 'react-dom/server'],
].forEach(function(pair) {
var coreModuleName = pair[0];
var domModuleName = pair[1];
var domServerModuleName = pair[2];
var domAlreadyDeclared = false;
var domServerAlreadyDeclared = false;
var coreRequireDeclarator;
root
.find(j.CallExpression)
.filter(p => isRequire(p, coreModuleName))
.forEach(p => {
if (p.parent.value.type === 'VariableDeclarator') {
if (p.parent.value.id.type === 'ObjectPattern') {
var pattern = p.parent.value.id;
var all = pattern.properties.every(function(prop) {
if (prop.key.type === 'Identifier') {
var name = prop.key.name;
return CORE_PROPERTIES.indexOf(name) !== -1;
}
return false;
});
if (all) {
// var {PropTypes} = require('React'); so leave alone
return;
}
}
if (coreRequireDeclarator) {
reportError(
p.value,
'Multiple declarations of React'
);
}
if (p.parent.value.id.type !== 'Identifier') {
reportError(
p.value,
'Unexpected destructuring in require of ' + coreModuleName
);
}
var name = p.parent.value.id.name;
var scope = p.scope.lookup(name);
if (scope.declares('ReactDOM')) {
console.log('Using existing ReactDOM var in ' + file.path);
domAlreadyDeclared = true;
}
if (scope.declares('ReactDOMServer')) {
console.log('Using existing ReactDOMServer var in ' + file.path);
domServerAlreadyDeclared = true;
}
coreRequireDeclarator = p.parent;
} else if (p.parent.value.type === 'AssignmentExpression') {
if (p.parent.value.left.type !== 'Identifier') {
reportError(
p.value,
'Unexpected destructuring in require of ' + coreModuleName
);
}
var name = p.parent.value.left.name;
var scope = p.scope.lookup(name);
var reactBindings = scope.getBindings()[name];
if (reactBindings.length !== 1) {
throw new Error(
'Unexpected number of bindings: ' + reactBindings.length
);
}
coreRequireDeclarator = reactBindings[0].parent;
if (coreRequireDeclarator.value.init &&
!isRequire(coreRequireDeclarator.get('init'), coreModuleName)) {
reportError(
coreRequireDeclarator.value,
'Unexpected initialization of ' + coreModuleName
);
}
if (scope.declares('ReactDOM')) {
console.log('Using existing ReactDOM var in ' + file.path);
domAlreadyDeclared = true;
}
if (scope.declares('ReactDOMServer')) {
console.log('Using existing ReactDOMServer var in ' + file.path);
domServerAlreadyDeclared = true;
}
}
});
if (!coreRequireDeclarator) {
return;
}
if (!domAlreadyDeclared &&
root.find(j.Identifier, {name: 'ReactDOM'}).size() > 0) {
throw new Error(
'ReactDOM is already defined in a different scope than React'
);
}
if (!domServerAlreadyDeclared &&
root.find(j.Identifier, {name: 'ReactDOMServer'}).size() > 0) {
throw new Error(
'ReactDOMServer is already defined in a different scope than React'
);
}
var coreName = coreRequireDeclarator.value.id.name;
var processed = new Set();
var requireAssignments = [];
var coreUses = 0;
var domUses = 0;
var domServerUses = 0;
root
.find(j.Identifier, {name: coreName})
.forEach(p => {
if (processed.has(p.value)) {
// https://github.com/facebook/jscodeshift/issues/36
return;
}
processed.add(p.value);
if (p.parent.value.type === 'MemberExpression' ||
p.parent.value.type === 'QualifiedTypeIdentifier') {
var left;
var right;
if (p.parent.value.type === 'MemberExpression') {
left = p.parent.value.object;
right = p.parent.value.property;
} else {
left = p.parent.value.qualification;
right = p.parent.value.id;
}
if (left === p.value) {
// React.foo (or React[foo])
if (right.type === 'Identifier') {
var name = right.name;
if (CORE_PROPERTIES.indexOf(name) !== -1) {
coreUses++;
} else if (DOM_PROPERTIES.indexOf(name) !== -1) {
domUses++;
j(p).replaceWith(j.identifier('ReactDOM'));
} else if (DOM_SERVER_PROPERTIES.indexOf(name) !== -1) {
domServerUses++;
j(p).replaceWith(j.identifier('ReactDOMServer'));
} else {
throw new Error('Unknown property React.' + name);
}
}
} else if (right === p.value) {
// foo.React, no need to transform
} else {
throw new Error('unimplemented');
}
} else if (p.parent.value.type === 'VariableDeclarator') {
if (p.parent.value.id === p.value) {
// var React = ...;
} else if (p.parent.value.init === p.value) {
// var ... = React;
var pattern = p.parent.value.id;
if (pattern.type === 'ObjectPattern') {
// var {PropTypes} = React;
// Most of these cases will just be looking at {PropTypes} so this
// is usually a no-op.
var coreProperties = [];
var domProperties = [];
pattern.properties.forEach(function(prop) {
if (prop.key.type === 'Identifier') {
var key = prop.key.name;
if (CORE_PROPERTIES.indexOf(key) !== -1) {
coreProperties.push(prop);
} else if (DOM_PROPERTIES.indexOf(key) !== -1) {
domProperties.push(prop);
} else {
throw new Error(
'Unknown property React.' + key + ' while destructuring'
);
}
} else {
throw new Error('unimplemented');
}
});
var domDeclarator = j.variableDeclarator(
j.objectPattern(domProperties),
j.identifier('ReactDOM')
);
if (coreProperties.length && !domProperties.length) {
// nothing to do
coreUses++;
} else if (domProperties.length && !coreProperties.length) {
domUses++;
j(p.parent).replaceWith(domDeclarator);
} else {
coreUses++;
domUses++;
var decl = j(p).closest(j.VariableDeclaration);
decl.insertAfter(j.variableDeclaration(
decl.get().value.kind,
[domDeclarator]
));
}
} else {
throw new Error('unimplemented');
}
} else {
throw new Error('unimplemented');
}
} else if (p.parent.value.type === 'AssignmentExpression') {
if (p.parent.value.left === p.value) {
if (isRequire(p.parent.get('right'), coreModuleName)) {
requireAssignments.push(p.parent);
} else {
reportError(
p.parent.value,
'Unexpected assignment to ' + coreModuleName
);
}
} else {
throw new Error('unimplemented');
}
} else {
reportError(p.value, 'unimplemented ' + p.parent.value.type);
}
});
coreUses += root.find(j.JSXElement).size();
function insertRequire(name, path) {
var req = j.callExpression(
j.identifier('require'),
[j.literal(path)]
);
requireAssignments.forEach(function(requireAssignment) {
requireAssignment.parent.insertAfter(
j.expressionStatement(
j.assignmentExpression('=', j.identifier(name), req)
)
);
});
coreRequireDeclarator.parent.insertAfter(j.variableDeclaration(
coreRequireDeclarator.parent.value.kind,
[j.variableDeclarator(
j.identifier(name),
coreRequireDeclarator.value.init ? req : null
)]
));
}
if (domServerUses > 0 && !domServerAlreadyDeclared) {
insertRequire('ReactDOMServer', domServerModuleName);
}
if (domUses > 0 && !domAlreadyDeclared) {
insertRequire('ReactDOM', domModuleName);
}
if ((domUses > 0 || domServerUses > 0) && coreUses === 0) {
j(coreRequireDeclarator).remove();
requireAssignments.forEach(r => j(r).remove());
}
});
return root.toSource({quote: 'single'});
};