@samitouri / QOS-React-2 / commits / d4cb2537af

Add pure-render-mixin transform

cpojer committed Mar 25, 2015 at 01:30 UTC d4cb2537af0c168637f72dc06fa3472eeabb3eef
11 files changed +383
npm-react-codemod/README.md
+12
@@ -22,6 +22,18 @@ calls.
22
23 * `react-codemod findDOMNode <file>`
24
25 +`pure-render-mixin.js` removes `PureRenderMixin` and inlines
26 +`shouldComponentUpdate` so that the ES6 class transform can pick up the React
27 +component and turn it into an ES6 class. NOTE: This currently only works if you
28 +are using the master version (>0.13.1) of React as it is using
29 +`React.addons.shallowCompare`
30 +
31 + * `react-codemod pure-render-mixin <file>`
32 + * If `--mixin-name=<name>` is specified it will look for the specified name
33 + instead of `PureRenderMixin`. Note that it is not possible to use a
34 + namespaced name for the mixin. `mixins: [React.addons.PureRenderMixin]` will
35 + not currently work.
36 +
37 ### Recast Options
38
39 Options to [recast](https://github.com/benjamn/recast)'s printer can be provided
npm-react-codemod/test/__tests__/transform-tests.js
+12
@@ -38,5 +38,17 @@ describe('Transform Tests', () => {
38 test('findDOMNode', 'findDOMNode-test');
39 });
40
41 + it('transforms the "pure-render-mixin" tests correctly', () => {
42 + test('pure-render-mixin', 'pure-render-mixin-test');
43 +
44 + test('pure-render-mixin', 'pure-render-mixin-test2');
45 +
46 + test('pure-render-mixin', 'pure-render-mixin-test3');
47 +
48 + test('pure-render-mixin', 'pure-render-mixin-test4', {
49 + 'mixin-name': 'ReactComponentWithPureRenderMixin',
50 + });
51 + });
52 +
53
54 });
npm-react-codemod/test/pure-render-mixin-test.js new
+45
@@ -0,0 +1,45 @@
1 +var React = require('react/addons');
2 +
3 +var PureRenderMixin = React.addons.PureRenderMixin;
4 +
5 +var MyComponent = React.createClass({
6 + mixins: [PureRenderMixin],
7 +
8 + render: function() {
9 + return <div />;
10 + }
11 +});
12 +
13 +var MyMixedComponent = React.createClass({
14 + mixins: [PureRenderMixin, SomeOtherMixin],
15 +
16 + render: function() {
17 + return <div />;
18 + }
19 +});
20 +
21 +var MyFooComponent = React.createClass({
22 + mixins: [PureRenderMixin, SomeOtherMixin],
23 +
24 + render: function() {
25 + return <div />;
26 + },
27 +
28 + foo: function() {
29 +
30 + }
31 +});
32 +
33 +var MyStupidComponent = React.createClass({
34 + mixins: [PureRenderMixin],
35 +
36 + shouldComponentUpdate: function() {
37 + return !!'wtf is this doing here?';
38 + },
39 +
40 + render: function() {
41 + return <div />;
42 + }
43 +});
44 +
45 +module.exports = MyComponent;
npm-react-codemod/test/pure-render-mixin-test.output.js new
+55
@@ -0,0 +1,55 @@
1 +var React = require('react/addons');
2 +
3 +var PureRenderMixin = React.addons.PureRenderMixin;
4 +
5 +var MyComponent = React.createClass({
6 + shouldComponentUpdate: function(nextProps, nextState) {
7 + return React.addons.shallowCompare(this, nextProps, nextState);
8 + },
9 +
10 + render: function() {
11 + return <div />;
12 + }
13 +});
14 +
15 +var MyMixedComponent = React.createClass({
16 + mixins: [SomeOtherMixin],
17 +
18 + shouldComponentUpdate: function(nextProps, nextState) {
19 + return React.addons.shallowCompare(this, nextProps, nextState);
20 + },
21 +
22 + render: function() {
23 + return <div />;
24 + }
25 +});
26 +
27 +var MyFooComponent = React.createClass({
28 + mixins: [SomeOtherMixin],
29 +
30 + render: function() {
31 + return <div />;
32 + },
33 +
34 + foo: function() {
35 +
36 + },
37 +
38 + shouldComponentUpdate: function(nextProps, nextState) {
39 + return React.addons.shallowCompare(this, nextProps, nextState);
40 + }
41 +});
42 +
43 +var MyStupidComponent = React.createClass({
44 + mixins: [PureRenderMixin],
45 +
46 + shouldComponentUpdate: function() {
47 + return !!'wtf is this doing here?';
48 + },
49 +
50 + render: function() {
51 + return <div />;
52 + }
53 +});
54 +
55 +module.exports = MyComponent;
npm-react-codemod/test/pure-render-mixin-test2.js new
+13
@@ -0,0 +1,13 @@
1 +var React = require('react/addons');
2 +
3 +var PureRenderMixin = React.addons.PureRenderMixin;
4 +
5 +var MyComponent = React.createClass({
6 + mixins: [PureRenderMixin],
7 +
8 + render: function() {
9 + return <div />;
10 + }
11 +});
12 +
13 +module.exports = MyComponent;
npm-react-codemod/test/pure-render-mixin-test2.output.js new
+13
@@ -0,0 +1,13 @@
1 +var React = require('react/addons');
2 +
3 +var MyComponent = React.createClass({
4 + shouldComponentUpdate: function(nextProps, nextState) {
5 + return React.addons.shallowCompare(this, nextProps, nextState);
6 + },
7 +
8 + render: function() {
9 + return <div />;
10 + }
11 +});
12 +
13 +module.exports = MyComponent;
npm-react-codemod/test/pure-render-mixin-test3.js new
+14
@@ -0,0 +1,14 @@
1 +var React = require('react/addons');
2 +
3 +var Foo = 'Foo';
4 +var PureRenderMixin = React.addons.PureRenderMixin;
5 +
6 +var MyComponent = React.createClass({
7 + mixins: [PureRenderMixin],
8 +
9 + render: function() {
10 + return <div />;
11 + }
12 +});
13 +
14 +module.exports = MyComponent;
npm-react-codemod/test/pure-render-mixin-test3.output.js new
+15
@@ -0,0 +1,15 @@
1 +var React = require('react/addons');
2 +
3 +var Foo = 'Foo';
4 +
5 +var MyComponent = React.createClass({
6 + shouldComponentUpdate: function(nextProps, nextState) {
7 + return React.addons.shallowCompare(this, nextProps, nextState);
8 + },
9 +
10 + render: function() {
11 + return <div />;
12 + }
13 +});
14 +
15 +module.exports = MyComponent;
npm-react-codemod/test/pure-render-mixin-test4.js new
+12
@@ -0,0 +1,12 @@
1 +var React = require('React');
2 +var ReactComponentWithPureRenderMixin = require('ReactComponentWithPureRenderMixin');
3 +
4 +var MyComponent = React.createClass({
5 + mixins: [ReactComponentWithPureRenderMixin],
6 +
7 + render: function() {
8 + return <div />;
9 + }
10 +});
11 +
12 +module.exports = MyComponent;
npm-react-codemod/test/pure-render-mixin-test4.output.js new
+13
@@ -0,0 +1,13 @@
1 +var React = require('React');
2 +
3 +var MyComponent = React.createClass({
4 + shouldComponentUpdate: function(nextProps, nextState) {
5 + return React.addons.shallowCompare(this, nextProps, nextState);
6 + },
7 +
8 + render: function() {
9 + return <div />;
10 + }
11 +});
12 +
13 +module.exports = MyComponent;
npm-react-codemod/transforms/pure-render-mixin.js new
+179
@@ -0,0 +1,179 @@
1 +/*eslint-disable no-comma-dangle*/
2 +
3 +'use strict';
4 +
5 +function removePureRenderMixin(file, api, options) {
6 + const j = api.jscodeshift;
7 +
8 + require('./utils/array-polyfills');
9 + const ReactUtils = require('./utils/ReactUtils')(j);
10 +
11 + const printOptions = options.printOptions || {quote: 'single'};
12 + const root = j(file.source);
13 +
14 + const PURE_RENDER_MIXIN = options['mixin-name'] || 'PureRenderMixin';
15 + const SHOULD_COMPONENT_UPDATE = 'shouldComponentUpdate';
16 + const NEXT_PROPS = 'nextProps';
17 + const NEXT_STATE = 'nextState';
18 +
19 + // ---------------------------------------------------------------------------
20 + // shouldComponentUpdate
21 + const createShouldComponentUpdateFunction = () =>
22 + j.functionExpression(
23 + null,
24 + [j.identifier(NEXT_PROPS), j.identifier(NEXT_STATE)],
25 + j.blockStatement([
26 + j.returnStatement(
27 + j.callExpression(
28 + j.memberExpression(
29 + j.identifier('React'),
30 + j.memberExpression(
31 + j.identifier('addons'),
32 + j.identifier('shallowCompare'),
33 + false
34 + ),
35 + false
36 + ),
37 + [
38 + j.thisExpression(),
39 + j.identifier(NEXT_PROPS),
40 + j.identifier(NEXT_STATE)
41 + ]
42 + )
43 + )
44 + ])
45 + );
46 +
47 + const createShouldComponentUpdateProperty = () =>
48 + j.property(
49 + 'init',
50 + j.identifier(SHOULD_COMPONENT_UPDATE),
51 + createShouldComponentUpdateFunction()
52 + );
53 +
54 + const hasShouldComponentUpdate = classPath =>
55 + ReactUtils.getReactCreateClassSpec(classPath)
56 + .properties.every(property =>
57 + property.key.name !== SHOULD_COMPONENT_UPDATE
58 + );
59 +
60 + // ---------------------------------------------------------------------------
61 + // Mixin related code
62 + const isPureRenderMixin = node => (
63 + node.type === 'Identifier' &&
64 + node.name === PURE_RENDER_MIXIN
65 + );
66 +
67 + const hasPureRenderMixin = classPath => {
68 + const spec = ReactUtils.getReactCreateClassSpec(classPath);
69 + const mixin = spec && spec.properties.find(ReactUtils.isMixinProperty);
70 + return mixin && mixin.value.elements.some(isPureRenderMixin);
71 + };
72 +
73 + const removeMixin = elements =>
74 + j.property(
75 + 'init',
76 + j.identifier('mixins'),
77 + j.arrayExpression(
78 + elements.filter(element => !isPureRenderMixin(element))
79 + )
80 + );
81 +
82 + // ---------------------------------------------------------------------------
83 + // Boom!
84 + const insertShouldComponentUpdate = properties => {
85 + const length = properties.length;
86 + const lastProp = properties[length - 1];
87 + // I wouldn't dare insert at the bottom if the last function is render
88 + if (
89 + lastProp.key.type === 'Identifier' &&
90 + lastProp.key.name === 'render'
91 + ) {
92 + properties.splice(
93 + length - 1,
94 + 1,
95 + createShouldComponentUpdateProperty(),
96 + lastProp
97 + );
98 + } else {
99 + properties.push(createShouldComponentUpdateProperty());
100 + }
101 + return properties;
102 + };
103 +
104 + const cleanupReactComponent = classPath => {
105 + const spec = ReactUtils.getReactCreateClassSpec(classPath);
106 + const properties = spec.properties
107 + .map(property => {
108 + if (ReactUtils.isMixinProperty(property)) {
109 + const elements = property.value.elements;
110 + return (elements.length !== 1) ? removeMixin(elements) : null;
111 + }
112 + return property;
113 + })
114 + .filter(property => !!property);
115 +
116 + ReactUtils.findReactCreateClassCallExpression(classPath).replaceWith(
117 + ReactUtils.createCreateReactClassCallExpression(
118 + insertShouldComponentUpdate(properties)
119 + )
120 + );
121 + };
122 +
123 + // Remove it if only two or fewer are left:
124 + // var PureRenderMixin = React.addons.PureRenderMixin;
125 + const hasPureRenderIdentifiers = path =>
126 + path.find(j.Identifier, {
127 + name: PURE_RENDER_MIXIN,
128 + }).size() > 2;
129 +
130 + const deletePureRenderMixin = path => {
131 + if (hasPureRenderIdentifiers(path)) {
132 + return;
133 + }
134 +
135 + const declaration = path
136 + .findVariableDeclarators(PURE_RENDER_MIXIN)
137 + .closest(j.VariableDeclaration);
138 +
139 + if (declaration.size > 1) {
140 + declaration.forEach(p =>
141 + j(p).replaceWith(
142 + j.variableDeclaration(
143 + 'var',
144 + p.value.declarations.filter(isPureRenderMixin)
145 + )
146 + )
147 + );
148 + } else {
149 + // Let's assume the variable declaration happens at the top level
150 + const program = declaration.closest(j.Program).get();
151 + const body = program.value.body;
152 + const index = body.indexOf(declaration.get().value);
153 + if (index !== -1) {
154 + body.splice(index, 1);
155 + }
156 + }
157 + };
158 +
159 + if (
160 + options['no-explicit-require'] ||
161 + ReactUtils.hasReact(root)
162 + ) {
163 + const didTransform = ReactUtils
164 + .findReactCreateClass(root)
165 + .filter(hasPureRenderMixin)
166 + .filter(hasShouldComponentUpdate)
167 + .forEach(cleanupReactComponent)
168 + .size() > 0;
169 +
170 + if (didTransform) {
171 + deletePureRenderMixin(root);
172 + return root.toSource(printOptions) + '\n';
173 + }
174 + }
175 +
176 + return null;
177 +}
178 +
179 +module.exports = removePureRenderMixin;