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.
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