From 7a6bf0c58c86f6cfb3a22f7c1daef229e3476bc9 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Fri, 14 Apr 2017 15:31:42 -0700 Subject: [PATCH] Add option to customize prop-types module path --- transforms/React-PropTypes-to-prop-types.js | 328 +++++++++--------- .../module-name.input.js | 17 + .../module-name.output.js | 18 + .../React-PropTypes-to-prop-types-test.js | 7 + 4 files changed, 207 insertions(+), 163 deletions(-) create mode 100644 transforms/__testfixtures__/React-PropTypes-to-prop-types/module-name.input.js create mode 100644 transforms/__testfixtures__/React-PropTypes-to-prop-types/module-name.output.js diff --git a/transforms/React-PropTypes-to-prop-types.js b/transforms/React-PropTypes-to-prop-types.js index 6172c4d8..29091879 100644 --- a/transforms/React-PropTypes-to-prop-types.js +++ b/transforms/React-PropTypes-to-prop-types.js @@ -10,187 +10,189 @@ 'use strict'; -// 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 > 'prop-types' && - (!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 > 'prop-types' && - (!target || name < targetName) - ) { - targetName = name; - target = path; - } - }); +module.exports = function(file, api, options) { + const j = api.jscodeshift; + const root = j(file.source); - return target; -} - -// 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) - .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)) { - const path = findImportAfterPropTypes(j, root); - if (path) { - const importStatement = j.importDeclaration( - [j.importDefaultSpecifier(j.identifier('PropTypes'))], - j.literal('prop-types') - ); - j(path).insertBefore(importStatement); - return; - } + const MODULE_NAME = options['module-name'] || 'prop-types'; + + // 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; } - const path = findRequireAfterPropTypes(j, root); - if (path) { - const requireStatement = useVar(j, root) - ? j.template.statement`var PropTypes = require('prop-types');\n` - : j.template.statement`const PropTypes = require('prop-types');\n`; - j(path.parent.parent).insertBefore(requireStatement); - return; + // 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; } - throw new Error('No PropTypes import found!'); -} + // 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) + .length > 0; + } -// Remove PropTypes destructure statements (eg const { ProptTypes } = React) -function removeDestructuredPropTypeStatements(j, root) { - let hasModifications = false; + // Program uses var keywords + function useVar(j, root) { + return root + .find(j.VariableDeclaration, {kind: 'const'}) + .length === 0; + } - root - .find(j.ObjectPattern) - .filter(path => ( - path.parent.node.init && - path.parent.node.init.name === 'React' && - path.node.properties.some( - property => property.key.name === 'PropTypes' - ) - )) - .forEach(path => { - hasModifications = true; - - // Remove the PropTypes key - path.node.properties = path.node.properties.filter( - property => property.key.name !== 'PropTypes' - ); - - // If this was the only property, remove the entire statement. - if (path.node.properties.length === 0) { - path.parent.parent.replace(''); + // If any PropTypes references exist, add a 'prop-types' import (or require) + function addPropTypesImport(j, root) { + if (useImportSyntax(j, root)) { + const path = findImportAfterPropTypes(j, root); + if (path) { + const importStatement = j.importDeclaration( + [j.importDefaultSpecifier(j.identifier('PropTypes'))], + j.literal(MODULE_NAME) + ); + j(path).insertBefore(importStatement); + return; } - }); + } + + const path = findRequireAfterPropTypes(j, root); + if (path) { + const requireStatement = useVar(j, root) + ? j.template.statement([`var PropTypes = require('${MODULE_NAME}');\n`]) + : j.template.statement([`const PropTypes = require('${MODULE_NAME}');\n`]); + j(path.parent.parent).insertBefore(requireStatement); + return; + } - return hasModifications; -} + throw new Error('No PropTypes import found!'); + } -// Remove old { PropTypes } imports -function removePropTypesImport(j, root) { - let hasModifications = false; + // Remove PropTypes destructure statements (eg const { ProptTypes } = React) + function removeDestructuredPropTypeStatements(j, root) { + let hasModifications = false; + + root + .find(j.ObjectPattern) + .filter(path => ( + path.parent.node.init && + path.parent.node.init.name === 'React' && + path.node.properties.some( + property => property.key.name === 'PropTypes' + ) + )) + .forEach(path => { + hasModifications = true; + + // Remove the PropTypes key + path.node.properties = path.node.properties.filter( + property => property.key.name !== 'PropTypes' + ); - root - .find(j.Identifier) - .filter(path => ( - path.node.name === 'PropTypes' && - path.parent.node.type === 'ImportSpecifier' - )) - .forEach(path => { - hasModifications = true; - - const importDeclaration = path.parent.parent.node; - importDeclaration.specifiers = importDeclaration.specifiers.filter( - specifier => ( - !specifier.imported || - specifier.imported.name !== 'PropTypes' - ) - ); + // If this was the only property, remove the entire statement. + if (path.node.properties.length === 0) { + path.parent.parent.replace(''); + } }); - return hasModifications; -} - -// Replace all React.PropTypes instances with PropTypes -function replacePropTypesReferences(j, root) { - let hasModifications = false; + return hasModifications; + } - root - .find(j.Identifier) - .filter(isReactPropTypes) - .forEach(path => { - hasModifications = true; - - // VariableDeclarator should be removed entirely - // eg 'PropTypes = React.PropTypes' - if (path.parent.parent.node.type === 'VariableDeclarator') { - j(path.parent.parent).remove(); - } else { - // MemberExpression should be updated - // eg 'foo = React.PropTypes.string' - j(path.parent).replaceWith( - j.identifier('PropTypes') + // 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' + )) + .forEach(path => { + hasModifications = true; + + const importDeclaration = path.parent.parent.node; + importDeclaration.specifiers = importDeclaration.specifiers.filter( + specifier => ( + !specifier.imported || + specifier.imported.name !== 'PropTypes' + ) ); - } - }); + }); - return hasModifications; -} + return hasModifications; + } -function removeEmptyReactImport(j, root) { - root - .find(j.ImportDeclaration) - .filter(path => ( - path.node.specifiers.length === 0 && - path.node.source.value === 'react' - )) - .replaceWith(); -} + // 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 'PropTypes = React.PropTypes' + if (path.parent.parent.node.type === 'VariableDeclarator') { + j(path.parent.parent).remove(); + } else { + // MemberExpression should be updated + // eg 'foo = React.PropTypes.string' + j(path.parent).replaceWith( + j.identifier('PropTypes') + ); + } + }); + + return hasModifications; + } -module.exports = function(file, api, options) { - const j = api.jscodeshift; - const root = j(file.source); + 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 = replacePropTypesReferences(j, root) || hasModifications; diff --git a/transforms/__testfixtures__/React-PropTypes-to-prop-types/module-name.input.js b/transforms/__testfixtures__/React-PropTypes-to-prop-types/module-name.input.js new file mode 100644 index 00000000..b0bd1ef4 --- /dev/null +++ b/transforms/__testfixtures__/React-PropTypes-to-prop-types/module-name.input.js @@ -0,0 +1,17 @@ +const React = require('React'); + +class ClassComponent extends React.Component { + static propTypes = { + text: React.PropTypes.string.isRequired, + }; + render() { + return
{this.props.text}
; + } +} + +function FunctionalComponent (props) { + return
{props.text}
; +} +FunctionalComponent.propTypes = { + text: React.PropTypes.string.isRequired, +}; \ No newline at end of file diff --git a/transforms/__testfixtures__/React-PropTypes-to-prop-types/module-name.output.js b/transforms/__testfixtures__/React-PropTypes-to-prop-types/module-name.output.js new file mode 100644 index 00000000..8b37b263 --- /dev/null +++ b/transforms/__testfixtures__/React-PropTypes-to-prop-types/module-name.output.js @@ -0,0 +1,18 @@ +const PropTypes = require('PropTypes'); +const React = require('React'); + +class ClassComponent extends React.Component { + static propTypes = { + text: PropTypes.string.isRequired, + }; + render() { + return
{this.props.text}
; + } +} + +function FunctionalComponent (props) { + return
{props.text}
; +} +FunctionalComponent.propTypes = { + text: PropTypes.string.isRequired, +}; diff --git a/transforms/__tests__/React-PropTypes-to-prop-types-test.js b/transforms/__tests__/React-PropTypes-to-prop-types-test.js index f01698fd..6db2b830 100644 --- a/transforms/__tests__/React-PropTypes-to-prop-types-test.js +++ b/transforms/__tests__/React-PropTypes-to-prop-types-test.js @@ -36,4 +36,11 @@ describe('React-PropTypes-to-prop-types', () => { `React-PropTypes-to-prop-types/${ test }` ) ); + + defineTest( + __dirname, + 'React-PropTypes-to-prop-types', + { 'module-name': 'PropTypes' }, + 'React-PropTypes-to-prop-types/module-name' + ); });