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

Add ES6 class transform

cpojer committed Mar 25, 2015 at 01:30 UTC d9c13c73b6f6c1e928258f6686939dd5503e7408
9 files changed +935
npm-react-codemod/README.md
+58
@@ -34,6 +34,64 @@ are using the master version (>0.13.1) of React as it is using
34 namespaced name for the mixin. `mixins: [React.addons.PureRenderMixin]` will
35 not currently work.
36
37 +`class.js` transforms `React.createClass` calls into ES6 classes.
38 +
39 + * `react-codemod class <file>`
40 + * If `--no-super-class=true` is specified it will not extend
41 + `React.Component` if `setState` and `forceUpdate` aren't being called in a
42 + class. We do recommend always extending from `React.Component`, especially
43 + if you are using or planning to use [Flow](http://flowtype.org/). Also make
44 + sure you are not calling `setState` anywhere outside of your component.
45 +
46 +All scripts take an option `--no-explicit-require=true` if you don't have a
47 +`require('React')` statement in your code files and if you access React as a
48 +global.
49 +
50 +### Explanation of the ES6 class transform
51 +
52 + * Ignore components with calls to deprecated APIs. This is very defensive, if
53 + the script finds any identifiers called `isMounted`, `getDOMNode`,
54 + `replaceProps`, `replaceState` or `setProps` it will skip the component.
55 + * Replaces `var A = React.createClass(spec)` with
56 + `class A (extends React.Component) {spec}`.
57 + * Pulls out all statics defined on `statics` plus the few special cased
58 + statics like `propTypes`, `childContextTypes`, `contextTypes` and
59 + `displayName` and assigns them after the class is created.
60 + `class A {}; A.foo = bar;`
61 + * Takes `getDefaultProps` and inlines it as a static `defaultProps`.
62 + If `getDefaultProps` is defined as a function with a single statement that
63 + returns an object, it optimizes and transforms
64 + `getDefaultProps() { return {foo: 'bar'}; }` into
65 + `A.defaultProps = {foo: 'bar'};`. If `getDefaultProps` contains more than
66 + one statement it will transform into a self-invoking function like this:
67 + `A.defaultProps = function() {…}();`. Note that this means that the function
68 + will be executed only a single time per app-lifetime. In practice this
69 + hasn't caused any issues – `getDefaultProps` should not contain any
70 + side-effects.
71 + * Binds class methods to the instance if methods are referenced without being
72 + called directly. It checks for `this.foo` but also traces variable
73 + assignments like `var self = this; self.foo`. It does not bind functions
74 + from the React API and ignores functions that are being called directly
75 + (unless it is both called directly and passed around to somewhere else)
76 + * Creates a constructor if necessary. This is necessary if either
77 + `getInitialState` exists in the `React.createClass` spec OR if functions
78 + need to be bound to the instance.
79 + * When `--no-super-class=true` is passed it only optionally extends
80 + `React.Component` when `setState` or `forceUpdate` are used within the
81 + class.
82 +
83 +The constructor logic is as follows:
84 + * Call `super(props, context)` if the base class needs to be extended.
85 + * Bind all functions that are passed around,
86 + like `this.foo = this.foo.bind(this)`
87 + * Inline `getInitialState` (and remove `getInitialState` from the spec). It
88 + also updates access of `this.props.foo` to `props.foo` and adds `props` as
89 + argument to the constructor. This is necessary in the case when the base
90 + class does not need to be extended where `this.props` will only be set by
91 + React after the constructor has been run.
92 + * Changes `return StateObject` from `getInitialState` to assign `this.state`
93 + directly.
94 +
95 ### Recast Options
96
97 Options to [recast](https://github.com/benjamn/recast)'s printer can be provided
npm-react-codemod/test/__tests__/transform-tests.js
+9
@@ -50,5 +50,14 @@ describe('Transform Tests', () => {
50 });
51 });
52
53 + it('transforms the "class" tests correctly', () => {
54 + test('class', 'class-test');
55 +
56 + test('class', 'class-test2', {
57 + 'no-super-class': true,
58 + });
59 +
60 + test('class', 'class-test3');
61 + });
62
63 });
npm-react-codemod/test/class-test.js new
+128
@@ -0,0 +1,128 @@
1 +'use strict';
2 +
3 +var React = require('React');
4 +var Relay = require('Relay');
5 +
6 +var Image = require('Image.react');
7 +
8 +/*
9 + * Multiline
10 + */
11 +var MyComponent = React.createClass({
12 + getInitialState: function() {
13 + var x = this.props.foo;
14 + return {
15 + heyoo: 23,
16 + };
17 + },
18 +
19 + foo: function() {
20 + this.setState({heyoo: 24});
21 + }
22 +});
23 +
24 +// Class comment
25 +var MyComponent2 = React.createClass({
26 + getDefaultProps: function() {
27 + return {a: 1};
28 + },
29 + foo: function() {
30 + pass(this.foo);
31 + this.forceUpdate();
32 + }
33 +});
34 +
35 +var MyComponent3 = React.createClass({
36 + statics: {
37 + someThing: 10,
38 + foo: function() {},
39 + },
40 + propTypes: {
41 + highlightEntities: React.PropTypes.bool,
42 + linkifyEntities: React.PropTypes.bool,
43 + text: React.PropTypes.shape({
44 + text: React.PropTypes.string,
45 + ranges: React.PropTypes.array
46 + }).isRequired
47 + },
48 +
49 + getDefaultProps: function() {
50 + foo();
51 + return {
52 + linkifyEntities: true,
53 + highlightEntities: false
54 + };
55 + },
56 +
57 + getInitialState: function() {
58 + this.props.foo();
59 + return {
60 + heyoo: 23,
61 + };
62 + },
63 +
64 + _renderText: function(text) {
65 + return <Text text={text} />;
66 + },
67 +
68 + _renderImageRange: function(text, range) {
69 + var image = range.image;
70 + if (image) {
71 + return (
72 + <Image
73 + src={image.uri}
74 + height={image.height / image.scale}
75 + width={image.width / image.scale}
76 + />
77 + );
78 + }
79 + },
80 +
81 + autobindMe: function() {},
82 + dontAutobindMe: function() {},
83 +
84 + // Function comment
85 + _renderRange: function(text, range) {
86 + var self = this;
87 +
88 + self.dontAutobindMe();
89 + call(self.autobindMe);
90 +
91 + var type = rage.type;
92 + var {highlightEntities} = this.props;
93 +
94 + if (type === 'ImageAtRange') {
95 + return this._renderImageRange(text, range);
96 + }
97 +
98 + if (this.props.linkifyEntities) {
99 + text =
100 + <Link href={usersURI}>
101 + {text}
102 + </Link>;
103 + } else {
104 + text = <span>{text}</span>;
105 + }
106 +
107 + return text;
108 + },
109 +
110 + /* This is a comment */
111 + render: function() {
112 + var content = this.props.text;
113 + return (
114 + <BaseText
115 + {...this.props}
116 + textRenderer={this._renderText}
117 + rangeRenderer={this._renderRange}
118 + text={content.text}
119 + />
120 + );
121 + }
122 +});
123 +
124 +module.exports = Relay.createContainer(MyComponent, {
125 + queries: {
126 + me: Relay.graphql`this is not graphql`,
127 + }
128 +});
npm-react-codemod/test/class-test.output.js new
+139
@@ -0,0 +1,139 @@
1 +'use strict';
2 +
3 +var React = require('React');
4 +var Relay = require('Relay');
5 +
6 +var Image = require('Image.react');
7 +
8 +/*
9 + * Multiline
10 + */
11 +class MyComponent extends React.Component {
12 + constructor(props, context) {
13 + super(props, context);
14 + var x = props.foo;
15 +
16 + this.state = {
17 + heyoo: 23,
18 + };
19 + }
20 +
21 + foo() {
22 + this.setState({heyoo: 24});
23 + }
24 +}
25 +
26 +// Class comment
27 +class MyComponent2 extends React.Component {
28 + constructor(props, context) {
29 + super(props, context);
30 + this.foo = this.foo.bind(this);
31 + }
32 +
33 + foo() {
34 + pass(this.foo);
35 + this.forceUpdate();
36 + }
37 +}
38 +
39 +MyComponent2.defaultProps = {a: 1};
40 +
41 +class MyComponent3 extends React.Component {
42 + constructor(props, context) {
43 + super(props, context);
44 + this._renderRange = this._renderRange.bind(this);
45 + this._renderText = this._renderText.bind(this);
46 + this.autobindMe = this.autobindMe.bind(this);
47 + props.foo();
48 +
49 + this.state = {
50 + heyoo: 23,
51 + };
52 + }
53 +
54 + _renderText(text) {
55 + return <Text text={text} />;
56 + }
57 +
58 + _renderImageRange(text, range) {
59 + var image = range.image;
60 + if (image) {
61 + return (
62 + <Image
63 + src={image.uri}
64 + height={image.height / image.scale}
65 + width={image.width / image.scale}
66 + />
67 + );
68 + }
69 + }
70 +
71 + autobindMe() {}
72 + dontAutobindMe() {}
73 +
74 + // Function comment
75 + _renderRange(text, range) {
76 + var self = this;
77 +
78 + self.dontAutobindMe();
79 + call(self.autobindMe);
80 +
81 + var type = rage.type;
82 + var {highlightEntities} = this.props;
83 +
84 + if (type === 'ImageAtRange') {
85 + return this._renderImageRange(text, range);
86 + }
87 +
88 + if (this.props.linkifyEntities) {
89 + text =
90 + <Link href={usersURI}>
91 + {text}
92 + </Link>;
93 + } else {
94 + text = <span>{text}</span>;
95 + }
96 +
97 + return text;
98 + }
99 +
100 + /* This is a comment */
101 + render() {
102 + var content = this.props.text;
103 + return (
104 + <BaseText
105 + {...this.props}
106 + textRenderer={this._renderText}
107 + rangeRenderer={this._renderRange}
108 + text={content.text}
109 + />
110 + );
111 + }
112 +}
113 +
114 +MyComponent3.defaultProps = function() {
115 + foo();
116 + return {
117 + linkifyEntities: true,
118 + highlightEntities: false
119 + };
120 +}();
121 +
122 +MyComponent3.foo = function() {};
123 +
124 +MyComponent3.propTypes = {
125 + highlightEntities: React.PropTypes.bool,
126 + linkifyEntities: React.PropTypes.bool,
127 + text: React.PropTypes.shape({
128 + text: React.PropTypes.string,
129 + ranges: React.PropTypes.array
130 + }).isRequired
131 +};
132 +
133 +MyComponent3.someThing = 10;
134 +
135 +module.exports = Relay.createContainer(MyComponent, {
136 + queries: {
137 + me: Relay.graphql`this is not graphql`,
138 + }
139 +});
npm-react-codemod/test/class-test2.js new
+33
@@ -0,0 +1,33 @@
1 +'use strict';
2 +
3 +var React = require('React');
4 +
5 +var IdontNeedAParent = React.createClass({
6 + render: function() {
7 + return <div />;
8 + }
9 +});
10 +
11 +var ButIDo = React.createClass({
12 + foo: function() {
13 + this.setState({banana: '?'});
14 + },
15 +
16 + render: function() {
17 + return <div />;
18 + }
19 +});
20 +
21 +var IAccessProps = React.createClass({
22 +
23 + getInitialState: function() {
24 + return {
25 + relayReleaseDate: this.props.soon,
26 + };
27 + },
28 +
29 + render: function() {
30 + return
31 + }
32 +
33 +});
npm-react-codemod/test/class-test2.output.js new
+31
@@ -0,0 +1,31 @@
1 +'use strict';
2 +
3 +var React = require('React');
4 +
5 +class IdontNeedAParent {
6 + render() {
7 + return <div />;
8 + }
9 +}
10 +
11 +class ButIDo extends React.Component {
12 + foo() {
13 + this.setState({banana: '?'});
14 + }
15 +
16 + render() {
17 + return <div />;
18 + }
19 +}
20 +
21 +class IAccessProps {
22 + constructor(props) {
23 + this.state = {
24 + relayReleaseDate: props.soon,
25 + };
26 + }
27 +
28 + render() {
29 + return
30 + }
31 +}
npm-react-codemod/test/class-test3.js new
+20
@@ -0,0 +1,20 @@
1 +'use strict';
2 +
3 +var React = require('React');
4 +
5 +// Comment
6 +module.exports = React.createClass({
7 + propTypes: {
8 + foo: React.PropTypes.bool,
9 + },
10 +
11 + getInitialState: function() {
12 + return {
13 + foo: 'bar',
14 + };
15 + },
16 +
17 + render: function() {
18 + return <div />;
19 + }
20 +});
npm-react-codemod/test/class-test3.output.js new
+22
@@ -0,0 +1,22 @@
1 +'use strict';
2 +
3 +var React = require('React');
4 +
5 +// Comment
6 +module.exports = class __exports extends React.Component {
7 + constructor(props, context) {
8 + super(props, context);
9 +
10 + this.state = {
11 + foo: 'bar',
12 + };
13 + }
14 +
15 + render() {
16 + return <div />;
17 + }
18 +};
19 +
20 +module.exports.propTypes = {
21 + foo: React.PropTypes.bool,
22 +};
npm-react-codemod/transforms/class.js new
+495
@@ -0,0 +1,495 @@
1 +/*eslint-disable no-comma-dangle*/
2 +
3 +'use strict';
4 +
5 +function updateReactCreateClassToES6(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 AUTOBIND_IGNORE_KEYS = {
15 + componentDidMount: true,
16 + componentDidUpdate: true,
17 + componentWillReceiveProps: true,
18 + componentWillMount: true,
19 + componentWillUpdate: true,
20 + componentWillUnmount: true,
21 + getDefaultProps: true,
22 + getInitialState: true,
23 + render: true,
24 + shouldComponentUpdate: true,
25 + };
26 +
27 + const BASE_COMPONENT_METHODS = ['setState', 'forceUpdate'];
28 +
29 + const DEFAULT_PROPS_FIELD = 'getDefaultProps';
30 + const DEFAULT_PROPS_KEY = 'defaultProps';
31 + const GET_INITIAL_STATE_FIELD = 'getInitialState';
32 +
33 + const DEPRECATED_APIS = [
34 + 'getDOMNode',
35 + 'isMounted',
36 + 'replaceProps',
37 + 'replaceState',
38 + 'setProps',
39 + ];
40 +
41 + const STATIC_KEYS = {
42 + childContextTypes: true,
43 + contextTypes: true,
44 + displayName: true,
45 + propTypes: true,
46 + };
47 +
48 + // ---------------------------------------------------------------------------
49 + // Checks if the module uses mixins or accesses deprecated APIs.
50 + const checkDeprecatedAPICalls = classPath =>
51 + DEPRECATED_APIS.reduce(
52 + (acc, name) =>
53 + acc + j(classPath)
54 + .find(j.Identifier, {name})
55 + .size(),
56 + 0
57 + ) > 0;
58 +
59 + const callsDeprecatedAPIs = classPath => {
60 + if (checkDeprecatedAPICalls(classPath)) {
61 + console.log(
62 + file.path + ': "' + ReactUtils.getComponentName(classPath) + '" ' +
63 + 'skipped because of deprecated API calls. Remove calls to ' +
64 + DEPRECATED_APIS.join(', ') + ' in your React component and re-run ' +
65 + 'this script.'
66 + );
67 + return false;
68 + }
69 + return true;
70 + };
71 +
72 + const hasMixins = classPath => {
73 + if (ReactUtils.hasMixins(classPath)) {
74 + console.log(
75 + file.path + ': "' + ReactUtils.getComponentName(classPath) + '" ' +
76 + ' skipped because of mixins.'
77 + );
78 + return false;
79 + }
80 + return true;
81 + };
82 +
83 + // ---------------------------------------------------------------------------
84 + // Helpers
85 + const createFindPropFn = prop => property => (
86 + property.key &&
87 + property.key.type === 'Identifier' &&
88 + property.key.name === prop
89 + );
90 +
91 + const filterDefaultPropsField = node =>
92 + createFindPropFn(DEFAULT_PROPS_FIELD)(node);
93 +
94 + const filterGetInitialStateField = node =>
95 + createFindPropFn(GET_INITIAL_STATE_FIELD)(node);
96 +
97 + const findGetDefaultProps = specPath =>
98 + specPath.properties.find(createFindPropFn(DEFAULT_PROPS_FIELD));
99 +
100 + const findGetInitialState = specPath =>
101 + specPath.properties.find(createFindPropFn(GET_INITIAL_STATE_FIELD));
102 +
103 + // This is conservative; only check for `setState` and `forceUpdate` literals
104 + // instead of also checking which objects they are called on.
105 + const shouldExtendReactComponent = classPath =>
106 + BASE_COMPONENT_METHODS.some(name => (
107 + j(classPath)
108 + .find(j.Identifier, {name})
109 + .size() > 0
110 + ));
111 +
112 + const withComments = (to, from) => {
113 + to.comments = from.comments;
114 + return to;
115 + };
116 +
117 + // ---------------------------------------------------------------------------
118 + // Collectors
119 + const isFunctionExpression = node => (
120 + node.key &&
121 + node.key.type === 'Identifier' &&
122 + node.value &&
123 + node.value.type === 'FunctionExpression'
124 + );
125 +
126 + const collectStatics = specPath => {
127 + const statics = specPath.properties.find(createFindPropFn('statics'));
128 + const staticsObject =
129 + (statics && statics.value && statics.value.properties) || [];
130 +
131 + const getDefaultProps = findGetDefaultProps(specPath);
132 + if (getDefaultProps) {
133 + staticsObject.push(createDefaultProps(getDefaultProps));
134 + }
135 +
136 + return (
137 + staticsObject.concat(specPath.properties.filter(property =>
138 + property.key && STATIC_KEYS[property.key.name]
139 + ))
140 + .sort((a, b) => a.key.name < b.key.name)
141 + );
142 + };
143 +
144 + const collectFunctions = specPath => specPath.properties
145 + .filter(prop =>
146 + !(filterDefaultPropsField(prop) || filterGetInitialStateField(prop))
147 + )
148 + .filter(isFunctionExpression);
149 +
150 + const findAutobindNamesFor = (root, fnNames, literalOrIdentifier) => {
151 + const node = literalOrIdentifier;
152 + const autobindNames = {};
153 +
154 + j(root)
155 + .find(j.MemberExpression, {
156 + object: node.name ? {
157 + type: node.type,
158 + name: node.name,
159 + } : {type: node.type},
160 + property: {
161 + type: 'Identifier',
162 + },
163 + })
164 + .filter(path => path.value.property && fnNames[path.value.property.name])
165 + .filter(path => {
166 + const call = path.parent.value;
167 + return !(
168 + call &&
169 + call.type === 'CallExpression' &&
170 + call.callee.type === 'MemberExpression' &&
171 + call.callee.object.type === node.type &&
172 + call.callee.object.name === node.name &&
173 + call.callee.property.type === 'Identifier' &&
174 + call.callee.property.name === path.value.property.name
175 + );
176 + })
177 + .forEach(path => autobindNames[path.value.property.name] = true);
178 +
179 + return Object.keys(autobindNames);
180 + };
181 +
182 + const collectAutoBindFunctions = (functions, classPath) => {
183 + const fnNames = {};
184 + functions
185 + .filter(fn => !AUTOBIND_IGNORE_KEYS[fn.key.name])
186 + .forEach(fn => fnNames[fn.key.name] = true);
187 +
188 + const autobindNames = {};
189 + const add = name => autobindNames[name] = true;
190 +
191 + // Find `this.<foo>`
192 + findAutobindNamesFor(classPath, fnNames, j.thisExpression()).forEach(add);
193 +
194 + // Find `self.<foo>` if `self = this`
195 + j(classPath)
196 + .findVariableDeclarators()
197 + .filter(path => (
198 + path.value.id.type === 'Identifier' &&
199 + path.value.init &&
200 + path.value.init.type === 'ThisExpression'
201 + ))
202 + .forEach(path =>
203 + findAutobindNamesFor(
204 + j(path).closest(j.FunctionExpression).get(),
205 + fnNames,
206 + path.value.id
207 + ).forEach(add)
208 + );
209 +
210 + return Object.keys(autobindNames).sort();
211 + };
212 +
213 + // ---------------------------------------------------------------------------
214 + // Boom!
215 + const createMethodDefinition = fn =>
216 + withComments(j.methodDefinition(
217 + '',
218 + fn.key,
219 + fn.value
220 + ), fn);
221 +
222 + const createBindAssignment = name =>
223 + j.expressionStatement(
224 + j.assignmentExpression(
225 + '=',
226 + j.memberExpression(
227 + j.thisExpression(),
228 + j.identifier(name),
229 + false
230 + ),
231 + j.callExpression(
232 + j.memberExpression(
233 + j.memberExpression(
234 + j.thisExpression(),
235 + j.identifier(name),
236 + false
237 + ),
238 + j.identifier('bind'),
239 + false
240 + ),
241 + [j.thisExpression()]
242 + )
243 + )
244 + );
245 +
246 + const createSuperCall = shouldAddSuperCall =>
247 + !shouldAddSuperCall ?
248 + [] :
249 + [j.expressionStatement(
250 + j.callExpression(
251 + j.identifier('super'),
252 + [j.identifier('props'), j.identifier('context')]
253 + )
254 + )];
255 +
256 + const updatePropsAccess = getInitialState =>
257 + getInitialState ?
258 + j(getInitialState)
259 + .find(j.MemberExpression, {
260 + object: {
261 + type: 'ThisExpression',
262 + },
263 + property: {
264 + type: 'Identifier',
265 + name: 'props',
266 + },
267 + })
268 + .forEach(path => j(path).replaceWith(j.identifier('props')))
269 + .size() > 0 :
270 + false;
271 +
272 + const inlineGetInitialState = getInitialState => {
273 + if (!getInitialState) {
274 + return [];
275 + }
276 +
277 + return getInitialState.value.body.body.map(statement => {
278 + if (statement.type === 'ReturnStatement') {
279 + return j.expressionStatement(
280 + j.assignmentExpression(
281 + '=',
282 + j.memberExpression(
283 + j.thisExpression(),
284 + j.identifier('state'),
285 + false
286 + ),
287 + statement.argument
288 + )
289 + );
290 + }
291 +
292 + return statement;
293 + });
294 + };
295 +
296 + const createConstructorArgs = (shouldAddSuperClass, hasPropsAccess) => {
297 + if (shouldAddSuperClass) {
298 + return [j.identifier('props'), j.identifier('context')];
299 + } else if (hasPropsAccess) {
300 + return [j.identifier('props')];
301 + } else {
302 + return [];
303 + }
304 + };
305 +
306 + const createConstructor = (
307 + getInitialState,
308 + autobindFunctions,
309 + shouldAddSuperClass
310 + ) => {
311 + if (!getInitialState && !autobindFunctions.length) {
312 + return [];
313 + }
314 +
315 + const hasPropsAccess = updatePropsAccess(getInitialState);
316 + return [createMethodDefinition({
317 + key: j.identifier('constructor'),
318 + value: j.functionExpression(
319 + null,
320 + createConstructorArgs(shouldAddSuperClass, hasPropsAccess),
321 + j.blockStatement(
322 + [].concat(
323 + createSuperCall(shouldAddSuperClass),
324 + autobindFunctions.map(createBindAssignment),
325 + inlineGetInitialState(getInitialState)
326 + )
327 + )
328 + ),
329 + })];
330 + };
331 +
332 + const createES6Class = (
333 + name,
334 + functions,
335 + getInitialState,
336 + autobindFunctions,
337 + comments,
338 + shouldAddSuperClass
339 + ) =>
340 + withComments(j.classDeclaration(
341 + // ast-types does not yet support empty class names
342 + // See: https://github.com/benjamn/ast-types/pull/102
343 + name ? j.identifier(name) : j.identifier('__exports'),
344 + j.classBody(
345 + [].concat(
346 + createConstructor(
347 + getInitialState,
348 + autobindFunctions,
349 + shouldAddSuperClass
350 + ),
351 + functions.map(createMethodDefinition)
352 + )
353 + ),
354 + shouldAddSuperClass ? j.memberExpression(
355 + j.identifier('React'),
356 + j.identifier('Component'),
357 + false
358 + ) : null
359 + ), {comments});
360 +
361 + const createStaticAssignment = (name, staticProperty) =>
362 + withComments(j.expressionStatement(
363 + j.assignmentExpression(
364 + '=',
365 + j.memberExpression(
366 + name,
367 + j.identifier(staticProperty.key.name),
368 + false
369 + ),
370 + staticProperty.value
371 + )
372 + ), staticProperty);
373 +
374 + const createStaticAssignmentExpressions = (name, statics) =>
375 + statics.map(staticProperty => createStaticAssignment(name, staticProperty));
376 +
377 + const hasSingleReturnStatement = value => (
378 + value.type === 'FunctionExpression' &&
379 + value.body &&
380 + value.body.type === 'BlockStatement' &&
381 + value.body.body &&
382 + value.body.body.length === 1 &&
383 + value.body.body[0].type === 'ReturnStatement' &&
384 + value.body.body[0].argument &&
385 + value.body.body[0].argument.type === 'ObjectExpression'
386 + );
387 +
388 + const createDefaultPropsValue = value => {
389 + if (hasSingleReturnStatement(value)) {
390 + return value.body.body[0].argument;
391 + } else {
392 + return j.callExpression(
393 + value,
394 + []
395 + );
396 + }
397 + };
398 +
399 + const createDefaultProps = prop =>
400 + withComments(
401 + j.property(
402 + 'init',
403 + j.identifier(DEFAULT_PROPS_KEY),
404 + createDefaultPropsValue(prop.value)
405 + ),
406 + prop
407 + );
408 +
409 + const getComments = classPath => {
410 + if (classPath.value.comments) {
411 + return classPath.value.comments;
412 + }
413 + const declaration = j(classPath).closest(j.VariableDeclaration);
414 + if (declaration.size()) {
415 + return declaration.get().value.comments;
416 + }
417 + return null;
418 + };
419 +
420 + const createModuleExportsMemberExpression = () =>
421 + j.memberExpression(
422 + j.identifier('module'),
423 + j.identifier('exports'),
424 + false
425 + );
426 +
427 + const updateToES6Class = (classPath, shouldExtend, isModuleExports) => {
428 + const specPath = ReactUtils.getReactCreateClassSpec(classPath);
429 + const name = ReactUtils.getComponentName(classPath);
430 + const statics = collectStatics(specPath);
431 + const functions = collectFunctions(specPath);
432 + const comments = getComments(classPath);
433 +
434 + const autobindFunctions = collectAutoBindFunctions(functions, classPath);
435 + const getInitialState = findGetInitialState(specPath);
436 +
437 + const staticName =
438 + name ? j.identifier(name) : createModuleExportsMemberExpression();
439 +
440 + var path;
441 + if (isModuleExports) {
442 + path = ReactUtils.findReactCreateClassCallExpression(classPath);
443 + } else {
444 + path = j(classPath).closest(j.VariableDeclaration);
445 + }
446 +
447 + path.replaceWith(
448 + createES6Class(
449 + name,
450 + functions,
451 + getInitialState,
452 + autobindFunctions,
453 + comments,
454 + shouldExtend || shouldExtendReactComponent(classPath)
455 + )
456 + );
457 +
458 + const staticAssignments =
459 + createStaticAssignmentExpressions(staticName, statics);
460 + if (isModuleExports) {
461 + const body = root.get().value.body;
462 + body.push.apply(body, staticAssignments);
463 + } else {
464 + staticAssignments
465 + .forEach(expression => path = path.insertAfter(expression));
466 + }
467 + };
468 +
469 + if (
470 + options['no-explicit-require'] || ReactUtils.hasReact(root)
471 + ) {
472 + const apply = (path, isModuleExports) =>
473 + path
474 + .filter(hasMixins)
475 + .filter(callsDeprecatedAPIs)
476 + .forEach(classPath => updateToES6Class(
477 + classPath,
478 + !options['no-super-class'],
479 + isModuleExports
480 + ));
481 +
482 + const didTransform = (
483 + apply(ReactUtils.findReactCreateClass(root), false).size() +
484 + apply(ReactUtils.findReactCreateClassModuleExports(root), true).size()
485 + ) > 0;
486 +
487 + if (didTransform) {
488 + return root.toSource(printOptions) + '\n';
489 + }
490 + }
491 +
492 + return null;
493 +}
494 +
495 +module.exports = updateReactCreateClassToES6;