react_on_rails 11.0.3 → 12.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (129) 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 +209 -4
  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 -634
  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 +74 -3
  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 +171 -100
  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} +133 -161
  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 +31 -3
  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 +93 -63
  90. data/lib/react_on_rails/server_rendering_pool.rb +1 -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 +65 -25
  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 +7 -2
  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 +70 -60
  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/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,6 +50,8 @@ 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.
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.
101
55
  def react_component(component_name, options = {})
102
56
  internal_result = internal_react_component(component_name, options)
103
57
  server_rendered_html = internal_result[:result]["html"]
@@ -111,26 +65,48 @@ module ReactOnRails
111
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
73
  raise ReactOnRails::Error, msg
120
-
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
87
  raise ReactOnRails::Error, msg
130
88
  end
131
89
  end
132
90
 
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
+ #
133
108
  def react_component_hash(component_name, options = {})
109
+ options[:prerender] = true
134
110
  internal_result = internal_react_component(component_name, options)
135
111
  server_rendered_html = internal_result[:result]["html"]
136
112
  console_script = internal_result[:result]["consoleReplayScript"]
@@ -147,10 +123,12 @@ module ReactOnRails
147
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
133
  raise ReactOnRails::Error, msg
156
134
  end
@@ -159,6 +137,9 @@ module ReactOnRails
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
@@ -246,7 +228,8 @@ module ReactOnRails
246
228
 
247
229
  def json_safe_and_pretty(hash_or_string)
248
230
  return "{}" if hash_or_string.nil?
249
- unless hash_or_string.class.in?([Hash, String])
231
+
232
+ unless hash_or_string.is_a?(String) || hash_or_string.is_a?(Hash)
250
233
  raise ReactOnRails::Error, "#{__method__} only accepts String or Hash as argument "\
251
234
  "(#{hash_or_string.class} given)."
252
235
  end
@@ -256,6 +239,60 @@ module ReactOnRails
256
239
  ReactOnRails::JsonOutput.escape(json_value)
257
240
  end
258
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
+
259
296
  private
260
297
 
261
298
  def build_react_component_result_for_server_rendered_string(
@@ -265,9 +302,15 @@ module ReactOnRails
265
302
  render_options: required("render_options")
266
303
  )
267
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
268
311
  content_tag_options[:id] = render_options.dom_id
269
312
 
270
- rendered_output = content_tag(:div,
313
+ rendered_output = content_tag(content_tag_options_html_tag.to_sym,
271
314
  server_rendered_html.html_safe,
272
315
  content_tag_options)
273
316
 
@@ -315,13 +358,11 @@ module ReactOnRails
315
358
 
316
359
  def compose_react_component_html_with_spec_and_console(component_specification_tag, rendered_output, console_script)
317
360
  # IMPORTANT: Ensure that we mark string as html_safe to avoid escaping.
318
- # rubocop:disable Layout/IndentHeredoc
319
- <<-HTML.html_safe
320
- #{rendered_output}
321
- #{component_specification_tag}
322
- #{console_script}
361
+ <<~HTML.html_safe
362
+ #{rendered_output}
363
+ #{component_specification_tag}
364
+ #{console_script}
323
365
  HTML
324
- # rubocop:enable Layout/IndentHeredoc
325
366
  end
326
367
 
327
368
  # prepend the rails_context if not yet applied
@@ -412,22 +453,13 @@ module ReactOnRails
412
453
  #
413
454
  # Read more here: http://timelessrepo.com/json-isnt-a-javascript-subset
414
455
 
415
- # rubocop:disable Layout/IndentHeredoc
416
- js_code = <<-JS
417
- (function() {
418
- var railsContext = #{rails_context(server_side: true).to_json};
419
- #{initialize_redux_stores}
420
- var props = #{props_string(props).gsub("\u2028", '\u2028').gsub("\u2029", '\u2029')};
421
- return ReactOnRails.serverRenderReactComponent({
422
- name: '#{react_component_name}',
423
- domNodeId: '#{render_options.dom_id}',
424
- props: props,
425
- trace: #{render_options.trace},
426
- railsContext: railsContext
427
- });
428
- })()
429
- JS
430
- # 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
+ )
431
463
 
432
464
  begin
433
465
  result = ReactOnRails::ServerRenderingPool.server_render_js_with_console_logging(js_code, render_options)
@@ -454,14 +486,15 @@ module ReactOnRails
454
486
  end
455
487
 
456
488
  def initialize_redux_stores
457
- return "" unless @registered_stores.present? || @registered_stores_defer_render.present?
458
- declarations = "var reduxProps, store, storeGenerator;\n".dup
459
- all_stores = (@registered_stores || []) + (@registered_stores_defer_render || [])
460
-
461
489
  result = <<-JS.dup
462
490
  ReactOnRails.clearHydratedStores();
463
491
  JS
464
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
+
465
498
  result << all_stores.each_with_object(declarations) do |redux_store_data, memo|
466
499
  store_name = redux_store_data[:store_name]
467
500
  props = props_string(redux_store_data[:props])
@@ -475,71 +508,10 @@ module ReactOnRails
475
508
  result
476
509
  end
477
510
 
478
- # This is the definitive list of the default values used for the rails_context, which is the
479
- # second parameter passed to both component and store generator functions.
480
- # rubocop:disable Metrics/AbcSize
481
- def rails_context(server_side: required("server_side"))
482
- @rails_context ||= begin
483
- result = {
484
- railsEnv: Rails.env,
485
- inMailer: in_mailer?,
486
- # Locale settings
487
- i18nLocale: I18n.locale,
488
- i18nDefaultLocale: I18n.default_locale
489
- }
490
- if defined?(request) && request.present?
491
- # Check for encoding of the request's original_url and try to force-encoding the
492
- # URLs as UTF-8. This situation can occur in browsers that do not encode the
493
- # entire URL as UTF-8 already, mostly on the Windows platform (IE11 and lower).
494
- original_url_normalized = request.original_url
495
- if original_url_normalized.encoding.to_s == "ASCII-8BIT"
496
- original_url_normalized = original_url_normalized.force_encoding("ISO-8859-1").encode("UTF-8")
497
- end
498
-
499
- # Using Addressable instead of standard URI to better deal with
500
- # non-ASCII characters (see https://github.com/shakacode/react_on_rails/pull/405)
501
- uri = Addressable::URI.parse(original_url_normalized)
502
- # uri = Addressable::URI.parse("http://foo.com:3000/posts?id=30&limit=5#time=1305298413")
503
-
504
- result.merge!(
505
- # URL settings
506
- href: uri.to_s,
507
- location: "#{uri.path}#{uri.query.present? ? "?#{uri.query}" : ''}",
508
- scheme: uri.scheme, # http
509
- host: uri.host, # foo.com
510
- port: uri.port,
511
- pathname: uri.path, # /posts
512
- search: uri.query, # id=30&limit=5
513
- httpAcceptLanguage: request.env["HTTP_ACCEPT_LANGUAGE"]
514
- )
515
- end
516
- if ReactOnRails.configuration.rendering_extension
517
- custom_context = ReactOnRails.configuration.rendering_extension.custom_context(self)
518
- result.merge!(custom_context) if custom_context
519
- end
520
- result
521
- end
522
-
523
- @rails_context.merge(serverSide: server_side)
524
- end
525
-
526
- # rubocop:enable Metrics/AbcSize
527
-
528
511
  def replay_console_option(val)
529
512
  val.nil? ? ReactOnRails.configuration.replay_console : val
530
513
  end
531
514
 
532
- def use_hot_reloading?
533
- ENV["REACT_ON_RAILS_ENV"] == "HOT"
534
- end
535
-
536
- def send_tag_method(tag_method_name, args)
537
- asset_type = use_hot_reloading? ? :hot : :static
538
- assets = Array(args[asset_type])
539
- options = args.delete_if { |key, _value| %i[hot static].include?(key) }
540
- send(tag_method_name, *assets, options) unless assets.empty?
541
- end
542
-
543
515
  def in_mailer?
544
516
  return false unless defined?(controller)
545
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
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "erb"
4
+
5
+ module ReactOnRails
6
+ module Locales
7
+ def self.compile
8
+ if ReactOnRails.configuration.i18n_output_format&.downcase == "js"
9
+ ReactOnRails::Locales::ToJs.new
10
+ else
11
+ ReactOnRails::Locales::ToJson.new
12
+ end
13
+ end
14
+
15
+ class Base
16
+ def initialize
17
+ return if i18n_dir.nil?
18
+ return unless obsolete?
19
+
20
+ @translations, @defaults = generate_translations
21
+ convert
22
+ end
23
+
24
+ private
25
+
26
+ def file_format; end
27
+
28
+ def obsolete?
29
+ return true if exist_files.empty?
30
+
31
+ files_are_outdated
32
+ end
33
+
34
+ def exist_files
35
+ @exist_files ||= files.select(&File.method(:exist?))
36
+ end
37
+
38
+ def files_are_outdated
39
+ latest_yml = locale_files.map(&File.method(:mtime)).max
40
+ earliest = exist_files.map(&File.method(:mtime)).min
41
+ latest_yml > earliest
42
+ end
43
+
44
+ def file_names
45
+ %w[translations default]
46
+ end
47
+
48
+ def files
49
+ @files ||= file_names.map { |n| file(n) }
50
+ end
51
+
52
+ def file(name)
53
+ "#{i18n_dir}/#{name}.#{file_format}"
54
+ end
55
+
56
+ def locale_files
57
+ @locale_files ||= begin
58
+ if i18n_yml_dir.present?
59
+ Dir["#{i18n_yml_dir}/**/*.yml"]
60
+ else
61
+ ReactOnRails::Utils.truthy_presence(
62
+ Rails.application && Rails.application.config.i18n.load_path
63
+ ).presence
64
+ end
65
+ end
66
+ end
67
+
68
+ def i18n_dir
69
+ @i18n_dir ||= ReactOnRails.configuration.i18n_dir
70
+ end
71
+
72
+ def i18n_yml_dir
73
+ @i18n_yml_dir ||= ReactOnRails.configuration.i18n_yml_dir
74
+ end
75
+
76
+ def default_locale
77
+ @default_locale ||= I18n.default_locale.to_s || "en"
78
+ end
79
+
80
+ def convert
81
+ file_names.each do |name|
82
+ template = send("template_#{name}")
83
+ path = file(name)
84
+ generate_file(template, path)
85
+ end
86
+ end
87
+
88
+ def generate_file(template, path)
89
+ result = ERB.new(template).result()
90
+ File.open(path, "w") do |f|
91
+ f.write(result)
92
+ end
93
+ end
94
+
95
+ def generate_translations
96
+ translations = {}
97
+ defaults = {}
98
+ locale_files.each do |f|
99
+ translation = YAML.safe_load(File.open(f))
100
+ key = translation.keys[0]
101
+ val = flatten(translation[key])
102
+ translations = translations.deep_merge(key => val)
103
+ defaults = defaults.deep_merge(flatten_defaults(val)) if key == default_locale
104
+ end
105
+ [translations.to_json, defaults.to_json]
106
+ end
107
+
108
+ def format(input)
109
+ input.to_s.tr(".", "_").camelize(:lower).to_sym
110
+ end
111
+
112
+ def flatten_defaults(val)
113
+ flatten(val).each_with_object({}) do |(k, v), h|
114
+ key = format(k)
115
+ h[key] = { id: k, defaultMessage: v }
116
+ end
117
+ end
118
+
119
+ def flatten(translations)
120
+ translations.each_with_object({}) do |(k, v), h|
121
+ if v.is_a? Hash
122
+ flatten(v).map { |hk, hv| h["#{k}.#{hk}".to_sym] = hv }
123
+ elsif v.is_a?(String)
124
+ h[k] = v.gsub("%{", "{")
125
+ elsif !v.is_a?(Array)
126
+ h[k] = v
127
+ end
128
+ end
129
+ end
130
+
131
+ def template_translations
132
+ <<-JS.strip_heredoc
133
+ export const translations = #{@translations};
134
+ JS
135
+ end
136
+
137
+ def template_default
138
+ <<-JS.strip_heredoc
139
+ import { defineMessages } from 'react-intl';
140
+
141
+ const defaultLocale = \'#{default_locale}\';
142
+
143
+ const defaultMessages = defineMessages(#{@defaults});
144
+
145
+ export { defaultMessages, defaultLocale };
146
+ JS
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "erb"
4
+
5
+ module ReactOnRails
6
+ module Locales
7
+ class ToJs < Base
8
+ def initialize
9
+ super
10
+ end
11
+
12
+ private
13
+
14
+ def file_format
15
+ "js"
16
+ end
17
+
18
+ def template_translations
19
+ <<-JS.strip_heredoc
20
+ export const translations = #{@translations};
21
+ JS
22
+ end
23
+
24
+ def template_default
25
+ <<-JS.strip_heredoc
26
+ import { defineMessages } from 'react-intl';
27
+
28
+ const defaultLocale = \'#{default_locale}\';
29
+
30
+ const defaultMessages = defineMessages(#{@defaults});
31
+
32
+ export { defaultMessages, defaultLocale };
33
+ JS
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "erb"
4
+
5
+ module ReactOnRails
6
+ module Locales
7
+ class ToJson < Base
8
+ def initialize
9
+ super
10
+ end
11
+
12
+ private
13
+
14
+ def file_format
15
+ "json"
16
+ end
17
+
18
+ def template_translations
19
+ @translations
20
+ end
21
+
22
+ def template_default
23
+ @defaults
24
+ end
25
+ end
26
+ end
27
+ end