react-rails 1.11.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +29 -0
  3. data/README.md +294 -214
  4. data/lib/assets/javascripts/react_ujs.js +429 -7
  5. data/lib/generators/react/component_generator.rb +24 -12
  6. data/lib/generators/react/install_generator.rb +76 -18
  7. data/lib/generators/templates/react_server_rendering.rb +2 -0
  8. data/lib/generators/templates/server_rendering.js +6 -0
  9. data/lib/generators/templates/server_rendering_pack.js +5 -0
  10. data/lib/react/jsx.rb +2 -0
  11. data/lib/react/rails/component_mount.rb +23 -5
  12. data/lib/react/rails/controller_lifecycle.rb +35 -7
  13. data/lib/react/rails/railtie.rb +17 -11
  14. data/lib/react/rails/version.rb +1 -1
  15. data/lib/react/server_rendering.rb +16 -4
  16. data/lib/react/server_rendering/{sprockets_renderer.rb → bundle_renderer.rb} +40 -20
  17. data/lib/react/server_rendering/{sprockets_renderer → bundle_renderer}/console_polyfill.js +0 -0
  18. data/lib/react/server_rendering/{sprockets_renderer → bundle_renderer}/console_replay.js +1 -1
  19. data/lib/react/server_rendering/bundle_renderer/console_reset.js +3 -0
  20. data/lib/react/server_rendering/{sprockets_renderer → bundle_renderer}/timeout_polyfill.js +0 -0
  21. data/lib/react/server_rendering/exec_js_renderer.rb +4 -1
  22. data/lib/react/server_rendering/webpacker_manifest_container.rb +34 -0
  23. data/lib/react/server_rendering/yaml_manifest_container.rb +1 -1
  24. metadata +16 -16
  25. data/lib/assets/javascripts/react_ujs_event_setup.js +0 -29
  26. data/lib/assets/javascripts/react_ujs_mount.js +0 -104
  27. data/lib/assets/javascripts/react_ujs_native.js +0 -18
  28. data/lib/assets/javascripts/react_ujs_pjax.js +0 -10
  29. data/lib/assets/javascripts/react_ujs_turbolinks.js +0 -9
  30. data/lib/assets/javascripts/react_ujs_turbolinks_classic.js +0 -10
  31. data/lib/assets/javascripts/react_ujs_turbolinks_classic_deprecated.js +0 -13
  32. 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
- empty_directory 'app/assets/javascripts/components'
16
- create_file 'app/assets/javascripts/components/.gitkeep' unless options[:skip_git]
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 inject_react
20
- require_react = "//= require react\n"
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(*%w(app assets javascripts components.js))
94
+ components_file = File.join(javascript_dir, "components.js")
48
95
  create_file components_file, components_js
49
96
  end
50
97
 
51
- private
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 manifest
54
- Pathname.new(destination_root).join('app/assets/javascripts', 'application.js')
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,2 @@
1
+ # To render React components in production, precompile the server rendering manifest:
2
+ Rails.application.config.assets.precompile += ["server_rendering.js"]
@@ -0,0 +1,6 @@
1
+ //= require react-server
2
+ //= require react_ujs
3
+ //= require ./components
4
+ //
5
+ // By default, this file is loaded for server-side rendering.
6
+ // It should require your components and any dependencies.
@@ -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)
@@ -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(env)
16
+ def setup(controller)
17
+ @controller = controller
17
18
  end
18
19
 
19
- def teardown(env)
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 React::ServerRendering.render(name, props, prerender_options) }
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
- before_action_with_fallback = respond_to?(:before_action) ? :before_action : :before_filter
9
- after_action_with_fallback = respond_to?(:after_action) ? :after_action : :after_filter
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
- def setup_react_component_helper
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
- def teardown_react_component_helper
22
- @__react_component_helper.teardown(self)
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
@@ -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 SprocketsRenderer
19
- config.react.server_renderer_options = {} # SprocketsRenderer provides defaults
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
- app.config.watchable_files.concat Dir["#{app.root}/app/assets/javascripts/**/*.jsx*"]
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::SprocketsRenderer
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|
@@ -2,6 +2,6 @@ module React
2
2
  module Rails
3
3
  # If you change this, make sure to update VERSIONS.md
4
4
  # And the version hint in README.md, if needed
5
- VERSION = "1.11.0"
5
+ VERSION = "2.0.0"
6
6
  end
7
7
  end
@@ -1,27 +1,39 @@
1
1
  require 'connection_pool'
2
2
  require 'react/server_rendering/exec_js_renderer'
3
- require 'react/server_rendering/sprockets_renderer'
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) { create_renderer }
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
- def self.create_renderer
22
- renderer.new(renderer_options)
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
- # - builds JS code out of the asset pipeline
9
+ # - fetches JS code from the Rails app (webpacker or sprockets)
9
10
  # - stringifies props
10
11
  # - implements console replay
11
- class SprocketsRenderer < ExecJSRenderer
12
+ class BundleRenderer < ExecJSRenderer
12
13
  # Reimplement console methods for replaying on the client
13
- CONSOLE_POLYFILL = File.read(File.join(File.dirname(__FILE__), "sprockets_renderer/console_polyfill.js"))
14
- CONSOLE_REPLAY = File.read(File.join(File.dirname(__FILE__), "sprockets_renderer/console_replay.js"))
15
- TIMEOUT_POLYFILL = File.read(File.join(File.dirname(__FILE__), "sprockets_renderer/timeout_polyfill.js"))
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, ["react-server.js", "components.js"])
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::SprocketsRenderer.asset_container_class = MyAssetContainer`.
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 ||= if self.class.asset_container_class.present?
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