react-rails 1.11.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +29 -0
- data/README.md +294 -214
- data/lib/assets/javascripts/react_ujs.js +429 -7
- data/lib/generators/react/component_generator.rb +24 -12
- data/lib/generators/react/install_generator.rb +76 -18
- data/lib/generators/templates/react_server_rendering.rb +2 -0
- data/lib/generators/templates/server_rendering.js +6 -0
- data/lib/generators/templates/server_rendering_pack.js +5 -0
- data/lib/react/jsx.rb +2 -0
- data/lib/react/rails/component_mount.rb +23 -5
- data/lib/react/rails/controller_lifecycle.rb +35 -7
- data/lib/react/rails/railtie.rb +17 -11
- data/lib/react/rails/version.rb +1 -1
- data/lib/react/server_rendering.rb +16 -4
- data/lib/react/server_rendering/{sprockets_renderer.rb → bundle_renderer.rb} +40 -20
- data/lib/react/server_rendering/{sprockets_renderer → bundle_renderer}/console_polyfill.js +0 -0
- data/lib/react/server_rendering/{sprockets_renderer → bundle_renderer}/console_replay.js +1 -1
- data/lib/react/server_rendering/bundle_renderer/console_reset.js +3 -0
- data/lib/react/server_rendering/{sprockets_renderer → bundle_renderer}/timeout_polyfill.js +0 -0
- data/lib/react/server_rendering/exec_js_renderer.rb +4 -1
- data/lib/react/server_rendering/webpacker_manifest_container.rb +34 -0
- data/lib/react/server_rendering/yaml_manifest_container.rb +1 -1
- metadata +16 -16
- data/lib/assets/javascripts/react_ujs_event_setup.js +0 -29
- data/lib/assets/javascripts/react_ujs_mount.js +0 -104
- data/lib/assets/javascripts/react_ujs_native.js +0 -18
- data/lib/assets/javascripts/react_ujs_pjax.js +0 -10
- data/lib/assets/javascripts/react_ujs_turbolinks.js +0 -9
- data/lib/assets/javascripts/react_ujs_turbolinks_classic.js +0 -10
- data/lib/assets/javascripts/react_ujs_turbolinks_classic_deprecated.js +0 -13
- data/lib/generators/react/ujs_generator.rb +0 -44
@@ -11,13 +11,70 @@ module React
|
|
11
11
|
default: false,
|
12
12
|
desc: 'Skip Git keeps'
|
13
13
|
|
14
|
+
class_option :skip_server_rendering,
|
15
|
+
type: :boolean,
|
16
|
+
default: false,
|
17
|
+
desc: "Don't generate server_rendering.js or config/initializers/react_server_rendering.rb"
|
18
|
+
|
19
|
+
# Make an empty `components/` directory in the right place:
|
14
20
|
def create_directory
|
15
|
-
|
16
|
-
|
21
|
+
components_dir = if webpacker?
|
22
|
+
Pathname.new(javascript_dir).parent.to_s
|
23
|
+
else
|
24
|
+
javascript_dir
|
25
|
+
end
|
26
|
+
empty_directory File.join(components_dir, 'components')
|
27
|
+
if !options[:skip_git]
|
28
|
+
create_file File.join(components_dir, 'components/.gitkeep')
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Add requires, setup UJS
|
33
|
+
def setup_react
|
34
|
+
if webpacker?
|
35
|
+
setup_react_webpacker
|
36
|
+
else
|
37
|
+
setup_react_sprockets
|
38
|
+
end
|
17
39
|
end
|
18
40
|
|
19
|
-
def
|
20
|
-
|
41
|
+
def create_server_rendering
|
42
|
+
if options[:skip_server_rendering]
|
43
|
+
return
|
44
|
+
elsif webpacker?
|
45
|
+
ssr_manifest_path = File.join(javascript_dir, "server_rendering.js")
|
46
|
+
template("server_rendering_pack.js", ssr_manifest_path)
|
47
|
+
else
|
48
|
+
ssr_manifest_path = File.join(javascript_dir, "server_rendering.js")
|
49
|
+
template("server_rendering.js", ssr_manifest_path)
|
50
|
+
initializer_path = "config/initializers/react_server_rendering.rb"
|
51
|
+
template("react_server_rendering.rb", initializer_path)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def webpacker?
|
58
|
+
!!defined?(Webpacker)
|
59
|
+
end
|
60
|
+
|
61
|
+
def javascript_dir
|
62
|
+
if webpacker?
|
63
|
+
Webpacker::Configuration.source_path
|
64
|
+
.join(Webpacker::Configuration.entry_path)
|
65
|
+
.relative_path_from(::Rails.root)
|
66
|
+
.to_s
|
67
|
+
else
|
68
|
+
'app/assets/javascripts'
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def manifest
|
73
|
+
Pathname.new(destination_root).join(javascript_dir, 'application.js')
|
74
|
+
end
|
75
|
+
|
76
|
+
def setup_react_sprockets
|
77
|
+
require_react = "//= require react\n//= require react_ujs\n//= require components\n"
|
21
78
|
|
22
79
|
if manifest.exist?
|
23
80
|
manifest_contents = File.read(manifest)
|
@@ -32,26 +89,27 @@ module React
|
|
32
89
|
else
|
33
90
|
create_file manifest, require_react
|
34
91
|
end
|
35
|
-
end
|
36
92
|
|
37
|
-
def inject_components
|
38
|
-
inject_into_file manifest, "//= require components\n", {after: "//= require react\n"}
|
39
|
-
end
|
40
|
-
|
41
|
-
def inject_react_ujs
|
42
|
-
inject_into_file manifest, "//= require react_ujs\n", {after: "//= require react\n"}
|
43
|
-
end
|
44
|
-
|
45
|
-
def create_components
|
46
93
|
components_js = "//= require_tree ./components\n"
|
47
|
-
components_file = File.join(
|
94
|
+
components_file = File.join(javascript_dir, "components.js")
|
48
95
|
create_file components_file, components_js
|
49
96
|
end
|
50
97
|
|
51
|
-
|
98
|
+
WEBPACKER_SETUP_UJS = <<-JS
|
99
|
+
// Support component names relative to this directory:
|
100
|
+
var componentRequireContext = require.context("components", true)
|
101
|
+
var ReactRailsUJS = require("react_ujs")
|
102
|
+
ReactRailsUJS.useContext(componentRequireContext)
|
103
|
+
JS
|
52
104
|
|
53
|
-
def
|
54
|
-
|
105
|
+
def setup_react_webpacker
|
106
|
+
yarn_binstub = File.expand_path("./bin/yarn", ::Rails.root)
|
107
|
+
`#{yarn_binstub} add react_ujs`
|
108
|
+
if manifest.exist?
|
109
|
+
append_file(manifest, WEBPACKER_SETUP_UJS)
|
110
|
+
else
|
111
|
+
create_file(manifest, WEBPACKER_SETUP_UJS)
|
112
|
+
end
|
55
113
|
end
|
56
114
|
end
|
57
115
|
end
|
@@ -0,0 +1,5 @@
|
|
1
|
+
// By default, this pack is loaded for server-side rendering.
|
2
|
+
// It must expose react_ujs as `ReactRailsUJS` and prepare a require context.
|
3
|
+
var componentRequireContext = require.context("components", true)
|
4
|
+
var ReactRailsUJS = require("react_ujs")
|
5
|
+
ReactRailsUJS.useContext(componentRequireContext)
|
data/lib/react/jsx.rb
CHANGED
@@ -17,6 +17,8 @@ module React
|
|
17
17
|
# - #transform(code) => new code
|
18
18
|
self.transformer_class = DEFAULT_TRANSFORMER
|
19
19
|
|
20
|
+
# @param code [String] JSX code to transform into JavaScript
|
21
|
+
# @return [String] plain, browser-ready JavaScript code
|
20
22
|
def self.transform(code)
|
21
23
|
self.transformer ||= transformer_class.new(transform_options)
|
22
24
|
self.transformer.transform(code)
|
@@ -11,12 +11,13 @@ module React
|
|
11
11
|
attr_accessor :output_buffer
|
12
12
|
mattr_accessor :camelize_props_switch
|
13
13
|
|
14
|
-
# ControllerLifecycle calls these hooks
|
14
|
+
# {ControllerLifecycle} calls these hooks
|
15
15
|
# You can use them in custom helper implementations
|
16
|
-
def setup(
|
16
|
+
def setup(controller)
|
17
|
+
@controller = controller
|
17
18
|
end
|
18
19
|
|
19
|
-
def teardown(
|
20
|
+
def teardown(controller)
|
20
21
|
end
|
21
22
|
|
22
23
|
# Render a UJS-type HTML tag annotated with data attributes, which
|
@@ -30,7 +31,7 @@ module React
|
|
30
31
|
|
31
32
|
prerender_options = options[:prerender]
|
32
33
|
if prerender_options
|
33
|
-
block = Proc.new{ concat
|
34
|
+
block = Proc.new{ concat(prerender_component(name, props, prerender_options)) }
|
34
35
|
end
|
35
36
|
|
36
37
|
html_options = options.reverse_merge(:data => {})
|
@@ -45,7 +46,24 @@ module React
|
|
45
46
|
# remove internally used properties so they aren't rendered to DOM
|
46
47
|
html_options.except!(:tag, :prerender, :camelize_props)
|
47
48
|
|
48
|
-
content_tag(html_tag, '', html_options, &block)
|
49
|
+
rendered_tag = content_tag(html_tag, '', html_options, &block)
|
50
|
+
if React::ServerRendering.renderer_options[:replay_console]
|
51
|
+
# Grab the server-rendered console replay script
|
52
|
+
# and move it _outside_ the container div
|
53
|
+
rendered_tag.sub!(/\n(<script class="react-rails-console-replay">.*<\/script>)<\/(\w+)>$/m,'</\2>\1')
|
54
|
+
rendered_tag.html_safe
|
55
|
+
else
|
56
|
+
rendered_tag
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
# If this controller has checked out a renderer, use that one.
|
63
|
+
# Otherwise, use {React::ServerRendering} directly (which will check one out for this rendering).
|
64
|
+
def prerender_component(component_name, props, prerender_options)
|
65
|
+
renderer = @controller.try(:react_rails_prerenderer) || React::ServerRendering
|
66
|
+
renderer.render(component_name, props, prerender_options)
|
49
67
|
end
|
50
68
|
end
|
51
69
|
end
|
@@ -1,25 +1,53 @@
|
|
1
1
|
module React
|
2
2
|
module Rails
|
3
|
+
# This module is included into ActionController so that
|
4
|
+
# per-request hooks can be called in the view helper.
|
3
5
|
module ControllerLifecycle
|
4
6
|
extend ActiveSupport::Concern
|
5
7
|
|
6
8
|
included do
|
7
9
|
# use both names to support Rails 3..5
|
8
|
-
|
9
|
-
|
10
|
-
public_send(before_action_with_fallback, :setup_react_component_helper)
|
11
|
-
public_send(after_action_with_fallback, :teardown_react_component_helper)
|
10
|
+
around_action_with_fallback = respond_to?(:around_action) ? :around_action : :around_filter
|
11
|
+
public_send(around_action_with_fallback, :use_react_component_helper)
|
12
12
|
attr_reader :__react_component_helper
|
13
13
|
end
|
14
14
|
|
15
|
-
|
15
|
+
module ClassMethods
|
16
|
+
# Call this in the controller to check out a prerender for the whole request.
|
17
|
+
# You can access the renderer with {#react_rails_prerenderer}.
|
18
|
+
def per_request_react_rails_prerenderer
|
19
|
+
around_action_with_fallback = respond_to?(:around_action) ? :around_action : :around_filter
|
20
|
+
public_send(around_action_with_fallback, :per_request_react_rails_prerenderer)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Instantiate the ViewHelper implementation and call its #setup method
|
25
|
+
# then let the controller action run,
|
26
|
+
# then call the ViewHelper implementation's #teardown method
|
27
|
+
def use_react_component_helper
|
16
28
|
new_helper = React::Rails::ViewHelper.helper_implementation_class.new
|
17
29
|
new_helper.setup(self)
|
18
30
|
@__react_component_helper = new_helper
|
31
|
+
yield
|
32
|
+
@__react_component_helper.teardown(self)
|
19
33
|
end
|
20
34
|
|
21
|
-
|
22
|
-
|
35
|
+
# If you want a per-request renderer, add this method as an around-action
|
36
|
+
#
|
37
|
+
# (`.per_request_react_rails_prerenderer` does this for you)
|
38
|
+
# @example Having one renderer instance for each controller action
|
39
|
+
# around_action :per_request_react_rails_prerenderer
|
40
|
+
def per_request_react_rails_prerenderer
|
41
|
+
React::ServerRendering.with_renderer do |renderer|
|
42
|
+
@__react_rails_prerenderer = renderer
|
43
|
+
yield
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
# An instance of a server renderer, for use during this request
|
49
|
+
def react_rails_prerenderer
|
50
|
+
@__react_rails_prerenderer
|
23
51
|
end
|
24
52
|
end
|
25
53
|
end
|
data/lib/react/rails/railtie.rb
CHANGED
@@ -15,14 +15,27 @@ module React
|
|
15
15
|
# Server rendering:
|
16
16
|
config.react.server_renderer_pool_size = 1 # increase if you're on JRuby
|
17
17
|
config.react.server_renderer_timeout = 20 # seconds
|
18
|
-
config.react.server_renderer = nil # defaults to
|
19
|
-
config.react.server_renderer_options = {} #
|
18
|
+
config.react.server_renderer = nil # defaults to BundleRenderer
|
19
|
+
config.react.server_renderer_options = {} # BundleRenderer provides defaults
|
20
|
+
# Changing files with these extensions in these directories will cause the server renderer to reload:
|
21
|
+
config.react.server_renderer_directories = ["/app/assets/javascripts/", "app/javascript"]
|
22
|
+
config.react.server_renderer_extensions = ["jsx", "js"]
|
20
23
|
# View helper implementation:
|
21
24
|
config.react.view_helper_implementation = nil # Defaults to ComponentMount
|
22
25
|
|
23
26
|
# Watch .jsx files for changes in dev, so we can reload the JS VMs with the new JS code.
|
24
27
|
initializer "react_rails.add_watchable_files", group: :all do |app|
|
25
|
-
|
28
|
+
# Watch files ending in `server_renderer_extensions` in each of `server_renderer_directories`
|
29
|
+
reload_paths = config.react.server_renderer_directories.reduce({}) do |memo, dir|
|
30
|
+
app_dir = File.join(app.root, dir)
|
31
|
+
memo[app_dir] = config.react.server_renderer_extensions
|
32
|
+
memo
|
33
|
+
end
|
34
|
+
|
35
|
+
# Rails checks these objects for changes:
|
36
|
+
app.reloaders << app.config.file_watcher.new([], reload_paths)
|
37
|
+
# Reload renderers in dev when files change
|
38
|
+
config.to_prepare { React::ServerRendering.reset_pool }
|
26
39
|
end
|
27
40
|
|
28
41
|
# Include the react-rails view helper lazily
|
@@ -81,20 +94,13 @@ module React
|
|
81
94
|
|
82
95
|
config.after_initialize do |app|
|
83
96
|
# The class isn't accessible in the configure block, so assign it here if it wasn't overridden:
|
84
|
-
app.config.react.server_renderer ||= React::ServerRendering::
|
97
|
+
app.config.react.server_renderer ||= React::ServerRendering::BundleRenderer
|
85
98
|
|
86
99
|
React::ServerRendering.pool_size = app.config.react.server_renderer_pool_size
|
87
100
|
React::ServerRendering.pool_timeout = app.config.react.server_renderer_timeout
|
88
101
|
React::ServerRendering.renderer_options = app.config.react.server_renderer_options
|
89
102
|
React::ServerRendering.renderer = app.config.react.server_renderer
|
90
|
-
|
91
103
|
React::ServerRendering.reset_pool
|
92
|
-
# Reload renderers in dev when files change
|
93
|
-
if Gem::Version.new(::Rails::VERSION::STRING) >= Gem::Version.new("5.x")
|
94
|
-
ActiveSupport::Reloader.to_prepare { React::ServerRendering.reset_pool }
|
95
|
-
else
|
96
|
-
ActionDispatch::Reloader.to_prepare { React::ServerRendering.reset_pool }
|
97
|
-
end
|
98
104
|
end
|
99
105
|
|
100
106
|
initializer "react_rails.setup_engine", :group => :all do |app|
|
data/lib/react/rails/version.rb
CHANGED
@@ -1,27 +1,39 @@
|
|
1
1
|
require 'connection_pool'
|
2
2
|
require 'react/server_rendering/exec_js_renderer'
|
3
|
-
require 'react/server_rendering/
|
3
|
+
require 'react/server_rendering/bundle_renderer'
|
4
4
|
|
5
5
|
module React
|
6
6
|
module ServerRendering
|
7
7
|
mattr_accessor :renderer, :renderer_options,
|
8
8
|
:pool_size, :pool_timeout
|
9
9
|
|
10
|
+
self.renderer_options = {}
|
11
|
+
|
12
|
+
# Discard the old ConnectionPool & create a new one.
|
13
|
+
# This will clear all state such as loaded code, JS VM state, or options.
|
14
|
+
# @return [void]
|
10
15
|
def self.reset_pool
|
11
16
|
options = {size: pool_size, timeout: pool_timeout}
|
12
|
-
@@pool = ConnectionPool.new(options) {
|
17
|
+
@@pool = ConnectionPool.new(options) { self.renderer.new(self.renderer_options) }
|
13
18
|
end
|
14
19
|
|
20
|
+
# Check a renderer out of the pool and use it to render the component.
|
21
|
+
# @param component_name [String] Component identifier, looked up by UJS
|
22
|
+
# @param props [String, Hash] Props for this component
|
23
|
+
# @param prerender_options [Hash] Renderer-specific options
|
24
|
+
# @return [String] Prerendered HTML from `component_name`
|
15
25
|
def self.render(component_name, props, prerender_options)
|
16
26
|
@@pool.with do |renderer|
|
17
27
|
renderer.render(component_name, props, prerender_options)
|
18
28
|
end
|
19
29
|
end
|
20
30
|
|
21
|
-
|
22
|
-
|
31
|
+
# Yield a renderer for an arbitrary block
|
32
|
+
def self.with_renderer
|
33
|
+
@@pool.with { |renderer| yield(renderer) }
|
23
34
|
end
|
24
35
|
|
36
|
+
# Raised when something went wrong during server rendering.
|
25
37
|
class PrerenderError < RuntimeError
|
26
38
|
def initialize(component_name, props, js_message)
|
27
39
|
message = ["Encountered error \"#{js_message.inspect}\" when prerendering #{component_name} with #{props}",
|
@@ -1,22 +1,24 @@
|
|
1
1
|
require "react/server_rendering/environment_container"
|
2
2
|
require "react/server_rendering/manifest_container"
|
3
|
+
require "react/server_rendering/webpacker_manifest_container"
|
3
4
|
require "react/server_rendering/yaml_manifest_container"
|
4
5
|
|
5
6
|
module React
|
6
7
|
module ServerRendering
|
7
8
|
# Extends ExecJSRenderer for the Rails environment
|
8
|
-
# -
|
9
|
+
# - fetches JS code from the Rails app (webpacker or sprockets)
|
9
10
|
# - stringifies props
|
10
11
|
# - implements console replay
|
11
|
-
class
|
12
|
+
class BundleRenderer < ExecJSRenderer
|
12
13
|
# Reimplement console methods for replaying on the client
|
13
|
-
CONSOLE_POLYFILL = File.read(File.join(File.dirname(__FILE__), "
|
14
|
-
CONSOLE_REPLAY = File.read(File.join(File.dirname(__FILE__), "
|
15
|
-
|
14
|
+
CONSOLE_POLYFILL = File.read(File.join(File.dirname(__FILE__), "bundle_renderer/console_polyfill.js"))
|
15
|
+
CONSOLE_REPLAY = File.read(File.join(File.dirname(__FILE__), "bundle_renderer/console_replay.js"))
|
16
|
+
CONSOLE_RESET = File.read(File.join(File.dirname(__FILE__), "bundle_renderer/console_reset.js"))
|
17
|
+
TIMEOUT_POLYFILL = File.read(File.join(File.dirname(__FILE__), "bundle_renderer/timeout_polyfill.js"))
|
16
18
|
|
17
19
|
def initialize(options={})
|
18
20
|
@replay_console = options.fetch(:replay_console, true)
|
19
|
-
filenames = options.fetch(:files, ["
|
21
|
+
filenames = options.fetch(:files, ["server_rendering.js"])
|
20
22
|
js_code = CONSOLE_POLYFILL.dup
|
21
23
|
js_code << TIMEOUT_POLYFILL.dup
|
22
24
|
js_code << options.fetch(:code, '')
|
@@ -38,6 +40,10 @@ module React
|
|
38
40
|
super(component_name, t_props, t_options)
|
39
41
|
end
|
40
42
|
|
43
|
+
def before_render(component_name, props, prerender_options)
|
44
|
+
@replay_console ? CONSOLE_RESET : ""
|
45
|
+
end
|
46
|
+
|
41
47
|
def after_render(component_name, props, prerender_options)
|
42
48
|
@replay_console ? CONSOLE_REPLAY : ""
|
43
49
|
end
|
@@ -53,23 +59,11 @@ module React
|
|
53
59
|
# default Rails setups.
|
54
60
|
#
|
55
61
|
# You can provide a custom asset container
|
56
|
-
# with `React::ServerRendering::
|
62
|
+
# with `React::ServerRendering::BundleRenderer.asset_container_class = MyAssetContainer`.
|
57
63
|
#
|
58
64
|
# @return [#find_asset(logical_path)] An object that returns asset contents by logical path
|
59
65
|
def asset_container
|
60
|
-
@asset_container ||=
|
61
|
-
self.class.asset_container_class.new
|
62
|
-
elsif assets_precompiled? && ManifestContainer.compatible?
|
63
|
-
ManifestContainer.new
|
64
|
-
elsif assets_precompiled? && YamlManifestContainer.compatible?
|
65
|
-
YamlManifestContainer.new
|
66
|
-
else
|
67
|
-
EnvironmentContainer.new
|
68
|
-
end
|
69
|
-
end
|
70
|
-
|
71
|
-
def assets_precompiled?
|
72
|
-
!::Rails.application.config.assets.compile
|
66
|
+
@asset_container ||= asset_container_class.new
|
73
67
|
end
|
74
68
|
|
75
69
|
private
|
@@ -97,6 +91,32 @@ module React
|
|
97
91
|
def prepare_props(props)
|
98
92
|
props.is_a?(String) ? props : props.to_json
|
99
93
|
end
|
94
|
+
|
95
|
+
def assets_precompiled?
|
96
|
+
!::Rails.application.config.assets.compile
|
97
|
+
end
|
98
|
+
|
99
|
+
# Detect what kind of asset system is in use and choose that container.
|
100
|
+
# Or, if the user has provided {.asset_container_class}, use that.
|
101
|
+
# @return [Class] suitable for {#asset_container}
|
102
|
+
def asset_container_class
|
103
|
+
if self.class.asset_container_class.present?
|
104
|
+
self.class.asset_container_class
|
105
|
+
elsif WebpackerManifestContainer.compatible?
|
106
|
+
WebpackerManifestContainer
|
107
|
+
elsif assets_precompiled?
|
108
|
+
if ManifestContainer.compatible?
|
109
|
+
ManifestContainer
|
110
|
+
elsif YamlManifestContainer.compatible?
|
111
|
+
YamlManifestContainer
|
112
|
+
else
|
113
|
+
# Even though they are precompiled, we can't find them :S
|
114
|
+
EnvironmentContainer
|
115
|
+
end
|
116
|
+
else
|
117
|
+
EnvironmentContainer
|
118
|
+
end
|
119
|
+
end
|
100
120
|
end
|
101
121
|
end
|
102
122
|
end
|