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;