react-codemod
Version:
React codemod scripts
422 lines (397 loc) • 14.1 kB
JavaScript
/**
* Copyright 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
;
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, options) {
var j = api.jscodeshift;
var printOptions = options.printOptions || { quote: 'single' };
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;
var coreImportDeclaration;
var coreName = null;
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;
coreName = coreRequireDeclarator.value.id.name;
} 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;
coreName = coreRequireDeclarator.value.id.name;
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;
}
}
});
root
.find(j.ImportDeclaration, { source: { value: coreModuleName } })
.forEach(p => {
if (coreImportDeclaration) {
reportError(p.value, 'Multiple declarations of React');
}
coreImportDeclaration = p;
var defaultSpecifier = p.value.specifiers.find(
sp => sp.type === j.ImportDefaultSpecifier.name
);
if (defaultSpecifier) {
var name = defaultSpecifier.local.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;
}
coreName = j(coreImportDeclaration)
.find(j.ImportDefaultSpecifier)
.get().value.local.name;
}
});
if (!coreRequireDeclarator && !coreImportDeclaration) {
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 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 if (p.parent.value.type === 'ImportDefaultSpecifier') {
// import React from "react";
} 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 (coreRequireDeclarator) {
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());
}
} else {
function findImportPath(name, path) {
return root
.find(j.ImportDeclaration, { source: { value: path } })
.filter(p =>
p.value.specifiers.find(
sp =>
sp.type === 'ImportDefaultSpecifier' && sp.local.name === name
)
);
}
function emitImport(name, path, knownProperties, uses) {
const usedProperties = coreImportDeclaration.value.specifiers
.filter(sp => sp.type === j.ImportSpecifier.name)
.filter(
sp =>
sp.imported.type === 'Identifier' &&
knownProperties.indexOf(sp.imported.name) !== -1
)
.map(sp => sp.imported.name);
const importDeclaration = findImportPath(name, path);
const specifiers = [];
// if ReactDOM needs to be in scope and it's not declared, or we're going to
// replace its declaration to add import specifiers...
if (uses > 0 && (!domAlreadyDeclared || importDeclaration.length > 0)) {
specifiers.push(j.importDefaultSpecifier(j.identifier(name)));
}
if (usedProperties.length > 0) {
j(coreImportDeclaration)
.find(j.ImportSpecifier)
.filter(p => usedProperties.indexOf(p.value.local.name) !== -1)
.remove();
specifiers.push(
...usedProperties.map(prop => j.importSpecifier(j.identifier(prop)))
);
}
if (specifiers.length > 0) {
if (importDeclaration.length > 0) {
importDeclaration.replaceWith(
j.importDeclaration(specifiers, j.literal(path))
);
} else {
coreImportDeclaration.insertAfter(
j.importDeclaration(specifiers, j.literal(path))
);
}
}
}
emitImport('ReactDOM', domModuleName, DOM_PROPERTIES, domUses);
emitImport(
'ReactDOMServer',
domServerModuleName,
DOM_SERVER_PROPERTIES,
domServerUses
);
const coreImportSpecifiers = j(coreImportDeclaration).find(
j.ImportSpecifier
);
if (coreImportSpecifiers.length === 0 && coreUses === 0) {
j(coreImportDeclaration).remove();
} else if (coreImportSpecifiers.length > 0 && coreUses === 0) {
j(coreImportDeclaration)
.find(j.ImportDefaultSpecifier)
.remove();
}
}
});
return root.toSource(printOptions);
};