Skip to content

Commit

Permalink
Add codemod for 'React.DOM.div' -> 'React.createElement("div"'
Browse files Browse the repository at this point in the history
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]: facebook/react#9398
and facebook/react#8356
  • Loading branch information
flarnie committed Apr 20, 2017
1 parent 75436fc commit abafba8
Show file tree
Hide file tree
Showing 16 changed files with 333 additions and 0 deletions.
190 changes: 190 additions & 0 deletions 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;
};
@@ -0,0 +1,7 @@
const React = require('react');

class Hello extends React.Component {
render() {
return React.DOM.div(null, `Hello ${this.props.toWhat}`);
}
}
@@ -0,0 +1,7 @@
const React = require('react');

class Hello extends React.Component {
render() {
return React.createElement('div', null, `Hello ${this.props.toWhat}`);
}
}
@@ -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}`);
}
}
@@ -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}`);
}
}
@@ -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}`);
}
}
@@ -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}`);
}
}
@@ -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}`);
}
}
@@ -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}`);
}
}
@@ -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');
@@ -0,0 +1,7 @@
import React from 'react';

class Hello extends React.Component {
render() {
return React.createElement('div', null, `Hello ${this.props.toWhat}`);
}
}
@@ -0,0 +1,7 @@
const React = require('react');

class Hello extends React.Component {
render() {
return React.createElement('div', null, `Hello ${this.props.toWhat}`);
}
}
34 changes: 34 additions & 0 deletions 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 }`
)
);
});

0 comments on commit abafba8

Please sign in to comment.