Add pure-render-mixin transform

This commit is contained in:
cpojer
2015-03-25 16:11:52 -07:00
parent 20004e94d3
commit d4cb2537af
11 changed files with 383 additions and 0 deletions
+12
View File
@@ -22,6 +22,18 @@ calls.
* `react-codemod findDOMNode <file>`
`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 <file>`
* If `--mixin-name=<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
@@ -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',
});
});
});
@@ -0,0 +1,45 @@
var React = require('react/addons');
var PureRenderMixin = React.addons.PureRenderMixin;
var MyComponent = React.createClass({
mixins: [PureRenderMixin],
render: function() {
return <div />;
}
});
var MyMixedComponent = React.createClass({
mixins: [PureRenderMixin, SomeOtherMixin],
render: function() {
return <div />;
}
});
var MyFooComponent = React.createClass({
mixins: [PureRenderMixin, SomeOtherMixin],
render: function() {
return <div />;
},
foo: function() {
}
});
var MyStupidComponent = React.createClass({
mixins: [PureRenderMixin],
shouldComponentUpdate: function() {
return !!'wtf is this doing here?';
},
render: function() {
return <div />;
}
});
module.exports = MyComponent;
@@ -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 <div />;
}
});
var MyMixedComponent = React.createClass({
mixins: [SomeOtherMixin],
shouldComponentUpdate: function(nextProps, nextState) {
return React.addons.shallowCompare(this, nextProps, nextState);
},
render: function() {
return <div />;
}
});
var MyFooComponent = React.createClass({
mixins: [SomeOtherMixin],
render: function() {
return <div />;
},
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 <div />;
}
});
module.exports = MyComponent;
@@ -0,0 +1,13 @@
var React = require('react/addons');
var PureRenderMixin = React.addons.PureRenderMixin;
var MyComponent = React.createClass({
mixins: [PureRenderMixin],
render: function() {
return <div />;
}
});
module.exports = MyComponent;
@@ -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 <div />;
}
});
module.exports = MyComponent;
@@ -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 <div />;
}
});
module.exports = MyComponent;
@@ -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 <div />;
}
});
module.exports = MyComponent;
@@ -0,0 +1,12 @@
var React = require('React');
var ReactComponentWithPureRenderMixin = require('ReactComponentWithPureRenderMixin');
var MyComponent = React.createClass({
mixins: [ReactComponentWithPureRenderMixin],
render: function() {
return <div />;
}
});
module.exports = MyComponent;
@@ -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 <div />;
}
});
module.exports = MyComponent;
@@ -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;