Skip to content

Class transform with property initializer #54

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 65 commits into from
Jul 8, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
2a509a6
added boilerplates
Jun 9, 2016
eacb1e1
renamed to avoid conflict
Jun 9, 2016
2c5b461
added a failing test
Jun 10, 2016
6d9e3cb
added initial working version
Jun 13, 2016
00790d4
added support for mixins
Jun 13, 2016
93e5931
added a test for ES6 export
Jun 13, 2016
a151336
renamed liftGetInitialState
Jun 13, 2016
20aafc3
changed to update the existing one instead
Jun 14, 2016
c2bda71
fixed arrow function return type
Jun 14, 2016
f5f6da0
temporally added npm shrinkwrap
Jun 14, 2016
f70dad2
changed initial state transformation
Jun 15, 2016
c136e9c
added react-addons-pure-render-mixin support
Jun 16, 2016
311f472
Revert "temporally added npm shrinkwrap"
Jun 17, 2016
747dc5b
dont sort and bind everything
Jun 18, 2016
c7136e2
prune unused requires safely and add support for primitives as class …
Jun 20, 2016
d4faea4
updated README
Jun 20, 2016
cb88509
updated constructor args (only use props/context when needed)
Jun 20, 2016
011371d
added support for static methods
Jun 21, 2016
c4c3c1a
dont bind getChildContext
Jun 21, 2016
3936f55
handle constructor arguments properly
Jun 22, 2016
ada3fb8
fixed incorrect behavior when mixins is a non-array value
Jun 22, 2016
0cfad0a
fix early returns in getInitialState
Jun 22, 2016
84f8b71
merged master and upgraded deps
Jun 22, 2016
5f7b1b2
WIP flow transformation; switched parser to Flow
Jun 23, 2016
a7f71a3
flow works now
Jun 23, 2016
231f629
added support for flow property initializers
Jun 23, 2016
b8bb77a
better way to detect early returns in getInitialState
Jun 23, 2016
e00677f
bail out if user uses getInitialState or getDefaultProps elsewhere
Jun 24, 2016
59e3671
bail out if arguments is found
Jun 24, 2016
4f64cc2
defer state property initializer evaluation when necessary
Jun 24, 2016
d67b6a8
no shadowing in constructor
Jun 24, 2016
3cc6d71
handle inner function declarations in getInitialState correctly
Jun 24, 2016
f06bcd6
fix lint errors
Jun 24, 2016
53730d9
displayName shouldn't show up twice
Jun 24, 2016
19b611a
fixed anonymous createClass
Jun 24, 2016
b86a89d
covered more flow edge cases
Jun 24, 2016
595bb97
handle nullable prop types correctly
Jun 25, 2016
5b058f8
always print parens for single arg arrow functions
Jun 27, 2016
b532b52
fixed edge case when class spec is not an object expression
Jun 27, 2016
da84faf
support literal keys in prop types
Jun 27, 2016
7214e92
added TypeParameter and NullTypeAnnotation to ast-types
Jun 28, 2016
bb15c72
renamed recast option to flowObjectCommas
Jun 28, 2016
9f1702b
no trailing comma for single-line flow object types
Jun 28, 2016
d7083fe
catch edge case where React.createClass() is called with nothing
Jun 29, 2016
ef23055
improved the logic of repositioning `state` property
Jun 29, 2016
46d13c0
added `pure-component` option
Jun 29, 2016
82d4a52
updated npm-shrinkwrap
Jun 29, 2016
f00fbb0
rename this.context to context in constructor
Jun 30, 2016
667e592
fixed how we identify shadowing issues
Jun 30, 2016
8b15535
updated README.md
Jun 30, 2016
953386a
use FlowFixMe for unrecognizable types
Jun 30, 2016
3e62530
retain getInitialState()'s return type when possible
Jul 1, 2016
3f700c1
stricter checking of referencing APIs that will be removed
Jul 1, 2016
51494ac
optional !== nullable
Jul 2, 2016
ab1e1a7
support void (undefined) in PropTypes.oneOf()
Jul 3, 2016
62978f4
annotate state type when it's inlined in the constructor
Jul 5, 2016
ee36b61
added 'remove-runtime-proptypes' option
Jul 5, 2016
1011552
changed inexplicit any to FlowFixMe
Jul 5, 2016
847de6d
Redesigned the way we start modding components.
Jul 6, 2016
b65424c
fixed default and rest params
Jul 7, 2016
738558c
retain top comments when pure-render-mixin is the first node of the body
Jul 7, 2016
3956765
fixed trailing comma for func rest params; updated recast
Jul 8, 2016
c39d3f7
handle top comments better
Jul 8, 2016
c02901c
OtherClass.getDefaultProps() -> OtherClass.defaultProps
Jul 8, 2016
1e9204b
updated README
Jul 8, 2016
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,8 @@ module.exports = {
ecmaFeatures: {
modules: false
},

rules: {
'no-use-before-define': 2,
},
};
86 changes: 41 additions & 45 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,51 +101,47 @@ guide](https://github.com/airbnb/javascript/blob/7684892951ef663e1c4e62ad57d662e
jscodeshift -t react-codemod/transforms/sort-comp.js <path>
```

### Explanation of the ES2015 class transform

* Ignore components with calls to deprecated APIs. This is very defensive, if
the script finds any identifiers called `isMounted`, `getDOMNode`,
`replaceProps`, `replaceState` or `setProps` it will skip the component.
* Replaces `var A = React.createClass(spec)` with
`class A (extends React.Component) {spec}`.
* Pulls out all statics defined on `statics` plus the few special cased
statics like `propTypes`, `childContextTypes`, `contextTypes` and
`displayName` and assigns them after the class is created.
`class A {}; A.foo = bar;`
* Takes `getDefaultProps` and inlines it as a static `defaultProps`.
If `getDefaultProps` is defined as a function with a single statement that
returns an object, it optimizes and transforms
`getDefaultProps() { return {foo: 'bar'}; }` into
`A.defaultProps = {foo: 'bar'};`. If `getDefaultProps` contains more than
one statement it will transform into a self-invoking function like this:
`A.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.
* Binds class methods to the instance if methods are referenced without being
called directly. It checks for `this.foo` but also traces variable
assignments like `var self = this; self.foo`. It does not bind functions
from the React API and ignores functions that are being called directly
(unless it is both called directly and passed around to somewhere else)
* Creates a constructor if necessary. This is necessary if either
`getInitialState` exists in the `React.createClass` spec OR if functions
need to be bound to the instance.
* When `--no-super-class` is passed it only optionally extends
`React.Component` when `setState` or `forceUpdate` are used within the
class.

The constructor logic is as follows:

* Call `super(props, context)` if the base class needs to be extended.
* Bind all functions that are passed around,
like `this.foo = this.foo.bind(this)`
* Inline `getInitialState` (and remove `getInitialState` from the spec). It
also updates access of `this.props.foo` to `props.foo` and adds `props` as
argument to the constructor. This is necessary in the case when the base
class does not need to be extended where `this.props` will only be set by
React after the constructor has been run.
* Changes `return StateObject` from `getInitialState` to assign `this.state`
directly.
### Explanation of the new ES2015 class transform with property initializers
1. Determine if mixins are convertible. We only transform a `createClass` call to an ES6 class component when:
- There are no mixins on the class, or
- `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`)
2. Ignore components that:
- 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
- Explicitly call `this.getInitialState()` and/or `this.getDefaultProps()` since an ES6 class component will no longer have these methods
- 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
- 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
- Have non-primitive right hand side values (like `foo: getStuff()`) in the class spec
3. Transform it to an ES6 class component
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
- Remove the `require`/`import` statement that imports pure render mixin when it's no longer being referenced
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 = {...};`)
- 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
3. Transform `getInitialState()`
- 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 = {...};`)
- 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
- If `getInitialState()` is not simple, we create a `constructor` and convert `getInitialState()` to an assignment to `this.state`
- `constructor` always have `props` as the first parameter
- We only put `context` as the second parameter when (one of) the following things happen in `getInitialState()`:
- It accesses `this.context`, or
- There's a direct method call `this.x()`, or
- `this` is referenced alone
- Rewrite accesses to `this.props` to `props` and accesses to `this.context` to `context` since the values will be passed as `constructor` arguments
- Remove _simple_ variable declarations like `var props = this.props;` and `var context = this.context`
- Rewrite top-level return statements (`return {...};`) to `this.state = {...}`
- 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
4. Transform all non-lifecycle methods and fields to class property initializers (like `onClick = () => {};`). All your Flow annotations will be preserved
- 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
5. Rewrite `AnotherClass.getDefaultProps()` to `AnotherClass.defaultProps`
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`)
- 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`
- Notice that Flow treats an optional propType as non-nullable
- 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`
- For `propTypes` fields that can't be recognized by Flow, `$FlowFixMe` will be used

#### Usage
```bash
./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>
```

### Recast Options

Expand Down
Loading