From 26f661fc51504f4291cd70027b4f7d7b2b4d2d03 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 | 190 ++++++++++++++++++ .../react-dom-basic-case.input.js | 7 + .../react-dom-basic-case.output.js | 7 + .../react-dom-deconstructed-import.input.js | 11 + .../react-dom-deconstructed-import.output.js | 11 + ...om-deconstructed-require-part-two.input.js | 11 + ...m-deconstructed-require-part-two.output.js | 11 + .../react-dom-deconstructed-require.input.js | 12 ++ .../react-dom-deconstructed-require.output.js | 12 ++ ...o-change-dom-from-other-libraries.input.js | 13 ++ ...-change-dom-from-other-libraries.output.js | 0 .../react-dom-no-change-import.input.js | 7 + .../react-dom-no-change-import.output.js | 0 .../react-dom-no-change-require.input.js | 7 + .../react-dom-no-change-require.output.js | 0 .../React-DOM-to-react-dom-factories-test.js | 34 ++++ 16 files changed, 333 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..a386978d --- /dev/null +++ b/transforms/React-DOM-to-react-dom-factories.js @@ -0,0 +1,190 @@ +/** + * 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 + ); + + /** + * Replaces 'DOM' with 'createElement' in places where we grab 'DOM' out of + * 'React' with destructuring. + */ + const replaceDestructuredDOMStatement = (j, root) => { + let hasModifications = false; + //--------- + // First update import statments. eg: + // 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; + + // Replace the DOM key with 'createElement' + path.node.specifiers = path.node.specifiers.map( + specifier => { + if (specifier.imported && specifier.imported.name === DOMModuleName) { + return j.importSpecifier(j.identifier('createElement')); + } else { + return specifier; + } + } + ); + }); + + //--------- + // Next update require statments. + // This matches both + // const { + // Component, + // DOM, + // } = React; + // and + // const { + // Component, + // DOM, + // } = require('react'); + root + .find(j.ObjectPattern) + .filter(path => ( + path.parent.node.init && + ( + // matches '} = React;' + path.parent.node.init.name === 'React' + || + ( + // matches "} = 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 = + replaceDestructuredDOMStatement(j, root) || hasModifications; + + const isDOMIdentifier = path => ( + path.node.name === DOMModuleName && + path.parent.parent.node.type === 'CallExpression' + ); + + /** + * Update cases where DOM.div is being called + * eg 'foo = DOM.div('a'...' + * replace with 'foo = 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; + } + + // matches '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(... -> 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; + } + + hasModifications = replaceReactDOMReferences(j, root) || hasModifications; + + 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..7543d34c --- /dev/null +++ b/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-basic-case.input.js @@ -0,0 +1,7 @@ +const React = require('react'); + +class Hello extends React.Component { + render() { + return React.DOM.div(null, `Hello ${this.props.toWhat}`); + } +} 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..fdc8bc66 --- /dev/null +++ b/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-basic-case.output.js @@ -0,0 +1,7 @@ +const React = require('react'); + +class Hello extends React.Component { + render() { + return React.createElement('div', null, `Hello ${this.props.toWhat}`); + } +} 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..32a4b2c3 --- /dev/null +++ b/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-deconstructed-import.input.js @@ -0,0 +1,11 @@ +import ReactDOM from 'ReactDOM'; +import { + Component, + DOM +} from 'react'; + +class Hello extends Component { + render() { + return DOM.div(null, `Hello ${this.props.toWhat}`); + } +} 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..9be51313 --- /dev/null +++ b/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-deconstructed-import.output.js @@ -0,0 +1,11 @@ +import ReactDOM from 'ReactDOM'; +import { + Component, + createElement +} from 'react'; + +class Hello extends Component { + render() { + return createElement('div', null, `Hello ${this.props.toWhat}`); + } +} 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..dd4bec3d --- /dev/null +++ b/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-deconstructed-require-part-two.input.js @@ -0,0 +1,11 @@ +const ReactDOM = require('ReactDOM'); +const { + Component, + DOM +} = require('react'); + +class Hello extends Component { + render() { + return DOM.div(null, `Hello ${this.props.toWhat}`); + } +} 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..b5c30c2e --- /dev/null +++ b/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-deconstructed-require-part-two.output.js @@ -0,0 +1,11 @@ +const ReactDOM = require('ReactDOM'); +const { + Component, + createElement +} = require('react'); + +class Hello extends Component { + render() { + return createElement('div', null, `Hello ${this.props.toWhat}`); + } +} 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..92706fc2 --- /dev/null +++ b/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-deconstructed-require.input.js @@ -0,0 +1,12 @@ +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}`); + } +} 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..a8ba7716 --- /dev/null +++ b/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-deconstructed-require.output.js @@ -0,0 +1,12 @@ +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}`); + } +} 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..ec669a5d --- /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 bar = 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..3c7cf3ff --- /dev/null +++ b/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-no-change-import.input.js @@ -0,0 +1,7 @@ +import React from 'react'; + +class Hello extends React.Component { + render() { + return React.createElement('div', null, `Hello ${this.props.toWhat}`); + } +} 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..fdc8bc66 --- /dev/null +++ b/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-no-change-require.input.js @@ -0,0 +1,7 @@ +const React = require('react'); + +class Hello extends React.Component { + render() { + return React.createElement('div', null, `Hello ${this.props.toWhat}`); + } +} 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 }` + ) + ); +});