react_on_rails 10.1.4 → 11.0.0.beta.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -1
  3. data/CHANGELOG.md +14 -2
  4. data/CONTRIBUTING.md +0 -7
  5. data/Gemfile +8 -13
  6. data/README.md +5 -4
  7. data/app/helpers/react_on_rails_helper.rb +1 -534
  8. data/docs/additional-reading/caching-and-performance.md +2 -29
  9. data/docs/additional-reading/server-rendering-tips.md +4 -4
  10. data/docs/basics/configuration.md +23 -9
  11. data/docs/basics/i18n.md +4 -0
  12. data/lib/generators/react_on_rails/base_generator.rb +2 -3
  13. data/lib/generators/react_on_rails/dev_tests_generator.rb +1 -1
  14. data/lib/generators/react_on_rails/install_generator.rb +1 -1
  15. data/lib/generators/react_on_rails/react_no_redux_generator.rb +1 -1
  16. data/lib/generators/react_on_rails/react_with_redux_generator.rb +1 -1
  17. data/lib/generators/react_on_rails/templates/base/base/config/initializers/react_on_rails.rb +2 -1
  18. data/lib/generators/react_on_rails/templates/dev_tests/spec/simplecov_helper.rb +2 -2
  19. data/lib/react_on_rails.rb +3 -2
  20. data/lib/react_on_rails/configuration.rb +27 -8
  21. data/lib/react_on_rails/error.rb +4 -0
  22. data/lib/react_on_rails/prerender_error.rb +1 -1
  23. data/lib/react_on_rails/react_on_rails_helper.rb +546 -0
  24. data/lib/react_on_rails/server_rendering_pool.rb +21 -11
  25. data/lib/react_on_rails/server_rendering_pool/{exec.rb → ruby_embedded_java_script.rb} +35 -38
  26. data/lib/react_on_rails/test_helper.rb +1 -1
  27. data/lib/react_on_rails/test_helper/webpack_assets_status_checker.rb +2 -2
  28. data/lib/react_on_rails/utils.rb +12 -73
  29. data/lib/react_on_rails/version.rb +1 -1
  30. data/lib/react_on_rails/version_checker.rb +4 -2
  31. data/lib/react_on_rails/webpacker_utils.rb +42 -0
  32. data/package.json +1 -1
  33. data/rakelib/example_type.rb +1 -1
  34. data/rakelib/examples.rake +1 -1
  35. data/rakelib/release.rake +0 -5
  36. data/rakelib/task_helpers.rb +1 -1
  37. data/react_on_rails.gemspec +12 -9
  38. metadata +30 -13
  39. data/lib/react_on_rails/server_rendering_pool/node.rb +0 -81
  40. data/lib/react_on_rails/test_helper/node_process_launcher.rb +0 -14
@@ -1,31 +1,4 @@
1
1
  # Caching and Performance
2
2
 
3
-
4
- ## Caching
5
-
6
- ### Fragment Caching
7
-
8
- If you wish to do fragment caching that includes React on Rails rendered components, be sure to
9
- include the bundle name of your server rendering bundle in your cache key. This is analogous to
10
- how Rails puts an MD5 hash of your views in the cache key so that if the views change, then your
11
- cache is busted. In the case of React code, if your React code changes, then your bundle name will
12
- change due to the typical inclusion of a hash in the name.
13
-
14
- Call this method to get the server bundle file name:
15
-
16
- ```ruby
17
- # Returns the hashed file name of the server bundle when using webpacker.
18
- # Nececessary fragment-caching keys.
19
- ReactOnRails::Utils.server_bundle_file_name
20
- ```
21
-
22
- ### HTTP Caching
23
-
24
- When creating a HTTP cache, you want the cache key to include your client bundle files.
25
-
26
- Call this method to get the client bundle file name. Note, you have to pass which bundle name.
27
-
28
- ```ruby
29
- # Returns the hashed file name when using webpacker. Useful for creating cache keys.
30
- ReactOnRails::Utils..bundle_file_name(bundle_name)
31
- ```
3
+ Caching and performance optimizations are now part of React on Rails Pro. For more information,
4
+ [contact Justin](mailto:justin@shakacode.com).
@@ -17,15 +17,15 @@ The point is that you have separate files for top level client or server side, a
17
17
  ## Troubleshooting Server Rendering
18
18
 
19
19
  1. First be sure your code works with server rendering disabled (`prerender: false`)
20
- 2. `export TRACE_REACT_ON_RAILS=TRUE` Turn this on to get both the invocation code for you component, as well as the whole file used to setup the JavaScript context.
20
+ 2. Be sure that `config.trace` is true. You will get the server invocation code that renders your component. If you're not using Webpacker, you will also get the whole file used to setup the JavaScript context.
21
21
 
22
- ## setTimeout and setInterval
22
+ ## setTimeout, setInterval, and clearTimeout
23
23
 
24
- These methods are polyfilled for server rendering to be no-ops. We don't log calls to these by default as some libraries, namely babel-polyfill, will call setTimout. If you wish to log calls to setTimeout and setInterval, set the ENV value: `export TRACE_REACT_ON_RAILS=TRUE`.
24
+ These methods are polyfilled for server rendering to be no-ops. We log calls to these when in `trace` mode. In the past, some libraries, namely babel-polyfill, did call setTimout.
25
25
 
26
26
  Here's an example of this which shows the line numbers that end up calling setTimeout:
27
27
  ```
28
- ➜ ~/shakacode/react_on_rails/gen-examples/examples/basic-server-rendering (add-rails-helper-to-generator u=) ✗ export TRACE_REACT_ON_RAILS=TRUE
28
+ ➜ ~/shakacode/react_on_rails/gen-examples/examples/basic-server-rendering (add-rails-helper-to-generator u=) ✗ export SERVER_TRACE_REACT_ON_RAILS=TRUE
29
29
  ➜ ~/shakacode/react_on_rails/gen-examples/examples/basic-server-rendering (add-rails-helper-to-generator u=) ✗ rspec
30
30
  Hello World
31
31
  Building Webpack client-rendering assets...
@@ -1,4 +1,4 @@
1
- Here is the full set of config options.
1
+ Here is the full set of config options. This file is `/config/initializers/react_on_rails.rb`
2
2
 
3
3
  ```ruby
4
4
  # frozen_string_literal: true
@@ -7,6 +7,13 @@ Here is the full set of config options.
7
7
  # Thus, you only need to pay careful attention to the non-commented settings in this file.
8
8
 
9
9
  ReactOnRails.configure do |config|
10
+ # `trace`: General debugging flag.
11
+ # The default is true for development, off otherwise.
12
+ # With true, you get detailed logs of rendering and stack traces if you call setTimout,
13
+ # setInterval, clearTimout when server rendering.
14
+ config.trace = Rails.env.development?
15
+
16
+
10
17
  # defaults to "" (top level)
11
18
  #
12
19
  config.node_modules_location = ""
@@ -66,6 +73,7 @@ ReactOnRails.configure do |config|
66
73
  #
67
74
  # While you may configure this to be the same as your client bundle file, this file is typically
68
75
  # different.
76
+ #
69
77
  config.server_bundle_js_file = "server-bundle.js"
70
78
 
71
79
  # If set to true, this forces Rails to reload the server bundle if it is modified
@@ -73,13 +81,15 @@ ReactOnRails.configure do |config|
73
81
  #
74
82
  config.development_mode = Rails.env.development?
75
83
 
76
- # For server rendering. This can be set to false so that server side messages are discarded.
84
+ # For server rendering so that it replays in the browser console.
85
+ # This can be set to false so that server side messages are not displayed in the browser.
77
86
  # Default is true. Be cautious about turning this off.
78
87
  # Default value is true
79
- #
88
+ #
80
89
  config.replay_console = true
81
90
 
82
- # Default is true. Logs server rendering messages to Rails.logger.info
91
+ # Default is true. Logs server rendering messages to Rails.logger.info. If false, you'll only
92
+ # see the server rendering messages in the browser console.
83
93
  #
84
94
  config.logging_on_server = true
85
95
 
@@ -88,7 +98,14 @@ ReactOnRails.configure do |config|
88
98
  #
89
99
  config.raise_on_prerender_error = false
90
100
 
91
- # Server rendering only (not for render_component helper)
101
+ ################################################################################
102
+ # Server Renderer Configuration for ExecJS
103
+ ################################################################################
104
+ # The default server rendering is ExecJS, probably using the mini_racer gem
105
+ # If you wish to use an alternative Node server rendering for higher performance,
106
+ # contact justin@shakacode.com for details.
107
+ #
108
+ # For ExecJS:
92
109
  # You can configure your pool of JS virtual machines and specify where it should load code:
93
110
  # On MRI, use `mini_racer` for the best performance
94
111
  # (see [discussion](https://github.com/reactjs/react-rails/pull/290))
@@ -111,7 +128,7 @@ ReactOnRails.configure do |config|
111
128
  # By default(without this option) all yaml files from Rails.root.join("config", "locales")
112
129
  # and installed gems are loaded
113
130
  config.i18n_yml_dir = Rails.root.join("config", "locales", "client")
114
-
131
+
115
132
  ################################################################################
116
133
  ################################################################################
117
134
  # CLIENT RENDERING OPTIONS
@@ -120,9 +137,6 @@ ReactOnRails.configure do |config|
120
137
  ################################################################################
121
138
  # default is false
122
139
  config.prerender = false
123
-
124
- # default is true for development, off otherwise
125
- config.trace = Rails.env.development?
126
140
  end
127
141
 
128
142
  ```
data/docs/basics/i18n.md CHANGED
@@ -71,3 +71,7 @@ You can refer to [react-webpack-rails-tutorial](https://github.com/shakacode/rea
71
71
  { formatMessage(defaultMessages.yourLocaleKeyInCamelCase) }
72
72
  )
73
73
  ```
74
+
75
+ # Notes
76
+
77
+ * See [Support for Rails' i18n pluralization #1000](https://github.com/shakacode/react_on_rails/issues/1000) for a discussion of issues around pluralization.
@@ -9,7 +9,7 @@ module ReactOnRails
9
9
  class BaseGenerator < Rails::Generators::Base
10
10
  include GeneratorHelper
11
11
  Rails::Generators.hide_namespace(namespace)
12
- source_root(File.expand_path("../templates", __FILE__))
12
+ source_root(File.expand_path("templates", __dir__))
13
13
 
14
14
  # --redux
15
15
  class_option :redux,
@@ -55,7 +55,6 @@ module ReactOnRails
55
55
  if File.exist?(spec_helper)
56
56
  add_configure_rspec_to_compile_assets(spec_helper)
57
57
  else
58
- # rubocop:disable Lint/UnneededDisable
59
58
  # rubocop:disable Layout/EmptyLinesAroundArguments
60
59
  GeneratorMessages.add_info(
61
60
  <<-MSG.strip_heredoc
@@ -70,7 +69,7 @@ module ReactOnRails
70
69
  MSG
71
70
  )
72
71
  # rubocop:enable Layout/EmptyLinesAroundArguments
73
- # rubocop:enable Lint/UnneededDisable
72
+
74
73
  end
75
74
  end
76
75
  end
@@ -8,7 +8,7 @@ module ReactOnRails
8
8
  class DevTestsGenerator < Rails::Generators::Base
9
9
  include GeneratorHelper
10
10
  Rails::Generators.hide_namespace(namespace)
11
- source_root(File.expand_path("../templates/dev_tests", __FILE__))
11
+ source_root(File.expand_path("templates/dev_tests", __dir__))
12
12
 
13
13
  # --example-server-rendering
14
14
  class_option :example_server_rendering,
@@ -10,7 +10,7 @@ module ReactOnRails
10
10
  include GeneratorHelper
11
11
 
12
12
  # fetch USAGE file for details generator description
13
- source_root(File.expand_path("../", __FILE__))
13
+ source_root(File.expand_path(__dir__))
14
14
 
15
15
  # --redux
16
16
  class_option :redux,
@@ -8,7 +8,7 @@ module ReactOnRails
8
8
  class ReactNoReduxGenerator < Rails::Generators::Base
9
9
  include GeneratorHelper
10
10
  Rails::Generators.hide_namespace(namespace)
11
- source_root(File.expand_path("../templates", __FILE__))
11
+ source_root(File.expand_path("templates", __dir__))
12
12
 
13
13
  def copy_base_files
14
14
  base_js_path = "base/base"
@@ -6,7 +6,7 @@ module ReactOnRails
6
6
  module Generators
7
7
  class ReactWithReduxGenerator < Rails::Generators::Base
8
8
  Rails::Generators.hide_namespace(namespace)
9
- source_root(File.expand_path("../templates", __FILE__))
9
+ source_root(File.expand_path("templates", __dir__))
10
10
 
11
11
  def create_redux_directories
12
12
  dirs = %w[actions constants containers reducers store startup]
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # See docs/basics/configuration.md for many more options
3
+ # See https://github.com/shakacode/react_on_rails/blob/master/docs/basics/configuration.md
4
+ # for many more options.
4
5
 
5
6
  ReactOnRails.configure do |config|
6
7
  # This configures the script to run to build the production assets by webpack. Set this to nil
@@ -6,13 +6,13 @@ if ENV["COVERAGE"] == "true"
6
6
  require "simplecov"
7
7
 
8
8
  # Using a command name prevents results from getting clobbered by other test suites
9
- example_name = File.basename(File.expand_path("../../../.", __FILE__))
9
+ example_name = File.basename(File.expand_path("../..", __dir__))
10
10
  SimpleCov.command_name(example_name)
11
11
 
12
12
  SimpleCov.start("rails") do
13
13
  # Consider the entire gem project as the root
14
14
  # (typically this will be the folder named "react_on_rails")
15
- gem_root_path = File.expand_path("../../../../../.", __FILE__)
15
+ gem_root_path = File.expand_path("../../../..", __dir__)
16
16
  root gem_root_path
17
17
 
18
18
  # Don't report anything that has "spec" in the path
@@ -2,6 +2,8 @@
2
2
 
3
3
  require "rails"
4
4
 
5
+ require "react_on_rails/error"
6
+ require "react_on_rails/react_on_rails_helper"
5
7
  require "react_on_rails/controller"
6
8
  require "react_on_rails/version"
7
9
  require "react_on_rails/version_checker"
@@ -13,9 +15,8 @@ require "react_on_rails/version_syntax_converter"
13
15
  require "react_on_rails/test_helper"
14
16
  require "react_on_rails/git_utils"
15
17
  require "react_on_rails/utils"
16
- require "react_on_rails/test_helper"
18
+ require "react_on_rails/webpacker_utils"
17
19
  require "react_on_rails/test_helper/webpack_assets_compiler"
18
20
  require "react_on_rails/test_helper/webpack_assets_status_checker"
19
21
  require "react_on_rails/test_helper/ensure_assets_compiled"
20
- require "react_on_rails/test_helper/node_process_launcher"
21
22
  require "react_on_rails/locales_to_js"
@@ -18,24 +18,43 @@ module ReactOnRails
18
18
  ensure_server_bundle_js_file_has_no_path
19
19
  check_i18n_directory_exists
20
20
  check_i18n_yml_directory_exists
21
+ check_server_render_method_is_only_execjs
22
+ end
23
+
24
+ def self.check_server_render_method_is_only_execjs
25
+ return if @configuration.server_render_method.blank? ||
26
+ @configuration.server_render_method == "ExecJS"
27
+
28
+ msg = <<-MSG.strip_heredoc
29
+ Error configuring /config/react_on_rails.rb: invalid value for `config.server_render_method`.
30
+ If you wish to use a server render method other than ExecJS, contact justin@shakacode.com
31
+ for details.
32
+ MSG
33
+ raise ReactOnRails::Error, msg
21
34
  end
22
35
 
23
36
  def self.check_i18n_directory_exists
24
37
  return if @configuration.i18n_dir.nil?
25
38
  return if Dir.exist?(@configuration.i18n_dir)
26
39
 
27
- raise "Error configuring /config/react_on_rails.rb: invalid value for `config.i18n_dir`. "\
28
- "Directory does not exist: #{@configuration.i18n_dir}. Set to value to nil or comment it "\
29
- "out if not using the React on Rails i18n feature."
40
+ msg = <<-MSG.strip_heredoc
41
+ Error configuring /config/react_on_rails.rb: invalid value for `config.i18n_dir`.
42
+ Directory does not exist: #{@configuration.i18n_dir}. Set to value to nil or comment it
43
+ out if not using the React on Rails i18n feature.
44
+ MSG
45
+ raise ReactOnRails::Error, msg
30
46
  end
31
47
 
32
48
  def self.check_i18n_yml_directory_exists
33
49
  return if @configuration.i18n_yml_dir.nil?
34
50
  return if Dir.exist?(@configuration.i18n_yml_dir)
35
51
 
36
- raise "Error configuring /config/react_on_rails.rb: invalid value for `config.i18n_yml_dir`. "\
37
- "Directory does not exist: #{@configuration.i18n_yml_dir}. Set to value to nil or comment it "\
38
- "out if not using this i18n with React on Rails, or if you want to use all translation files."
52
+ msg = <<-MSG.strip_heredoc
53
+ Error configuring /config/react_on_rails.rb: invalid value for `config.i18n_yml_dir`.
54
+ Directory does not exist: #{@configuration.i18n_yml_dir}. Set to value to nil or comment it
55
+ out if not using this i18n with React on Rails, or if you want to use all translation files.
56
+ MSG
57
+ raise ReactOnRails::Error, msg
39
58
  end
40
59
 
41
60
  def self.ensure_generated_assets_dir_present
@@ -99,7 +118,7 @@ module ReactOnRails
99
118
  # skip_display_none is deprecated
100
119
  webpack_generated_files: %w[manifest.json],
101
120
  rendering_extension: nil,
102
- server_render_method: "ExecJS",
121
+ server_render_method: nil,
103
122
  symlink_non_digested_assets_regex: nil,
104
123
  build_test_command: "",
105
124
  build_production_command: ""
@@ -127,7 +146,7 @@ module ReactOnRails
127
146
  rendering_extension: nil, build_test_command: nil,
128
147
  build_production_command: nil,
129
148
  i18n_dir: nil, i18n_yml_dir: nil,
130
- server_render_method: "ExecJS", symlink_non_digested_assets_regex: nil)
149
+ server_render_method: nil, symlink_non_digested_assets_regex: nil)
131
150
  self.node_modules_location = node_modules_location.present? ? node_modules_location : Rails.root
132
151
  self.server_bundle_js_file = server_bundle_js_file
133
152
  self.generated_assets_dirs = generated_assets_dirs
@@ -0,0 +1,4 @@
1
+ module ReactOnRails
2
+ class Error < StandardError
3
+ end
4
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  # rubocop:disable: Layout/IndentHeredoc
4
4
  module ReactOnRails
5
- class PrerenderError < RuntimeError
5
+ class PrerenderError < StandardError
6
6
  # err might be nil if JS caught the error
7
7
  def initialize(component_name: nil, err: nil, props: nil,
8
8
  js_code: nil, console_messages: nil)
@@ -0,0 +1,546 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop:disable Metrics/ModuleLength
4
+ # NOTE:
5
+ # For any heredoc JS:
6
+ # 1. The white spacing in this file matters!
7
+ # 2. Keep all #{some_var} fully to the left so that all indentation is done evenly in that var
8
+ require "react_on_rails/prerender_error"
9
+ require "addressable/uri"
10
+ require "react_on_rails/utils"
11
+ require "react_on_rails/json_output"
12
+ require "active_support/concern"
13
+
14
+ module ReactOnRails
15
+ module Helper
16
+ include ReactOnRails::Utils::Required
17
+
18
+ COMPONENT_HTML_KEY = "componentHtml".freeze
19
+
20
+ # The env_javascript_include_tag and env_stylesheet_link_tag support the usage of a webpack
21
+ # dev server for providing the JS and CSS assets during development mode. See
22
+ # https://github.com/shakacode/react-webpack-rails-tutorial/ for a working example.
23
+ #
24
+ # The key options are `static` and `hot` which specify what you want for static vs. hot. Both of
25
+ # these params are optional, and support either a single value, or an array.
26
+ #
27
+ # static vs. hot is picked based on whether
28
+ # ENV["REACT_ON_RAILS_ENV"] == "HOT"
29
+ #
30
+ # <%= env_stylesheet_link_tag(static: 'application_static',
31
+ # hot: 'application_non_webpack',
32
+ # media: 'all',
33
+ # 'data-turbolinks-track' => "reload") %>
34
+ #
35
+ # <!-- These do not use turbolinks, so no data-turbolinks-track -->
36
+ # <!-- This is to load the hot assets. -->
37
+ # <%= env_javascript_include_tag(hot: ['http://localhost:3500/vendor-bundle.js',
38
+ # 'http://localhost:3500/app-bundle.js']) %>
39
+ #
40
+ # <!-- These do use turbolinks -->
41
+ # <%= env_javascript_include_tag(static: 'application_static',
42
+ # hot: 'application_non_webpack',
43
+ # 'data-turbolinks-track' => "reload") %>
44
+ #
45
+ # NOTE: for Turbolinks 2.x, use 'data-turbolinks-track' => true
46
+ # See application.html.erb for usage example
47
+ # https://github.com/shakacode/react-webpack-rails-tutorial/blob/master/app%2Fviews%2Flayouts%2Fapplication.html.erb
48
+ def env_javascript_include_tag(args = {})
49
+ send_tag_method(:javascript_include_tag, args)
50
+ end
51
+
52
+ # Helper to set CSS assets depending on if we want static or "hot", which means from the
53
+ # Webpack dev server.
54
+ #
55
+ # In this example, application_non_webpack is simply a CSS asset pipeline file which includes
56
+ # styles not placed in the webpack build.
57
+ #
58
+ # We don't need styles from the webpack build, as those will come via the JavaScript include
59
+ # tags.
60
+ #
61
+ # The key options are `static` and `hot` which specify what you want for static vs. hot. Both of
62
+ # these params are optional, and support either a single value, or an array.
63
+ #
64
+ # <%= env_stylesheet_link_tag(static: 'application_static',
65
+ # hot: 'application_non_webpack',
66
+ # media: 'all',
67
+ # 'data-turbolinks-track' => true) %>
68
+ #
69
+ def env_stylesheet_link_tag(args = {})
70
+ send_tag_method(:stylesheet_link_tag, args)
71
+ end
72
+
73
+ # react_component_name: can be a React component, created using a ES6 class, or
74
+ # React.createClass, or a
75
+ # `generator function` that returns a React component
76
+ # using ES6
77
+ # let MyReactComponentApp = (props, railsContext) => <MyReactComponent {...props}/>;
78
+ # or using ES5
79
+ # var MyReactComponentApp = function(props, railsContext) { return <YourReactComponent {...props}/>; }
80
+ # Exposing the react_component_name is necessary to both a plain ReactComponent as well as
81
+ # a generator:
82
+ # See README.md for how to "register" your react components.
83
+ # See spec/dummy/client/app/startup/serverRegistration.jsx and
84
+ # spec/dummy/client/app/startup/ClientRegistration.jsx for examples of this
85
+ #
86
+ # options:
87
+ # props: Ruby Hash or JSON string which contains the properties to pass to the react object. Do
88
+ # not pass any props if you are separately initializing the store by the `redux_store` helper.
89
+ # prerender: <true/false> set to false when debugging!
90
+ # id: You can optionally set the id, or else a unique one is automatically generated.
91
+ # html_options: You can set other html attributes that will go on this component
92
+ # trace: <true/false> set to true to print additional debugging information in the browser
93
+ # default is true for development, off otherwise
94
+ # replay_console: <true/false> Default is true. False will disable echoing server rendering
95
+ # logs to browser. While this can make troubleshooting server rendering difficult,
96
+ # so long as you have the default configuration of logging_on_server set to
97
+ # true, you'll still see the errors on the server.
98
+ # raise_on_prerender_error: <true/false> Default to false. True will raise exception on server
99
+ # if the JS code throws
100
+ # Any other options are passed to the content tag, including the id.
101
+ def react_component(component_name, raw_options = {})
102
+ internal_result = internal_react_component(component_name, raw_options)
103
+ server_rendered_html = internal_result["result"]["html"]
104
+ console_script = internal_result["result"]["consoleReplayScript"]
105
+
106
+ if server_rendered_html.is_a?(String)
107
+ build_react_component_result_for_server_rendered_string(
108
+ server_rendered_html: server_rendered_html,
109
+ component_specification_tag: internal_result["tag"],
110
+ console_script: console_script,
111
+ options: internal_result["options"]
112
+ )
113
+ elsif server_rendered_html.is_a?(Hash)
114
+ msg = <<-MSG.strip_heredoc
115
+ Use react_component_hash (not react_component) to return a Hash to your ruby view code. See
116
+ https://github.com/shakacode/react_on_rails/blob/master/spec/dummy/client/app/startup/ReactHelmetServerApp.jsx
117
+ for an example of the necessary javascript configuration."
118
+ MSG
119
+ raise ReactOnRailsPro::Error, msg
120
+
121
+ else
122
+ msg = <<-MSG.strip_heredoc
123
+ ReactOnRails: server_rendered_html is expected to be a String. If you're trying to
124
+ use a generator function to return a Hash to your ruby view code, then use
125
+ react_component_hash instead of react_component and see
126
+ https://github.com/shakacode/react_on_rails/blob/master/spec/dummy/client/app/startup/ReactHelmetServerApp.jsx
127
+ for an example of the necessary javascript configuration."
128
+ MSG
129
+ raise ReactOnRailsPro::Error, msg
130
+ end
131
+ end
132
+
133
+ def react_component_hash(component_name, raw_options = {})
134
+ internal_result = internal_react_component(component_name, raw_options)
135
+ server_rendered_html = internal_result["result"]["html"]
136
+ console_script = internal_result["result"]["consoleReplayScript"]
137
+
138
+ if server_rendered_html.is_a?(String) && internal_result["result"]["hasErrors"]
139
+ server_rendered_html = { COMPONENT_HTML_KEY => internal_result["result"]["html"] }
140
+ end
141
+
142
+ if server_rendered_html.is_a?(Hash)
143
+ build_react_component_result_for_server_rendered_hash(
144
+ server_rendered_html: server_rendered_html,
145
+ component_specification_tag: internal_result["tag"],
146
+ console_script: console_script,
147
+ options: internal_result["options"]
148
+ )
149
+ else
150
+ msg = <<-MSG.strip_heredoc
151
+ Generator function used by react_component_hash is expected to return an Object. See
152
+ https://github.com/shakacode/react_on_rails/blob/master/spec/dummy/client/app/startup/ReactHelmetServerApp.jsx
153
+ for an example of the necessary javascript configuration.
154
+ MSG
155
+ raise ReactOnRailsPro::Error, msg
156
+ end
157
+ end
158
+
159
+ # Separate initialization of store from react_component allows multiple react_component calls to
160
+ # use the same Redux store.
161
+ #
162
+ # store_name: name of the store, corresponding to your call to ReactOnRails.registerStores in your
163
+ # JavaScript code.
164
+ # props: Ruby Hash or JSON string which contains the properties to pass to the redux store.
165
+ # Options
166
+ # defer: false -- pass as true if you wish to render this below your component.
167
+ def redux_store(store_name, props: {}, defer: false)
168
+ redux_store_data = { store_name: store_name,
169
+ props: props }
170
+ if defer
171
+ @registered_stores_defer_render ||= []
172
+ @registered_stores_defer_render << redux_store_data
173
+ "YOU SHOULD NOT SEE THIS ON YOUR VIEW -- Uses as a code block, like <% redux_store %> "\
174
+ "and not <%= redux store %>"
175
+ else
176
+ @registered_stores ||= []
177
+ @registered_stores << redux_store_data
178
+ result = render_redux_store_data(redux_store_data)
179
+ prepend_render_rails_context(result)
180
+ end
181
+ end
182
+
183
+ # Place this view helper (no parameters) at the end of your shared layout. This tell
184
+ # ReactOnRails where to client render the redux store hydration data. Since we're going
185
+ # to be setting up the stores in the controllers, we need to know where on the view to put the
186
+ # client side rendering of this hydration data, which is a hidden div with a matching class
187
+ # that contains a data props.
188
+ def redux_store_hydration_data
189
+ return if @registered_stores_defer_render.blank?
190
+ @registered_stores_defer_render.reduce("".dup) do |accum, redux_store_data|
191
+ accum << render_redux_store_data(redux_store_data)
192
+ end.html_safe
193
+ end
194
+
195
+ def sanitized_props_string(props)
196
+ ReactOnRails::JsonOutput.escape(props.is_a?(String) ? props : props.to_json)
197
+ end
198
+
199
+ # Helper method to take javascript expression and returns the output from evaluating it.
200
+ # If you have more than one line that needs to be executed, wrap it in an IIFE.
201
+ # JS exceptions are caught and console messages are handled properly.
202
+ def server_render_js(js_expression, options = {})
203
+ wrapper_js = <<-JS.strip_heredoc
204
+ (function() {
205
+ var htmlResult = '';
206
+ var consoleReplayScript = '';
207
+ var hasErrors = false;
208
+
209
+ try {
210
+ htmlResult =
211
+ (function() {
212
+ return #{js_expression};
213
+ })();
214
+ } catch(e) {
215
+ htmlResult = ReactOnRails.handleError({e: e, name: null,
216
+ jsCode: '#{escape_javascript(js_expression)}', serverSide: true});
217
+ hasErrors = true;
218
+ }
219
+
220
+ consoleReplayScript = ReactOnRails.buildConsoleReplay();
221
+
222
+ return JSON.stringify({
223
+ html: htmlResult,
224
+ consoleReplayScript: consoleReplayScript,
225
+ hasErrors: hasErrors
226
+ });
227
+
228
+ })()
229
+ JS
230
+
231
+ result = ReactOnRails::ServerRenderingPool.server_render_js_with_console_logging(wrapper_js)
232
+
233
+ html = result["html"]
234
+ console_log_script = result["consoleLogScript"]
235
+ raw("#{html}#{replay_console_option(options[:replay_console_option]) ? console_log_script : ''}")
236
+ rescue ExecJS::ProgramError => err
237
+ raise ReactOnRails::PrerenderError, component_name: "N/A (server_render_js called)",
238
+ err: err,
239
+ js_code: wrapper_js
240
+ end
241
+
242
+ def json_safe_and_pretty(hash_or_string)
243
+ return "{}" if hash_or_string.nil?
244
+ unless hash_or_string.class.in?([Hash, String])
245
+ raise ReactOnRails::Error, "#{__method__} only accepts String or Hash as argument "\
246
+ "(#{hash_or_string.class} given)."
247
+ end
248
+
249
+ json_value = hash_or_string.is_a?(String) ? hash_or_string : hash_or_string.to_json
250
+
251
+ ReactOnRails::JsonOutput.escape(json_value)
252
+ end
253
+
254
+ private
255
+
256
+ def build_react_component_result_for_server_rendered_string(
257
+ server_rendered_html: required("server_rendered_html"),
258
+ component_specification_tag: required("component_specification_tag"),
259
+ console_script: required("console_script"),
260
+ options: required("options")
261
+ )
262
+ content_tag_options = options.html_options
263
+ content_tag_options[:id] = options.dom_id
264
+
265
+ rendered_output = content_tag(:div,
266
+ server_rendered_html.html_safe,
267
+ content_tag_options)
268
+
269
+ result_console_script = options.replay_console ? console_script : ""
270
+ result = compose_react_component_html_with_spec_and_console(
271
+ component_specification_tag, rendered_output, result_console_script
272
+ )
273
+
274
+ prepend_render_rails_context(result)
275
+ end
276
+
277
+ def build_react_component_result_for_server_rendered_hash(
278
+ server_rendered_html: required("server_rendered_html"),
279
+ component_specification_tag: required("component_specification_tag"),
280
+ console_script: required("console_script"),
281
+ options: required("options")
282
+ )
283
+ content_tag_options = options.html_options
284
+ content_tag_options[:id] = options.dom_id
285
+
286
+ unless server_rendered_html[COMPONENT_HTML_KEY]
287
+ raise ReactOnRailsPro::Error, "server_rendered_html hash expected to contain \"#{COMPONENT_HTML_KEY}\" key."
288
+ end
289
+
290
+ rendered_output = content_tag(:div,
291
+ server_rendered_html[COMPONENT_HTML_KEY].html_safe,
292
+ content_tag_options)
293
+
294
+ result_console_script = options.replay_console ? console_script : ""
295
+ result = compose_react_component_html_with_spec_and_console(
296
+ component_specification_tag, rendered_output, result_console_script
297
+ )
298
+
299
+ # Other HTML strings need to be marked as html_safe too:
300
+ server_rendered_hash_except_component = server_rendered_html.except(COMPONENT_HTML_KEY)
301
+ server_rendered_hash_except_component.each do |key, html_string|
302
+ server_rendered_hash_except_component[key] = html_string.html_safe
303
+ end
304
+
305
+ result_with_rails_context = prepend_render_rails_context(result)
306
+ { COMPONENT_HTML_KEY => result_with_rails_context }.merge(
307
+ server_rendered_hash_except_component
308
+ )
309
+ end
310
+
311
+ def compose_react_component_html_with_spec_and_console(component_specification_tag, rendered_output, console_script)
312
+ # IMPORTANT: Ensure that we mark string as html_safe to avoid escaping.
313
+ # rubocop:disable Layout/IndentHeredoc
314
+ <<-HTML.html_safe
315
+ #{rendered_output}
316
+ #{component_specification_tag}
317
+ #{console_script}
318
+ HTML
319
+ # rubocop:enable Layout/IndentHeredoc
320
+ end
321
+
322
+ # prepend the rails_context if not yet applied
323
+ def prepend_render_rails_context(render_value)
324
+ return render_value if @rendered_rails_context
325
+
326
+ data = rails_context(server_side: false)
327
+
328
+ @rendered_rails_context = true
329
+
330
+ rails_context_content = content_tag(:script,
331
+ json_safe_and_pretty(data).html_safe,
332
+ type: "application/json",
333
+ id: "js-react-on-rails-context")
334
+
335
+ "#{rails_context_content}\n#{render_value}".html_safe
336
+ end
337
+
338
+ def internal_react_component(component_name, raw_options = {})
339
+ # Create the JavaScript and HTML to allow either client or server rendering of the
340
+ # react_component.
341
+ #
342
+ # Create the JavaScript setup of the global to initialize the client rendering
343
+ # (re-hydrate the data). This enables react rendered on the client to see that the
344
+ # server has already rendered the HTML.
345
+
346
+ options = ReactOnRails::ReactComponent::Options.new(name: component_name, options: raw_options)
347
+
348
+ # Setup the page_loaded_js, which is the same regardless of prerendering or not!
349
+ # The reason is that React is smart about not doing extra work if the server rendering did its job.
350
+ component_specification_tag = content_tag(:script,
351
+ json_safe_and_pretty(options.props).html_safe,
352
+ type: "application/json",
353
+ class: "js-react-on-rails-component",
354
+ "data-component-name" => options.name,
355
+ "data-trace" => (options.trace ? true : nil),
356
+ "data-dom-id" => options.dom_id)
357
+
358
+ # Create the HTML rendering part
359
+ result = server_rendered_react_component_html(options.props,
360
+ options.name,
361
+ options.dom_id,
362
+ prerender: options.prerender,
363
+ trace: options.trace,
364
+ raise_on_prerender_error: options.raise_on_prerender_error)
365
+
366
+ { "options" => options, "tag" => component_specification_tag, "result" => result }
367
+ end
368
+
369
+ def render_redux_store_data(redux_store_data)
370
+ result = content_tag(:script,
371
+ json_safe_and_pretty(redux_store_data[:props]).html_safe,
372
+ type: "application/json",
373
+ "data-js-react-on-rails-store" => redux_store_data[:store_name].html_safe)
374
+
375
+ prepend_render_rails_context(result)
376
+ end
377
+
378
+ def props_string(props)
379
+ props.is_a?(String) ? props : props.to_json
380
+ end
381
+
382
+ # Returns Array [0]: html, [1]: script to console log
383
+ # NOTE, these are NOT html_safe!
384
+ def server_rendered_react_component_html(
385
+ props, react_component_name, dom_id,
386
+ prerender: required("prerender"),
387
+ trace: required("trace"),
388
+ raise_on_prerender_error: required("raise_on_prerender_error")
389
+ )
390
+ return { "html" => "", "consoleReplayScript" => "" } unless prerender
391
+
392
+ # On server `location` option is added (`location = request.fullpath`)
393
+ # React Router needs this to match the current route
394
+
395
+ # Make sure that we use up-to-date bundle file used for server rendering, which is defined
396
+ # by config file value for config.server_bundle_js_file
397
+ ReactOnRails::ServerRenderingPool.reset_pool_if_server_bundle_was_modified
398
+
399
+ # Since this code is not inserted on a web page, we don't need to escape props
400
+ #
401
+ # However, as JSON (returned from `props_string(props)`) isn't JavaScript,
402
+ # but we want treat it as such, we need to compensate for the difference.
403
+ #
404
+ # \u2028 and \u2029 are valid characters in strings in JSON, but are treated
405
+ # as newline separators in JavaScript. As no newlines are allowed in
406
+ # strings in JavaScript, this causes an exception.
407
+ #
408
+ # We fix this by replacing these unicode characters with their escaped versions.
409
+ # This should be safe, as the only place they can appear is in strings anyway.
410
+ #
411
+ # Read more here: http://timelessrepo.com/json-isnt-a-javascript-subset
412
+
413
+ # rubocop:disable Layout/IndentHeredoc
414
+ wrapper_js = <<-JS
415
+ (function() {
416
+ var railsContext = #{rails_context(server_side: true).to_json};
417
+ #{initialize_redux_stores}
418
+ var props = #{props_string(props).gsub("\u2028", '\u2028').gsub("\u2029", '\u2029')};
419
+ return ReactOnRails.serverRenderReactComponent({
420
+ name: '#{react_component_name}',
421
+ domNodeId: '#{dom_id}',
422
+ props: props,
423
+ trace: #{trace},
424
+ railsContext: railsContext
425
+ });
426
+ })()
427
+ JS
428
+ # rubocop:enable Layout/IndentHeredoc
429
+
430
+ result = ReactOnRails::ServerRenderingPool.server_render_js_with_console_logging(wrapper_js)
431
+
432
+ if result["hasErrors"] && raise_on_prerender_error
433
+ # We caught this exception on our backtrace handler
434
+ raise ReactOnRails::PrerenderError, component_name: react_component_name,
435
+ # Sanitize as this might be browser logged
436
+ props: sanitized_props_string(props),
437
+ err: nil,
438
+ js_code: wrapper_js,
439
+ console_messages: result["consoleReplayScript"]
440
+
441
+ end
442
+ result
443
+ rescue ExecJS::ProgramError => err
444
+ # This error came from execJs
445
+ raise ReactOnRails::PrerenderError, component_name: react_component_name,
446
+ # Sanitize as this might be browser logged
447
+ props: sanitized_props_string(props),
448
+ err: err,
449
+ js_code: wrapper_js
450
+ end
451
+
452
+ def initialize_redux_stores
453
+ return "" unless @registered_stores.present? || @registered_stores_defer_render.present?
454
+ declarations = "var reduxProps, store, storeGenerator;\n".dup
455
+ all_stores = (@registered_stores || []) + (@registered_stores_defer_render || [])
456
+
457
+ result = <<-JS.dup
458
+ ReactOnRails.clearHydratedStores();
459
+ JS
460
+
461
+ result << all_stores.each_with_object(declarations) do |redux_store_data, memo|
462
+ store_name = redux_store_data[:store_name]
463
+ props = props_string(redux_store_data[:props])
464
+ memo << <<-JS.strip_heredoc
465
+ reduxProps = #{props};
466
+ storeGenerator = ReactOnRails.getStoreGenerator('#{store_name}');
467
+ store = storeGenerator(reduxProps, railsContext);
468
+ ReactOnRails.setStore('#{store_name}', store);
469
+ JS
470
+ end
471
+ result
472
+ end
473
+
474
+ # This is the definitive list of the default values used for the rails_context, which is the
475
+ # second parameter passed to both component and store generator functions.
476
+ # rubocop:disable Metrics/AbcSize
477
+ def rails_context(server_side: required("server_side"))
478
+ @rails_context ||= begin
479
+ result = {
480
+ inMailer: in_mailer?,
481
+ # Locale settings
482
+ i18nLocale: I18n.locale,
483
+ i18nDefaultLocale: I18n.default_locale
484
+ }
485
+ if defined?(request) && request.present?
486
+ # Check for encoding of the request's original_url and try to force-encoding the
487
+ # URLs as UTF-8. This situation can occur in browsers that do not encode the
488
+ # entire URL as UTF-8 already, mostly on the Windows platform (IE11 and lower).
489
+ original_url_normalized = request.original_url
490
+ if original_url_normalized.encoding.to_s == "ASCII-8BIT"
491
+ original_url_normalized = original_url_normalized.force_encoding("ISO-8859-1").encode("UTF-8")
492
+ end
493
+
494
+ # Using Addressable instead of standard URI to better deal with
495
+ # non-ASCII characters (see https://github.com/shakacode/react_on_rails/pull/405)
496
+ uri = Addressable::URI.parse(original_url_normalized)
497
+ # uri = Addressable::URI.parse("http://foo.com:3000/posts?id=30&limit=5#time=1305298413")
498
+
499
+ result.merge!(
500
+ # URL settings
501
+ href: uri.to_s,
502
+ location: "#{uri.path}#{uri.query.present? ? "?#{uri.query}" : ''}",
503
+ scheme: uri.scheme, # http
504
+ host: uri.host, # foo.com
505
+ port: uri.port,
506
+ pathname: uri.path, # /posts
507
+ search: uri.query, # id=30&limit=5
508
+ httpAcceptLanguage: request.env["HTTP_ACCEPT_LANGUAGE"]
509
+ )
510
+ end
511
+ if ReactOnRails.configuration.rendering_extension
512
+ custom_context = ReactOnRails.configuration.rendering_extension.custom_context(self)
513
+ result.merge!(custom_context) if custom_context
514
+ end
515
+ result
516
+ end
517
+
518
+ @rails_context.merge(serverSide: server_side)
519
+ end
520
+
521
+ # rubocop:enable Metrics/AbcSize
522
+
523
+ def replay_console_option(val)
524
+ val.nil? ? ReactOnRails.configuration.replay_console : val
525
+ end
526
+
527
+ def use_hot_reloading?
528
+ ENV["REACT_ON_RAILS_ENV"] == "HOT"
529
+ end
530
+
531
+ def send_tag_method(tag_method_name, args)
532
+ asset_type = use_hot_reloading? ? :hot : :static
533
+ assets = Array(args[asset_type])
534
+ options = args.delete_if { |key, _value| %i[hot static].include?(key) }
535
+ send(tag_method_name, *assets, options) unless assets.empty?
536
+ end
537
+
538
+ def in_mailer?
539
+ return false unless defined?(controller)
540
+ return false unless defined?(ActionMailer::Base)
541
+
542
+ controller.is_a?(ActionMailer::Base)
543
+ end
544
+ end
545
+ end
546
+ # rubocop:enable Metrics/ModuleLength