Skip to content

Commit 8e370b9

Browse files
authored
Class transform with property initializer (#54)
* added boilerplates * renamed to avoid conflict * added a failing test * added initial working version * added support for mixins * added a test for ES6 export * renamed liftGetInitialState * changed to update the existing one instead * fixed arrow function return type * temporally added npm shrinkwrap * changed initial state transformation * added react-addons-pure-render-mixin support * Revert "temporally added npm shrinkwrap" This reverts commit f5f6da0. * dont sort and bind everything * prune unused requires safely and add support for primitives as class properties * updated README * updated constructor args (only use props/context when needed) * added support for static methods * dont bind getChildContext * handle constructor arguments properly * fixed incorrect behavior when mixins is a non-array value * fix early returns in getInitialState * WIP flow transformation; switched parser to Flow * flow works now * added support for flow property initializers * better way to detect early returns in getInitialState * bail out if user uses getInitialState or getDefaultProps elsewhere * bail out if arguments is found * defer state property initializer evaluation when necessary * no shadowing in constructor * handle inner function declarations in getInitialState correctly * fix lint errors * displayName shouldn't show up twice * fixed anonymous createClass * covered more flow edge cases * handle nullable prop types correctly * always print parens for single arg arrow functions * fixed edge case when class spec is not an object expression * support literal keys in prop types * added TypeParameter and NullTypeAnnotation to ast-types * renamed recast option to flowObjectCommas * no trailing comma for single-line flow object types * catch edge case where React.createClass() is called with nothing * improved the logic of repositioning `state` property * added `pure-component` option * updated npm-shrinkwrap * rename this.context to context in constructor * fixed how we identify shadowing issues * updated README.md * use FlowFixMe for unrecognizable types * retain getInitialState()'s return type when possible * stricter checking of referencing APIs that will be removed * optional !== nullable * support void (undefined) in PropTypes.oneOf() * annotate state type when it's inlined in the constructor * added 'remove-runtime-proptypes' option * changed inexplicit any to FlowFixMe * Redesigned the way we start modding components. Now we search and grab `React.createClass` _call expressions_ directly. The only time that we can't simply replace a `createClass` call path with a new class is when the parent of that is a variable declaration: `var Foo = React.createClass({...});` needs to be replaced entirely with `class Foo extends React.Component {...}` Now we delay checking it and only when we need to replace a path we take a look at `path.parentPath.value.type` to see if it's a variable declarator. With this commit we should be able to mod any kind of anonymous `createClass` calls no matter how they are defined. * fixed default and rest params * retain top comments when pure-render-mixin is the first node of the body * fixed trailing comma for func rest params; updated recast * handle top comments better * OtherClass.getDefaultProps() -> OtherClass.defaultProps * updated README
1 parent b27b1f2 commit 8e370b9

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+6305
-469
lines changed

.eslintrc.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,8 @@ module.exports = {
1010
ecmaFeatures: {
1111
modules: false
1212
},
13+
14+
rules: {
15+
'no-use-before-define': 2,
16+
},
1317
};

README.md

Lines changed: 41 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -101,51 +101,47 @@ guide](https://github.com/airbnb/javascript/blob/7684892951ef663e1c4e62ad57d662e
101101
jscodeshift -t react-codemod/transforms/sort-comp.js <path>
102102
```
103103

104-
### Explanation of the ES2015 class transform
105-
106-
* Ignore components with calls to deprecated APIs. This is very defensive, if
107-
the script finds any identifiers called `isMounted`, `getDOMNode`,
108-
`replaceProps`, `replaceState` or `setProps` it will skip the component.
109-
* Replaces `var A = React.createClass(spec)` with
110-
`class A (extends React.Component) {spec}`.
111-
* Pulls out all statics defined on `statics` plus the few special cased
112-
statics like `propTypes`, `childContextTypes`, `contextTypes` and
113-
`displayName` and assigns them after the class is created.
114-
`class A {}; A.foo = bar;`
115-
* Takes `getDefaultProps` and inlines it as a static `defaultProps`.
116-
If `getDefaultProps` is defined as a function with a single statement that
117-
returns an object, it optimizes and transforms
118-
`getDefaultProps() { return {foo: 'bar'}; }` into
119-
`A.defaultProps = {foo: 'bar'};`. If `getDefaultProps` contains more than
120-
one statement it will transform into a self-invoking function like this:
121-
`A.defaultProps = function() {…}();`. Note that this means that the function
122-
will be executed only a single time per app-lifetime. In practice this
123-
hasn't caused any issues – `getDefaultProps` should not contain any
124-
side-effects.
125-
* Binds class methods to the instance if methods are referenced without being
126-
called directly. It checks for `this.foo` but also traces variable
127-
assignments like `var self = this; self.foo`. It does not bind functions
128-
from the React API and ignores functions that are being called directly
129-
(unless it is both called directly and passed around to somewhere else)
130-
* Creates a constructor if necessary. This is necessary if either
131-
`getInitialState` exists in the `React.createClass` spec OR if functions
132-
need to be bound to the instance.
133-
* When `--no-super-class` is passed it only optionally extends
134-
`React.Component` when `setState` or `forceUpdate` are used within the
135-
class.
136-
137-
The constructor logic is as follows:
138-
139-
* Call `super(props, context)` if the base class needs to be extended.
140-
* Bind all functions that are passed around,
141-
like `this.foo = this.foo.bind(this)`
142-
* Inline `getInitialState` (and remove `getInitialState` from the spec). It
143-
also updates access of `this.props.foo` to `props.foo` and adds `props` as
144-
argument to the constructor. This is necessary in the case when the base
145-
class does not need to be extended where `this.props` will only be set by
146-
React after the constructor has been run.
147-
* Changes `return StateObject` from `getInitialState` to assign `this.state`
148-
directly.
104+
### Explanation of the new ES2015 class transform with property initializers
105+
1. Determine if mixins are convertible. We only transform a `createClass` call to an ES6 class component when:
106+
- There are no mixins on the class, or
107+
- `options['pure-component']` is true, the `mixins` property is an array and it _only_ contains pure render mixin (the specific module name can be specified using `options['mixin-module-name']`, which defaults to `react-addons-pure-render-mixin`)
108+
2. Ignore components that:
109+
- Call deprecated APIs. This is very defensive, if the script finds any identifiers called `isMounted`, `getDOMNode`, `replaceProps`, `replaceState` or `setProps` it will skip the component
110+
- Explicitly call `this.getInitialState()` and/or `this.getDefaultProps()` since an ES6 class component will no longer have these methods
111+
- Use `arguments` in methods since arrow functions don't have `arguments`. Also please notice that `arguments` should be [very carefully used](https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments) and it's generally better to switch to spread (`...args`) instead
112+
- Have inconvertible `getInitialState()`. Specifically if you have variable declarations like `var props = ...` and the right hand side is not `this.props` then we can't inline the state initialization in the `constructor` due to variable shadowing issues
113+
- Have non-primitive right hand side values (like `foo: getStuff()`) in the class spec
114+
3. Transform it to an ES6 class component
115+
1. Replace `var A = React.createClass(spec)` with `class A extends React.Component {spec}`. If a component uses pure render mixin and passes the mixins test (as described above), it will extend `React.PureComponent` instead
116+
- Remove the `require`/`import` statement that imports pure render mixin when it's no longer being referenced
117+
2. Pull out all statics defined on `statics` plus the few special cased statics like `childContextTypes`, `contextTypes`, `displayName`, `getDefaultProps()`, and `propTypes` and transform them to `static` properties (`static propTypes = {...};`)
118+
- If `getDefaultProps()` is simple (i.e. it only contains a return statement that returns an object) it will be converted to a simple assignment (`static defaultProps = {...};`). Otherwise an IIFE (immediately-invoked function expression) will be created (`static defaultProps = function() { ... }();`). Note that this means that the function will be executed only a single time per app-lifetime. In practice this hasn't caused any issues — `getDefaultProps` should not contain any side-effects
119+
3. Transform `getInitialState()`
120+
- If there's no `getInitialState()` or the `getInitialState()` function is simple (i.e., it only contains a return statement that returns an object) then we don't need a constructor; `state` will be lifted to a property initializer (`state = {...};`)
121+
- However, if the object contains references to `this` other than `this.props` and/or `this.context`, we can't be sure about what you'll need from `this`. We need to ensure that our property initializers' evaluation order is safe, so we defer `state`'s initialization by moving it all the way down until all other property initializers have been initialized
122+
- If `getInitialState()` is not simple, we create a `constructor` and convert `getInitialState()` to an assignment to `this.state`
123+
- `constructor` always have `props` as the first parameter
124+
- We only put `context` as the second parameter when (one of) the following things happen in `getInitialState()`:
125+
- It accesses `this.context`, or
126+
- There's a direct method call `this.x()`, or
127+
- `this` is referenced alone
128+
- Rewrite accesses to `this.props` to `props` and accesses to `this.context` to `context` since the values will be passed as `constructor` arguments
129+
- Remove _simple_ variable declarations like `var props = this.props;` and `var context = this.context`
130+
- Rewrite top-level return statements (`return {...};`) to `this.state = {...}`
131+
- Add `return;` after the assignment when the return statement is part of a control flow statement (not a direct child of `getInitialState()`'s body) and not in an inner function declaration
132+
4. Transform all non-lifecycle methods and fields to class property initializers (like `onClick = () => {};`). All your Flow annotations will be preserved
133+
- It's actually not necessary to transform all methods to arrow functions (i.e., to bind them), but this behavior is the same as `createClass()` and we can make sure that we won't accidentally break stuff
134+
5. Rewrite `AnotherClass.getDefaultProps()` to `AnotherClass.defaultProps`
135+
4. Generate Flow annotations from `propTypes` and put it on the class (this only happens when there's `/* @flow */` in your code and `options['flow']` is `true`)
136+
- Flow actually understands `propTypes` in `createClass` calls but not ES6 class components. Here the transformation logic is identical to [how](https://github.com/facebook/flow/blob/master/src/typing/statement.ml#L3526) Flow treats `propTypes`
137+
- Notice that Flow treats an optional propType as non-nullable
138+
- For example, `foo: React.PropTypes.number` is valid when you pass `{}`, `{foo: null}`, or `{foo: undefined}` as props at **runtime**. However, when Flow infers type from a `createClass` call, only `{}` and `{foo: undefined}` are valid; `{foo: null}` is not. Thus the equivalent type annotation in Flow is actually `{foo?: number}`. The question mark on the left hand side indicates `{}` and `{foo: undefined}` are fine, but when `foo` is present it must be a `number`
139+
- For `propTypes` fields that can't be recognized by Flow, `$FlowFixMe` will be used
140+
141+
#### Usage
142+
```bash
143+
./node_modules/.bin/jscodeshift -t ./transforms/class.js --mixin-module-name=react-addons-pure-render-mixin --flow=true --pure-component=true --remove-runtime-proptypes=false <path>
144+
```
149145

150146
### Recast Options
151147

0 commit comments

Comments
 (0)