From d4cb2537af0c168637f72dc06fa3472eeabb3eef Mon Sep 17 00:00:00 2001 From: cpojer Date: Wed, 25 Mar 2015 01:30:23 -0700 Subject: [PATCH] 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 69c9cce41c..2bcd084f23 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 d551036d07..b7dfde0323 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 0000000000..dcb374328f --- /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 0000000000..31fdec6c87 --- /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 0000000000..e5acfdabe5 --- /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 0000000000..90d0a78208 --- /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 0000000000..4044f4779c --- /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 0000000000..37cc2fe1d3 --- /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 0000000000..cbf384e0f4 --- /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 0000000000..14c9cf2f94 --- /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 0000000000..1fb2ec7a25 --- /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;