react/packages/react-codemod/transforms/react-to-react-dom.js
2015-09-08 23:06:25 -07:00

322 lines
10 KiB
JavaScript

/**
* 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 => {
var name, scope;
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') {
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
);
}
name = p.parent.value.id.name;
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
);
}
name = p.parent.value.left.name;
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'});
};