react_on_rails 11.1.8 → 12.0.0.pre.beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. checksums.yaml +4 -4
  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 +37 -11
  11. data/.travis.yml +10 -20
  12. data/CHANGELOG.md +89 -3
  13. data/CONTRIBUTING.md +61 -80
  14. data/Gemfile +3 -5
  15. data/{COMM-LICENSE → REACT-ON-RAILS-PRO-LICENSE} +6 -9
  16. data/README.md +121 -71
  17. data/Rakefile +0 -7
  18. data/SUMMARY.md +10 -12
  19. data/book.json +5 -5
  20. data/docs/additional-reading/asset-pipeline.md +8 -16
  21. data/docs/additional-reading/images.md +1 -1
  22. data/docs/additional-reading/rails_view_rendering_from_inline_javascript.md +2 -1
  23. data/docs/additional-reading/react-helmet.md +30 -10
  24. data/docs/additional-reading/react-router.md +52 -75
  25. data/docs/additional-reading/server-rendering-tips.md +12 -7
  26. data/docs/additional-reading/upgrade-webpacker-v3-to-v4.md +10 -0
  27. data/docs/api/javascript-api.md +3 -3
  28. data/docs/api/redux-store-api.md +4 -2
  29. data/docs/api/view-helpers-api.md +17 -14
  30. data/docs/basics/configuration.md +64 -61
  31. data/docs/basics/deployment.md +1 -2
  32. data/docs/basics/i18n.md +44 -22
  33. data/docs/basics/installation-into-an-existing-rails-app.md +2 -2
  34. data/docs/basics/minitest-configuration.md +31 -0
  35. data/docs/basics/react-server-rendering.md +1 -1
  36. data/docs/basics/recommended-project-structure.md +1 -1
  37. data/docs/basics/{generator-functions-and-railscontext.md → render-functions-and-railscontext.md} +59 -21
  38. data/docs/basics/rspec-configuration.md +2 -2
  39. data/docs/basics/upgrading-react-on-rails.md +61 -3
  40. data/docs/basics/webpack-configuration.md +15 -1
  41. data/docs/contributor-info/errors-with-hooks.md +45 -0
  42. data/docs/contributor-info/pull-requests.md +44 -0
  43. data/docs/misc/doctrine.md +1 -1
  44. data/docs/{misc-pending → outdated}/code-splitting.md +13 -9
  45. data/docs/{additional-reading → outdated}/heroku-deployment.md +0 -6
  46. data/docs/{basics → outdated}/how-react-on-rails-works.md +3 -3
  47. data/docs/{misc-pending → outdated}/manual-installation-overview.md +5 -5
  48. data/docs/{additional-reading → outdated}/rails-assets-relative-paths.md +3 -3
  49. data/docs/{misc-pending → outdated}/rails-assets.md +4 -7
  50. data/docs/{misc → outdated}/rails3.md +0 -0
  51. data/docs/testimonials/hvmn.md +3 -3
  52. data/docs/testimonials/resortpass.md +13 -0
  53. data/docs/testimonials/testimonials.md +11 -1
  54. data/docs/tutorial.md +69 -28
  55. data/jest.config.js +4 -0
  56. data/lib/generators/react_on_rails/base_generator.rb +2 -2
  57. data/lib/generators/react_on_rails/dev_tests_generator.rb +2 -1
  58. data/lib/generators/react_on_rails/generator_helper.rb +4 -6
  59. data/lib/generators/react_on_rails/install_generator.rb +2 -0
  60. data/lib/generators/react_on_rails/templates/base/base/Procfile.dev +3 -1
  61. data/lib/generators/react_on_rails/templates/base/base/Procfile.dev-hmr +18 -0
  62. data/lib/generators/react_on_rails/templates/base/base/app/javascript/bundles/HelloWorld/components/HelloWorld.jsx +20 -40
  63. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/components/HelloWorld.jsx +4 -8
  64. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/store/helloWorldStore.js +1 -3
  65. data/lib/react_on_rails.rb +4 -1
  66. data/lib/react_on_rails/configuration.rb +15 -23
  67. data/lib/react_on_rails/error.rb +2 -0
  68. data/lib/react_on_rails/git_utils.rb +2 -0
  69. data/lib/react_on_rails/helper.rb +111 -162
  70. data/lib/react_on_rails/json_output.rb +1 -1
  71. data/lib/react_on_rails/json_parse_error.rb +2 -0
  72. data/lib/react_on_rails/locales/base.rb +142 -0
  73. data/lib/react_on_rails/locales/to_js.rb +37 -0
  74. data/lib/react_on_rails/locales/to_json.rb +27 -0
  75. data/lib/react_on_rails/prerender_error.rb +11 -15
  76. data/lib/react_on_rails/react_component/render_options.rb +4 -0
  77. data/lib/react_on_rails/server_rendering_js_code.rb +42 -0
  78. data/lib/react_on_rails/server_rendering_pool/ruby_embedded_java_script.rb +71 -51
  79. data/lib/react_on_rails/test_helper/ensure_assets_compiled.rb +7 -8
  80. data/lib/react_on_rails/utils.rb +15 -20
  81. data/lib/react_on_rails/version.rb +1 -1
  82. data/lib/react_on_rails/version_checker.rb +5 -1
  83. data/lib/react_on_rails/webpacker_utils.rb +16 -2
  84. data/lib/tasks/assets.rake +5 -45
  85. data/lib/tasks/locale.rake +8 -2
  86. data/package-scripts.yml +49 -0
  87. data/package.json +41 -31
  88. data/rakelib/dummy_apps.rake +1 -9
  89. data/rakelib/example_type.rb +3 -1
  90. data/rakelib/examples.rake +3 -0
  91. data/rakelib/lint.rake +2 -7
  92. data/rakelib/node_package.rake +2 -2
  93. data/rakelib/release.rake +3 -8
  94. data/rakelib/run_rspec.rake +5 -18
  95. data/react_on_rails.gemspec +3 -5
  96. data/tsconfig.json +14 -0
  97. data/webpackConfigLoader.js +5 -4
  98. data/yarn.lock +7042 -2327
  99. metadata +39 -57
  100. data/Gemfile.rails32 +0 -74
  101. data/docs/additional-reading/babel.md +0 -5
  102. data/docs/additional-reading/hot-reloading-rails-development-asset-pipeline.md +0 -47
  103. data/docs/api/ruby-api-hot-reload-view-helpers.md +0 -44
  104. data/lib/generators/react_on_rails/templates/base/base/Procfile.dev-server +0 -12
  105. data/lib/react_on_rails/assets_precompile.rb +0 -150
  106. data/lib/react_on_rails/locales_to_js.rb +0 -136
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ReactOnRails
2
4
  class Error < StandardError
3
5
  end
@@ -6,8 +6,10 @@ module ReactOnRails
6
6
  module GitUtils
7
7
  def self.uncommitted_changes?(message_handler)
8
8
  return false if ENV["COVERAGE"] == "true"
9
+
9
10
  status = `git status --porcelain`
10
11
  return false if $CHILD_STATUS.success? && status.empty?
12
+
11
13
  error = if !$CHILD_STATUS.success?
12
14
  "You do not have Git installed. Please install Git, and commit your changes before continuing"
13
15
  else
@@ -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,7 +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.
101
- # random_dom_id can be set to override the global default.
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.
102
55
  def react_component(component_name, options = {})
103
56
  internal_result = internal_react_component(component_name, options)
104
57
  server_rendered_html = internal_result[:result]["html"]
@@ -112,20 +65,24 @@ module ReactOnRails
112
65
  render_options: internal_result[:render_options]
113
66
  )
114
67
  elsif server_rendered_html.is_a?(Hash)
115
- msg = <<-MSG.strip_heredoc
116
- Use react_component_hash (not react_component) to return a Hash to your ruby view code. See
117
- https://github.com/shakacode/react_on_rails/blob/master/spec/dummy/client/app/startup/ReactHelmetServerApp.jsx
118
- 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.
119
72
  MSG
120
73
  raise ReactOnRails::Error, msg
121
-
122
74
  else
123
- msg = <<-MSG.strip_heredoc
124
- ReactOnRails: server_rendered_html is expected to be a String for #{component_name}. If you're
125
- trying to use a generator function to return a Hash to your ruby view code, then use
126
- react_component_hash instead of react_component and see
127
- https://github.com/shakacode/react_on_rails/blob/master/spec/dummy/client/app/startup/ReactHelmetServerApp.jsx
128
- for an example of the JavaScript code."
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.
129
86
  MSG
130
87
  raise ReactOnRails::Error, msg
131
88
  end
@@ -136,7 +93,7 @@ module ReactOnRails
136
93
  # It is exactly like react_component except for the following:
137
94
  # 1. prerender: true is automatically added, as this method doesn't make sense for client only
138
95
  # rendering.
139
- # 2. Your JavaScript for server rendering must return an Object for the key server_rendered_html.
96
+ # 2. Your JavaScript render function for server rendering must return an Object rather than a React component.
140
97
  # 3. Your view code must expect an object and not a string.
141
98
  #
142
99
  # Here is an example of the view code:
@@ -166,10 +123,12 @@ module ReactOnRails
166
123
  render_options: internal_result[:render_options]
167
124
  )
168
125
  else
169
- msg = <<-MSG.strip_heredoc
170
- Generator function used by react_component_hash for #{component_name} is expected to return
126
+ msg = <<~MSG
127
+ render function used by react_component_hash for #{component_name} is expected to return
171
128
  an Object. See https://github.com/shakacode/react_on_rails/blob/master/spec/dummy/client/app/startup/ReactHelmetServerApp.jsx
172
- for an example of the JavaScript code."
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.
173
132
  MSG
174
133
  raise ReactOnRails::Error, msg
175
134
  end
@@ -178,6 +137,9 @@ module ReactOnRails
178
137
  # Separate initialization of store from react_component allows multiple react_component calls to
179
138
  # use the same Redux store.
180
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
+ #
181
143
  # store_name: name of the store, corresponding to your call to ReactOnRails.registerStores in your
182
144
  # JavaScript code.
183
145
  # props: Ruby Hash or JSON string which contains the properties to pass to the redux store.
@@ -206,6 +168,7 @@ module ReactOnRails
206
168
  # that contains a data props.
207
169
  def redux_store_hydration_data
208
170
  return if @registered_stores_defer_render.blank?
171
+
209
172
  @registered_stores_defer_render.reduce("".dup) do |accum, redux_store_data|
210
173
  accum << render_redux_store_data(redux_store_data)
211
174
  end.html_safe
@@ -265,6 +228,7 @@ module ReactOnRails
265
228
 
266
229
  def json_safe_and_pretty(hash_or_string)
267
230
  return "{}" if hash_or_string.nil?
231
+
268
232
  unless hash_or_string.is_a?(String) || hash_or_string.is_a?(Hash)
269
233
  raise ReactOnRails::Error, "#{__method__} only accepts String or Hash as argument "\
270
234
  "(#{hash_or_string.class} given)."
@@ -275,6 +239,60 @@ module ReactOnRails
275
239
  ReactOnRails::JsonOutput.escape(json_value)
276
240
  end
277
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
+
278
296
  private
279
297
 
280
298
  def build_react_component_result_for_server_rendered_string(
@@ -284,9 +302,15 @@ module ReactOnRails
284
302
  render_options: required("render_options")
285
303
  )
286
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
287
311
  content_tag_options[:id] = render_options.dom_id
288
312
 
289
- rendered_output = content_tag(:div,
313
+ rendered_output = content_tag(content_tag_options_html_tag.to_sym,
290
314
  server_rendered_html.html_safe,
291
315
  content_tag_options)
292
316
 
@@ -334,13 +358,11 @@ module ReactOnRails
334
358
 
335
359
  def compose_react_component_html_with_spec_and_console(component_specification_tag, rendered_output, console_script)
336
360
  # IMPORTANT: Ensure that we mark string as html_safe to avoid escaping.
337
- # rubocop:disable Layout/IndentHeredoc
338
- <<-HTML.html_safe
339
- #{rendered_output}
340
- #{component_specification_tag}
341
- #{console_script}
361
+ <<~HTML.html_safe
362
+ #{rendered_output}
363
+ #{component_specification_tag}
364
+ #{console_script}
342
365
  HTML
343
- # rubocop:enable Layout/IndentHeredoc
344
366
  end
345
367
 
346
368
  # prepend the rails_context if not yet applied
@@ -405,9 +427,7 @@ module ReactOnRails
405
427
 
406
428
  # Returns object with values that are NOT html_safe!
407
429
  def server_rendered_react_component(render_options)
408
- if !render_options.prerender || ReactOnRails::Utils.server_bundle_path_is_http?
409
- return { "html" => "", "consoleReplayScript" => "" }
410
- end
430
+ return { "html" => "", "consoleReplayScript" => "" } unless render_options.prerender
411
431
 
412
432
  react_component_name = render_options.react_component_name
413
433
  props = render_options.props
@@ -433,22 +453,13 @@ module ReactOnRails
433
453
  #
434
454
  # Read more here: http://timelessrepo.com/json-isnt-a-javascript-subset
435
455
 
436
- # rubocop:disable Layout/IndentHeredoc
437
- js_code = <<-JS
438
- (function() {
439
- var railsContext = #{rails_context(server_side: true).to_json};
440
- #{initialize_redux_stores}
441
- var props = #{props_string(props).gsub("\u2028", '\u2028').gsub("\u2029", '\u2029')};
442
- return ReactOnRails.serverRenderReactComponent({
443
- name: '#{react_component_name}',
444
- domNodeId: '#{render_options.dom_id}',
445
- props: props,
446
- trace: #{render_options.trace},
447
- railsContext: railsContext
448
- });
449
- })()
450
- JS
451
- # 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
+ )
452
463
 
453
464
  begin
454
465
  result = ReactOnRails::ServerRenderingPool.server_render_js_with_console_logging(js_code, render_options)
@@ -480,6 +491,7 @@ module ReactOnRails
480
491
  JS
481
492
 
482
493
  return result unless @registered_stores.present? || @registered_stores_defer_render.present?
494
+
483
495
  declarations = "var reduxProps, store, storeGenerator;\n".dup
484
496
  all_stores = (@registered_stores || []) + (@registered_stores_defer_render || [])
485
497
 
@@ -496,73 +508,10 @@ module ReactOnRails
496
508
  result
497
509
  end
498
510
 
499
- # This is the definitive list of the default values used for the rails_context, which is the
500
- # second parameter passed to both component and store generator functions.
501
- # rubocop:disable Metrics/AbcSize
502
- def rails_context(server_side: required("server_side"))
503
- @rails_context ||= begin
504
- result = {
505
- railsEnv: Rails.env,
506
- inMailer: in_mailer?,
507
- # Locale settings
508
- i18nLocale: I18n.locale,
509
- i18nDefaultLocale: I18n.default_locale,
510
- rorVersion: ReactOnRails::VERSION,
511
- rorPro: ReactOnRails::Utils.react_on_rails_pro?
512
- }
513
- if defined?(request) && request.present?
514
- # Check for encoding of the request's original_url and try to force-encoding the
515
- # URLs as UTF-8. This situation can occur in browsers that do not encode the
516
- # entire URL as UTF-8 already, mostly on the Windows platform (IE11 and lower).
517
- original_url_normalized = request.original_url
518
- if original_url_normalized.encoding.to_s == "ASCII-8BIT"
519
- original_url_normalized = original_url_normalized.force_encoding("ISO-8859-1").encode("UTF-8")
520
- end
521
-
522
- # Using Addressable instead of standard URI to better deal with
523
- # non-ASCII characters (see https://github.com/shakacode/react_on_rails/pull/405)
524
- uri = Addressable::URI.parse(original_url_normalized)
525
- # uri = Addressable::URI.parse("http://foo.com:3000/posts?id=30&limit=5#time=1305298413")
526
-
527
- result.merge!(
528
- # URL settings
529
- href: uri.to_s,
530
- location: "#{uri.path}#{uri.query.present? ? "?#{uri.query}" : ''}",
531
- scheme: uri.scheme, # http
532
- host: uri.host, # foo.com
533
- port: uri.port,
534
- pathname: uri.path, # /posts
535
- search: uri.query, # id=30&limit=5
536
- httpAcceptLanguage: request.env["HTTP_ACCEPT_LANGUAGE"]
537
- )
538
- end
539
- if ReactOnRails.configuration.rendering_extension
540
- custom_context = ReactOnRails.configuration.rendering_extension.custom_context(self)
541
- result.merge!(custom_context) if custom_context
542
- end
543
- result
544
- end
545
-
546
- @rails_context.merge(serverSide: server_side)
547
- end
548
-
549
- # rubocop:enable Metrics/AbcSize
550
-
551
511
  def replay_console_option(val)
552
512
  val.nil? ? ReactOnRails.configuration.replay_console : val
553
513
  end
554
514
 
555
- def use_hot_reloading?
556
- ENV["REACT_ON_RAILS_ENV"] == "HOT"
557
- end
558
-
559
- def send_tag_method(tag_method_name, args)
560
- asset_type = use_hot_reloading? ? :hot : :static
561
- assets = Array(args[asset_type])
562
- options = args.delete_if { |key, _value| %i[hot static].include?(key) }
563
- send(tag_method_name, *assets, options) unless assets.empty?
564
- end
565
-
566
515
  def in_mailer?
567
516
  return false unless defined?(controller)
568
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ReactOnRails
2
4
  class JsonParseError < ::ReactOnRails::Error
3
5
  attr_reader :json
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "erb"
4
+
5
+ module ReactOnRails
6
+ module Locales
7
+ class Base
8
+ def initialize
9
+ return if i18n_dir.nil?
10
+ return unless obsolete?
11
+
12
+ @translations, @defaults = generate_translations
13
+ convert
14
+ end
15
+
16
+ private
17
+
18
+ def file_format; end
19
+
20
+ def obsolete?
21
+ return true if exist_files.empty?
22
+
23
+ files_are_outdated
24
+ end
25
+
26
+ def exist_files
27
+ @exist_files ||= files.select(&File.method(:exist?))
28
+ end
29
+
30
+ def files_are_outdated
31
+ latest_yml = locale_files.map(&File.method(:mtime)).max
32
+ earliest = exist_files.map(&File.method(:mtime)).min
33
+ latest_yml > earliest
34
+ end
35
+
36
+ def file_names
37
+ %w[translations default]
38
+ end
39
+
40
+ def files
41
+ @files ||= file_names.map { |n| file(n) }
42
+ end
43
+
44
+ def file(name)
45
+ "#{i18n_dir}/#{name}.#{file_format}"
46
+ end
47
+
48
+ def locale_files
49
+ @locale_files ||= begin
50
+ if i18n_yml_dir.present?
51
+ Dir["#{i18n_yml_dir}/**/*.yml"]
52
+ else
53
+ ReactOnRails::Utils.truthy_presence(
54
+ Rails.application && Rails.application.config.i18n.load_path
55
+ ).presence
56
+ end
57
+ end
58
+ end
59
+
60
+ def i18n_dir
61
+ @i18n_dir ||= ReactOnRails.configuration.i18n_dir
62
+ end
63
+
64
+ def i18n_yml_dir
65
+ @i18n_yml_dir ||= ReactOnRails.configuration.i18n_yml_dir
66
+ end
67
+
68
+ def default_locale
69
+ @default_locale ||= I18n.default_locale.to_s || "en"
70
+ end
71
+
72
+ def convert
73
+ file_names.each do |name|
74
+ template = send("template_#{name}")
75
+ path = file(name)
76
+ generate_file(template, path)
77
+ end
78
+ end
79
+
80
+ def generate_file(template, path)
81
+ result = ERB.new(template).result()
82
+ File.open(path, "w") do |f|
83
+ f.write(result)
84
+ end
85
+ end
86
+
87
+ def generate_translations
88
+ translations = {}
89
+ defaults = {}
90
+ locale_files.each do |f|
91
+ translation = YAML.safe_load(File.open(f))
92
+ key = translation.keys[0]
93
+ val = flatten(translation[key])
94
+ translations = translations.deep_merge(key => val)
95
+ defaults = defaults.deep_merge(flatten_defaults(val)) if key == default_locale
96
+ end
97
+ [translations.to_json, defaults.to_json]
98
+ end
99
+
100
+ def format(input)
101
+ input.to_s.tr(".", "_").camelize(:lower).to_sym
102
+ end
103
+
104
+ def flatten_defaults(val)
105
+ flatten(val).each_with_object({}) do |(k, v), h|
106
+ key = format(k)
107
+ h[key] = { id: k, defaultMessage: v }
108
+ end
109
+ end
110
+
111
+ def flatten(translations)
112
+ translations.each_with_object({}) do |(k, v), h|
113
+ if v.is_a? Hash
114
+ flatten(v).map { |hk, hv| h["#{k}.#{hk}".to_sym] = hv }
115
+ elsif v.is_a?(String)
116
+ h[k] = v.gsub("%{", "{")
117
+ elsif !v.is_a?(Array)
118
+ h[k] = v
119
+ end
120
+ end
121
+ end
122
+
123
+ def template_translations
124
+ <<-JS.strip_heredoc
125
+ export const translations = #{@translations};
126
+ JS
127
+ end
128
+
129
+ def template_default
130
+ <<-JS.strip_heredoc
131
+ import { defineMessages } from 'react-intl';
132
+
133
+ const defaultLocale = \'#{default_locale}\';
134
+
135
+ const defaultMessages = defineMessages(#{@defaults});
136
+
137
+ export { defaultMessages, defaultLocale };
138
+ JS
139
+ end
140
+ end
141
+ end
142
+ end