react_on_rails 11.0.0 → 12.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 (130) hide show
  1. checksums.yaml +5 -5
  2. data/.circleci/config.yml +320 -0
  3. data/.eslintignore +2 -1
  4. data/.eslintrc +30 -2
  5. data/.github/FUNDING.yml +1 -0
  6. data/.gitignore +3 -1
  7. data/.prettierignore +10 -0
  8. data/.prettierrc +23 -0
  9. data/.release-it.json +3 -0
  10. data/.rubocop.yml +39 -11
  11. data/.travis.yml +11 -22
  12. data/CHANGELOG.md +235 -6
  13. data/CONTRIBUTING.md +64 -84
  14. data/Gemfile +3 -5
  15. data/KUDOS.md +4 -1
  16. data/{docs/LICENSE.md → LICENSE.md} +1 -1
  17. data/PROJECTS.md +7 -1
  18. data/REACT-ON-RAILS-PRO-LICENSE +95 -0
  19. data/README.md +233 -630
  20. data/Rakefile +1 -5
  21. data/SUMMARY.md +42 -29
  22. data/book.json +5 -5
  23. data/docs/additional-reading/asset-pipeline.md +8 -16
  24. data/docs/additional-reading/convert-rails-5-api-only-app.md +19 -0
  25. data/docs/additional-reading/credits.md +10 -0
  26. data/docs/additional-reading/images.md +1 -1
  27. data/docs/additional-reading/rails-engine-integration.md +7 -0
  28. data/docs/additional-reading/rails_view_rendering_from_inline_javascript.md +2 -1
  29. data/docs/additional-reading/react-helmet.md +30 -10
  30. data/docs/additional-reading/react-router.md +52 -75
  31. data/docs/additional-reading/server-rendering-tips.md +14 -11
  32. data/docs/additional-reading/upgrade-webpacker-v3-to-v4.md +10 -0
  33. data/docs/additional-reading/webpack.md +2 -2
  34. data/docs/api/javascript-api.md +24 -4
  35. data/docs/api/redux-store-api.md +102 -0
  36. data/docs/api/view-helpers-api.md +133 -0
  37. data/docs/articles.md +20 -0
  38. data/docs/basics/client-vs-server-rendering.md +23 -0
  39. data/docs/basics/configuration.md +145 -61
  40. data/docs/basics/deployment.md +4 -0
  41. data/docs/basics/{generator.md → generator-details.md} +5 -8
  42. data/docs/basics/heroku-deployment.md +24 -0
  43. data/docs/basics/hmr-and-hot-reloading-with-the-webpack-dev-server.md +49 -0
  44. data/docs/basics/i18n.md +45 -23
  45. data/docs/basics/installation-into-an-existing-rails-app.md +59 -0
  46. data/docs/basics/minitest-configuration.md +31 -0
  47. data/docs/basics/react-server-rendering.md +29 -0
  48. data/docs/{additional-reading → basics}/recommended-project-structure.md +38 -10
  49. data/docs/basics/render-functions-and-railscontext.md +205 -0
  50. data/docs/basics/rspec-configuration.md +73 -0
  51. data/docs/basics/upgrading-react-on-rails.md +75 -4
  52. data/docs/basics/webpack-configuration.md +50 -0
  53. data/docs/contributor-info/errors-with-hooks.md +45 -0
  54. data/docs/contributor-info/pull-requests.md +44 -0
  55. data/docs/misc/doctrine.md +2 -2
  56. data/docs/{additional-reading → outdated}/code-splitting.md +19 -9
  57. data/docs/outdated/how-react-on-rails-works.md +44 -0
  58. data/docs/{basics/installation-overview.md → outdated/manual-installation-overview.md} +8 -13
  59. data/docs/{additional-reading → outdated}/rails-assets-relative-paths.md +3 -3
  60. data/docs/{additional-reading → outdated}/rails-assets.md +5 -13
  61. data/docs/testimonials/hvmn.md +25 -0
  62. data/docs/testimonials/resortpass.md +13 -0
  63. data/docs/testimonials/testimonials.md +28 -0
  64. data/docs/tutorial.md +182 -44
  65. data/jest.config.js +4 -0
  66. data/lib/generators/USAGE +1 -1
  67. data/lib/generators/react_on_rails/base_generator.rb +2 -2
  68. data/lib/generators/react_on_rails/dev_tests_generator.rb +3 -2
  69. data/lib/generators/react_on_rails/generator_helper.rb +4 -6
  70. data/lib/generators/react_on_rails/install_generator.rb +2 -0
  71. data/lib/generators/react_on_rails/templates/base/base/Procfile.dev +3 -1
  72. data/lib/generators/react_on_rails/templates/base/base/Procfile.dev-hmr +26 -0
  73. data/lib/generators/react_on_rails/templates/base/base/app/javascript/bundles/HelloWorld/components/HelloWorld.jsx +20 -40
  74. data/lib/generators/react_on_rails/templates/base/base/config/initializers/react_on_rails.rb +4 -1
  75. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/components/HelloWorld.jsx +4 -8
  76. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/store/helloWorldStore.js +1 -3
  77. data/lib/react_on_rails/configuration.rb +175 -102
  78. data/lib/react_on_rails/error.rb +2 -0
  79. data/lib/react_on_rails/git_utils.rb +2 -0
  80. data/lib/react_on_rails/{react_on_rails_helper.rb → helper.rb} +201 -225
  81. data/lib/react_on_rails/json_output.rb +1 -1
  82. data/lib/react_on_rails/json_parse_error.rb +28 -0
  83. data/lib/react_on_rails/locales/base.rb +150 -0
  84. data/lib/react_on_rails/locales/to_js.rb +37 -0
  85. data/lib/react_on_rails/locales/to_json.rb +27 -0
  86. data/lib/react_on_rails/prerender_error.rb +56 -18
  87. data/lib/react_on_rails/react_component/render_options.rb +98 -0
  88. data/lib/react_on_rails/server_rendering_js_code.rb +42 -0
  89. data/lib/react_on_rails/server_rendering_pool/ruby_embedded_java_script.rb +139 -106
  90. data/lib/react_on_rails/server_rendering_pool.rb +4 -16
  91. data/lib/react_on_rails/test_helper/ensure_assets_compiled.rb +7 -8
  92. data/lib/react_on_rails/test_helper/webpack_assets_compiler.rb +17 -0
  93. data/lib/react_on_rails/test_helper/webpack_assets_status_checker.rb +10 -6
  94. data/lib/react_on_rails/test_helper.rb +18 -7
  95. data/lib/react_on_rails/utils.rb +66 -26
  96. data/lib/react_on_rails/version.rb +1 -1
  97. data/lib/react_on_rails/version_checker.rb +5 -1
  98. data/lib/react_on_rails/version_syntax_converter.rb +14 -12
  99. data/lib/react_on_rails/webpacker_utils.rb +44 -10
  100. data/lib/react_on_rails.rb +8 -3
  101. data/lib/tasks/assets.rake +33 -46
  102. data/lib/tasks/locale.rake +4 -2
  103. data/package-scripts.yml +49 -0
  104. data/package.json +49 -40
  105. data/rakelib/dummy_apps.rake +1 -9
  106. data/rakelib/example_type.rb +3 -1
  107. data/rakelib/examples.rake +3 -0
  108. data/rakelib/lint.rake +2 -7
  109. data/rakelib/node_package.rake +2 -2
  110. data/rakelib/release.rake +4 -8
  111. data/rakelib/run_rspec.rake +5 -18
  112. data/react_on_rails.gemspec +5 -6
  113. data/tsconfig.json +14 -0
  114. data/webpackConfigLoader.js +5 -4
  115. data/yarn.lock +7645 -1821
  116. metadata +71 -61
  117. data/Gemfile.rails32 +0 -74
  118. data/docs/additional-reading/babel.md +0 -5
  119. data/docs/additional-reading/caching-and-performance.md +0 -4
  120. data/docs/additional-reading/heroku-deployment.md +0 -92
  121. data/docs/additional-reading/hot-reloading-rails-development.md +0 -57
  122. data/docs/additional-reading/node-server-rendering.md +0 -5
  123. data/docs/additional-reading/rspec-configuration.md +0 -56
  124. data/docs/api/ruby-api-hot-reload-view-helpers.md +0 -44
  125. data/docs/api/ruby-api.md +0 -8
  126. data/lib/generators/react_on_rails/templates/base/base/Procfile.dev-server +0 -12
  127. data/lib/react_on_rails/assets_precompile.rb +0 -150
  128. data/lib/react_on_rails/locales_to_js.rb +0 -134
  129. data/lib/react_on_rails/react_component/options.rb +0 -64
  130. /data/docs/{misc → outdated}/rails3.md +0 -0
@@ -15,68 +15,20 @@ module ReactOnRails
15
15
  module Helper
16
16
  include ReactOnRails::Utils::Required
17
17
 
18
- COMPONENT_HTML_KEY = "componentHtml".freeze
18
+ COMPONENT_HTML_KEY = "componentHtml"
19
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.
20
+ # react_component_name: can be a React function or class component or a "render function".
21
+ # "render functions" differ from a React function in that they take two parameters, the
22
+ # props and the railsContext, like this:
23
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.
24
+ # let MyReactComponentApp = (props, railsContext) => <MyReactComponent {...props}/>;
26
25
  #
27
- # static vs. hot is picked based on whether
28
- # ENV["REACT_ON_RAILS_ENV"] == "HOT"
26
+ # Alternately, you can define the render function with an additional property
27
+ # `.renderFunction = true`:
29
28
  #
30
- # <%= env_stylesheet_link_tag(static: 'application_static',
31
- # hot: 'application_non_webpack',
32
- # media: 'all',
33
- # 'data-turbolinks-track' => "reload") %>
29
+ # let MyReactComponentApp = (props) => <MyReactComponent {...props}/>;
30
+ # MyReactComponent.renderFunction = true;
34
31
  #
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
32
  # Exposing the react_component_name is necessary to both a plain ReactComponent as well as
81
33
  # a generator:
82
34
  # See README.md for how to "register" your react components.
@@ -98,67 +50,96 @@ module ReactOnRails
98
50
  # raise_on_prerender_error: <true/false> Default to false. True will raise exception on server
99
51
  # if the JS code throws
100
52
  # 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"]
53
+ # random_dom_id can be set to override the default from the config/initializers. That's only
54
+ # used if you have multiple instance of the same component on the Rails view.
55
+ def react_component(component_name, options = {})
56
+ internal_result = internal_react_component(component_name, options)
57
+ server_rendered_html = internal_result[:result]["html"]
58
+ console_script = internal_result[:result]["consoleReplayScript"]
105
59
 
106
60
  if server_rendered_html.is_a?(String)
107
61
  build_react_component_result_for_server_rendered_string(
108
62
  server_rendered_html: server_rendered_html,
109
- component_specification_tag: internal_result["tag"],
63
+ component_specification_tag: internal_result[:tag],
110
64
  console_script: console_script,
111
- options: internal_result["options"]
65
+ render_options: internal_result[:render_options]
112
66
  )
113
67
  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."
68
+ msg = <<~MSG
69
+ Use react_component_hash (not react_component) to return a Hash to your ruby view code. See
70
+ https://github.com/shakacode/react_on_rails/blob/master/spec/dummy/client/app/startup/ReactHelmetServerApp.jsx
71
+ for an example of the necessary javascript configuration.
118
72
  MSG
119
- raise ReactOnRailsPro::Error, msg
120
-
73
+ raise ReactOnRails::Error, msg
121
74
  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."
75
+ class_name = server_rendered_html.class.name
76
+ msg = <<~MSG
77
+ ReactOnRails: server_rendered_html is expected to be a String or Hash for #{component_name}.
78
+ Type is #{class_name}
79
+ Value:
80
+ #{server_rendered_html}
81
+
82
+ If you're trying to use a render function to return a Hash to your ruby view code, then use
83
+ react_component_hash instead of react_component and see
84
+ https://github.com/shakacode/react_on_rails/blob/master/spec/dummy/client/app/startup/ReactHelmetServerApp.jsx
85
+ for an example of the JavaScript code.
128
86
  MSG
129
- raise ReactOnRailsPro::Error, msg
87
+ raise ReactOnRails::Error, msg
130
88
  end
131
89
  end
132
90
 
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"] }
91
+ # react_component_hash is used to return multiple HTML strings for server rendering, such as for
92
+ # adding meta-tags to a page.
93
+ # It is exactly like react_component except for the following:
94
+ # 1. prerender: true is automatically added, as this method doesn't make sense for client only
95
+ # rendering.
96
+ # 2. Your JavaScript render function for server rendering must return an Object rather than a React component.
97
+ # 3. Your view code must expect an object and not a string.
98
+ #
99
+ # Here is an example of the view code:
100
+ # <% react_helmet_app = react_component_hash("ReactHelmetApp", prerender: true,
101
+ # props: { helloWorldData: { name: "Mr. Server Side Rendering"}},
102
+ # id: "react-helmet-0", trace: true) %>
103
+ # <% content_for :title do %>
104
+ # <%= react_helmet_app['title'] %>
105
+ # <% end %>
106
+ # <%= react_helmet_app["componentHtml"] %>
107
+ #
108
+ def react_component_hash(component_name, options = {})
109
+ options[:prerender] = true
110
+ internal_result = internal_react_component(component_name, options)
111
+ server_rendered_html = internal_result[:result]["html"]
112
+ console_script = internal_result[:result]["consoleReplayScript"]
113
+
114
+ if server_rendered_html.is_a?(String) && internal_result[:result]["hasErrors"]
115
+ server_rendered_html = { COMPONENT_HTML_KEY => internal_result[:result]["html"] }
140
116
  end
141
117
 
142
118
  if server_rendered_html.is_a?(Hash)
143
119
  build_react_component_result_for_server_rendered_hash(
144
120
  server_rendered_html: server_rendered_html,
145
- component_specification_tag: internal_result["tag"],
121
+ component_specification_tag: internal_result[:tag],
146
122
  console_script: console_script,
147
- options: internal_result["options"]
123
+ render_options: internal_result[:render_options]
148
124
  )
149
125
  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.
126
+ msg = <<~MSG
127
+ render function used by react_component_hash for #{component_name} is expected to return
128
+ an Object. See https://github.com/shakacode/react_on_rails/blob/master/spec/dummy/client/app/startup/ReactHelmetServerApp.jsx
129
+ for an example of the JavaScript code.
130
+ Note, your render function must either take 2 params or have the property
131
+ `.renderFunction = true` added to it to distinguish it from a React Function Component.
154
132
  MSG
155
- raise ReactOnRailsPro::Error, msg
133
+ raise ReactOnRails::Error, msg
156
134
  end
157
135
  end
158
136
 
159
137
  # Separate initialization of store from react_component allows multiple react_component calls to
160
138
  # use the same Redux store.
161
139
  #
140
+ # NOTE: This technique not recommended as it prevents dynamic code splitting for performance.
141
+ # Instead, you should use the standard react_component view helper.
142
+ #
162
143
  # store_name: name of the store, corresponding to your call to ReactOnRails.registerStores in your
163
144
  # JavaScript code.
164
145
  # props: Ruby Hash or JSON string which contains the properties to pass to the redux store.
@@ -187,6 +168,7 @@ module ReactOnRails
187
168
  # that contains a data props.
188
169
  def redux_store_hydration_data
189
170
  return if @registered_stores_defer_render.blank?
171
+
190
172
  @registered_stores_defer_render.reduce("".dup) do |accum, redux_store_data|
191
173
  accum << render_redux_store_data(redux_store_data)
192
174
  end.html_safe
@@ -199,8 +181,12 @@ module ReactOnRails
199
181
  # Helper method to take javascript expression and returns the output from evaluating it.
200
182
  # If you have more than one line that needs to be executed, wrap it in an IIFE.
201
183
  # JS exceptions are caught and console messages are handled properly.
184
+ # Options include:{ prerender:, trace:, raise_on_prerender_error: }
202
185
  def server_render_js(js_expression, options = {})
203
- wrapper_js = <<-JS.strip_heredoc
186
+ render_options = ReactOnRails::ReactComponent::RenderOptions
187
+ .new(react_component_name: "generic-js", options: options)
188
+
189
+ js_code = <<-JS.strip_heredoc
204
190
  (function() {
205
191
  var htmlResult = '';
206
192
  var consoleReplayScript = '';
@@ -228,20 +214,22 @@ module ReactOnRails
228
214
  })()
229
215
  JS
230
216
 
231
- result = ReactOnRails::ServerRenderingPool.server_render_js_with_console_logging(wrapper_js)
217
+ result = ReactOnRails::ServerRenderingPool
218
+ .server_render_js_with_console_logging(js_code, render_options)
232
219
 
233
220
  html = result["html"]
234
221
  console_log_script = result["consoleLogScript"]
235
- raw("#{html}#{replay_console_option(options[:replay_console_option]) ? console_log_script : ''}")
222
+ raw("#{html}#{render_options.replay_console ? console_log_script : ''}")
236
223
  rescue ExecJS::ProgramError => err
237
224
  raise ReactOnRails::PrerenderError, component_name: "N/A (server_render_js called)",
238
225
  err: err,
239
- js_code: wrapper_js
226
+ js_code: js_code
240
227
  end
241
228
 
242
229
  def json_safe_and_pretty(hash_or_string)
243
230
  return "{}" if hash_or_string.nil?
244
- unless hash_or_string.class.in?([Hash, String])
231
+
232
+ unless hash_or_string.is_a?(String) || hash_or_string.is_a?(Hash)
245
233
  raise ReactOnRails::Error, "#{__method__} only accepts String or Hash as argument "\
246
234
  "(#{hash_or_string.class} given)."
247
235
  end
@@ -251,22 +239,82 @@ module ReactOnRails
251
239
  ReactOnRails::JsonOutput.escape(json_value)
252
240
  end
253
241
 
242
+ # This is the definitive list of the default values used for the rails_context, which is the
243
+ # second parameter passed to both component and store render functions.
244
+ # This method can be called from views and from the controller, as `helpers.rails_context`
245
+ #
246
+ # rubocop:disable Metrics/AbcSize
247
+ def rails_context(server_side: true)
248
+ # ALERT: Keep in sync with node_package/src/types/index.ts for the properties of RailsContext
249
+ @rails_context ||= begin
250
+ result = {
251
+ railsEnv: Rails.env,
252
+ inMailer: in_mailer?,
253
+ # Locale settings
254
+ i18nLocale: I18n.locale,
255
+ i18nDefaultLocale: I18n.default_locale,
256
+ rorVersion: ReactOnRails::VERSION,
257
+ rorPro: ReactOnRails::Utils.react_on_rails_pro?
258
+ }
259
+ if defined?(request) && request.present?
260
+ # Check for encoding of the request's original_url and try to force-encoding the
261
+ # URLs as UTF-8. This situation can occur in browsers that do not encode the
262
+ # entire URL as UTF-8 already, mostly on the Windows platform (IE11 and lower).
263
+ original_url_normalized = request.original_url
264
+ if original_url_normalized.encoding.to_s == "ASCII-8BIT"
265
+ original_url_normalized = original_url_normalized.force_encoding("ISO-8859-1").encode("UTF-8")
266
+ end
267
+
268
+ # Using Addressable instead of standard URI to better deal with
269
+ # non-ASCII characters (see https://github.com/shakacode/react_on_rails/pull/405)
270
+ uri = Addressable::URI.parse(original_url_normalized)
271
+ # uri = Addressable::URI.parse("http://foo.com:3000/posts?id=30&limit=5#time=1305298413")
272
+
273
+ result.merge!(
274
+ # URL settings
275
+ href: uri.to_s,
276
+ location: "#{uri.path}#{uri.query.present? ? "?#{uri.query}" : ''}",
277
+ scheme: uri.scheme, # http
278
+ host: uri.host, # foo.com
279
+ port: uri.port,
280
+ pathname: uri.path, # /posts
281
+ search: uri.query, # id=30&limit=5
282
+ httpAcceptLanguage: request.env["HTTP_ACCEPT_LANGUAGE"]
283
+ )
284
+ end
285
+ if ReactOnRails.configuration.rendering_extension
286
+ custom_context = ReactOnRails.configuration.rendering_extension.custom_context(self)
287
+ result.merge!(custom_context) if custom_context
288
+ end
289
+ result
290
+ end
291
+
292
+ @rails_context.merge(serverSide: server_side)
293
+ end
294
+ # rubocop:enable Metrics/AbcSize
295
+
254
296
  private
255
297
 
256
298
  def build_react_component_result_for_server_rendered_string(
257
299
  server_rendered_html: required("server_rendered_html"),
258
300
  component_specification_tag: required("component_specification_tag"),
259
301
  console_script: required("console_script"),
260
- options: required("options")
302
+ render_options: required("render_options")
261
303
  )
262
- content_tag_options = options.html_options
263
- content_tag_options[:id] = options.dom_id
304
+ content_tag_options = render_options.html_options
305
+ if content_tag_options.key?(:tag)
306
+ content_tag_options_html_tag = content_tag_options[:tag]
307
+ content_tag_options.delete(:tag)
308
+ else
309
+ content_tag_options_html_tag = "div"
310
+ end
311
+ content_tag_options[:id] = render_options.dom_id
264
312
 
265
- rendered_output = content_tag(:div,
313
+ rendered_output = content_tag(content_tag_options_html_tag.to_sym,
266
314
  server_rendered_html.html_safe,
267
315
  content_tag_options)
268
316
 
269
- result_console_script = options.replay_console ? console_script : ""
317
+ result_console_script = render_options.replay_console ? console_script : ""
270
318
  result = compose_react_component_html_with_spec_and_console(
271
319
  component_specification_tag, rendered_output, result_console_script
272
320
  )
@@ -278,20 +326,20 @@ module ReactOnRails
278
326
  server_rendered_html: required("server_rendered_html"),
279
327
  component_specification_tag: required("component_specification_tag"),
280
328
  console_script: required("console_script"),
281
- options: required("options")
329
+ render_options: required("render_options")
282
330
  )
283
- content_tag_options = options.html_options
284
- content_tag_options[:id] = options.dom_id
331
+ content_tag_options = render_options.html_options
332
+ content_tag_options[:id] = render_options.dom_id
285
333
 
286
334
  unless server_rendered_html[COMPONENT_HTML_KEY]
287
- raise ReactOnRailsPro::Error, "server_rendered_html hash expected to contain \"#{COMPONENT_HTML_KEY}\" key."
335
+ raise ReactOnRails::Error, "server_rendered_html hash expected to contain \"#{COMPONENT_HTML_KEY}\" key."
288
336
  end
289
337
 
290
338
  rendered_output = content_tag(:div,
291
339
  server_rendered_html[COMPONENT_HTML_KEY].html_safe,
292
340
  content_tag_options)
293
341
 
294
- result_console_script = options.replay_console ? console_script : ""
342
+ result_console_script = render_options.replay_console ? console_script : ""
295
343
  result = compose_react_component_html_with_spec_and_console(
296
344
  component_specification_tag, rendered_output, result_console_script
297
345
  )
@@ -310,13 +358,11 @@ module ReactOnRails
310
358
 
311
359
  def compose_react_component_html_with_spec_and_console(component_specification_tag, rendered_output, console_script)
312
360
  # 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}
361
+ <<~HTML.html_safe
362
+ #{rendered_output}
363
+ #{component_specification_tag}
364
+ #{console_script}
318
365
  HTML
319
- # rubocop:enable Layout/IndentHeredoc
320
366
  end
321
367
 
322
368
  # prepend the rails_context if not yet applied
@@ -335,7 +381,7 @@ module ReactOnRails
335
381
  "#{rails_context_content}\n#{render_value}".html_safe
336
382
  end
337
383
 
338
- def internal_react_component(component_name, raw_options = {})
384
+ def internal_react_component(react_component_name, options = {})
339
385
  # Create the JavaScript and HTML to allow either client or server rendering of the
340
386
  # react_component.
341
387
  #
@@ -343,27 +389,27 @@ module ReactOnRails
343
389
  # (re-hydrate the data). This enables react rendered on the client to see that the
344
390
  # server has already rendered the HTML.
345
391
 
346
- options = ReactOnRails::ReactComponent::Options.new(name: component_name, options: raw_options)
392
+ render_options = ReactOnRails::ReactComponent::RenderOptions.new(react_component_name: react_component_name,
393
+ options: options)
347
394
 
348
395
  # Setup the page_loaded_js, which is the same regardless of prerendering or not!
349
396
  # The reason is that React is smart about not doing extra work if the server rendering did its job.
350
397
  component_specification_tag = content_tag(:script,
351
- json_safe_and_pretty(options.props).html_safe,
398
+ json_safe_and_pretty(render_options.props).html_safe,
352
399
  type: "application/json",
353
400
  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)
401
+ "data-component-name" => render_options.react_component_name,
402
+ "data-trace" => (render_options.trace ? true : nil),
403
+ "data-dom-id" => render_options.dom_id)
357
404
 
358
405
  # 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 }
406
+ result = server_rendered_react_component(render_options)
407
+
408
+ {
409
+ render_options: render_options,
410
+ tag: component_specification_tag,
411
+ result: result
412
+ }
367
413
  end
368
414
 
369
415
  def render_redux_store_data(redux_store_data)
@@ -379,15 +425,12 @@ module ReactOnRails
379
425
  props.is_a?(String) ? props : props.to_json
380
426
  end
381
427
 
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
428
+ # Returns object with values that are NOT html_safe!
429
+ def server_rendered_react_component(render_options)
430
+ return { "html" => "", "consoleReplayScript" => "" } unless render_options.prerender
431
+
432
+ react_component_name = render_options.react_component_name
433
+ props = render_options.props
391
434
 
392
435
  # On server `location` option is added (`location = request.fullpath`)
393
436
  # React Router needs this to match the current route
@@ -410,54 +453,48 @@ module ReactOnRails
410
453
  #
411
454
  # Read more here: http://timelessrepo.com/json-isnt-a-javascript-subset
412
455
 
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
456
+ js_code = ReactOnRails::ServerRenderingJsCode.server_rendering_component_js_code(
457
+ props_string: props_string(props).gsub("\u2028", '\u2028').gsub("\u2029", '\u2029'),
458
+ rails_context: rails_context(server_side: true).to_json,
459
+ redux_stores: initialize_redux_stores,
460
+ react_component_name: react_component_name,
461
+ render_options: render_options
462
+ )
429
463
 
430
- result = ReactOnRails::ServerRenderingPool.server_render_js_with_console_logging(wrapper_js)
464
+ begin
465
+ result = ReactOnRails::ServerRenderingPool.server_render_js_with_console_logging(js_code, render_options)
466
+ rescue StandardError => err
467
+ # This error came from the renderer
468
+ raise ReactOnRails::PrerenderError, component_name: react_component_name,
469
+ # Sanitize as this might be browser logged
470
+ props: sanitized_props_string(props),
471
+ err: err,
472
+ js_code: js_code
473
+ end
431
474
 
432
- if result["hasErrors"] && raise_on_prerender_error
475
+ if result["hasErrors"] && render_options.raise_on_prerender_error
433
476
  # We caught this exception on our backtrace handler
434
477
  raise ReactOnRails::PrerenderError, component_name: react_component_name,
435
478
  # Sanitize as this might be browser logged
436
479
  props: sanitized_props_string(props),
437
480
  err: nil,
438
- js_code: wrapper_js,
481
+ js_code: js_code,
439
482
  console_messages: result["consoleReplayScript"]
440
483
 
441
484
  end
442
485
  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
486
  end
451
487
 
452
488
  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
489
  result = <<-JS.dup
458
490
  ReactOnRails.clearHydratedStores();
459
491
  JS
460
492
 
493
+ return result unless @registered_stores.present? || @registered_stores_defer_render.present?
494
+
495
+ declarations = "var reduxProps, store, storeGenerator;\n".dup
496
+ all_stores = (@registered_stores || []) + (@registered_stores_defer_render || [])
497
+
461
498
  result << all_stores.each_with_object(declarations) do |redux_store_data, memo|
462
499
  store_name = redux_store_data[:store_name]
463
500
  props = props_string(redux_store_data[:props])
@@ -471,71 +508,10 @@ module ReactOnRails
471
508
  result
472
509
  end
473
510
 
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
- railsEnv: Rails.env,
481
- inMailer: in_mailer?,
482
- # Locale settings
483
- i18nLocale: I18n.locale,
484
- i18nDefaultLocale: I18n.default_locale
485
- }
486
- if defined?(request) && request.present?
487
- # Check for encoding of the request's original_url and try to force-encoding the
488
- # URLs as UTF-8. This situation can occur in browsers that do not encode the
489
- # entire URL as UTF-8 already, mostly on the Windows platform (IE11 and lower).
490
- original_url_normalized = request.original_url
491
- if original_url_normalized.encoding.to_s == "ASCII-8BIT"
492
- original_url_normalized = original_url_normalized.force_encoding("ISO-8859-1").encode("UTF-8")
493
- end
494
-
495
- # Using Addressable instead of standard URI to better deal with
496
- # non-ASCII characters (see https://github.com/shakacode/react_on_rails/pull/405)
497
- uri = Addressable::URI.parse(original_url_normalized)
498
- # uri = Addressable::URI.parse("http://foo.com:3000/posts?id=30&limit=5#time=1305298413")
499
-
500
- result.merge!(
501
- # URL settings
502
- href: uri.to_s,
503
- location: "#{uri.path}#{uri.query.present? ? "?#{uri.query}" : ''}",
504
- scheme: uri.scheme, # http
505
- host: uri.host, # foo.com
506
- port: uri.port,
507
- pathname: uri.path, # /posts
508
- search: uri.query, # id=30&limit=5
509
- httpAcceptLanguage: request.env["HTTP_ACCEPT_LANGUAGE"]
510
- )
511
- end
512
- if ReactOnRails.configuration.rendering_extension
513
- custom_context = ReactOnRails.configuration.rendering_extension.custom_context(self)
514
- result.merge!(custom_context) if custom_context
515
- end
516
- result
517
- end
518
-
519
- @rails_context.merge(serverSide: server_side)
520
- end
521
-
522
- # rubocop:enable Metrics/AbcSize
523
-
524
511
  def replay_console_option(val)
525
512
  val.nil? ? ReactOnRails.configuration.replay_console : val
526
513
  end
527
514
 
528
- def use_hot_reloading?
529
- ENV["REACT_ON_RAILS_ENV"] == "HOT"
530
- end
531
-
532
- def send_tag_method(tag_method_name, args)
533
- asset_type = use_hot_reloading? ? :hot : :static
534
- assets = Array(args[asset_type])
535
- options = args.delete_if { |key, _value| %i[hot static].include?(key) }
536
- send(tag_method_name, *assets, options) unless assets.empty?
537
- end
538
-
539
515
  def in_mailer?
540
516
  return false unless defined?(controller)
541
517
  return false unless defined?(ActionMailer::Base)
@@ -11,7 +11,7 @@ module ReactOnRails
11
11
  "\u2028" => '\u2028',
12
12
  "\u2029" => '\u2029'
13
13
  }.freeze
14
- ESCAPE_REGEXP = /[\u2028\u2029&><]/u
14
+ ESCAPE_REGEXP = /[\u2028\u2029&><]/u.freeze
15
15
 
16
16
  def self.escape(json)
17
17
  return escape_without_erb_util(json) if Utils.rails_version_less_than_4_1_1
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ReactOnRails
4
+ class JsonParseError < ::ReactOnRails::Error
5
+ attr_reader :json
6
+
7
+ def initialize(parse_error, json)
8
+ @json = json
9
+ @original_error = parse_error
10
+ super(parse_error.message)
11
+ end
12
+
13
+ def to_honeybadger_context
14
+ to_error_context
15
+ end
16
+
17
+ def raven_context
18
+ to_error_context
19
+ end
20
+
21
+ def to_error_context
22
+ {
23
+ original_error: @original_error,
24
+ json: @json
25
+ }
26
+ end
27
+ end
28
+ end