react_on_rails 11.1.7 → 11.3.1.beta.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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/.eslintrc +8 -2
  3. data/.prettierignore +1 -0
  4. data/.prettierrc +20 -0
  5. data/.release-it.json +3 -0
  6. data/.travis.yml +10 -9
  7. data/CHANGELOG.md +60 -3
  8. data/CONTRIBUTING.md +2 -10
  9. data/Gemfile +1 -2
  10. data/Gemfile.rails32 +0 -1
  11. data/README.md +30 -14
  12. data/SUMMARY.md +6 -2
  13. data/docs/additional-reading/{hot-reloading-rails-development.md → hot-reloading-rails-development-asset-pipeline.md} +2 -12
  14. data/docs/additional-reading/images.md +1 -1
  15. data/docs/additional-reading/rails_view_rendering_from_inline_javascript.md +2 -1
  16. data/docs/additional-reading/upgrade-webpacker-v3-to-v4.md +10 -0
  17. data/docs/api/view-helpers-api.md +12 -8
  18. data/docs/basics/configuration.md +4 -4
  19. data/docs/basics/generator-details.md +1 -1
  20. data/docs/basics/minitest-configuration.md +31 -0
  21. data/docs/basics/react-server-rendering.md +3 -1
  22. data/docs/basics/recommended-project-structure.md +24 -1
  23. data/docs/basics/rspec-configuration.md +2 -2
  24. data/docs/basics/upgrading-react-on-rails.md +11 -1
  25. data/docs/basics/webpack-configuration.md +11 -0
  26. data/docs/misc-pending/code-splitting.md +2 -2
  27. data/docs/misc-pending/manual-installation-overview.md +1 -1
  28. data/docs/testimonials/hvmn.md +25 -0
  29. data/docs/testimonials/resortpass.md +13 -0
  30. data/docs/testimonials/testimonials.md +28 -0
  31. data/docs/tutorial.md +123 -11
  32. data/lib/generators/react_on_rails/dev_tests_generator.rb +1 -0
  33. data/lib/generators/react_on_rails/install_generator.rb +2 -0
  34. data/lib/react_on_rails.rb +1 -0
  35. data/lib/react_on_rails/assets_precompile.rb +3 -0
  36. data/lib/react_on_rails/configuration.rb +2 -1
  37. data/lib/react_on_rails/git_utils.rb +2 -0
  38. data/lib/react_on_rails/helper.rb +71 -70
  39. data/lib/react_on_rails/json_output.rb +1 -1
  40. data/lib/react_on_rails/locales_to_js.rb +2 -0
  41. data/lib/react_on_rails/react_component/render_options.rb +4 -0
  42. data/lib/react_on_rails/server_rendering_js_code.rb +42 -0
  43. data/lib/react_on_rails/server_rendering_pool/ruby_embedded_java_script.rb +44 -14
  44. data/lib/react_on_rails/utils.rb +5 -1
  45. data/lib/react_on_rails/version.rb +1 -1
  46. data/lib/react_on_rails/version_checker.rb +4 -1
  47. data/lib/react_on_rails/webpacker_utils.rb +9 -1
  48. data/package-scripts.yml +46 -0
  49. data/package.json +24 -15
  50. data/rakelib/release.rake +3 -2
  51. data/react_on_rails.gemspec +1 -1
  52. data/webpackConfigLoader.js +2 -2
  53. data/yarn.lock +4431 -1689
  54. metadata +18 -10
  55. data/docs/testimonials.md +0 -11
@@ -36,6 +36,7 @@ module ReactOnRails
36
36
 
37
37
  def replace_prerender_if_server_rendering
38
38
  return unless options.example_server_rendering
39
+
39
40
  hello_world_index = File.join(destination_root, "app", "views", "hello_world", "index.html.erb")
40
41
  hello_world_contents = File.read(hello_world_index)
41
42
  new_hello_world_contents = hello_world_contents.gsub(/prerender: false/,
@@ -62,6 +62,7 @@ module ReactOnRails
62
62
 
63
63
  def missing_yarn?
64
64
  return false unless ReactOnRails::Utils.running_on_windows? ? `where yarn`.blank? : `which yarn`.blank?
65
+
65
66
  error = "yarn is required. Please install it before continuing. https://yarnpkg.com/en/docs/install"
66
67
  GeneratorMessages.add_error(error)
67
68
  true
@@ -69,6 +70,7 @@ module ReactOnRails
69
70
 
70
71
  def missing_node?
71
72
  return false unless ReactOnRails::Utils.running_on_windows? ? `where node`.blank? : `which node`.blank?
73
+
72
74
  error = "** nodejs is required. Please install it before continuing. https://nodejs.org/en/"
73
75
  GeneratorMessages.add_error(error)
74
76
  true
@@ -11,6 +11,7 @@ require "react_on_rails/version"
11
11
  require "react_on_rails/version_checker"
12
12
  require "react_on_rails/configuration"
13
13
  require "react_on_rails/server_rendering_pool"
14
+ require "react_on_rails/server_rendering_js_code"
14
15
  require "react_on_rails/engine"
15
16
  require "react_on_rails/react_component/render_options"
16
17
  require "react_on_rails/version_syntax_converter"
@@ -64,6 +64,7 @@ module ReactOnRails
64
64
  # references from webpack's CSS would be invalid. The fix is to symlink the double-digested
65
65
  # file back to the original digested name, and make a similar symlink for the gz version.
66
66
  return unless @symlink_non_digested_assets_regex
67
+
67
68
  manifest_glob = Dir.glob(@assets_path.join(".sprockets-manifest-*.json")) +
68
69
  Dir.glob(@assets_path.join("manifest-*.json")) +
69
70
  Dir.glob(@assets_path.join("manifest.yml"))
@@ -86,6 +87,7 @@ module ReactOnRails
86
87
  manifest_data.each do |original_filename, rails_digested_filename|
87
88
  # TODO: we should remove any original_filename that is NOT in the webpack deploy folder.
88
89
  next unless original_filename =~ @symlink_non_digested_assets_regex
90
+
89
91
  # We're symlinking from the digested filename back to the original filename which has
90
92
  # already been symlinked by Webpack
91
93
  symlink_file(rails_digested_filename, original_filename)
@@ -100,6 +102,7 @@ module ReactOnRails
100
102
  def delete_broken_symlinks
101
103
  Dir.glob(@assets_path.join("*")).each do |filename|
102
104
  next unless File.lstat(filename).symlink?
105
+
103
106
  begin
104
107
  target = File.readlink(filename)
105
108
  rescue StandardError
@@ -21,7 +21,7 @@ module ReactOnRails
21
21
  prerender: false,
22
22
  replay_console: true,
23
23
  logging_on_server: true,
24
- raise_on_prerender_error: false,
24
+ raise_on_prerender_error: Rails.env.development?,
25
25
  trace: Rails.env.development?,
26
26
  development_mode: Rails.env.development?,
27
27
  server_renderer_pool_size: DEFAULT_POOL_SIZE,
@@ -218,6 +218,7 @@ module ReactOnRails
218
218
 
219
219
  def configure_skip_display_none_deprecation
220
220
  return if skip_display_none.nil?
221
+
221
222
  Rails.logger.warn "[DEPRECATION] ReactOnRails: remove skip_display_none from configuration."
222
223
  end
223
224
  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
@@ -136,7 +136,7 @@ module ReactOnRails
136
136
  # It is exactly like react_component except for the following:
137
137
  # 1. prerender: true is automatically added, as this method doesn't make sense for client only
138
138
  # rendering.
139
- # 2. Your JavaScript for server rendering must return an Object for the key server_rendered_html.
139
+ # 2. Your JavaScript generator function for server rendering must return an Object rather than a React component.
140
140
  # 3. Your view code must expect an object and not a string.
141
141
  #
142
142
  # Here is an example of the view code:
@@ -206,6 +206,7 @@ module ReactOnRails
206
206
  # that contains a data props.
207
207
  def redux_store_hydration_data
208
208
  return if @registered_stores_defer_render.blank?
209
+
209
210
  @registered_stores_defer_render.reduce("".dup) do |accum, redux_store_data|
210
211
  accum << render_redux_store_data(redux_store_data)
211
212
  end.html_safe
@@ -265,6 +266,7 @@ module ReactOnRails
265
266
 
266
267
  def json_safe_and_pretty(hash_or_string)
267
268
  return "{}" if hash_or_string.nil?
269
+
268
270
  unless hash_or_string.is_a?(String) || hash_or_string.is_a?(Hash)
269
271
  raise ReactOnRails::Error, "#{__method__} only accepts String or Hash as argument "\
270
272
  "(#{hash_or_string.class} given)."
@@ -275,6 +277,59 @@ module ReactOnRails
275
277
  ReactOnRails::JsonOutput.escape(json_value)
276
278
  end
277
279
 
280
+ # This is the definitive list of the default values used for the rails_context, which is the
281
+ # second parameter passed to both component and store generator functions.
282
+ # This method can be called from views and from the controller, as `helpers.rails_context`
283
+ #
284
+ # rubocop:disable Metrics/AbcSize
285
+ def rails_context(server_side: true)
286
+ @rails_context ||= begin
287
+ result = {
288
+ railsEnv: Rails.env,
289
+ inMailer: in_mailer?,
290
+ # Locale settings
291
+ i18nLocale: I18n.locale,
292
+ i18nDefaultLocale: I18n.default_locale,
293
+ rorVersion: ReactOnRails::VERSION,
294
+ rorPro: ReactOnRails::Utils.react_on_rails_pro?
295
+ }
296
+ if defined?(request) && request.present?
297
+ # Check for encoding of the request's original_url and try to force-encoding the
298
+ # URLs as UTF-8. This situation can occur in browsers that do not encode the
299
+ # entire URL as UTF-8 already, mostly on the Windows platform (IE11 and lower).
300
+ original_url_normalized = request.original_url
301
+ if original_url_normalized.encoding.to_s == "ASCII-8BIT"
302
+ original_url_normalized = original_url_normalized.force_encoding("ISO-8859-1").encode("UTF-8")
303
+ end
304
+
305
+ # Using Addressable instead of standard URI to better deal with
306
+ # non-ASCII characters (see https://github.com/shakacode/react_on_rails/pull/405)
307
+ uri = Addressable::URI.parse(original_url_normalized)
308
+ # uri = Addressable::URI.parse("http://foo.com:3000/posts?id=30&limit=5#time=1305298413")
309
+
310
+ result.merge!(
311
+ # URL settings
312
+ href: uri.to_s,
313
+ location: "#{uri.path}#{uri.query.present? ? "?#{uri.query}" : ''}",
314
+ scheme: uri.scheme, # http
315
+ host: uri.host, # foo.com
316
+ port: uri.port,
317
+ pathname: uri.path, # /posts
318
+ search: uri.query, # id=30&limit=5
319
+ httpAcceptLanguage: request.env["HTTP_ACCEPT_LANGUAGE"]
320
+ )
321
+ end
322
+ if ReactOnRails.configuration.rendering_extension
323
+ custom_context = ReactOnRails.configuration.rendering_extension.custom_context(self)
324
+ result.merge!(custom_context) if custom_context
325
+ end
326
+ result
327
+ end
328
+
329
+ @rails_context.merge(serverSide: server_side)
330
+ end
331
+ # rubocop:enable Metrics/AbcSize
332
+
278
333
  private
279
334
 
280
335
  def build_react_component_result_for_server_rendered_string(
@@ -284,9 +339,15 @@ module ReactOnRails
284
339
  render_options: required("render_options")
285
340
  )
286
341
  content_tag_options = render_options.html_options
342
+ if content_tag_options.key?(:tag)
343
+ content_tag_options_html_tag = content_tag_options[:tag]
344
+ content_tag_options.delete(:tag)
345
+ else
346
+ content_tag_options_html_tag = "div"
347
+ end
287
348
  content_tag_options[:id] = render_options.dom_id
288
349
 
289
- rendered_output = content_tag(:div,
350
+ rendered_output = content_tag(content_tag_options_html_tag.to_sym,
290
351
  server_rendered_html.html_safe,
291
352
  content_tag_options)
292
353
 
@@ -431,22 +492,13 @@ module ReactOnRails
431
492
  #
432
493
  # Read more here: http://timelessrepo.com/json-isnt-a-javascript-subset
433
494
 
434
- # rubocop:disable Layout/IndentHeredoc
435
- js_code = <<-JS
436
- (function() {
437
- var railsContext = #{rails_context(server_side: true).to_json};
438
- #{initialize_redux_stores}
439
- var props = #{props_string(props).gsub("\u2028", '\u2028').gsub("\u2029", '\u2029')};
440
- return ReactOnRails.serverRenderReactComponent({
441
- name: '#{react_component_name}',
442
- domNodeId: '#{render_options.dom_id}',
443
- props: props,
444
- trace: #{render_options.trace},
445
- railsContext: railsContext
446
- });
447
- })()
448
- JS
449
- # rubocop:enable Layout/IndentHeredoc
495
+ js_code = ReactOnRails::ServerRenderingJsCode.server_rendering_component_js_code(
496
+ props_string: props_string(props).gsub("\u2028", '\u2028').gsub("\u2029", '\u2029'),
497
+ rails_context: rails_context(server_side: true).to_json,
498
+ redux_stores: initialize_redux_stores,
499
+ react_component_name: react_component_name,
500
+ render_options: render_options
501
+ )
450
502
 
451
503
  begin
452
504
  result = ReactOnRails::ServerRenderingPool.server_render_js_with_console_logging(js_code, render_options)
@@ -478,6 +530,7 @@ module ReactOnRails
478
530
  JS
479
531
 
480
532
  return result unless @registered_stores.present? || @registered_stores_defer_render.present?
533
+
481
534
  declarations = "var reduxProps, store, storeGenerator;\n".dup
482
535
  all_stores = (@registered_stores || []) + (@registered_stores_defer_render || [])
483
536
 
@@ -494,58 +547,6 @@ module ReactOnRails
494
547
  result
495
548
  end
496
549
 
497
- # This is the definitive list of the default values used for the rails_context, which is the
498
- # second parameter passed to both component and store generator functions.
499
- # rubocop:disable Metrics/AbcSize
500
- def rails_context(server_side: required("server_side"))
501
- @rails_context ||= begin
502
- result = {
503
- railsEnv: Rails.env,
504
- inMailer: in_mailer?,
505
- # Locale settings
506
- i18nLocale: I18n.locale,
507
- i18nDefaultLocale: I18n.default_locale,
508
- rorVersion: ReactOnRails::VERSION,
509
- rorPro: ReactOnRails::Utils.react_on_rails_pro?
510
- }
511
- if defined?(request) && request.present?
512
- # Check for encoding of the request's original_url and try to force-encoding the
513
- # URLs as UTF-8. This situation can occur in browsers that do not encode the
514
- # entire URL as UTF-8 already, mostly on the Windows platform (IE11 and lower).
515
- original_url_normalized = request.original_url
516
- if original_url_normalized.encoding.to_s == "ASCII-8BIT"
517
- original_url_normalized = original_url_normalized.force_encoding("ISO-8859-1").encode("UTF-8")
518
- end
519
-
520
- # Using Addressable instead of standard URI to better deal with
521
- # non-ASCII characters (see https://github.com/shakacode/react_on_rails/pull/405)
522
- uri = Addressable::URI.parse(original_url_normalized)
523
- # uri = Addressable::URI.parse("http://foo.com:3000/posts?id=30&limit=5#time=1305298413")
524
-
525
- result.merge!(
526
- # URL settings
527
- href: uri.to_s,
528
- location: "#{uri.path}#{uri.query.present? ? "?#{uri.query}" : ''}",
529
- scheme: uri.scheme, # http
530
- host: uri.host, # foo.com
531
- port: uri.port,
532
- pathname: uri.path, # /posts
533
- search: uri.query, # id=30&limit=5
534
- httpAcceptLanguage: request.env["HTTP_ACCEPT_LANGUAGE"]
535
- )
536
- end
537
- if ReactOnRails.configuration.rendering_extension
538
- custom_context = ReactOnRails.configuration.rendering_extension.custom_context(self)
539
- result.merge!(custom_context) if custom_context
540
- end
541
- result
542
- end
543
-
544
- @rails_context.merge(serverSide: server_side)
545
- end
546
-
547
- # rubocop:enable Metrics/AbcSize
548
-
549
550
  def replay_console_option(val)
550
551
  val.nil? ? ReactOnRails.configuration.replay_console : val
551
552
  end
@@ -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
@@ -7,6 +7,7 @@ module ReactOnRails
7
7
  def initialize
8
8
  return if i18n_dir.nil?
9
9
  return unless obsolete?
10
+
10
11
  @translations, @defaults = generate_translations
11
12
  convert
12
13
  end
@@ -15,6 +16,7 @@ module ReactOnRails
15
16
 
16
17
  def obsolete?
17
18
  return true if exist_js_files.empty?
19
+
18
20
  js_files_are_outdated
19
21
  end
20
22
 
@@ -72,6 +72,10 @@ module ReactOnRails
72
72
  "{ react_component_name = #{react_component_name}, options = #{options}, request_digest = #{request_digest}"
73
73
  end
74
74
 
75
+ def internal_option(key)
76
+ options[key]
77
+ end
78
+
75
79
  private
76
80
 
77
81
  attr_reader :options
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ReactOnRails
4
+ module ServerRenderingJsCode
5
+ class << self
6
+ def js_code_renderer
7
+ @js_code_renderer ||= if ReactOnRails::Utils.react_on_rails_pro?
8
+ ReactOnRailsPro::ServerRenderingJsCode
9
+ else
10
+ self
11
+ end
12
+ end
13
+
14
+ def server_rendering_component_js_code(
15
+ props_string: nil,
16
+ rails_context: nil,
17
+ redux_stores: nil,
18
+ react_component_name: nil,
19
+ render_options: nil
20
+ )
21
+ js_code_renderer.render(props_string, rails_context, redux_stores, react_component_name, render_options)
22
+ end
23
+
24
+ def render(props_string, rails_context, redux_stores, react_component_name, render_options)
25
+ <<-JS
26
+ (function() {
27
+ var railsContext = #{rails_context};
28
+ #{redux_stores}
29
+ var props = #{props_string};
30
+ return ReactOnRails.serverRenderReactComponent({
31
+ name: '#{react_component_name}',
32
+ domNodeId: '#{render_options.dom_id}',
33
+ props: props,
34
+ trace: #{render_options.trace},
35
+ railsContext: railsContext
36
+ });
37
+ })()
38
+ JS
39
+ end
40
+ end
41
+ end
42
+ end
@@ -4,7 +4,9 @@ require "open-uri"
4
4
 
5
5
  module ReactOnRails
6
6
  module ServerRenderingPool
7
+ # rubocop:disable Metrics/ClassLength
7
8
  class RubyEmbeddedJavaScript
9
+ # rubocop:enable Metrics/ClassLength
8
10
  class << self
9
11
  def reset_pool
10
12
  options = {
@@ -17,12 +19,17 @@ module ReactOnRails
17
19
  def reset_pool_if_server_bundle_was_modified
18
20
  return unless ReactOnRails.configuration.development_mode
19
21
 
20
- file_mtime = File.mtime(ReactOnRails::Utils.server_bundle_js_file_path)
21
- @server_bundle_timestamp ||= file_mtime
22
- return if @server_bundle_timestamp == file_mtime
22
+ if ReactOnRails::Utils.server_bundle_path_is_http?
23
+ return if @server_bundle_url == ReactOnRails::Utils.server_bundle_js_file_path
23
24
 
24
- @server_bundle_timestamp = file_mtime
25
+ @server_bundle_url = ReactOnRails::Utils.server_bundle_js_file_path
26
+ else
27
+ file_mtime = File.mtime(ReactOnRails::Utils.server_bundle_js_file_path)
28
+ @server_bundle_timestamp ||= file_mtime
29
+ return if @server_bundle_timestamp == file_mtime
25
30
 
31
+ @server_bundle_timestamp = file_mtime
32
+ end
26
33
  ReactOnRails::ServerRenderingPool.reset_pool
27
34
  end
28
35
 
@@ -72,6 +79,7 @@ module ReactOnRails
72
79
 
73
80
  def trace_js_code_used(msg, js_code, file_name = "tmp/server-generated.js", force: false)
74
81
  return unless ReactOnRails.configuration.trace || force
82
+
75
83
  # Set to anything to print generated code.
76
84
  File.write(file_name, js_code)
77
85
  msg = <<-MSG.strip_heredoc
@@ -95,19 +103,25 @@ module ReactOnRails
95
103
  end
96
104
  end
97
105
 
106
+ def read_bundle_js_code
107
+ server_js_file = ReactOnRails::Utils.server_bundle_js_file_path
108
+ if ReactOnRails::Utils.server_bundle_path_is_http?
109
+ file_url_to_string(server_js_file)
110
+ else
111
+ File.read(server_js_file)
112
+ end
113
+ rescue StandardError => e
114
+ msg = "You specified server rendering JS file: #{server_js_file}, but it cannot be "\
115
+ "read. You may set the server_bundle_js_file in your configuration to be \"\" to "\
116
+ "avoid this warning.\nError is: #{e}"
117
+ raise ReactOnRails::Error, msg
118
+ end
119
+
98
120
  def create_js_context
99
121
  return if ReactOnRails.configuration.server_bundle_js_file.blank?
100
122
 
101
- server_js_file = ReactOnRails::Utils.server_bundle_js_file_path
123
+ bundle_js_code = read_bundle_js_code
102
124
 
103
- begin
104
- bundle_js_code = File.read(server_js_file)
105
- rescue StandardError => e
106
- msg = "You specified server rendering JS file: #{server_js_file}, but it cannot be "\
107
- "read. You may set the server_bundle_js_file in your configuration to be \"\" to "\
108
- "avoid this warning.\nError is: #{e}"
109
- raise ReactOnRails::Error, msg
110
- end
111
125
  # rubocop:disable Layout/IndentHeredoc
112
126
  base_js_code = <<-JS
113
127
  #{console_polyfill}
@@ -118,7 +132,10 @@ module ReactOnRails
118
132
  file_name = "tmp/base_js_code.js"
119
133
  begin
120
134
  if ReactOnRails.configuration.trace
121
- Rails.logger.info { "[react_on_rails] Created JavaScript context with file #{server_js_file}" }
135
+ Rails.logger.info do
136
+ "[react_on_rails] Created JavaScript context with file "\
137
+ "#{ReactOnRails::Utils.server_bundle_js_file_path}"
138
+ end
122
139
  end
123
140
  ExecJS.compile(base_js_code)
124
141
  rescue StandardError => e
@@ -189,6 +206,19 @@ var console = { history: [] };
189
206
  JS
190
207
  # rubocop:enable Layout/IndentHeredoc
191
208
  end
209
+
210
+ private
211
+
212
+ def file_url_to_string(url)
213
+ response = Net::HTTP.get_response(URI.parse(url))
214
+ content_type_header = response["content-type"]
215
+ match = content_type_header.match(/\A.*; charset=(?<encoding>.*)\z/)
216
+ encoding_type = match[:encoding]
217
+ response.body.force_encoding(encoding_type)
218
+ rescue StandardError => e
219
+ msg = "file_url_to_string #{url} failed\nError is: #{e}"
220
+ raise ReactOnRails::Error, msg
221
+ end
192
222
  end
193
223
  end
194
224
  end