Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion packages/react-codemod/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
320 changes: 320 additions & 0 deletions packages/react-codemod/transforms/react-to-react-dom.js
Original file line number Diff line number Diff line change
@@ -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'});
};