react-rails 1.9.0 → 3.2.1

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 (61) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +345 -9
  3. data/README.md +137 -389
  4. data/lib/assets/javascripts/JSXTransformer.js +6 -6
  5. data/lib/assets/javascripts/react_ujs.js +1 -7
  6. data/lib/assets/react-source/development/react-server.js +423 -21528
  7. data/lib/assets/react-source/development/react.js +191 -21409
  8. data/lib/assets/react-source/production/react-server.js +2 -19
  9. data/lib/assets/react-source/production/react.js +2 -19
  10. data/lib/generators/react/component_generator.rb +203 -73
  11. data/lib/generators/react/install_generator.rb +98 -25
  12. data/lib/generators/templates/component.es6.jsx +9 -12
  13. data/lib/generators/templates/component.es6.tsx +24 -0
  14. data/lib/generators/templates/component.js.jsx +15 -18
  15. data/lib/generators/templates/component.js.jsx.coffee +5 -8
  16. data/lib/generators/templates/component.js.jsx.tsx +36 -0
  17. data/lib/generators/templates/react_server_rendering.rb +4 -0
  18. data/lib/generators/templates/server_rendering.js +6 -0
  19. data/lib/generators/templates/server_rendering_pack.js +5 -0
  20. data/lib/react/jsx/babel_transformer.rb +12 -6
  21. data/lib/react/jsx/jsx_transformer.rb +7 -6
  22. data/lib/react/jsx/processor.rb +3 -1
  23. data/lib/react/jsx/sprockets_strategy.rb +12 -6
  24. data/lib/react/jsx/template.rb +7 -6
  25. data/lib/react/jsx.rb +11 -7
  26. data/lib/react/rails/asset_variant.rb +7 -8
  27. data/lib/react/rails/component_mount.rb +48 -14
  28. data/lib/react/rails/controller_lifecycle.rb +36 -7
  29. data/lib/react/rails/controller_renderer.rb +13 -4
  30. data/lib/react/rails/railtie.rb +34 -29
  31. data/lib/react/rails/test_helper.rb +25 -0
  32. data/lib/react/rails/version.rb +4 -2
  33. data/lib/react/rails/view_helper.rb +3 -1
  34. data/lib/react/rails.rb +9 -7
  35. data/lib/react/server_rendering/{sprockets_renderer → bundle_renderer}/console_replay.js +1 -1
  36. data/lib/react/server_rendering/bundle_renderer/console_reset.js +3 -0
  37. data/lib/react/server_rendering/bundle_renderer/timeout_polyfill.js +26 -0
  38. data/lib/react/server_rendering/bundle_renderer.rb +117 -0
  39. data/lib/react/server_rendering/environment_container.rb +2 -0
  40. data/lib/react/server_rendering/exec_js_renderer.rb +43 -16
  41. data/lib/react/server_rendering/manifest_container.rb +5 -1
  42. data/lib/react/server_rendering/separate_server_bundle_container.rb +19 -0
  43. data/lib/react/server_rendering/yaml_manifest_container.rb +12 -4
  44. data/lib/react/server_rendering.rb +26 -12
  45. data/lib/react-rails.rb +12 -4
  46. data/lib/react.rb +8 -4
  47. metadata +106 -41
  48. data/lib/assets/javascripts/react_ujs_event_setup.js +0 -29
  49. data/lib/assets/javascripts/react_ujs_mount.js +0 -104
  50. data/lib/assets/javascripts/react_ujs_native.js +0 -18
  51. data/lib/assets/javascripts/react_ujs_pjax.js +0 -10
  52. data/lib/assets/javascripts/react_ujs_turbolinks.js +0 -9
  53. data/lib/assets/javascripts/react_ujs_turbolinks_classic.js +0 -10
  54. data/lib/assets/javascripts/react_ujs_turbolinks_classic_deprecated.js +0 -13
  55. data/lib/assets/react-source/development-with-addons/react-server.js +0 -24053
  56. data/lib/assets/react-source/development-with-addons/react.js +0 -23890
  57. data/lib/assets/react-source/production-with-addons/react-server.js +0 -19
  58. data/lib/assets/react-source/production-with-addons/react.js +0 -19
  59. data/lib/generators/react/ujs_generator.rb +0 -44
  60. data/lib/react/server_rendering/sprockets_renderer.rb +0 -79
  61. /data/lib/react/server_rendering/{sprockets_renderer → bundle_renderer}/console_polyfill.js +0 -0
@@ -0,0 +1,26 @@
1
+ function getStackTrace() {
2
+ var stack;
3
+ try {
4
+ throw new Error('');
5
+ }
6
+ catch (error) {
7
+ stack = error.stack || '';
8
+ }
9
+ stack = stack.split('\\n').map(function (line) {
10
+ return line.trim();
11
+ });
12
+ return stack.splice(stack[0] == 'Error' ? 2 : 1);
13
+ };
14
+
15
+ function printError(functionName){
16
+ console.error(functionName + ' is not defined for execJS. See https://github.com/sstephenson/execjs#faq. Note babel-polyfill may call this.');
17
+ console.error(getStackTrace().join('\\n'));
18
+ };
19
+
20
+ function setTimeout() {
21
+ printError('setTimeout');
22
+ };
23
+
24
+ function clearTimeout() {
25
+ printError('clearTimeout');
26
+ };
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "react/server_rendering/environment_container"
4
+ require "react/server_rendering/manifest_container"
5
+ require "react/server_rendering/yaml_manifest_container"
6
+ require "react/server_rendering/separate_server_bundle_container"
7
+
8
+ module React
9
+ module ServerRendering
10
+ # Extends ExecJSRenderer for the Rails environment
11
+ # - fetches JS code from the Rails app (Shakapacker or Sprockets)
12
+ # - stringifies props
13
+ # - implements console replay
14
+ class BundleRenderer < ExecJSRenderer
15
+ # Reimplement console methods for replaying on the client
16
+ CONSOLE_POLYFILL = File.read(File.join(File.dirname(__FILE__), "bundle_renderer/console_polyfill.js"))
17
+ CONSOLE_REPLAY = File.read(File.join(File.dirname(__FILE__), "bundle_renderer/console_replay.js"))
18
+ CONSOLE_RESET = File.read(File.join(File.dirname(__FILE__), "bundle_renderer/console_reset.js"))
19
+ TIMEOUT_POLYFILL = File.read(File.join(File.dirname(__FILE__), "bundle_renderer/timeout_polyfill.js"))
20
+
21
+ def initialize(options = {})
22
+ @replay_console = options.fetch(:replay_console, true)
23
+ filenames = options.fetch(:files, ["server_rendering.js"])
24
+ js_code = CONSOLE_POLYFILL.dup
25
+ js_code << TIMEOUT_POLYFILL.dup
26
+ js_code << options.fetch(:code, "")
27
+
28
+ filenames.each do |filename|
29
+ js_code << asset_container.find_asset(filename)
30
+ end
31
+
32
+ super(options.merge(code: js_code))
33
+ end
34
+
35
+ # Prerender options are expected to be a Hash however might also be a symbol.
36
+ # pass prerender: :static to use renderToStaticMarkup
37
+ # pass prerender: true to enable default prerender
38
+ # pass prerender: {} to proxy some custom options
39
+ def render(component_name, props, prerender_options)
40
+ t_options = prepare_options(prerender_options)
41
+ t_props = prepare_props(props)
42
+ super(component_name, t_props, t_options)
43
+ end
44
+
45
+ def before_render(_component_name, _props, _prerender_options)
46
+ @replay_console ? CONSOLE_RESET : ""
47
+ end
48
+
49
+ def after_render(_component_name, _props, _prerender_options)
50
+ @replay_console ? CONSOLE_REPLAY : ""
51
+ end
52
+
53
+ class << self
54
+ attr_accessor :asset_container_class
55
+ end
56
+
57
+ # Get an object which exposes assets by their logical path.
58
+ #
59
+ # Out of the box, it supports a Sprockets::Environment (application.assets)
60
+ # and a Sprockets::Manifest (application.assets_manifest), which covers the
61
+ # default Rails setups.
62
+ #
63
+ # You can provide a custom asset container
64
+ # with `React::ServerRendering::BundleRenderer.asset_container_class = MyAssetContainer`.
65
+ #
66
+ # @return [#find_asset(logical_path)] An object that returns asset contents by logical path
67
+ def asset_container
68
+ @asset_container ||= asset_container_class.new
69
+ end
70
+
71
+ private
72
+
73
+ def prepare_options(options)
74
+ r_func = render_function(options)
75
+ opts = case options
76
+ when Hash then options
77
+ else
78
+ {}
79
+ end
80
+ # This seems redundant to pass
81
+ opts.merge(render_function: r_func)
82
+ end
83
+
84
+ def render_function(opts)
85
+ if opts == :static
86
+ "renderToStaticMarkup"
87
+ else
88
+ "renderToString"
89
+ end
90
+ end
91
+
92
+ def prepare_props(props)
93
+ props.is_a?(String) ? props : props.to_json
94
+ end
95
+
96
+ def assets_precompiled?
97
+ !::Rails.application.config.assets.compile
98
+ end
99
+
100
+ # Detect what kind of asset system is in use and choose that container.
101
+ # Or, if the user has provided {.asset_container_class}, use that.
102
+ # @return [Class] suitable for {#asset_container}
103
+ def asset_container_class
104
+ return self.class.asset_container_class if self.class.asset_container_class.present?
105
+ return SeparateServerBundleContainer if SeparateServerBundleContainer.compatible?
106
+
107
+ return EnvironmentContainer unless assets_precompiled?
108
+
109
+ return ManifestContainer if ManifestContainer.compatible?
110
+ return YamlManifestContainer if YamlManifestContainer.compatible?
111
+
112
+ # Even though they are precompiled, we can't find them :S
113
+ EnvironmentContainer
114
+ end
115
+ end
116
+ end
117
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module React
2
4
  module ServerRendering
3
5
  # Return asset contents by getting them from a Sprockets::Environment instance.
@@ -1,41 +1,68 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module React
2
4
  module ServerRendering
3
5
  # A bare-bones renderer for React.js + Exec.js
4
- # - Depends on global ReactDOMServer in the ExecJS context
6
+ # - Depends on global ReactRailsUJS in the ExecJS context
5
7
  # - No Rails dependency
6
8
  # - No browser concerns
7
9
  class ExecJSRenderer
8
- def initialize(options={})
10
+ # @return [ExecJS::Runtime::Context] The JS context for this renderer
11
+ attr_reader :context
12
+
13
+ def initialize(options = {})
9
14
  js_code = options[:code] || raise("Pass `code:` option to instantiate a JS context!")
10
- @context = ExecJS.compile(GLOBAL_WRAPPER + js_code)
15
+ full_code = GLOBAL_WRAPPER + js_code
16
+ # File.write("./test/dummy/tmp/latest_js_context.js", full_code)
17
+ @context = ExecJS.compile(full_code)
11
18
  end
12
19
 
13
20
  def render(component_name, props, prerender_options)
14
- render_function = prerender_options.fetch(:render_function, "renderToString")
15
- js_code = <<-JS
16
- (function () {
17
- #{before_render(component_name, props, prerender_options)}
18
- var result = ReactDOMServer.#{render_function}(React.createElement(#{component_name}, #{props}));
19
- #{after_render(component_name, props, prerender_options)}
20
- return result;
21
- })()
22
- JS
23
- @context.eval(js_code).html_safe
21
+ js_executed_before = before_render(component_name, props, prerender_options)
22
+ js_executed_after = after_render(component_name, props, prerender_options)
23
+ js_main_section = main_render(component_name, props, prerender_options)
24
+ render_from_parts(js_executed_before, js_main_section, js_executed_after)
24
25
  rescue ExecJS::ProgramError => err
25
26
  raise React::ServerRendering::PrerenderError.new(component_name, props, err)
26
27
  end
27
28
 
28
29
  # Hooks for inserting JS before/after rendering
29
- def before_render(component_name, props, prerender_options); ""; end
30
- def after_render(component_name, props, prerender_options); ""; end
30
+ def before_render(_component_name, _props, _prerender_options)
31
+ ""
32
+ end
33
+
34
+ def after_render(_component_name, _props, _prerender_options)
35
+ ""
36
+ end
31
37
 
32
38
  # Handle Node.js & other ExecJS contexts
33
39
  GLOBAL_WRAPPER = <<-JS
34
40
  var global = global || this;
35
41
  var self = self || this;
36
- var window = window || this;
37
42
  JS
38
43
 
44
+ private
45
+
46
+ def render_from_parts(before, main, after)
47
+ js_code = compose_js(before, main, after)
48
+ @context.eval(js_code).html_safe
49
+ end
50
+
51
+ def main_render(component_name, props, prerender_options)
52
+ render_function = prerender_options.fetch(:render_function, "renderToString")
53
+ "this.ReactRailsUJS.serverRender('#{render_function}', '#{component_name}', #{props})"
54
+ end
55
+
56
+ def compose_js(before, main, after)
57
+ <<-JS
58
+ (function () {
59
+ #{before}
60
+ var result = #{main};
61
+ #{after}
62
+ return result;
63
+ })()
64
+ JS
65
+ end
39
66
  end
40
67
  end
41
68
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module React
2
4
  module ServerRendering
3
5
  # Get asset content by reading the compiled file from disk using a Sprockets::Manifest.
@@ -10,7 +12,9 @@ module React
10
12
  end
11
13
 
12
14
  def find_asset(logical_path)
13
- asset_path = @manifest.assets[logical_path] || raise("No compiled asset for #{logical_path}, was it precompiled?")
15
+ asset_path = @manifest.assets[logical_path] || raise(
16
+ "No compiled asset for #{logical_path}, was it precompiled?"
17
+ )
14
18
  asset_full_path = ::Rails.root.join("public", @manifest.dir, asset_path)
15
19
  File.read(asset_full_path)
16
20
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open-uri"
4
+
5
+ module React
6
+ module ServerRendering
7
+ # Get a compiled file from Shakapacker's output path
8
+ class SeparateServerBundleContainer
9
+ def self.compatible?
10
+ !!defined?(Shakapacker)
11
+ end
12
+
13
+ def find_asset(filename)
14
+ asset_path = Shakapacker.config.public_output_path.join(filename).to_s
15
+ File.read(asset_path)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -1,23 +1,31 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module React
2
4
  module ServerRendering
3
- # Get asset content by reading the compiled file from disk using the generated maniftest.yml file
5
+ # Get asset content by reading the compiled file from disk using the generated manifest.yml file
4
6
  #
5
7
  # This is good for Rails production when assets are compiled to public/assets
6
8
  # but sometimes, they're compiled to other directories (or other servers)
7
9
  class YamlManifestContainer
8
10
  def initialize
9
- @assets = YAML.load_file(::Rails.root.join("public", ::Rails.application.config.assets.prefix, "manifest.yml"))
11
+ @assets = YAML.load_file(public_asset_path("manifest.yml"))
10
12
  end
11
13
 
12
14
  def find_asset(logical_path)
13
15
  asset_path = @assets[logical_path] || raise("No compiled asset for #{logical_path}, was it precompiled?")
14
- asset_full_path = ::Rails.root.join("public", ::Rails.application.config.assets.prefix, asset_path)
15
- File.read(asset_full_path)
16
+ File.read(public_asset_path(asset_path))
16
17
  end
17
18
 
18
19
  def self.compatible?
19
20
  ::Rails::VERSION::MAJOR == 3
20
21
  end
22
+
23
+ private
24
+
25
+ def public_asset_path(asset_name)
26
+ asset_path = File.join("public", ::Rails.application.config.assets.prefix, asset_name)
27
+ ::Rails.root.join(asset_path)
28
+ end
21
29
  end
22
30
  end
23
31
  end
@@ -1,33 +1,47 @@
1
- require 'connection_pool'
2
- require 'react/server_rendering/exec_js_renderer'
3
- require 'react/server_rendering/sprockets_renderer'
1
+ # frozen_string_literal: true
2
+
3
+ require "connection_pool"
4
+ require "react/server_rendering/exec_js_renderer"
5
+ require "react/server_rendering/bundle_renderer"
4
6
 
5
7
  module React
6
8
  module ServerRendering
7
9
  mattr_accessor :renderer, :renderer_options,
8
- :pool_size, :pool_timeout
10
+ :pool_size, :pool_timeout
11
+
12
+ self.renderer_options = {}
9
13
 
14
+ # Discard the old ConnectionPool & create a new one.
15
+ # This will clear all state such as loaded code, JS VM state, or options.
16
+ # @return [void]
10
17
  def self.reset_pool
11
- options = {size: pool_size, timeout: pool_timeout}
12
- @@pool = ConnectionPool.new(options) { create_renderer }
18
+ options = { size: pool_size, timeout: pool_timeout }
19
+ @pool = ConnectionPool.new(options) { renderer.new(renderer_options) }
13
20
  end
14
21
 
22
+ # Check a renderer out of the pool and use it to render the component.
23
+ # @param component_name [String] Component identifier, looked up by UJS
24
+ # @param props [String, Hash] Props for this component
25
+ # @param prerender_options [Hash] Renderer-specific options
26
+ # @return [String] Prerendered HTML from `component_name`
15
27
  def self.render(component_name, props, prerender_options)
16
- @@pool.with do |renderer|
28
+ @pool.with do |renderer|
17
29
  renderer.render(component_name, props, prerender_options)
18
30
  end
19
31
  end
20
32
 
21
- def self.create_renderer
22
- renderer.new(renderer_options)
33
+ # Yield a renderer for an arbitrary block
34
+ def self.with_renderer(&block)
35
+ @pool.with(&block)
23
36
  end
24
37
 
38
+ # Raised when something went wrong during server rendering.
25
39
  class PrerenderError < RuntimeError
26
40
  def initialize(component_name, props, js_message)
27
- message = ["Encountered error \"#{js_message}\" when prerendering #{component_name} with #{props}",
28
- js_message.backtrace.join("\n")].join("\n")
41
+ message = ["Encountered error \"#{js_message.inspect}\" when prerendering #{component_name} with #{props}",
42
+ js_message.backtrace.join("\n")].join("\n")
29
43
  super(message)
30
44
  end
31
45
  end
32
46
  end
33
- end
47
+ end
data/lib/react-rails.rb CHANGED
@@ -1,4 +1,12 @@
1
- require 'react'
2
- require 'react/jsx'
3
- require 'react/rails'
4
- require 'react/server_rendering'
1
+ # frozen_string_literal: true
2
+
3
+ require "react"
4
+ require "react/jsx"
5
+ require "react/rails"
6
+ require "react/server_rendering"
7
+
8
+ module React
9
+ module Rails
10
+ autoload :TestHelper, "react/rails/test_helper"
11
+ end
12
+ end
data/lib/react.rb CHANGED
@@ -1,19 +1,23 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module React
2
4
  # Recursively camelize `props`, returning a new Hash
3
5
  # @param props [Object] If it's a Hash or Array, it will be recursed. Otherwise it will be returned.
4
6
  # @return [Hash] a new hash whose keys are camelized strings
5
7
  def self.camelize_props(props)
6
- case props
8
+ props_as_json = props.as_json
9
+
10
+ case props_as_json
7
11
  when Hash
8
- props.each_with_object({}) do |(key, value), new_props|
12
+ props_as_json.each_with_object({}) do |(key, value), new_props|
9
13
  new_key = key.to_s.camelize(:lower)
10
14
  new_value = camelize_props(value)
11
15
  new_props[new_key] = new_value
12
16
  end
13
17
  when Array
14
- props.map { |item| camelize_props(item) }
18
+ props_as_json.map { |item| camelize_props(item) }
15
19
  else
16
- props
20
+ props_as_json
17
21
  end
18
22
  end
19
23
  end