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;