From e361fcb3e63cc12ee03f2b6d53cf83dec51c160b Mon Sep 17 00:00:00 2001 From: cpojer Date: Wed, 25 Mar 2015 01:27:45 -0700 Subject: [PATCH 1/6] Add skeleton for react-codemod --- .eslintignore | 6 ++- npm-react-codemod/.npmignore | 1 + npm-react-codemod/README.md | 19 +++++++++ npm-react-codemod/package.json | 32 +++++++++++++++ npm-react-codemod/react-codemod | 7 ++++ .../test/__tests__/transform-tests.js | 39 +++++++++++++++++++ 6 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 npm-react-codemod/.npmignore create mode 100644 npm-react-codemod/README.md create mode 100644 npm-react-codemod/package.json create mode 100755 npm-react-codemod/react-codemod create mode 100644 npm-react-codemod/test/__tests__/transform-tests.js diff --git a/.eslintignore b/.eslintignore index 85490b0ad4227..862afe0fb99be 100644 --- a/.eslintignore +++ b/.eslintignore @@ -19,4 +19,8 @@ docs/js/ examples/ # Ignore built files. build/ - +# react-codemod +npm-react-codemod/test/ +npm-react-codemod/scripts/ +npm-react-codemod/build/ +npm-react-codemod/node_modules/ diff --git a/npm-react-codemod/.npmignore b/npm-react-codemod/.npmignore new file mode 100644 index 0000000000000..651b253185d02 --- /dev/null +++ b/npm-react-codemod/.npmignore @@ -0,0 +1 @@ +/transforms/ diff --git a/npm-react-codemod/README.md b/npm-react-codemod/README.md new file mode 100644 index 0000000000000..5ebb3c21ccc24 --- /dev/null +++ b/npm-react-codemod/README.md @@ -0,0 +1,19 @@ +## react-codemod + +This repository contains a collection of codemod scripts based on +[JSCodeshift](https://github.com/facebook/jscodeshift) that help update React +APIs. + +### Setup & Run + + * `npm install -g react-codemod` + * `react-codemod ` + * Use the `-d` option for a dry-run and use `-p` to print the output + for comparison + +### Recast Options + +Options to [recast](https://github.com/benjamn/recast)'s printer can be provided +through the `printOptions` command line argument + + * `react-codemod class --printOptions='{"quote":"double"}'` diff --git a/npm-react-codemod/package.json b/npm-react-codemod/package.json new file mode 100644 index 0000000000000..46b5ceb2be13a --- /dev/null +++ b/npm-react-codemod/package.json @@ -0,0 +1,32 @@ +{ + "name": "react-codemod", + "version": "1.0.0", + "description": "React codemod scripts", + "license": "BSD-3-Clause", + "repository": { + "type": "git", + "url": "https://github.com/facebook/react" + }, + "scripts": { + "build": "rm -rf build; babel transforms/ --out-dir=build/", + "test": "jest", + "prepublish": "npm run build" + }, + "bin": { + "react-codemod": "./react-codemod" + }, + "dependencies": { + "jscodeshift": "^0.1.0" + }, + "devDependencies": { + "babel": "^4.7.16", + "babel-jest": "^4.0.0", + "jest-cli": "^0.4.0" + }, + "jest": { + "scriptPreprocessor": "./node_modules/babel-jest", + "testPathDirs": [ + "test" + ] + } +} diff --git a/npm-react-codemod/react-codemod b/npm-react-codemod/react-codemod new file mode 100755 index 0000000000000..236cca8d3223d --- /dev/null +++ b/npm-react-codemod/react-codemod @@ -0,0 +1,7 @@ +#!/bin/bash + +DIR=$(npm root -g)/react-codemod +TRANSFORM=$1 +shift + +$DIR/node_modules/.bin/jscodeshift -t $DIR/build/$TRANSFORM.js $@ diff --git a/npm-react-codemod/test/__tests__/transform-tests.js b/npm-react-codemod/test/__tests__/transform-tests.js new file mode 100644 index 0000000000000..7d73c7f0607ca --- /dev/null +++ b/npm-react-codemod/test/__tests__/transform-tests.js @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2015-present, 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"; + +jest.autoMockOff(); + +var fs = require('fs'); +var jscodeshift = require('jscodeshift'); + +function read(fileName) { + return fs.readFileSync(__dirname + '/../' + fileName, 'utf8'); +} + +function test(transformName, testFileName, options) { + var path = testFileName + '.js'; + var source = read(testFileName + '.js'); + var output = read(testFileName + '.output.js'); + + var transform = require('../../transforms/' + transformName); + expect( + (transform({path, source}, {jscodeshift}, options || {}) || '').trim() + ).toEqual( + output.trim() + ); +} + +describe('Transform Tests', () => { + + + +}); From 328274bbba44aa6d75dfa21242ac9f0609c5948f Mon Sep 17 00:00:00 2001 From: cpojer Date: Wed, 25 Mar 2015 01:29:31 -0700 Subject: [PATCH 2/6] Add ReactUtils and array polyfills. --- .../transforms/utils/ReactUtils.js | 133 ++++++++++++++++++ .../transforms/utils/array-polyfills.js | 36 +++++ 2 files changed, 169 insertions(+) create mode 100644 npm-react-codemod/transforms/utils/ReactUtils.js create mode 100644 npm-react-codemod/transforms/utils/array-polyfills.js diff --git a/npm-react-codemod/transforms/utils/ReactUtils.js b/npm-react-codemod/transforms/utils/ReactUtils.js new file mode 100644 index 0000000000000..c8842d98490c4 --- /dev/null +++ b/npm-react-codemod/transforms/utils/ReactUtils.js @@ -0,0 +1,133 @@ +/*eslint-disable no-comma-dangle*/ + +'use strict'; + +module.exports = function(j) { + const REACT_CREATE_CLASS_MEMBER_EXPRESSION = { + type: 'MemberExpression', + object: { + name: 'React', + }, + property: { + name: 'createClass', + }, + }; + + // --------------------------------------------------------------------------- + // Checks if the file requires a certain module + const hasModule = (path, module) => + path + .findVariableDeclarators() + .filter(j.filters.VariableDeclarator.requiresModule(module)) + .size() === 1; + + const hasReact = path => ( + hasModule(path, 'React') || + hasModule(path, 'react') || + hasModule(path, 'react/addons') + ); + + // --------------------------------------------------------------------------- + // Finds all variable declarations that call React.createClass + const findReactCreateClassCallExpression = path => + j(path).find(j.CallExpression, { + callee: REACT_CREATE_CLASS_MEMBER_EXPRESSION, + }); + + const findReactCreateClass = path => + path + .findVariableDeclarators() + .filter(path => findReactCreateClassCallExpression(path).size() > 0); + + const findReactCreateClassModuleExports = path => + path + .find(j.AssignmentExpression, { + left: { + type: 'MemberExpression', + object: { + type: 'Identifier', + name: 'module', + }, + property: { + type: 'Identifier', + name: 'exports', + }, + }, + right: { + type: 'CallExpression', + callee: REACT_CREATE_CLASS_MEMBER_EXPRESSION, + }, + }); + + // --------------------------------------------------------------------------- + // Finds all classes that extend React.Component + const findReactES6ClassDeclaration = path => + path + .find(j.ClassDeclaration, { + superClass: { + type: 'MemberExpression', + object: { + type: 'Identifier', + name: 'React', + }, + property: { + type: 'Identifier', + name: 'Component', + }, + }, + }); + + // --------------------------------------------------------------------------- + // Checks if the React class has mixins + const isMixinProperty = property => { + const key = property.key; + const value = property.value; + return ( + key.name === 'mixins' && + value.type === 'ArrayExpression' && + Array.isArray(value.elements) && + value.elements.length + ); + }; + + const hasMixins = classPath => { + const spec = getReactCreateClassSpec(classPath); + return spec && spec.properties.some(isMixinProperty); + }; + + // --------------------------------------------------------------------------- + // Others + const getReactCreateClassSpec = classPath => { + const spec = (classPath.value.init || classPath.value.right).arguments[0]; + if (spec.type === 'ObjectExpression' && Array.isArray(spec.properties)) { + return spec; + } + }; + + const createCreateReactClassCallExpression = properties => + j.callExpression( + j.memberExpression( + j.identifier('React'), + j.identifier('createClass'), + false + ), + [j.objectExpression(properties)] + ); + + const getComponentName = + classPath => classPath.node.id && classPath.node.id.name; + + return { + createCreateReactClassCallExpression, + findReactES6ClassDeclaration, + findReactCreateClass, + findReactCreateClassCallExpression, + findReactCreateClassModuleExports, + getComponentName, + getReactCreateClassSpec, + hasMixins, + hasModule, + hasReact, + isMixinProperty, + }; +}; diff --git a/npm-react-codemod/transforms/utils/array-polyfills.js b/npm-react-codemod/transforms/utils/array-polyfills.js new file mode 100644 index 0000000000000..eed5be59a8ff4 --- /dev/null +++ b/npm-react-codemod/transforms/utils/array-polyfills.js @@ -0,0 +1,36 @@ +/*eslint-disable no-extend-native*/ + +'use strict'; + +function findIndex(predicate, context) { + if (this == null) { + throw new TypeError( + 'Array.prototype.findIndex called on null or undefined' + ); + } + if (typeof predicate !== 'function') { + throw new TypeError('predicate must be a function'); + } + var list = Object(this); + var length = list.length >>> 0; + for (var i = 0; i < length; i++) { + if (predicate.call(context, list[i], i, list)) { + return i; + } + } + return -1; +} + +if (!Array.prototype.findIndex) { + Array.prototype.findIndex = findIndex; +} + +if (!Array.prototype.find) { + Array.prototype.find = function(predicate, context) { + if (this == null) { + throw new TypeError('Array.prototype.find called on null or undefined'); + } + var index = findIndex.call(this, predicate, context); + return index === -1 ? undefined : this[index]; + }; +} From 20004e94d399541da70d08fd12b64dafb498ebce Mon Sep 17 00:00:00 2001 From: cpojer Date: Wed, 25 Mar 2015 01:29:48 -0700 Subject: [PATCH 3/6] Add findDOMNode transform --- npm-react-codemod/README.md | 11 ++ .../test/__tests__/transform-tests.js | 3 + npm-react-codemod/test/findDOMNode-test.js | 34 +++++ .../test/findDOMNode-test.output.js | 34 +++++ npm-react-codemod/transforms/findDOMNode.js | 135 ++++++++++++++++++ 5 files changed, 217 insertions(+) create mode 100644 npm-react-codemod/test/findDOMNode-test.js create mode 100644 npm-react-codemod/test/findDOMNode-test.output.js create mode 100644 npm-react-codemod/transforms/findDOMNode.js diff --git a/npm-react-codemod/README.md b/npm-react-codemod/README.md index 5ebb3c21ccc24..69c9cce41c497 100644 --- a/npm-react-codemod/README.md +++ b/npm-react-codemod/README.md @@ -11,6 +11,17 @@ APIs. * Use the `-d` option for a dry-run and use `-p` to print the output for comparison +### Included Scripts + +`findDOMNode.js` updates `this.getDOMNode()` or `this.refs.foo.getDOMNode()` +calls inside of `React.createClass` components to `React.findDOMNode(foo)`. Note +that it will only look at code inside of `React.createClass` calls and only +update calls on the component instance or its refs. You can use this script to +update most calls to `getDOMNode` and then manually go through the remaining +calls. + + * `react-codemod findDOMNode ` + ### Recast Options Options to [recast](https://github.com/benjamn/recast)'s printer can be provided diff --git a/npm-react-codemod/test/__tests__/transform-tests.js b/npm-react-codemod/test/__tests__/transform-tests.js index 7d73c7f0607ca..d551036d07b54 100644 --- a/npm-react-codemod/test/__tests__/transform-tests.js +++ b/npm-react-codemod/test/__tests__/transform-tests.js @@ -34,6 +34,9 @@ function test(transformName, testFileName, options) { describe('Transform Tests', () => { + it('transforms the "findDOMNode" tests correctly', () => { + test('findDOMNode', 'findDOMNode-test'); + }); }); diff --git a/npm-react-codemod/test/findDOMNode-test.js b/npm-react-codemod/test/findDOMNode-test.js new file mode 100644 index 0000000000000..fbdb0a95f6106 --- /dev/null +++ b/npm-react-codemod/test/findDOMNode-test.js @@ -0,0 +1,34 @@ +'use strict'; + +var React = require('React'); + +var Composer = React.createClass({ + componentWillReceiveProps: function(nextProps) { + this.getDOMNode(); + return foo(this.refs.input.getDOMNode()); + }, + + foo: function() { + var ref = 'foo'; + var element = this.refs[ref]; + var domNode = element.getDOMNode(); + }, + + bar: function() { + var thing = this.refs.foo; + thing.getDOMNode(); + }, + + foobar: function() { + passThisOn(this.refs.main.refs.list.getDOMNode()); + } +}); + +var SomeDialog = React.createClass({ + render: function() { + call(this.refs.SomeThing); + return ( +
+ ); + } +}); diff --git a/npm-react-codemod/test/findDOMNode-test.output.js b/npm-react-codemod/test/findDOMNode-test.output.js new file mode 100644 index 0000000000000..45bfca5bbb124 --- /dev/null +++ b/npm-react-codemod/test/findDOMNode-test.output.js @@ -0,0 +1,34 @@ +'use strict'; + +var React = require('React'); + +var Composer = React.createClass({ + componentWillReceiveProps: function(nextProps) { + React.findDOMNode(this); + return foo(React.findDOMNode(this.refs.input)); + }, + + foo: function() { + var ref = 'foo'; + var element = this.refs[ref]; + var domNode = React.findDOMNode(element); + }, + + bar: function() { + var thing = this.refs.foo; + React.findDOMNode(thing); + }, + + foobar: function() { + passThisOn(React.findDOMNode(this.refs.main.refs.list)); + } +}); + +var SomeDialog = React.createClass({ + render: function() { + call(this.refs.SomeThing); + return ( +
+ ); + } +}); diff --git a/npm-react-codemod/transforms/findDOMNode.js b/npm-react-codemod/transforms/findDOMNode.js new file mode 100644 index 0000000000000..0a7425b02dbef --- /dev/null +++ b/npm-react-codemod/transforms/findDOMNode.js @@ -0,0 +1,135 @@ +/*eslint-disable no-comma-dangle*/ + +'use strict'; + +function getDOMNodeToFindDOMNode(file, api, options) { + const j = api.jscodeshift; + + require('./utils/array-polyfills'); + const ReactUtils = require('./utils/ReactUtils')(j); + + const printOptions = options.printOptions || {quote: 'single'}; + const root = j(file.source); + + const createReactFindDOMNodeCall = arg => j.callExpression( + j.memberExpression( + j.identifier('React'), + j.identifier('findDOMNode'), + false + ), + [arg] + ); + + const updateRefCall = (path, refName) => { + j(path) + .find(j.CallExpression, { + callee: { + object: { + type: 'Identifier', + name: refName, + }, + property: { + type: 'Identifier', + name: 'getDOMNode', + }, + }, + }) + .forEach(callPath => j(callPath).replaceWith( + createReactFindDOMNodeCall(j.identifier(refName)) + )); + }; + + const updateToFindDOMNode = classPath => { + var sum = 0; + + // this.getDOMNode() + sum += j(classPath) + .find(j.CallExpression, { + callee: { + object: { + type: 'ThisExpression', + }, + property: { + type: 'Identifier', + name: 'getDOMNode', + }, + }, + }) + .forEach(path => j(path).replaceWith( + createReactFindDOMNodeCall(j.thisExpression()) + )) + .size(); + + // this.refs.xxx.getDOMNode() or this.refs.xxx.refs.yyy.getDOMNode() + sum += j(classPath) + .find(j.MemberExpression, { + object: { + type: 'MemberExpression', + object: { + type: 'MemberExpression', + object: { + type: 'ThisExpression', + }, + property: { + type: 'Identifier', + name: 'refs', + }, + }, + }, + }) + .closest(j.CallExpression) + .filter(path => ( + path.value.callee.property && + path.value.callee.property.type === 'Identifier' && + path.value.callee.property.name === 'getDOMNode' + )) + .forEach(path => j(path).replaceWith( + createReactFindDOMNodeCall(path.value.callee.object) + )) + .size(); + + // someVariable.getDOMNode() wherre `someVariable = this.refs.xxx` + sum += j(classPath) + .findVariableDeclarators() + .filter(path => { + const init = path.value.init; + const value = init && init.object; + return ( + value && + value.type === 'MemberExpression' && + value.object && + value.object.type === 'ThisExpression' && + value.property && + value.property.type === 'Identifier' && + value.property.name === 'refs' && + init.property && + init.property.type === 'Identifier' + ); + }) + .forEach(path => j(path) + .closest(j.FunctionExpression) + .forEach(fnPath => updateRefCall(fnPath, path.value.id.name)) + ) + .size(); + + return sum > 0; + }; + + if ( + options['no-explicit-require'] || + ReactUtils.hasReact(root) + ) { + const didTransform = ReactUtils + .findReactCreateClass(root) + .filter(updateToFindDOMNode) + .size() > 0; + + if (didTransform) { + return root.toSource(printOptions) + '\n'; + } + } + + return null; +} + +module.exports = getDOMNodeToFindDOMNode; From d4cb2537af0c168637f72dc06fa3472eeabb3eef Mon Sep 17 00:00:00 2001 From: cpojer Date: Wed, 25 Mar 2015 01:30:23 -0700 Subject: [PATCH 4/6] Add pure-render-mixin transform --- npm-react-codemod/README.md | 12 ++ .../test/__tests__/transform-tests.js | 12 ++ .../test/pure-render-mixin-test.js | 45 +++++ .../test/pure-render-mixin-test.output.js | 55 ++++++ .../test/pure-render-mixin-test2.js | 13 ++ .../test/pure-render-mixin-test2.output.js | 13 ++ .../test/pure-render-mixin-test3.js | 14 ++ .../test/pure-render-mixin-test3.output.js | 15 ++ .../test/pure-render-mixin-test4.js | 12 ++ .../test/pure-render-mixin-test4.output.js | 13 ++ .../transforms/pure-render-mixin.js | 179 ++++++++++++++++++ 11 files changed, 383 insertions(+) create mode 100644 npm-react-codemod/test/pure-render-mixin-test.js create mode 100644 npm-react-codemod/test/pure-render-mixin-test.output.js create mode 100644 npm-react-codemod/test/pure-render-mixin-test2.js create mode 100644 npm-react-codemod/test/pure-render-mixin-test2.output.js create mode 100644 npm-react-codemod/test/pure-render-mixin-test3.js create mode 100644 npm-react-codemod/test/pure-render-mixin-test3.output.js create mode 100644 npm-react-codemod/test/pure-render-mixin-test4.js create mode 100644 npm-react-codemod/test/pure-render-mixin-test4.output.js create mode 100644 npm-react-codemod/transforms/pure-render-mixin.js diff --git a/npm-react-codemod/README.md b/npm-react-codemod/README.md index 69c9cce41c497..2bcd084f23909 100644 --- a/npm-react-codemod/README.md +++ b/npm-react-codemod/README.md @@ -22,6 +22,18 @@ calls. * `react-codemod findDOMNode ` +`pure-render-mixin.js` removes `PureRenderMixin` and inlines +`shouldComponentUpdate` so that the ES6 class transform can pick up the React +component and turn it into an ES6 class. NOTE: This currently only works if you +are using the master version (>0.13.1) of React as it is using +`React.addons.shallowCompare` + + * `react-codemod pure-render-mixin ` + * If `--mixin-name=` is specified it will look for the specified name + instead of `PureRenderMixin`. Note that it is not possible to use a + namespaced name for the mixin. `mixins: [React.addons.PureRenderMixin]` will + not currently work. + ### Recast Options Options to [recast](https://github.com/benjamn/recast)'s printer can be provided diff --git a/npm-react-codemod/test/__tests__/transform-tests.js b/npm-react-codemod/test/__tests__/transform-tests.js index d551036d07b54..b7dfde03237f3 100644 --- a/npm-react-codemod/test/__tests__/transform-tests.js +++ b/npm-react-codemod/test/__tests__/transform-tests.js @@ -38,5 +38,17 @@ describe('Transform Tests', () => { test('findDOMNode', 'findDOMNode-test'); }); + it('transforms the "pure-render-mixin" tests correctly', () => { + test('pure-render-mixin', 'pure-render-mixin-test'); + + test('pure-render-mixin', 'pure-render-mixin-test2'); + + test('pure-render-mixin', 'pure-render-mixin-test3'); + + test('pure-render-mixin', 'pure-render-mixin-test4', { + 'mixin-name': 'ReactComponentWithPureRenderMixin', + }); + }); + }); diff --git a/npm-react-codemod/test/pure-render-mixin-test.js b/npm-react-codemod/test/pure-render-mixin-test.js new file mode 100644 index 0000000000000..dcb374328f62a --- /dev/null +++ b/npm-react-codemod/test/pure-render-mixin-test.js @@ -0,0 +1,45 @@ +var React = require('react/addons'); + +var PureRenderMixin = React.addons.PureRenderMixin; + +var MyComponent = React.createClass({ + mixins: [PureRenderMixin], + + render: function() { + return
; + } +}); + +var MyMixedComponent = React.createClass({ + mixins: [PureRenderMixin, SomeOtherMixin], + + render: function() { + return
; + } +}); + +var MyFooComponent = React.createClass({ + mixins: [PureRenderMixin, SomeOtherMixin], + + render: function() { + return
; + }, + + foo: function() { + + } +}); + +var MyStupidComponent = React.createClass({ + mixins: [PureRenderMixin], + + shouldComponentUpdate: function() { + return !!'wtf is this doing here?'; + }, + + render: function() { + return
; + } +}); + +module.exports = MyComponent; diff --git a/npm-react-codemod/test/pure-render-mixin-test.output.js b/npm-react-codemod/test/pure-render-mixin-test.output.js new file mode 100644 index 0000000000000..31fdec6c873a8 --- /dev/null +++ b/npm-react-codemod/test/pure-render-mixin-test.output.js @@ -0,0 +1,55 @@ +var React = require('react/addons'); + +var PureRenderMixin = React.addons.PureRenderMixin; + +var MyComponent = React.createClass({ + shouldComponentUpdate: function(nextProps, nextState) { + return React.addons.shallowCompare(this, nextProps, nextState); + }, + + render: function() { + return
; + } +}); + +var MyMixedComponent = React.createClass({ + mixins: [SomeOtherMixin], + + shouldComponentUpdate: function(nextProps, nextState) { + return React.addons.shallowCompare(this, nextProps, nextState); + }, + + render: function() { + return
; + } +}); + +var MyFooComponent = React.createClass({ + mixins: [SomeOtherMixin], + + render: function() { + return
; + }, + + foo: function() { + + }, + + shouldComponentUpdate: function(nextProps, nextState) { + return React.addons.shallowCompare(this, nextProps, nextState); + } +}); + +var MyStupidComponent = React.createClass({ + mixins: [PureRenderMixin], + + shouldComponentUpdate: function() { + return !!'wtf is this doing here?'; + }, + + render: function() { + return
; + } +}); + +module.exports = MyComponent; diff --git a/npm-react-codemod/test/pure-render-mixin-test2.js b/npm-react-codemod/test/pure-render-mixin-test2.js new file mode 100644 index 0000000000000..e5acfdabe5318 --- /dev/null +++ b/npm-react-codemod/test/pure-render-mixin-test2.js @@ -0,0 +1,13 @@ +var React = require('react/addons'); + +var PureRenderMixin = React.addons.PureRenderMixin; + +var MyComponent = React.createClass({ + mixins: [PureRenderMixin], + + render: function() { + return
; + } +}); + +module.exports = MyComponent; diff --git a/npm-react-codemod/test/pure-render-mixin-test2.output.js b/npm-react-codemod/test/pure-render-mixin-test2.output.js new file mode 100644 index 0000000000000..90d0a7820873d --- /dev/null +++ b/npm-react-codemod/test/pure-render-mixin-test2.output.js @@ -0,0 +1,13 @@ +var React = require('react/addons'); + +var MyComponent = React.createClass({ + shouldComponentUpdate: function(nextProps, nextState) { + return React.addons.shallowCompare(this, nextProps, nextState); + }, + + render: function() { + return
; + } +}); + +module.exports = MyComponent; diff --git a/npm-react-codemod/test/pure-render-mixin-test3.js b/npm-react-codemod/test/pure-render-mixin-test3.js new file mode 100644 index 0000000000000..4044f4779cca2 --- /dev/null +++ b/npm-react-codemod/test/pure-render-mixin-test3.js @@ -0,0 +1,14 @@ +var React = require('react/addons'); + +var Foo = 'Foo'; +var PureRenderMixin = React.addons.PureRenderMixin; + +var MyComponent = React.createClass({ + mixins: [PureRenderMixin], + + render: function() { + return
; + } +}); + +module.exports = MyComponent; diff --git a/npm-react-codemod/test/pure-render-mixin-test3.output.js b/npm-react-codemod/test/pure-render-mixin-test3.output.js new file mode 100644 index 0000000000000..37cc2fe1d3811 --- /dev/null +++ b/npm-react-codemod/test/pure-render-mixin-test3.output.js @@ -0,0 +1,15 @@ +var React = require('react/addons'); + +var Foo = 'Foo'; + +var MyComponent = React.createClass({ + shouldComponentUpdate: function(nextProps, nextState) { + return React.addons.shallowCompare(this, nextProps, nextState); + }, + + render: function() { + return
; + } +}); + +module.exports = MyComponent; diff --git a/npm-react-codemod/test/pure-render-mixin-test4.js b/npm-react-codemod/test/pure-render-mixin-test4.js new file mode 100644 index 0000000000000..cbf384e0f4e7c --- /dev/null +++ b/npm-react-codemod/test/pure-render-mixin-test4.js @@ -0,0 +1,12 @@ +var React = require('React'); +var ReactComponentWithPureRenderMixin = require('ReactComponentWithPureRenderMixin'); + +var MyComponent = React.createClass({ + mixins: [ReactComponentWithPureRenderMixin], + + render: function() { + return
; + } +}); + +module.exports = MyComponent; diff --git a/npm-react-codemod/test/pure-render-mixin-test4.output.js b/npm-react-codemod/test/pure-render-mixin-test4.output.js new file mode 100644 index 0000000000000..14c9cf2f944a6 --- /dev/null +++ b/npm-react-codemod/test/pure-render-mixin-test4.output.js @@ -0,0 +1,13 @@ +var React = require('React'); + +var MyComponent = React.createClass({ + shouldComponentUpdate: function(nextProps, nextState) { + return React.addons.shallowCompare(this, nextProps, nextState); + }, + + render: function() { + return
; + } +}); + +module.exports = MyComponent; diff --git a/npm-react-codemod/transforms/pure-render-mixin.js b/npm-react-codemod/transforms/pure-render-mixin.js new file mode 100644 index 0000000000000..1fb2ec7a25ff0 --- /dev/null +++ b/npm-react-codemod/transforms/pure-render-mixin.js @@ -0,0 +1,179 @@ +/*eslint-disable no-comma-dangle*/ + +'use strict'; + +function removePureRenderMixin(file, api, options) { + const j = api.jscodeshift; + + require('./utils/array-polyfills'); + const ReactUtils = require('./utils/ReactUtils')(j); + + const printOptions = options.printOptions || {quote: 'single'}; + const root = j(file.source); + + const PURE_RENDER_MIXIN = options['mixin-name'] || 'PureRenderMixin'; + const SHOULD_COMPONENT_UPDATE = 'shouldComponentUpdate'; + const NEXT_PROPS = 'nextProps'; + const NEXT_STATE = 'nextState'; + + // --------------------------------------------------------------------------- + // shouldComponentUpdate + const createShouldComponentUpdateFunction = () => + j.functionExpression( + null, + [j.identifier(NEXT_PROPS), j.identifier(NEXT_STATE)], + j.blockStatement([ + j.returnStatement( + j.callExpression( + j.memberExpression( + j.identifier('React'), + j.memberExpression( + j.identifier('addons'), + j.identifier('shallowCompare'), + false + ), + false + ), + [ + j.thisExpression(), + j.identifier(NEXT_PROPS), + j.identifier(NEXT_STATE) + ] + ) + ) + ]) + ); + + const createShouldComponentUpdateProperty = () => + j.property( + 'init', + j.identifier(SHOULD_COMPONENT_UPDATE), + createShouldComponentUpdateFunction() + ); + + const hasShouldComponentUpdate = classPath => + ReactUtils.getReactCreateClassSpec(classPath) + .properties.every(property => + property.key.name !== SHOULD_COMPONENT_UPDATE + ); + + // --------------------------------------------------------------------------- + // Mixin related code + const isPureRenderMixin = node => ( + node.type === 'Identifier' && + node.name === PURE_RENDER_MIXIN + ); + + const hasPureRenderMixin = classPath => { + const spec = ReactUtils.getReactCreateClassSpec(classPath); + const mixin = spec && spec.properties.find(ReactUtils.isMixinProperty); + return mixin && mixin.value.elements.some(isPureRenderMixin); + }; + + const removeMixin = elements => + j.property( + 'init', + j.identifier('mixins'), + j.arrayExpression( + elements.filter(element => !isPureRenderMixin(element)) + ) + ); + + // --------------------------------------------------------------------------- + // Boom! + const insertShouldComponentUpdate = properties => { + const length = properties.length; + const lastProp = properties[length - 1]; + // I wouldn't dare insert at the bottom if the last function is render + if ( + lastProp.key.type === 'Identifier' && + lastProp.key.name === 'render' + ) { + properties.splice( + length - 1, + 1, + createShouldComponentUpdateProperty(), + lastProp + ); + } else { + properties.push(createShouldComponentUpdateProperty()); + } + return properties; + }; + + const cleanupReactComponent = classPath => { + const spec = ReactUtils.getReactCreateClassSpec(classPath); + const properties = spec.properties + .map(property => { + if (ReactUtils.isMixinProperty(property)) { + const elements = property.value.elements; + return (elements.length !== 1) ? removeMixin(elements) : null; + } + return property; + }) + .filter(property => !!property); + + ReactUtils.findReactCreateClassCallExpression(classPath).replaceWith( + ReactUtils.createCreateReactClassCallExpression( + insertShouldComponentUpdate(properties) + ) + ); + }; + + // Remove it if only two or fewer are left: + // var PureRenderMixin = React.addons.PureRenderMixin; + const hasPureRenderIdentifiers = path => + path.find(j.Identifier, { + name: PURE_RENDER_MIXIN, + }).size() > 2; + + const deletePureRenderMixin = path => { + if (hasPureRenderIdentifiers(path)) { + return; + } + + const declaration = path + .findVariableDeclarators(PURE_RENDER_MIXIN) + .closest(j.VariableDeclaration); + + if (declaration.size > 1) { + declaration.forEach(p => + j(p).replaceWith( + j.variableDeclaration( + 'var', + p.value.declarations.filter(isPureRenderMixin) + ) + ) + ); + } else { + // Let's assume the variable declaration happens at the top level + const program = declaration.closest(j.Program).get(); + const body = program.value.body; + const index = body.indexOf(declaration.get().value); + if (index !== -1) { + body.splice(index, 1); + } + } + }; + + if ( + options['no-explicit-require'] || + ReactUtils.hasReact(root) + ) { + const didTransform = ReactUtils + .findReactCreateClass(root) + .filter(hasPureRenderMixin) + .filter(hasShouldComponentUpdate) + .forEach(cleanupReactComponent) + .size() > 0; + + if (didTransform) { + deletePureRenderMixin(root); + return root.toSource(printOptions) + '\n'; + } + } + + return null; +} + +module.exports = removePureRenderMixin; From d9c13c73b6f6c1e928258f6686939dd5503e7408 Mon Sep 17 00:00:00 2001 From: cpojer Date: Wed, 25 Mar 2015 01:30:35 -0700 Subject: [PATCH 5/6] Add ES6 class transform --- npm-react-codemod/README.md | 58 ++ .../test/__tests__/transform-tests.js | 9 + npm-react-codemod/test/class-test.js | 128 +++++ npm-react-codemod/test/class-test.output.js | 139 +++++ npm-react-codemod/test/class-test2.js | 33 ++ npm-react-codemod/test/class-test2.output.js | 31 ++ npm-react-codemod/test/class-test3.js | 20 + npm-react-codemod/test/class-test3.output.js | 22 + npm-react-codemod/transforms/class.js | 495 ++++++++++++++++++ 9 files changed, 935 insertions(+) create mode 100644 npm-react-codemod/test/class-test.js create mode 100644 npm-react-codemod/test/class-test.output.js create mode 100644 npm-react-codemod/test/class-test2.js create mode 100644 npm-react-codemod/test/class-test2.output.js create mode 100644 npm-react-codemod/test/class-test3.js create mode 100644 npm-react-codemod/test/class-test3.output.js create mode 100644 npm-react-codemod/transforms/class.js diff --git a/npm-react-codemod/README.md b/npm-react-codemod/README.md index 2bcd084f23909..d3870703c6811 100644 --- a/npm-react-codemod/README.md +++ b/npm-react-codemod/README.md @@ -34,6 +34,64 @@ are using the master version (>0.13.1) of React as it is using namespaced name for the mixin. `mixins: [React.addons.PureRenderMixin]` will not currently work. +`class.js` transforms `React.createClass` calls into ES6 classes. + + * `react-codemod class ` + * If `--no-super-class=true` is specified it will not extend + `React.Component` if `setState` and `forceUpdate` aren't being called in a + class. We do recommend always extending from `React.Component`, especially + if you are using or planning to use [Flow](http://flowtype.org/). Also make + sure you are not calling `setState` anywhere outside of your component. + +All scripts take an option `--no-explicit-require=true` if you don't have a +`require('React')` statement in your code files and if you access React as a +global. + +### Explanation of the ES6 class transform + + * Ignore components with calls to deprecated APIs. This is very defensive, if + the script finds any identifiers called `isMounted`, `getDOMNode`, + `replaceProps`, `replaceState` or `setProps` it will skip the component. + * Replaces `var A = React.createClass(spec)` with + `class A (extends React.Component) {spec}`. + * Pulls out all statics defined on `statics` plus the few special cased + statics like `propTypes`, `childContextTypes`, `contextTypes` and + `displayName` and assigns them after the class is created. + `class A {}; A.foo = bar;` + * Takes `getDefaultProps` and inlines it as a static `defaultProps`. + If `getDefaultProps` is defined as a function with a single statement that + returns an object, it optimizes and transforms + `getDefaultProps() { return {foo: 'bar'}; }` into + `A.defaultProps = {foo: 'bar'};`. If `getDefaultProps` contains more than + one statement it will transform into a self-invoking function like this: + `A.defaultProps = function() {…}();`. Note that this means that the function + will be executed only a single time per app-lifetime. In practice this + hasn't caused any issues – `getDefaultProps` should not contain any + side-effects. + * Binds class methods to the instance if methods are referenced without being + called directly. It checks for `this.foo` but also traces variable + assignments like `var self = this; self.foo`. It does not bind functions + from the React API and ignores functions that are being called directly + (unless it is both called directly and passed around to somewhere else) + * Creates a constructor if necessary. This is necessary if either + `getInitialState` exists in the `React.createClass` spec OR if functions + need to be bound to the instance. + * When `--no-super-class=true` is passed it only optionally extends + `React.Component` when `setState` or `forceUpdate` are used within the + class. + +The constructor logic is as follows: + * Call `super(props, context)` if the base class needs to be extended. + * Bind all functions that are passed around, + like `this.foo = this.foo.bind(this)` + * Inline `getInitialState` (and remove `getInitialState` from the spec). It + also updates access of `this.props.foo` to `props.foo` and adds `props` as + argument to the constructor. This is necessary in the case when the base + class does not need to be extended where `this.props` will only be set by + React after the constructor has been run. + * Changes `return StateObject` from `getInitialState` to assign `this.state` + directly. + ### Recast Options Options to [recast](https://github.com/benjamn/recast)'s printer can be provided diff --git a/npm-react-codemod/test/__tests__/transform-tests.js b/npm-react-codemod/test/__tests__/transform-tests.js index b7dfde03237f3..de3c118749a50 100644 --- a/npm-react-codemod/test/__tests__/transform-tests.js +++ b/npm-react-codemod/test/__tests__/transform-tests.js @@ -50,5 +50,14 @@ describe('Transform Tests', () => { }); }); + it('transforms the "class" tests correctly', () => { + test('class', 'class-test'); + + test('class', 'class-test2', { + 'no-super-class': true, + }); + + test('class', 'class-test3'); + }); }); diff --git a/npm-react-codemod/test/class-test.js b/npm-react-codemod/test/class-test.js new file mode 100644 index 0000000000000..b01c8b13b5a40 --- /dev/null +++ b/npm-react-codemod/test/class-test.js @@ -0,0 +1,128 @@ +'use strict'; + +var React = require('React'); +var Relay = require('Relay'); + +var Image = require('Image.react'); + +/* + * Multiline + */ +var MyComponent = React.createClass({ + getInitialState: function() { + var x = this.props.foo; + return { + heyoo: 23, + }; + }, + + foo: function() { + this.setState({heyoo: 24}); + } +}); + +// Class comment +var MyComponent2 = React.createClass({ + getDefaultProps: function() { + return {a: 1}; + }, + foo: function() { + pass(this.foo); + this.forceUpdate(); + } +}); + +var MyComponent3 = React.createClass({ + statics: { + someThing: 10, + foo: function() {}, + }, + propTypes: { + highlightEntities: React.PropTypes.bool, + linkifyEntities: React.PropTypes.bool, + text: React.PropTypes.shape({ + text: React.PropTypes.string, + ranges: React.PropTypes.array + }).isRequired + }, + + getDefaultProps: function() { + foo(); + return { + linkifyEntities: true, + highlightEntities: false + }; + }, + + getInitialState: function() { + this.props.foo(); + return { + heyoo: 23, + }; + }, + + _renderText: function(text) { + return ; + }, + + _renderImageRange: function(text, range) { + var image = range.image; + if (image) { + return ( + + ); + } + }, + + autobindMe: function() {}, + dontAutobindMe: function() {}, + + // Function comment + _renderRange: function(text, range) { + var self = this; + + self.dontAutobindMe(); + call(self.autobindMe); + + var type = rage.type; + var {highlightEntities} = this.props; + + if (type === 'ImageAtRange') { + return this._renderImageRange(text, range); + } + + if (this.props.linkifyEntities) { + text = + + {text} + ; + } else { + text = {text}; + } + + return text; + }, + + /* This is a comment */ + render: function() { + var content = this.props.text; + return ( + + ); + } +}); + +module.exports = Relay.createContainer(MyComponent, { + queries: { + me: Relay.graphql`this is not graphql`, + } +}); diff --git a/npm-react-codemod/test/class-test.output.js b/npm-react-codemod/test/class-test.output.js new file mode 100644 index 0000000000000..b8a46713f1f7c --- /dev/null +++ b/npm-react-codemod/test/class-test.output.js @@ -0,0 +1,139 @@ +'use strict'; + +var React = require('React'); +var Relay = require('Relay'); + +var Image = require('Image.react'); + +/* + * Multiline + */ +class MyComponent extends React.Component { + constructor(props, context) { + super(props, context); + var x = props.foo; + + this.state = { + heyoo: 23, + }; + } + + foo() { + this.setState({heyoo: 24}); + } +} + +// Class comment +class MyComponent2 extends React.Component { + constructor(props, context) { + super(props, context); + this.foo = this.foo.bind(this); + } + + foo() { + pass(this.foo); + this.forceUpdate(); + } +} + +MyComponent2.defaultProps = {a: 1}; + +class MyComponent3 extends React.Component { + constructor(props, context) { + super(props, context); + this._renderRange = this._renderRange.bind(this); + this._renderText = this._renderText.bind(this); + this.autobindMe = this.autobindMe.bind(this); + props.foo(); + + this.state = { + heyoo: 23, + }; + } + + _renderText(text) { + return ; + } + + _renderImageRange(text, range) { + var image = range.image; + if (image) { + return ( + + ); + } + } + + autobindMe() {} + dontAutobindMe() {} + + // Function comment + _renderRange(text, range) { + var self = this; + + self.dontAutobindMe(); + call(self.autobindMe); + + var type = rage.type; + var {highlightEntities} = this.props; + + if (type === 'ImageAtRange') { + return this._renderImageRange(text, range); + } + + if (this.props.linkifyEntities) { + text = + + {text} + ; + } else { + text = {text}; + } + + return text; + } + + /* This is a comment */ + render() { + var content = this.props.text; + return ( + + ); + } +} + +MyComponent3.defaultProps = function() { + foo(); + return { + linkifyEntities: true, + highlightEntities: false + }; +}(); + +MyComponent3.foo = function() {}; + +MyComponent3.propTypes = { + highlightEntities: React.PropTypes.bool, + linkifyEntities: React.PropTypes.bool, + text: React.PropTypes.shape({ + text: React.PropTypes.string, + ranges: React.PropTypes.array + }).isRequired +}; + +MyComponent3.someThing = 10; + +module.exports = Relay.createContainer(MyComponent, { + queries: { + me: Relay.graphql`this is not graphql`, + } +}); diff --git a/npm-react-codemod/test/class-test2.js b/npm-react-codemod/test/class-test2.js new file mode 100644 index 0000000000000..7773f3cd29c4c --- /dev/null +++ b/npm-react-codemod/test/class-test2.js @@ -0,0 +1,33 @@ +'use strict'; + +var React = require('React'); + +var IdontNeedAParent = React.createClass({ + render: function() { + return
; + } +}); + +var ButIDo = React.createClass({ + foo: function() { + this.setState({banana: '?'}); + }, + + render: function() { + return
; + } +}); + +var IAccessProps = React.createClass({ + + getInitialState: function() { + return { + relayReleaseDate: this.props.soon, + }; + }, + + render: function() { + return + } + +}); diff --git a/npm-react-codemod/test/class-test2.output.js b/npm-react-codemod/test/class-test2.output.js new file mode 100644 index 0000000000000..9021d6ffa79a7 --- /dev/null +++ b/npm-react-codemod/test/class-test2.output.js @@ -0,0 +1,31 @@ +'use strict'; + +var React = require('React'); + +class IdontNeedAParent { + render() { + return
; + } +} + +class ButIDo extends React.Component { + foo() { + this.setState({banana: '?'}); + } + + render() { + return
; + } +} + +class IAccessProps { + constructor(props) { + this.state = { + relayReleaseDate: props.soon, + }; + } + + render() { + return + } +} diff --git a/npm-react-codemod/test/class-test3.js b/npm-react-codemod/test/class-test3.js new file mode 100644 index 0000000000000..ba03cf8dbd4fe --- /dev/null +++ b/npm-react-codemod/test/class-test3.js @@ -0,0 +1,20 @@ +'use strict'; + +var React = require('React'); + +// Comment +module.exports = React.createClass({ + propTypes: { + foo: React.PropTypes.bool, + }, + + getInitialState: function() { + return { + foo: 'bar', + }; + }, + + render: function() { + return
; + } +}); diff --git a/npm-react-codemod/test/class-test3.output.js b/npm-react-codemod/test/class-test3.output.js new file mode 100644 index 0000000000000..7da46bff5c288 --- /dev/null +++ b/npm-react-codemod/test/class-test3.output.js @@ -0,0 +1,22 @@ +'use strict'; + +var React = require('React'); + +// Comment +module.exports = class __exports extends React.Component { + constructor(props, context) { + super(props, context); + + this.state = { + foo: 'bar', + }; + } + + render() { + return
; + } +}; + +module.exports.propTypes = { + foo: React.PropTypes.bool, +}; diff --git a/npm-react-codemod/transforms/class.js b/npm-react-codemod/transforms/class.js new file mode 100644 index 0000000000000..5d521201535ae --- /dev/null +++ b/npm-react-codemod/transforms/class.js @@ -0,0 +1,495 @@ +/*eslint-disable no-comma-dangle*/ + +'use strict'; + +function updateReactCreateClassToES6(file, api, options) { + const j = api.jscodeshift; + + require('./utils/array-polyfills'); + const ReactUtils = require('./utils/ReactUtils')(j); + + const printOptions = options.printOptions || {quote: 'single'}; + const root = j(file.source); + + const AUTOBIND_IGNORE_KEYS = { + componentDidMount: true, + componentDidUpdate: true, + componentWillReceiveProps: true, + componentWillMount: true, + componentWillUpdate: true, + componentWillUnmount: true, + getDefaultProps: true, + getInitialState: true, + render: true, + shouldComponentUpdate: true, + }; + + const BASE_COMPONENT_METHODS = ['setState', 'forceUpdate']; + + const DEFAULT_PROPS_FIELD = 'getDefaultProps'; + const DEFAULT_PROPS_KEY = 'defaultProps'; + const GET_INITIAL_STATE_FIELD = 'getInitialState'; + + const DEPRECATED_APIS = [ + 'getDOMNode', + 'isMounted', + 'replaceProps', + 'replaceState', + 'setProps', + ]; + + const STATIC_KEYS = { + childContextTypes: true, + contextTypes: true, + displayName: true, + propTypes: true, + }; + + // --------------------------------------------------------------------------- + // Checks if the module uses mixins or accesses deprecated APIs. + const checkDeprecatedAPICalls = classPath => + DEPRECATED_APIS.reduce( + (acc, name) => + acc + j(classPath) + .find(j.Identifier, {name}) + .size(), + 0 + ) > 0; + + const callsDeprecatedAPIs = classPath => { + if (checkDeprecatedAPICalls(classPath)) { + console.log( + file.path + ': "' + ReactUtils.getComponentName(classPath) + '" ' + + 'skipped because of deprecated API calls. Remove calls to ' + + DEPRECATED_APIS.join(', ') + ' in your React component and re-run ' + + 'this script.' + ); + return false; + } + return true; + }; + + const hasMixins = classPath => { + if (ReactUtils.hasMixins(classPath)) { + console.log( + file.path + ': "' + ReactUtils.getComponentName(classPath) + '" ' + + ' skipped because of mixins.' + ); + return false; + } + return true; + }; + + // --------------------------------------------------------------------------- + // Helpers + const createFindPropFn = prop => property => ( + property.key && + property.key.type === 'Identifier' && + property.key.name === prop + ); + + const filterDefaultPropsField = node => + createFindPropFn(DEFAULT_PROPS_FIELD)(node); + + const filterGetInitialStateField = node => + createFindPropFn(GET_INITIAL_STATE_FIELD)(node); + + const findGetDefaultProps = specPath => + specPath.properties.find(createFindPropFn(DEFAULT_PROPS_FIELD)); + + const findGetInitialState = specPath => + specPath.properties.find(createFindPropFn(GET_INITIAL_STATE_FIELD)); + + // This is conservative; only check for `setState` and `forceUpdate` literals + // instead of also checking which objects they are called on. + const shouldExtendReactComponent = classPath => + BASE_COMPONENT_METHODS.some(name => ( + j(classPath) + .find(j.Identifier, {name}) + .size() > 0 + )); + + const withComments = (to, from) => { + to.comments = from.comments; + return to; + }; + + // --------------------------------------------------------------------------- + // Collectors + const isFunctionExpression = node => ( + node.key && + node.key.type === 'Identifier' && + node.value && + node.value.type === 'FunctionExpression' + ); + + const collectStatics = specPath => { + const statics = specPath.properties.find(createFindPropFn('statics')); + const staticsObject = + (statics && statics.value && statics.value.properties) || []; + + const getDefaultProps = findGetDefaultProps(specPath); + if (getDefaultProps) { + staticsObject.push(createDefaultProps(getDefaultProps)); + } + + return ( + staticsObject.concat(specPath.properties.filter(property => + property.key && STATIC_KEYS[property.key.name] + )) + .sort((a, b) => a.key.name < b.key.name) + ); + }; + + const collectFunctions = specPath => specPath.properties + .filter(prop => + !(filterDefaultPropsField(prop) || filterGetInitialStateField(prop)) + ) + .filter(isFunctionExpression); + + const findAutobindNamesFor = (root, fnNames, literalOrIdentifier) => { + const node = literalOrIdentifier; + const autobindNames = {}; + + j(root) + .find(j.MemberExpression, { + object: node.name ? { + type: node.type, + name: node.name, + } : {type: node.type}, + property: { + type: 'Identifier', + }, + }) + .filter(path => path.value.property && fnNames[path.value.property.name]) + .filter(path => { + const call = path.parent.value; + return !( + call && + call.type === 'CallExpression' && + call.callee.type === 'MemberExpression' && + call.callee.object.type === node.type && + call.callee.object.name === node.name && + call.callee.property.type === 'Identifier' && + call.callee.property.name === path.value.property.name + ); + }) + .forEach(path => autobindNames[path.value.property.name] = true); + + return Object.keys(autobindNames); + }; + + const collectAutoBindFunctions = (functions, classPath) => { + const fnNames = {}; + functions + .filter(fn => !AUTOBIND_IGNORE_KEYS[fn.key.name]) + .forEach(fn => fnNames[fn.key.name] = true); + + const autobindNames = {}; + const add = name => autobindNames[name] = true; + + // Find `this.` + findAutobindNamesFor(classPath, fnNames, j.thisExpression()).forEach(add); + + // Find `self.` if `self = this` + j(classPath) + .findVariableDeclarators() + .filter(path => ( + path.value.id.type === 'Identifier' && + path.value.init && + path.value.init.type === 'ThisExpression' + )) + .forEach(path => + findAutobindNamesFor( + j(path).closest(j.FunctionExpression).get(), + fnNames, + path.value.id + ).forEach(add) + ); + + return Object.keys(autobindNames).sort(); + }; + + // --------------------------------------------------------------------------- + // Boom! + const createMethodDefinition = fn => + withComments(j.methodDefinition( + '', + fn.key, + fn.value + ), fn); + + const createBindAssignment = name => + j.expressionStatement( + j.assignmentExpression( + '=', + j.memberExpression( + j.thisExpression(), + j.identifier(name), + false + ), + j.callExpression( + j.memberExpression( + j.memberExpression( + j.thisExpression(), + j.identifier(name), + false + ), + j.identifier('bind'), + false + ), + [j.thisExpression()] + ) + ) + ); + + const createSuperCall = shouldAddSuperCall => + !shouldAddSuperCall ? + [] : + [j.expressionStatement( + j.callExpression( + j.identifier('super'), + [j.identifier('props'), j.identifier('context')] + ) + )]; + + const updatePropsAccess = getInitialState => + getInitialState ? + j(getInitialState) + .find(j.MemberExpression, { + object: { + type: 'ThisExpression', + }, + property: { + type: 'Identifier', + name: 'props', + }, + }) + .forEach(path => j(path).replaceWith(j.identifier('props'))) + .size() > 0 : + false; + + const inlineGetInitialState = getInitialState => { + if (!getInitialState) { + return []; + } + + return getInitialState.value.body.body.map(statement => { + if (statement.type === 'ReturnStatement') { + return j.expressionStatement( + j.assignmentExpression( + '=', + j.memberExpression( + j.thisExpression(), + j.identifier('state'), + false + ), + statement.argument + ) + ); + } + + return statement; + }); + }; + + const createConstructorArgs = (shouldAddSuperClass, hasPropsAccess) => { + if (shouldAddSuperClass) { + return [j.identifier('props'), j.identifier('context')]; + } else if (hasPropsAccess) { + return [j.identifier('props')]; + } else { + return []; + } + }; + + const createConstructor = ( + getInitialState, + autobindFunctions, + shouldAddSuperClass + ) => { + if (!getInitialState && !autobindFunctions.length) { + return []; + } + + const hasPropsAccess = updatePropsAccess(getInitialState); + return [createMethodDefinition({ + key: j.identifier('constructor'), + value: j.functionExpression( + null, + createConstructorArgs(shouldAddSuperClass, hasPropsAccess), + j.blockStatement( + [].concat( + createSuperCall(shouldAddSuperClass), + autobindFunctions.map(createBindAssignment), + inlineGetInitialState(getInitialState) + ) + ) + ), + })]; + }; + + const createES6Class = ( + name, + functions, + getInitialState, + autobindFunctions, + comments, + shouldAddSuperClass + ) => + withComments(j.classDeclaration( + // ast-types does not yet support empty class names + // See: https://github.com/benjamn/ast-types/pull/102 + name ? j.identifier(name) : j.identifier('__exports'), + j.classBody( + [].concat( + createConstructor( + getInitialState, + autobindFunctions, + shouldAddSuperClass + ), + functions.map(createMethodDefinition) + ) + ), + shouldAddSuperClass ? j.memberExpression( + j.identifier('React'), + j.identifier('Component'), + false + ) : null + ), {comments}); + + const createStaticAssignment = (name, staticProperty) => + withComments(j.expressionStatement( + j.assignmentExpression( + '=', + j.memberExpression( + name, + j.identifier(staticProperty.key.name), + false + ), + staticProperty.value + ) + ), staticProperty); + + const createStaticAssignmentExpressions = (name, statics) => + statics.map(staticProperty => createStaticAssignment(name, staticProperty)); + + const hasSingleReturnStatement = value => ( + value.type === 'FunctionExpression' && + value.body && + value.body.type === 'BlockStatement' && + value.body.body && + value.body.body.length === 1 && + value.body.body[0].type === 'ReturnStatement' && + value.body.body[0].argument && + value.body.body[0].argument.type === 'ObjectExpression' + ); + + const createDefaultPropsValue = value => { + if (hasSingleReturnStatement(value)) { + return value.body.body[0].argument; + } else { + return j.callExpression( + value, + [] + ); + } + }; + + const createDefaultProps = prop => + withComments( + j.property( + 'init', + j.identifier(DEFAULT_PROPS_KEY), + createDefaultPropsValue(prop.value) + ), + prop + ); + + const getComments = classPath => { + if (classPath.value.comments) { + return classPath.value.comments; + } + const declaration = j(classPath).closest(j.VariableDeclaration); + if (declaration.size()) { + return declaration.get().value.comments; + } + return null; + }; + + const createModuleExportsMemberExpression = () => + j.memberExpression( + j.identifier('module'), + j.identifier('exports'), + false + ); + + const updateToES6Class = (classPath, shouldExtend, isModuleExports) => { + const specPath = ReactUtils.getReactCreateClassSpec(classPath); + const name = ReactUtils.getComponentName(classPath); + const statics = collectStatics(specPath); + const functions = collectFunctions(specPath); + const comments = getComments(classPath); + + const autobindFunctions = collectAutoBindFunctions(functions, classPath); + const getInitialState = findGetInitialState(specPath); + + const staticName = + name ? j.identifier(name) : createModuleExportsMemberExpression(); + + var path; + if (isModuleExports) { + path = ReactUtils.findReactCreateClassCallExpression(classPath); + } else { + path = j(classPath).closest(j.VariableDeclaration); + } + + path.replaceWith( + createES6Class( + name, + functions, + getInitialState, + autobindFunctions, + comments, + shouldExtend || shouldExtendReactComponent(classPath) + ) + ); + + const staticAssignments = + createStaticAssignmentExpressions(staticName, statics); + if (isModuleExports) { + const body = root.get().value.body; + body.push.apply(body, staticAssignments); + } else { + staticAssignments + .forEach(expression => path = path.insertAfter(expression)); + } + }; + + if ( + options['no-explicit-require'] || ReactUtils.hasReact(root) + ) { + const apply = (path, isModuleExports) => + path + .filter(hasMixins) + .filter(callsDeprecatedAPIs) + .forEach(classPath => updateToES6Class( + classPath, + !options['no-super-class'], + isModuleExports + )); + + const didTransform = ( + apply(ReactUtils.findReactCreateClass(root), false).size() + + apply(ReactUtils.findReactCreateClassModuleExports(root), true).size() + ) > 0; + + if (didTransform) { + return root.toSource(printOptions) + '\n'; + } + } + + return null; +} + +module.exports = updateReactCreateClassToES6; From 1865c042d7cbf27316f64ba4f56c06a5fe42f966 Mon Sep 17 00:00:00 2001 From: cpojer Date: Wed, 25 Mar 2015 16:15:07 -0700 Subject: [PATCH 6/6] Update README for npm-react-codemod --- npm-react-codemod/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/npm-react-codemod/README.md b/npm-react-codemod/README.md index d3870703c6811..f8c439c561091 100644 --- a/npm-react-codemod/README.md +++ b/npm-react-codemod/README.md @@ -13,7 +13,7 @@ APIs. ### Included Scripts -`findDOMNode.js` updates `this.getDOMNode()` or `this.refs.foo.getDOMNode()` +`findDOMNode` updates `this.getDOMNode()` or `this.refs.foo.getDOMNode()` calls inside of `React.createClass` components to `React.findDOMNode(foo)`. Note that it will only look at code inside of `React.createClass` calls and only update calls on the component instance or its refs. You can use this script to @@ -22,7 +22,7 @@ calls. * `react-codemod findDOMNode ` -`pure-render-mixin.js` removes `PureRenderMixin` and inlines +`pure-render-mixin` removes `PureRenderMixin` and inlines `shouldComponentUpdate` so that the ES6 class transform can pick up the React component and turn it into an ES6 class. NOTE: This currently only works if you are using the master version (>0.13.1) of React as it is using @@ -34,7 +34,7 @@ are using the master version (>0.13.1) of React as it is using namespaced name for the mixin. `mixins: [React.addons.PureRenderMixin]` will not currently work. -`class.js` transforms `React.createClass` calls into ES6 classes. +`class` transforms `React.createClass` calls into ES6 classes. * `react-codemod class ` * If `--no-super-class=true` is specified it will not extend