in transforms/React-PropTypes-to-prop-types.js [11:293]
module.exports = function(file, api, options) {
const j = api.jscodeshift;
const printOptions = options.printOptions || { quote: 'single' };
const root = j(file.source);
const MODULE_NAME = options['module-name'] || 'prop-types';
let localPropTypesName = 'PropTypes';
// Find alpha-sorted import that would follow prop-types
function findImportAfterPropTypes(j, root) {
let target, targetName;
root.find(j.ImportDeclaration).forEach(path => {
const name = path.value.source.value.toLowerCase();
if (name > MODULE_NAME && (!target || name < targetName)) {
targetName = name;
target = path;
}
});
return target;
}
// Find alpha-sorted require that would follow prop-types
function findRequireAfterPropTypes(j, root) {
let target, targetName;
root
.find(j.CallExpression, { callee: { name: 'require' } })
.forEach(path => {
const name = path.node.arguments[0].value.toLowerCase();
if (name > MODULE_NAME && (!target || name < targetName)) {
targetName = name;
target = path;
}
});
return target;
}
function hasPropTypesImport(j, root) {
return (
root.find(j.ImportDeclaration, {
source: { value: 'prop-types' }
}).length > 0
);
}
function hasPropTypesRequire(j, root) {
return (
root.find(j.CallExpression, {
callee: { name: 'require' },
arguments: { 0: { value: 'prop-types' } }
}).length > 0
);
}
// React.PropTypes
const isReactPropTypes = path =>
path.node.name === 'PropTypes' &&
path.parent.node.type === 'MemberExpression' &&
path.parent.node.object.name === 'React';
// Program uses ES import syntax
function useImportSyntax(j, root) {
return (
root.find(j.ImportDeclaration, {
importKind: 'value'
}).length > 0
);
}
// Program uses var keywords
function useVar(j, root) {
return root.find(j.VariableDeclaration, { kind: 'const' }).length === 0;
}
// If any PropTypes references exist, add a 'prop-types' import (or require)
function addPropTypesImport(j, root) {
if (useImportSyntax(j, root)) {
// Handle cases where 'prop-types' already exists;
// eg the file has already been codemodded but more React.PropTypes were added.
if (hasPropTypesImport(j, root)) {
return;
}
const path = findImportAfterPropTypes(j, root);
if (path) {
const importStatement = j.importDeclaration(
[j.importDefaultSpecifier(j.identifier(localPropTypesName))],
j.literal(MODULE_NAME)
);
// If there is a leading comment, retain it
// https://github.com/facebook/jscodeshift/blob/master/recipes/retain-first-comment.md
const firstNode = root.find(j.Program).get('body', 0).node;
const { comments } = firstNode;
if (comments) {
delete firstNode.comments;
importStatement.comments = comments;
}
j(path).insertBefore(importStatement);
return;
}
}
// Handle cases where 'prop-types' already exists;
// eg the file has already been codemodded but more React.PropTypes were added.
if (hasPropTypesRequire(j, root)) {
return;
}
const path = findRequireAfterPropTypes(j, root);
if (path) {
const requireStatement = useVar(j, root)
? j.template.statement([
`var ${localPropTypesName} = require('${MODULE_NAME}');\n`
])
: j.template.statement([
`const ${localPropTypesName} = require('${MODULE_NAME}');\n`
]);
j(path.parent.parent).insertBefore(requireStatement);
return;
}
throw new Error('No PropTypes import found!');
}
// Remove PropTypes destructure statements, e.g.
// const { PropTypes } = React;
// or
// const { PropTypes } = require('react');
function removeDestructuredPropTypeStatements(j, root) {
let hasModifications = false;
root
.find(j.ObjectPattern)
.filter(path => {
const init = path.parent.node.init;
if (!init) {
return false;
}
if (
!(
init.name === 'React' || // const { PropTypes } = React
(init.type === 'CallExpression' &&
init.callee.name === 'require' &&
init.arguments.length &&
init.arguments[0].value === 'react')
)
// const { PropTypes } = require('react')
) {
return false;
}
return path.node.properties.some(
property => property.key.name === 'PropTypes'
);
})
.forEach(path => {
hasModifications = true;
// Find any nested destructures hanging off of the PropTypes node
const nestedPropTypesChildren = path.node.properties.find(
property =>
property.key.name === 'PropTypes' &&
property.value.type === 'ObjectPattern'
);
// Remove the PropTypes key
path.node.properties = path.node.properties.filter(property => {
if (property.key.name === 'PropTypes') {
if (property.value && property.value.type === 'Identifier') {
localPropTypesName = property.value.name;
}
return false;
} else {
return true;
}
});
// Add back any nested destructured children.
if (nestedPropTypesChildren) {
// TODO: This shouldn't just use a template string but the `ast-types` docs were too opaque for me to follow.
const propTypeChildren = nestedPropTypesChildren.value.properties
.map(property => property.key.name)
.join(', ');
const destructureStatement = j.template.statement([
`const { ${propTypeChildren} } = ${localPropTypesName};`
]);
j(path.parent.parent).insertBefore(destructureStatement);
}
// If this was the only property, remove the entire statement.
if (path.node.properties.length === 0) {
path.parent.parent.replace('');
}
});
return hasModifications;
}
// Remove old { PropTypes } imports
function removePropTypesImport(j, root) {
let hasModifications = false;
root
.find(j.Identifier)
.filter(
path =>
path.node.name === 'PropTypes' &&
path.parent.node.type === 'ImportSpecifier' &&
path.parent.parent.node.source.value === 'react'
)
.forEach(path => {
hasModifications = true;
localPropTypesName = path.parent.node.local.name;
const importDeclaration = path.parent.parent.node;
importDeclaration.specifiers = importDeclaration.specifiers.filter(
specifier =>
!specifier.imported || specifier.imported.name !== 'PropTypes'
);
});
return hasModifications;
}
// Replace all React.PropTypes instances with PropTypes
function replacePropTypesReferences(j, root) {
let hasModifications = false;
root
.find(j.Identifier)
.filter(isReactPropTypes)
.forEach(path => {
hasModifications = true;
// VariableDeclarator should be removed entirely
// eg 'const PropTypes = React.PropTypes'
// Don't remove pointers though
// eg 'const ReactPropTypes = PropTypes'
if (
path.parent.parent.node.type === 'VariableDeclarator' &&
path.parent.parent.node.id.name === 'PropTypes'
) {
j(path.parent.parent).remove();
} else {
// MemberExpression should be updated
// eg 'foo = React.PropTypes.string'
j(path.parent).replaceWith(j.identifier(localPropTypesName));
}
});
return hasModifications;
}
function removeEmptyReactImport(j, root) {
root
.find(j.ImportDeclaration)
.filter(
path =>
path.node.specifiers.length === 0 &&
path.node.source.value === 'react'
)
.replaceWith();
}
let hasModifications = false;
hasModifications = removePropTypesImport(j, root) || hasModifications;
hasModifications = replacePropTypesReferences(j, root) || hasModifications;
hasModifications =
removeDestructuredPropTypeStatements(j, root) || hasModifications;
if (hasModifications) {
addPropTypesImport(j, root);
removeEmptyReactImport(j, root);
}
return hasModifications ? root.toSource(printOptions) : null;
};