react_on_rails 11.1.7 → 11.3.1.beta.0

Sign up to get free protection for your applications and to get access to all the features.
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