react_on_rails 10.0.0 → 11.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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/.eslintrc +4 -0
  3. data/.rubocop.yml +2 -2
  4. data/.travis.yml +3 -0
  5. data/CHANGELOG.md +81 -88
  6. data/CONTRIBUTING.md +0 -7
  7. data/Dockerfile_tests +12 -0
  8. data/Gemfile +8 -13
  9. data/NEWS.md +2 -0
  10. data/PROJECTS.md +10 -2
  11. data/README.md +137 -121
  12. data/app/helpers/react_on_rails_helper.rb +1 -533
  13. data/docker-compose.yml +11 -0
  14. data/docs/additional-reading/caching-and-performance.md +4 -0
  15. data/docs/additional-reading/capistrano-deployment.md +18 -0
  16. data/docs/additional-reading/elastic-beanstalk.md +53 -33
  17. data/docs/additional-reading/server-rendering-tips.md +4 -4
  18. data/docs/additional-reading/troubleshooting-when-using-webpacker.md +90 -0
  19. data/docs/api/ruby-api.md +7 -2
  20. data/docs/basics/configuration.md +23 -9
  21. data/docs/basics/i18n.md +4 -0
  22. data/docs/basics/upgrading-react-on-rails.md +181 -0
  23. data/docs/tutorial.md +7 -11
  24. data/lib/generators/react_on_rails/base_generator.rb +6 -3
  25. data/lib/generators/react_on_rails/dev_tests_generator.rb +1 -1
  26. data/lib/generators/react_on_rails/install_generator.rb +1 -1
  27. data/lib/generators/react_on_rails/react_no_redux_generator.rb +1 -1
  28. data/lib/generators/react_on_rails/react_with_redux_generator.rb +2 -2
  29. data/lib/generators/react_on_rails/templates/base/base/app/javascript/bundles/HelloWorld/components/HelloWorld.jsx +1 -1
  30. data/lib/generators/react_on_rails/templates/base/base/config/initializers/react_on_rails.rb +3 -2
  31. data/lib/generators/react_on_rails/templates/dev_tests/spec/simplecov_helper.rb +2 -2
  32. data/lib/react_on_rails/assets_precompile.rb +8 -4
  33. data/lib/react_on_rails/configuration.rb +34 -20
  34. data/lib/react_on_rails/error.rb +4 -0
  35. data/lib/react_on_rails/locales_to_js.rb +2 -2
  36. data/lib/react_on_rails/prerender_error.rb +2 -2
  37. data/lib/react_on_rails/react_on_rails_helper.rb +547 -0
  38. data/lib/react_on_rails/server_rendering_pool/{exec.rb → ruby_embedded_java_script.rb} +37 -40
  39. data/lib/react_on_rails/server_rendering_pool.rb +21 -10
  40. data/lib/react_on_rails/test_helper/webpack_assets_status_checker.rb +2 -2
  41. data/lib/react_on_rails/test_helper.rb +1 -1
  42. data/lib/react_on_rails/utils.rb +14 -52
  43. data/lib/react_on_rails/version.rb +1 -1
  44. data/lib/react_on_rails/version_checker.rb +33 -12
  45. data/lib/react_on_rails/webpacker_utils.rb +42 -0
  46. data/lib/react_on_rails.rb +3 -2
  47. data/package.json +2 -5
  48. data/rakelib/dummy_apps.rake +2 -1
  49. data/rakelib/example_type.rb +1 -1
  50. data/rakelib/examples.rake +3 -2
  51. data/rakelib/lint.rake +3 -2
  52. data/rakelib/node_package.rake +2 -1
  53. data/rakelib/release.rake +4 -5
  54. data/rakelib/run_rspec.rake +3 -4
  55. data/rakelib/task_helpers.rb +1 -1
  56. data/react_on_rails.gemspec +23 -13
  57. data/yarn.lock +3 -39
  58. metadata +75 -49
  59. data/lib/react_on_rails/server_rendering_pool/node.rb +0 -83
  60. data/lib/react_on_rails/test_helper/node_process_launcher.rb +0 -14
@@ -0,0 +1,547 @@
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
+ 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
+ def replay_console_option(val)
525
+ val.nil? ? ReactOnRails.configuration.replay_console : val
526
+ end
527
+
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
+ def in_mailer?
540
+ return false unless defined?(controller)
541
+ return false unless defined?(ActionMailer::Base)
542
+
543
+ controller.is_a?(ActionMailer::Base)
544
+ end
545
+ end
546
+ end
547
+ # rubocop:enable Metrics/ModuleLength
@@ -4,8 +4,7 @@ require "open-uri"
4
4
 
5
5
  module ReactOnRails
6
6
  module ServerRenderingPool
7
- # This implementation of the rendering pool uses ExecJS to execute javasript code
8
- class Exec
7
+ class RubyEmbeddedJavaScript
9
8
  def self.reset_pool
10
9
  options = {
11
10
  size: ReactOnRails.configuration.server_renderer_pool_size,
@@ -17,18 +16,11 @@ module ReactOnRails
17
16
  def self.reset_pool_if_server_bundle_was_modified
18
17
  return unless ReactOnRails.configuration.development_mode
19
18
 
20
- server_bundle_js_file_path = ReactOnRails::Utils.server_bundle_js_file_path
21
-
22
- if Utils.using_webpacker? && Webpacker.dev_server.running?
23
- return if @last_loaded_server_bundle == server_bundle_js_file_path
24
- @last_loaded_server_bundle = server_bundle_js_file_path
25
- else
26
- # we're not hashing the server name and we can use the mtime
27
- file_mtime = File.mtime(server_bundle_js_file_path)
28
- @server_bundle_timestamp ||= file_mtime
29
- return if @server_bundle_timestamp == file_mtime
30
- @server_bundle_timestamp = file_mtime
31
- end
19
+ file_mtime = File.mtime(ReactOnRails::Utils.server_bundle_js_file_path)
20
+ @server_bundle_timestamp ||= file_mtime
21
+ return if @server_bundle_timestamp == file_mtime
22
+
23
+ @server_bundle_timestamp = file_mtime
32
24
 
33
25
  ReactOnRails::ServerRenderingPool.reset_pool
34
26
  end
@@ -42,9 +34,10 @@ module ReactOnRails
42
34
  # js_code MUST RETURN json stringify Object
43
35
  # Calling code will probably call 'html_safe' on return value before rendering to the view.
44
36
  def self.server_render_js_with_console_logging(js_code)
45
- if trace_react_on_rails?
37
+ if ReactOnRails.configuration.trace
46
38
  @file_index ||= 1
47
- trace_messsage(js_code, "tmp/server-generated-#{@file_index % 10}.js")
39
+ trace_js_code_used("Evaluating code to server render.", js_code,
40
+ "tmp/server-generated-#{@file_index % 10}.js")
48
41
  @file_index += 1
49
42
  end
50
43
  json_string = eval_js(js_code)
@@ -68,18 +61,21 @@ module ReactOnRails
68
61
  class << self
69
62
  private
70
63
 
71
- def trace_messsage(js_code, file_name = "tmp/server-generated.js", force = false)
72
- return unless trace_react_on_rails? || force
64
+ def trace_js_code_used(msg, js_code, file_name = "tmp/server-generated.js", force: false)
65
+ return unless ReactOnRails.configuration.trace || force
73
66
  # Set to anything to print generated code.
74
- puts "Z" * 80
75
- puts "react_renderer.rb: 92"
76
- puts "wrote file #{file_name}"
77
67
  File.write(file_name, js_code)
78
- puts "Z" * 80
79
- end
80
-
81
- def trace_react_on_rails?
82
- ENV["TRACE_REACT_ON_RAILS"].present?
68
+ msg = <<-MSG.strip_heredoc
69
+ #{'Z' * 80}
70
+ [react_on_rails] #{msg}
71
+ JavaScript code used: #{file_name}
72
+ #{'Z' * 80}
73
+ MSG
74
+ if force
75
+ Rails.logger.error(msg)
76
+ else
77
+ Rails.logger.info(msg)
78
+ end
83
79
  end
84
80
 
85
81
  def eval_js(js_code)
@@ -95,34 +91,35 @@ module ReactOnRails
95
91
 
96
92
  server_js_file = ReactOnRails::Utils.server_bundle_js_file_path
97
93
 
98
- # bundle_js_code = File.read(server_js_file)
99
94
  begin
100
- bundle_js_code = open(server_js_file, &:read)
101
- rescue # rubocop:disable Lint/RescueWithoutErrorClass => e
95
+ bundle_js_code = File.read(server_js_file)
96
+ rescue StandardError => e
102
97
  msg = "You specified server rendering JS file: #{server_js_file}, but it cannot be "\
103
98
  "read. You may set the server_bundle_js_file in your configuration to be \"\" to "\
104
99
  "avoid this warning.\nError is: #{e}"
105
- raise msg
100
+ raise ReactOnRails::Error, msg
106
101
  end
107
102
  # rubocop:disable Layout/IndentHeredoc
108
103
  base_js_code = <<-JS
109
104
  #{console_polyfill}
110
- #{execjs_timer_polyfills}
111
- #{bundle_js_code};
105
+ #{execjs_timer_polyfills}
106
+ #{bundle_js_code};
112
107
  JS
113
108
  # rubocop:enable Layout/IndentHeredoc
114
109
  file_name = "tmp/base_js_code.js"
115
110
  begin
116
- trace_messsage(base_js_code, file_name)
111
+ if ReactOnRails.configuration.trace
112
+ Rails.logger.info { "[react_on_rails] Created JavaScript context with file #{server_js_file}" }
113
+ end
117
114
  ExecJS.compile(base_js_code)
118
- rescue # rubocop:disable Lint/RescueWithoutErrorClass => e
115
+ rescue StandardError => e
119
116
  msg = "ERROR when compiling base_js_code! "\
120
117
  "See file #{file_name} to "\
121
118
  "correlate line numbers of error. Error is\n\n#{e.message}"\
122
119
  "\n\n#{e.backtrace.join("\n")}"
123
- puts msg
124
120
  Rails.logger.error(msg)
125
- trace_messsage(base_js_code, file_name, true)
121
+ trace_js_code_used("Error when compiling JavaScript code for the context.", base_js_code,
122
+ file_name, force: true)
126
123
  raise e
127
124
  end
128
125
  end
@@ -158,10 +155,10 @@ function clearTimeout() {
158
155
  end
159
156
 
160
157
  def undefined_for_exec_js_logging(function_name)
161
- if trace_react_on_rails?
162
- "console.error('#{function_name} is not defined for execJS. See "\
163
- "https://github.com/sstephenson/execjs#faq. Note babel-polyfill may call this.');\n"\
164
- " console.error(getStackTrace().join('\\n'));"
158
+ if ReactOnRails.configuration.trace
159
+ "console.error('[React on Rails Rendering] #{function_name} is not defined for execJS. See "\
160
+ "https://github.com/sstephenson/execjs#faq. Note babel-polyfill may call this.');\n"\
161
+ " console.error(getStackTrace().join('\\n'));"
165
162
  else
166
163
  ""
167
164
  end