From aac13f8be742e22a1137f5140d8920500fa09ec9 Mon Sep 17 00:00:00 2001 From: Flarnie Marchan Date: Wed, 19 Apr 2017 13:13:52 -0700 Subject: [PATCH] Add codemod for 'React.DOM.div' -> 'React.createElement("div"' Since we are deprecating the 'React.DOM.*' factories,[1] we want to provide a codemod so that it's easier for folks to upgrade their code. This will include an option to use 'React.createFactory' instead of 'React.createElement' in case there is a use case where that is preferred. There is one use of `React.DOM.*` that I have seen which is not covered here - sometimes it has mistakenly been used for Flow typing. In the cases I have found it is not proper syntax and doesn't seem like something we should cover with this codemod. [1]: https://github.com/facebook/react/issues/9398 and https://github.com/facebook/react/pull/8356 --- .../React-DOM-to-react-dom-factories.js | 195 ++++++++++++++++++ .../react-dom-basic-case.input.js | 12 ++ .../react-dom-basic-case.output.js | 12 ++ .../react-dom-deconstructed-import.input.js | 16 ++ .../react-dom-deconstructed-import.output.js | 16 ++ ...om-deconstructed-require-part-two.input.js | 16 ++ ...m-deconstructed-require-part-two.output.js | 16 ++ .../react-dom-deconstructed-require.input.js | 17 ++ .../react-dom-deconstructed-require.output.js | 17 ++ ...o-change-dom-from-other-libraries.input.js | 13 ++ ...-change-dom-from-other-libraries.output.js | 0 .../react-dom-no-change-import.input.js | 12 ++ .../react-dom-no-change-import.output.js | 0 .../react-dom-no-change-require.input.js | 12 ++ .../react-dom-no-change-require.output.js | 0 .../React-DOM-to-react-dom-factories-test.js | 34 +++ 16 files changed, 388 insertions(+) create mode 100644 transforms/React-DOM-to-react-dom-factories.js create mode 100644 transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-basic-case.input.js create mode 100644 transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-basic-case.output.js create mode 100644 transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-deconstructed-import.input.js create mode 100644 transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-deconstructed-import.output.js create mode 100644 transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-deconstructed-require-part-two.input.js create mode 100644 transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-deconstructed-require-part-two.output.js create mode 100644 transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-deconstructed-require.input.js create mode 100644 transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-deconstructed-require.output.js create mode 100644 transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-no-change-dom-from-other-libraries.input.js create mode 100644 transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-no-change-dom-from-other-libraries.output.js create mode 100644 transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-no-change-import.input.js create mode 100644 transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-no-change-import.output.js create mode 100644 transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-no-change-require.input.js create mode 100644 transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-no-change-require.output.js create mode 100644 transforms/__tests__/React-DOM-to-react-dom-factories-test.js diff --git a/transforms/React-DOM-to-react-dom-factories.js b/transforms/React-DOM-to-react-dom-factories.js new file mode 100644 index 00000000..30d41962 --- /dev/null +++ b/transforms/React-DOM-to-react-dom-factories.js @@ -0,0 +1,195 @@ +/** + * 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'; + +module.exports = function(file, api, options) { + const j = api.jscodeshift; + const root = j(file.source); + + let hasModifications; + + const DOMModuleName = 'DOM'; + + const isDOMSpecifier = specifier => ( + specifier.imported && + specifier.imported.name === DOMModuleName + ); + const removeDeconstructedDOMStatement = (j, root) => { + let hasModifications = false; + // import { + // DOM, + // foo, + // } from 'react'; + root + .find(j.ImportDeclaration) + .filter(path => ( + path.node.specifiers.filter(isDOMSpecifier).length > 0 && + path.node.source.value === 'react' + )) + .forEach(path => { + hasModifications = true; + + path.node.specifiers = path.node.specifiers.map( + specifier => { + if (specifier.imported && specifier.imported.name === DOMModuleName) { + return j.importSpecifier(j.identifier('createElement')); + } else { + return specifier; + } + } + ); + }); + + root + .find(j.ObjectPattern) + .filter(path => ( + path.parent.node.init && + ( + // const { + // Component, + // DOM, + // } = React; + path.parent.node.init.name === 'React' + || + ( + // const { + // Component, + // DOM, + // } = require('react'); + path.parent.node.init.type === 'CallExpression' && + path.parent.node.init.callee.name === 'require' && + path.parent.node.init.arguments[0].value === 'react' + ) + ) && + path.node.properties.some(property => { + return property.key.name === DOMModuleName; + }) + )) + .forEach(path => { + hasModifications = true; + + // Replace the DOM key with 'createElement' + path.node.properties = path.node.properties.map((property) => { + if (property.key.name === DOMModuleName) { + return j.identifier('createElement'); + } else { + return property; + } + }); + }); + return hasModifications; + }; + + hasModifications = + removeDeconstructedDOMStatement(j, root) || hasModifications; + + // is 'DOM' + const isDOMIdentifier = path => ( + path.node.name === DOMModuleName && + path.parent.parent.node.type === 'CallExpression' + ); + + // We update cases where DOM.div is being called + // eg 'foo = DOM.div('a'...' + // replace with 'foo = React.createElement('div', 'a'...' + function replaceDOMReferences(j, root) { + let hasModifications = false; + + root + .find(j.Identifier) + .filter(isDOMIdentifier) + .forEach(path => { + hasModifications = true; + + const DOMargs = path.parent.parent.node.arguments; + const DOMFactoryPath = path.parent.node.property; + const DOMFactoryType = DOMFactoryPath.name; + + // DOM.div(... -> createElement(... + j(path.parent).replaceWith( + j.identifier('createElement') + ); + // createElement(... -> createElement('div', ... + DOMargs.unshift(j.literal(DOMFactoryType)); + }); + + return hasModifications; + } + + // We only need to update 'DOM.div' syntax if there was a deconstructed + // reference to React.DOM + if (hasModifications) { + hasModifications = replaceDOMReferences(j, root) || hasModifications; + } + + // is 'React.DOM' + const isReactDOMIdentifier = path => ( + path.node.name === DOMModuleName && + ( + path.parent.node.type === 'MemberExpression' && + path.parent.node.object.name === 'React' + ) + ); + + // Update React.DOM references + // eg 'foo = React.DOM.div('a'...' + // replace with 'foo = React.createElement('div', 'a'...' + function replaceReactDOMReferences(j, root) { + let hasModifications = false; + + root + .find(j.Identifier) + .filter(isReactDOMIdentifier) + .forEach(path => { + hasModifications = true; + const DOMargs = path.parent.parent.parent.node.arguments; + const DOMFactoryPath = path.parent.parent.node.property; + const DOMFactoryType = DOMFactoryPath.name; + + // React.DOM.div(... -> DOM.div(... + /* + j(DOMFactoryPath).replaceWith( + j.identifier('createElement') // TODO; fix + ); + */ + // React.DOM.div(... -> React.DOM.createElement(... + path.parent.parent.node.property = j.identifier('createElement'); + // React.DOM.createElement(... -> React.createElement(... + j(path.parent).replaceWith(j.identifier('React')); + // React.createElement(... -> React.createElement('div'... + DOMargs.unshift(j.literal(DOMFactoryType)); + }); + + return hasModifications; + } + + function removeEmptyReactImport(j, root) { + root + .find(j.ImportDeclaration) + .filter(path => ( + path.node.specifiers.length === 0 && + path.node.source.value === 'react' + )) + .replaceWith(); + } + + hasModifications = replaceReactDOMReferences(j, root) || hasModifications; + + if (hasModifications) { + // we shouldn't really need this? + removeEmptyReactImport(j, root); + } + + return hasModifications + ? root.toSource({ quote: 'single' }) + : null; + +}; diff --git a/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-basic-case.input.js b/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-basic-case.input.js new file mode 100644 index 00000000..61d44b2d --- /dev/null +++ b/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-basic-case.input.js @@ -0,0 +1,12 @@ +const React = require('react'); + +class Hello extends React.Component { + render() { + return React.DOM.div(null, `Hello ${this.props.toWhat}`); + } +} + +ReactDOM.render( + React.createElement(Hello, {toWhat: 'World'}, null), + document.getElementById('root') +); diff --git a/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-basic-case.output.js b/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-basic-case.output.js new file mode 100644 index 00000000..36e1eec9 --- /dev/null +++ b/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-basic-case.output.js @@ -0,0 +1,12 @@ +const React = require('react'); + +class Hello extends React.Component { + render() { + return React.createElement('div', null, `Hello ${this.props.toWhat}`); + } +} + +ReactDOM.render( + React.createElement(Hello, {toWhat: 'World'}, null), + document.getElementById('root') +); diff --git a/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-deconstructed-import.input.js b/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-deconstructed-import.input.js new file mode 100644 index 00000000..84e12097 --- /dev/null +++ b/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-deconstructed-import.input.js @@ -0,0 +1,16 @@ +import ReactDOM from 'ReactDOM'; +import { + Component, + DOM +} from 'react'; + +class Hello extends Component { + render() { + return DOM.div(null, `Hello ${this.props.toWhat}`); + } +} + +ReactDOM.render( + React.createElement(Hello, {toWhat: 'World'}, null), + document.getElementById('root') +); diff --git a/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-deconstructed-import.output.js b/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-deconstructed-import.output.js new file mode 100644 index 00000000..61be85e6 --- /dev/null +++ b/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-deconstructed-import.output.js @@ -0,0 +1,16 @@ +import ReactDOM from 'ReactDOM'; +import { + Component, + createElement +} from 'react'; + +class Hello extends Component { + render() { + return createElement('div', null, `Hello ${this.props.toWhat}`); + } +} + +ReactDOM.render( + React.createElement(Hello, {toWhat: 'World'}, null), + document.getElementById('root') +); diff --git a/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-deconstructed-require-part-two.input.js b/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-deconstructed-require-part-two.input.js new file mode 100644 index 00000000..248643e0 --- /dev/null +++ b/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-deconstructed-require-part-two.input.js @@ -0,0 +1,16 @@ +const ReactDOM = require('ReactDOM'); +const { + Component, + DOM +} = require('react'); + +class Hello extends Component { + render() { + return DOM.div(null, `Hello ${this.props.toWhat}`); + } +} + +ReactDOM.render( + React.createElement(Hello, {toWhat: 'World'}, null), + document.getElementById('root') +); diff --git a/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-deconstructed-require-part-two.output.js b/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-deconstructed-require-part-two.output.js new file mode 100644 index 00000000..a8842d93 --- /dev/null +++ b/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-deconstructed-require-part-two.output.js @@ -0,0 +1,16 @@ +const ReactDOM = require('ReactDOM'); +const { + Component, + createElement +} = require('react'); + +class Hello extends Component { + render() { + return createElement('div', null, `Hello ${this.props.toWhat}`); + } +} + +ReactDOM.render( + React.createElement(Hello, {toWhat: 'World'}, null), + document.getElementById('root') +); diff --git a/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-deconstructed-require.input.js b/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-deconstructed-require.input.js new file mode 100644 index 00000000..4ad72cfd --- /dev/null +++ b/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-deconstructed-require.input.js @@ -0,0 +1,17 @@ +const React = require('react'); +const ReactDOM = require('ReactDOM'); +const { + Component, + DOM +} = React; + +class Hello extends Component { + render() { + return DOM.div(null, `Hello ${this.props.toWhat}`); + } +} + +ReactDOM.render( + React.createElement(Hello, {toWhat: 'World'}, null), + document.getElementById('root') +); diff --git a/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-deconstructed-require.output.js b/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-deconstructed-require.output.js new file mode 100644 index 00000000..2924d9e4 --- /dev/null +++ b/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-deconstructed-require.output.js @@ -0,0 +1,17 @@ +const React = require('react'); +const ReactDOM = require('ReactDOM'); +const { + Component, + createElement +} = React; + +class Hello extends Component { + render() { + return createElement('div', null, `Hello ${this.props.toWhat}`); + } +} + +ReactDOM.render( + React.createElement(Hello, {toWhat: 'World'}, null), + document.getElementById('root') +); diff --git a/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-no-change-dom-from-other-libraries.input.js b/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-no-change-dom-from-other-libraries.input.js new file mode 100644 index 00000000..9a5b5f98 --- /dev/null +++ b/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-no-change-dom-from-other-libraries.input.js @@ -0,0 +1,13 @@ +let {DOM} = require('Free'); +import {DOM} from 'Free'; + +const foo = DOM.div('a', 'b', 'c'); +const foo = Free.DOM.div('a', 'b', 'c'); + +DOM = 'this is a test!'; + +foo.DOM = {}; + +foo.DOM.div = () => null; + +const bar = foo.DOM.div('a', 'b', 'c'); diff --git a/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-no-change-dom-from-other-libraries.output.js b/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-no-change-dom-from-other-libraries.output.js new file mode 100644 index 00000000..e69de29b diff --git a/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-no-change-import.input.js b/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-no-change-import.input.js new file mode 100644 index 00000000..f6bd210c --- /dev/null +++ b/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-no-change-import.input.js @@ -0,0 +1,12 @@ +import React from 'react'; + +class Hello extends React.Component { + render() { + return React.createElement('div', null, `Hello ${this.props.toWhat}`); + } +} + +ReactDOM.render( + React.createElement(Hello, {toWhat: 'World'}, null), + document.getElementById('root') +); diff --git a/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-no-change-import.output.js b/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-no-change-import.output.js new file mode 100644 index 00000000..e69de29b diff --git a/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-no-change-require.input.js b/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-no-change-require.input.js new file mode 100644 index 00000000..36e1eec9 --- /dev/null +++ b/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-no-change-require.input.js @@ -0,0 +1,12 @@ +const React = require('react'); + +class Hello extends React.Component { + render() { + return React.createElement('div', null, `Hello ${this.props.toWhat}`); + } +} + +ReactDOM.render( + React.createElement(Hello, {toWhat: 'World'}, null), + document.getElementById('root') +); diff --git a/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-no-change-require.output.js b/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-no-change-require.output.js new file mode 100644 index 00000000..e69de29b diff --git a/transforms/__tests__/React-DOM-to-react-dom-factories-test.js b/transforms/__tests__/React-DOM-to-react-dom-factories-test.js new file mode 100644 index 00000000..dc0b01fb --- /dev/null +++ b/transforms/__tests__/React-DOM-to-react-dom-factories-test.js @@ -0,0 +1,34 @@ +/** + * 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'; + +const tests = [ + 'react-dom-basic-case', + 'react-dom-deconstructed-import', + 'react-dom-deconstructed-require', + 'react-dom-deconstructed-require-part-two', + 'react-dom-no-change-import', + 'react-dom-no-change-require', + 'react-dom-no-change-dom-from-other-libraries', +]; + +const defineTest = require('jscodeshift/dist/testUtils').defineTest; + +describe('React-DOM-to-react-dom-factories', () => { + tests.forEach(test => + defineTest( + __dirname, + 'React-DOM-to-react-dom-factories', + null, + `React-DOM-to-react-dom-factories/${ test }` + ) + ); +});