Skip to content

New Babel Transformer #295

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 9 commits into from
Jun 19, 2015
Merged
83 changes: 60 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
# react-rails


`react-rails` makes it easy to use [React](http://facebook.github.io/react/) and [JSX](http://facebook.github.io/react/docs/jsx-in-depth.html) in your Ruby on Rails (3.2+) application. `react-rails` can:
`react-rails` makes it easy to use [React](http://facebook.github.io/react/) and [JSX](http://facebook.github.io/react/docs/jsx-in-depth.html)
in your Ruby on Rails (3.2+) application. `react-rails` can:

- Provide [various `react` builds](#reactjs-builds) to your asset bundle
- Transform [`.jsx` in the asset pipeline](#jsx)
Expand All @@ -32,7 +33,8 @@ rails g react:install
```

This will:
- create a `components.js` manifest file and a `app/assets/javascripts/components/` directory, where you will put your components
- create a `components.js` manifest file and a `app/assets/javascripts/components/` directory,
where you will put your components
- place the following in your `application.js`:

```js
Expand All @@ -45,7 +47,8 @@ This will:

### React.js builds

You can pick which React.js build (development, production, with or without [add-ons]((http://facebook.github.io/react/docs/addons.html))) to serve in each environment by adding a config. Here are the defaults:
You can pick which React.js build (development, production, with or without [add-ons]((http://facebook.github.io/react/docs/addons.html)))
to serve in each environment by adding a config. Here are the defaults:

```ruby
# config/environments/development.rb
Expand All @@ -67,14 +70,40 @@ MyApp::Application.configure do
end
```

After restarting your Rails server, `//= require react` will provide the build of React.js which was specified by the configurations.
After restarting your Rails server, `//= require react` will provide the build of React.js which
was specified by the configurations.

`react-rails` offers a few other options for versions & builds of React.js. See [VERSIONS.md](https://github.com/reactjs/react-rails/blob/master/VERSIONS.md) for more info about using the `react-source` gem or dropping in your own copies of React.js.
`react-rails` offers a few other options for versions & builds of React.js.
See [VERSIONS.md](https://github.com/reactjs/react-rails/blob/master/VERSIONS.md) for more info about
using the `react-source` gem or dropping in your own copies of React.js.

### JSX

After installing `react-rails`, restart your server. Now, `.js.jsx` files will be transformed in the asset pipeline.

`react-rails` currently ships with two transformers, to convert jsx code -

* `BabelTransformer` using [Babel](http://babeljs.io), which is the default transformer.
* `JSXTransformer` using `JSXTransformer.js`

#### BabelTransformer options

You can use babel's [transformers](http://babeljs.io/docs/advanced/transformers/) and [custom plugins](http://babeljs.io/docs/advanced/plugins/),
and pass [options](http://babeljs.io/docs/usage/options/) to the babel transpiler adding following configurations:

```ruby
config.react.jsx_transform_options = {
blacklist: ['spec.functionName', 'validation.react'], // default options
optional: ["transformerName"], // pass extra babel options
whitelist: ["useStrict"] // even more options
}
```
Under the hood, `react-rails` users [ruby-babel-transpiler](https://github.com/babel/ruby-babel-transpiler), for transformation.

#### JSXTransformer options

To use old JSXTransformer you can use `React::JSX.transformer_class = React::JSX::JSXTransformer`

You can use JSX `--harmony` or `--strip-types` options by adding a configuration:

```ruby
Expand All @@ -85,17 +114,11 @@ config.react.jsx_transform_options = {
}
```

To use CoffeeScript, create `.js.jsx.coffee` files and embed JSX inside backticks, for example:

```coffee
Component = React.createClass
render: ->
`<ExampleComponent videos={this.props.videos} />`
```

### Rendering & mounting

`react-rails` includes a view helper (`react_component`) and an unobtrusive JavaScript driver (`react_ujs`) which work together to put React components on the page. You should require the UJS driver in your manifest after `react` (and after `turbolinks` if you use [Turbolinks](https://github.com/rails/turbolinks)).
`react-rails` includes a view helper (`react_component`) and an unobtrusive JavaScript driver (`react_ujs`)
which work together to put React components on the page. You should require the UJS driver
in your manifest after `react` (and after `turbolinks` if you use [Turbolinks](https://github.com/rails/turbolinks)).

The __view helper__ puts a `div` on the page with the requested component class & props. For example:

Expand All @@ -105,9 +128,12 @@ The __view helper__ puts a `div` on the page with the requested component class
<div data-react-class="HelloMessage" data-react-props="{&quot;name&quot;:&quot;John&quot;}"></div>
```

On page load, the __`react_ujs` driver__ will scan the page and mount components using `data-react-class` and `data-react-props`. Before page unload, it will unmount components (if you want to disable this behavior, remove `data-react-class` attribute in `componentDidMount`).
On page load, the __`react_ujs` driver__ will scan the page and mount components using `data-react-class`
and `data-react-props`. Before page unload, it will unmount components (if you want to disable this behavior,
remove `data-react-class` attribute in `componentDidMount`).

`react_ujs` uses Turbolinks events if they're available, otherwise, it uses native events. __Turbolinks >= 2.4.0__ is recommended because it exposes better events.
`react_ujs` uses Turbolinks events if they're available, otherwise, it uses native events.
__Turbolinks >= 2.4.0__ is recommended because it exposes better events.

The view helper's signature is:

Expand Down Expand Up @@ -141,16 +167,19 @@ _(It will be also be mounted by the UJS on page load.)_

There are some requirements for this to work:

- `react-rails` must load your code. By convention it uses `components.js`, which was created by the install task. This file must include your components _and_ their dependencies (eg, Underscore.js).
- Your components must be accessible in the global scope. If you are using `.js.jsx.coffee` files then the wrapper function needs to be taken into account:
- `react-rails` must load your code. By convention it uses `components.js`, which was created
by the install task. This file must include your components _and_ their dependencies (eg, Underscore.js).
- Your components must be accessible in the global scope.
If you are using `.js.jsx.coffee` files then the wrapper function needs to be taken into account:

```coffee
# @ is `window`:
@Component = React.createClass
render: ->
`<ExampleComponent videos={this.props.videos} />`
```
- Your code can't reference `document`. Prerender processes don't have access to `document`, so jQuery and some other libs won't work in this environment :(
- Your code can't reference `document`. Prerender processes don't have access to `document`,
so jQuery and some other libs won't work in this environment :(

You can configure your pool of JS virtual machines and specify where it should load code:

Expand All @@ -171,7 +200,11 @@ end

### Component generator

`react-rails` ships with a Rails generator to help you get started with a simple component scaffold. You can run it using `rails generate react:component ComponentName`. The generator takes an optional list of arguments for default propTypes, which follow the conventions set in the [Reusable Components](http://facebook.github.io/react/docs/reusable-components.html) section of the React documentation.
`react-rails` ships with a Rails generator to help you get started with a simple component scaffold.
You can run it using `rails generate react:component ComponentName`.
The generator takes an optional list of arguments for default propTypes,
which follow the conventions set in the [Reusable Components](http://facebook.github.io/react/docs/reusable-components.html)
section of the React documentation.

For example:

Expand Down Expand Up @@ -222,11 +255,13 @@ The following additional arguments have special behavior:
* `oneOf` behaves like an enum, and takes an optional list of strings in the form of `'name:oneOf{one,two,three}'`.
* `oneOfType` takes an optional list of react and custom types in the form of `'model:oneOfType{string,number,OtherType}'`.

Note that the arguments for `oneOf` and `oneOfType` must be enclosed in single quotes to prevent your terminal from expanding them into an argument list.
Note that the arguments for `oneOf` and `oneOfType` must be enclosed in single quotes
to prevent your terminal from expanding them into an argument list.

### Jbuilder & react-rails

If you use Jbuilder to pass a JSON string to `react_component`, make sure your JSON is a stringified hash, not an array. This is not the Rails default -- you should add the root node yourself. For example:
If you use Jbuilder to pass a JSON string to `react_component`, make sure your JSON is a stringified hash,
not an array. This is not the Rails default -- you should add the root node yourself. For example:

```ruby
# BAD: returns a stringified array
Expand All @@ -244,7 +279,9 @@ end

## CoffeeScript

It is possible to use JSX with CoffeeScript. We need to embed JSX inside backticks so CoffeeScript ignores the syntax it doesn't understand. Here's an example:
It is possible to use JSX with CoffeeScript. To use CoffeeScript, create files with an extension `.js.jsx.coffee`.
We also need to embed JSX code inside backticks so that CoffeeScript ignores the syntax it doesn't understand.
Here's an example:

```coffee
Component = React.createClass
Expand Down
5 changes: 3 additions & 2 deletions lib/react/jsx.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
require 'execjs'
require 'react/jsx/template'
require 'react/jsx/transformer'
require 'react/jsx/jsx_transformer'
require 'react/jsx/babel_transformer'
require 'rails'

module React
Expand All @@ -11,7 +12,7 @@ module JSX
# to provide your own transformer. It must implement:
# - #initialize(options)
# - #transform(code) => new code
self.transformer_class = Transformer
self.transformer_class = BabelTransformer

def self.transform(code)
transformer.transform(code)
Expand Down
21 changes: 21 additions & 0 deletions lib/react/jsx/babel_transformer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
require 'babel/transpiler'
module React
module JSX
class BabelTransformer
DEPRECATED_OPTIONS = [:harmony, :strip_types, :asset_path]
DEFAULT_TRANSFORM_OPTIONS = { blacklist: ['spec.functionName', 'validation.react', 'strict'] }
def initialize(options)
if (options.keys & DEPRECATED_OPTIONS).any?
ActiveSupport::Deprecation.warn("Setting config.react.jsx_transform_options for :harmony, :strip_types, and :asset_path keys is now deprecated and has no effect with the default Babel Transformer."+
"Please use new Babel Transformer options :whitelist, :plugin instead.")
end

@transform_options = DEFAULT_TRANSFORM_OPTIONS.merge(options)
end

def transform(code)
Babel::Transpiler.transform(code, @transform_options)['code']
end
end
end
end
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module React
module JSX
class Transformer
class JSXTransformer
DEFAULT_ASSET_PATH = 'JSXTransformer.js'

def initialize(options)
Expand Down Expand Up @@ -30,4 +30,4 @@ def jsx_transform_code
end
end
end
end
end
1 change: 1 addition & 0 deletions react-rails.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Gem::Specification.new do |s|
s.add_dependency 'execjs'
s.add_dependency 'rails', '>= 3.2'
s.add_dependency 'tilt'
s.add_dependency 'babel-transpiler', '>=0.7.0'

s.files = Dir[
'lib/**/*',
Expand Down
3 changes: 2 additions & 1 deletion test/dummy/config/environments/test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
config.eager_load = false

# Configure static asset server for tests with Cache-Control for performance.
config.serve_static_assets = true
# Disabled since we dont use it and this option is deprecated from Rails 4.2 onwards
# config.serve_static_assets = true
config.static_cache_control = "public, max-age=3600"

# Show full error reports and disable caching.
Expand Down
4 changes: 2 additions & 2 deletions test/generators/component_generator_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ def filename
end

test "generates working jsx" do
expected_name_div = Regexp.escape('React.createElement("div", null, "Name: ", this.props.name)')
expected_shape_div = Regexp.escape('React.createElement("div", null, "Address: ", this.props.address)')
expected_name_div = /React\.createElement\(\s*"div",\s*null,\s*\"Name:\s*\",\s*this\.props\.name\s*\)/x
expected_shape_div = /React\.createElement\(\s*"div",\s*null,\s*\"Address:\s*\",\s*this\.props\.address\s*\)/x

run_generator %w(GeneratedComponent name:string address:shape)
jsx = React::JSX.transform(File.read(File.join(destination_root, filename)))
Expand Down
50 changes: 41 additions & 9 deletions test/react/jsx_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,24 @@ def transform(code)

class JSXTransformTest < ActionDispatch::IntegrationTest
setup do
clear_sprockets_cache
reset_transformer
end

teardown do
reset_transformer
end

def reset_transformer
clear_sprockets_cache
React::JSX.transformer_class = React::JSX::Transformer
React::JSX.transformer_class = React::JSX::BabelTransformer
React::JSX.transform_options = {}
end

test 'asset pipeline should transform JSX' do
get '/assets/example.js'
assert_response :success
assert_equal EXPECTED_JS, @response.body

assert_equal EXPECTED_JS.gsub(/\s/, ''), @response.body.gsub(/\s/, '')
end

test 'asset pipeline should transform JSX + Coffeescript' do
Expand All @@ -54,6 +59,39 @@ class JSXTransformTest < ActionDispatch::IntegrationTest
assert_equal EXPECTED_JS_2.gsub(/\s/, ''), @response.body.gsub(/\s/, '')
end

test 'use a custom transformer' do
React::JSX.transformer_class = NullTransformer
manually_expire_asset('example2.js')
get '/assets/example2.js'
assert_equal "TRANSFORMED CODE!;\n", @response.body
end

def test_babel_transformer_accepts_babel_transformation_options
React::JSX.transform_options = {blacklist: ['spec.functionName', 'validation.react', "strict"]}
get '/assets/example.js'
assert_response :success

assert [email protected]?('strict')
end

end

class JSXTransformerTest < ActionDispatch::IntegrationTest

setup do
reset_transformer
end

teardown do
reset_transformer
end

def reset_transformer
clear_sprockets_cache
React::JSX.transformer_class = React::JSX::JSXTransformer
React::JSX.transform_options = {}
end

test 'can use dropped-in version of JSX transformer' do
hidden_path = Rails.root.join("vendor/assets/react/JSXTransformer__.js")
replacing_path = Rails.root.join("vendor/assets/react/JSXTransformer.js")
Expand Down Expand Up @@ -99,10 +137,4 @@ class JSXTransformTest < ActionDispatch::IntegrationTest
assert_equal 'test_confirmation_token_jsx_transformed;', @response.body
end

test 'use a custom transformer' do
React::JSX.transformer_class = NullTransformer
manually_expire_asset('example2.js')
get '/assets/example2.js'
assert_equal "TRANSFORMED CODE!;\n", @response.body
end
end
8 changes: 6 additions & 2 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
# Configure Rails Environment
ENV["RAILS_ENV"] = "test"

require File.expand_path("../dummy/config/environment.rb", __FILE__)
require File.expand_path("../dummy/config/environment.rb", __FILE__)
require "rails/test_help"
require "rails/generators"
require "pathname"
require 'minitest/mock'

CACHE_PATH = Pathname.new File.expand_path("../dummy/tmp/cache", __FILE__)
CACHE_PATH = Pathname.new File.expand_path("../dummy/tmp/cache", __FILE__)

Rails.backtrace_cleaner.remove_silencers!

Expand Down Expand Up @@ -40,6 +40,10 @@ def asset.fresh?(env); false; end
ActiveSupport::TestCase.fixture_path = File.expand_path("../fixtures", __FILE__)
end

if ActiveSupport::TestCase.respond_to?(:test_order=)
ActiveSupport::TestCase.test_order = :random
end

def wait_for_turbolinks_to_be_available
sleep(1)
end