Skip to content
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

Add npm-react-codemod #3506

Merged
merged 6 commits into from
Mar 25, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 5 additions & 1 deletion .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,8 @@ docs/js/
examples/
# Ignore built files.
build/

# react-codemod
npm-react-codemod/test/
npm-react-codemod/scripts/
npm-react-codemod/build/
npm-react-codemod/node_modules/
1 change: 1 addition & 0 deletions npm-react-codemod/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/transforms/
100 changes: 100 additions & 0 deletions npm-react-codemod/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
## react-codemod

This repository contains a collection of codemod scripts based on
[JSCodeshift](https://github.com/facebook/jscodeshift) that help update React
APIs.

### Setup & Run

* `npm install -g react-codemod`
* `react-codemod <codemod-script> <file>`
* Use the `-d` option for a dry-run and use `-p` to print the output
for comparison

### Included Scripts

`findDOMNode` updates `this.getDOMNode()` or `this.refs.foo.getDOMNode()`
calls inside of `React.createClass` components to `React.findDOMNode(foo)`. Note
that it will only look at code inside of `React.createClass` calls and only
update calls on the component instance or its refs. You can use this script to
update most calls to `getDOMNode` and then manually go through the remaining
calls.

* `react-codemod findDOMNode <file>`

`pure-render-mixin` 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.

`class` transforms `React.createClass` calls into ES6 classes.

* `react-codemod class <file>`
* If `--no-super-class=true` is specified it will not extend
`React.Component` if `setState` and `forceUpdate` aren't being called in a
class. We do recommend always extending from `React.Component`, especially
if you are using or planning to use [Flow](http://flowtype.org/). Also make
sure you are not calling `setState` anywhere outside of your component.

All scripts take an option `--no-explicit-require=true` if you don't have a
`require('React')` statement in your code files and if you access React as a
global.

### Explanation of the ES6 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=true` 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.

### Recast Options

Options to [recast](https://github.com/benjamn/recast)'s printer can be provided
through the `printOptions` command line argument

* `react-codemod class <file> --printOptions='{"quote":"double"}'`
32 changes: 32 additions & 0 deletions npm-react-codemod/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "react-codemod",
"version": "1.0.0",
"description": "React codemod scripts",
"license": "BSD-3-Clause",
"repository": {
"type": "git",
"url": "https://github.com/facebook/react"
},
"scripts": {
"build": "rm -rf build; babel transforms/ --out-dir=build/",
"test": "jest",
"prepublish": "npm run build"
},
"bin": {
"react-codemod": "./react-codemod"
},
"dependencies": {
"jscodeshift": "^0.1.0"
},
"devDependencies": {
"babel": "^4.7.16",
"babel-jest": "^4.0.0",
"jest-cli": "^0.4.0"
},
"jest": {
"scriptPreprocessor": "./node_modules/babel-jest",
"testPathDirs": [
"test"
]
}
}
7 changes: 7 additions & 0 deletions npm-react-codemod/react-codemod
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/bin/bash

DIR=$(npm root -g)/react-codemod
TRANSFORM=$1
shift

$DIR/node_modules/.bin/jscodeshift -t $DIR/build/$TRANSFORM.js $@
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty sure this isn't going to work on Windows. I thought we were going to make this a js script that used node to shell out to jscodeshift?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

meh, that's no fun. Let me see what I can do.

63 changes: 63 additions & 0 deletions npm-react-codemod/test/__tests__/transform-tests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/

"use strict";

jest.autoMockOff();

var fs = require('fs');
var jscodeshift = require('jscodeshift');

function read(fileName) {
return fs.readFileSync(__dirname + '/../' + fileName, 'utf8');
}

function test(transformName, testFileName, options) {
var path = testFileName + '.js';
var source = read(testFileName + '.js');
var output = read(testFileName + '.output.js');

var transform = require('../../transforms/' + transformName);
expect(
(transform({path, source}, {jscodeshift}, options || {}) || '').trim()
).toEqual(
output.trim()
);
}

describe('Transform Tests', () => {

it('transforms the "findDOMNode" tests correctly', () => {
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',
});
});

it('transforms the "class" tests correctly', () => {
test('class', 'class-test');

test('class', 'class-test2', {
'no-super-class': true,
});

test('class', 'class-test3');
});

});
128 changes: 128 additions & 0 deletions npm-react-codemod/test/class-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
'use strict';

var React = require('React');
var Relay = require('Relay');

var Image = require('Image.react');

/*
* Multiline
*/
var MyComponent = React.createClass({
getInitialState: function() {
var x = this.props.foo;
return {
heyoo: 23,
};
},

foo: function() {
this.setState({heyoo: 24});
}
});

// Class comment
var MyComponent2 = React.createClass({
getDefaultProps: function() {
return {a: 1};
},
foo: function() {
pass(this.foo);
this.forceUpdate();
}
});

var MyComponent3 = React.createClass({
statics: {
someThing: 10,
foo: function() {},
},
propTypes: {
highlightEntities: React.PropTypes.bool,
linkifyEntities: React.PropTypes.bool,
text: React.PropTypes.shape({
text: React.PropTypes.string,
ranges: React.PropTypes.array
}).isRequired
},

getDefaultProps: function() {
foo();
return {
linkifyEntities: true,
highlightEntities: false
};
},

getInitialState: function() {
this.props.foo();
return {
heyoo: 23,
};
},

_renderText: function(text) {
return <Text text={text} />;
},

_renderImageRange: function(text, range) {
var image = range.image;
if (image) {
return (
<Image
src={image.uri}
height={image.height / image.scale}
width={image.width / image.scale}
/>
);
}
},

autobindMe: function() {},
dontAutobindMe: function() {},

// Function comment
_renderRange: function(text, range) {
var self = this;

self.dontAutobindMe();
call(self.autobindMe);

var type = rage.type;
var {highlightEntities} = this.props;

if (type === 'ImageAtRange') {
return this._renderImageRange(text, range);
}

if (this.props.linkifyEntities) {
text =
<Link href={usersURI}>
{text}
</Link>;
} else {
text = <span>{text}</span>;
}

return text;
},

/* This is a comment */
render: function() {
var content = this.props.text;
return (
<BaseText
{...this.props}
textRenderer={this._renderText}
rangeRenderer={this._renderRange}
text={content.text}
/>
);
}
});

module.exports = Relay.createContainer(MyComponent, {
queries: {
me: Relay.graphql`this is not graphql`,
}
});
Loading