react_on_rails 16.2.0.beta.3 → 16.2.0.beta.8

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 (104) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +42 -5
  3. data/CLAUDE.md +59 -0
  4. data/CONTRIBUTING.md +49 -1
  5. data/Gemfile.development_dependencies +1 -1
  6. data/Gemfile.lock +25 -10
  7. data/SWITCHING_CI_CONFIGS.md +55 -6
  8. data/Steepfile +51 -0
  9. data/bin/ci-rerun-failures +68 -22
  10. data/bin/ci-run-failed-specs +26 -2
  11. data/bin/ci-switch-config +262 -34
  12. data/bin/lefthook/check-trailing-newlines +2 -12
  13. data/bin/lefthook/eslint-lint +0 -10
  14. data/bin/lefthook/prettier-format +0 -10
  15. data/bin/lefthook/ruby-autofix +3 -6
  16. data/knip.ts +35 -9
  17. data/lib/generators/react_on_rails/templates/base/base/config/initializers/react_on_rails.rb.tt +32 -52
  18. data/lib/generators/react_on_rails/templates/base/base/config/shakapacker.yml +5 -1
  19. data/lib/react_on_rails/configuration.rb +56 -12
  20. data/lib/react_on_rails/controller.rb +3 -3
  21. data/lib/react_on_rails/dev/server_manager.rb +11 -4
  22. data/lib/react_on_rails/doctor.rb +249 -2
  23. data/lib/react_on_rails/helper.rb +12 -3
  24. data/lib/react_on_rails/pro_helper.rb +2 -44
  25. data/lib/react_on_rails/react_component/render_options.rb +7 -7
  26. data/lib/react_on_rails/utils.rb +40 -0
  27. data/lib/react_on_rails/version.rb +1 -1
  28. data/react_on_rails_pro/CHANGELOG.md +142 -29
  29. data/react_on_rails_pro/CONTRIBUTING.md +2 -13
  30. data/react_on_rails_pro/Gemfile.development_dependencies +1 -0
  31. data/react_on_rails_pro/Gemfile.lock +24 -3
  32. data/react_on_rails_pro/README.md +559 -38
  33. data/react_on_rails_pro/docs/code-splitting-loadable-components.md +1 -1
  34. data/react_on_rails_pro/docs/contributors-info/releasing.md +2 -2
  35. data/react_on_rails_pro/docs/installation.md +129 -109
  36. data/react_on_rails_pro/docs/node-renderer/basics.md +29 -22
  37. data/react_on_rails_pro/docs/node-renderer/error-reporting-and-tracing.md +8 -8
  38. data/react_on_rails_pro/docs/node-renderer/js-configuration.md +25 -23
  39. data/react_on_rails_pro/docs/node-renderer/troubleshooting.md +2 -0
  40. data/react_on_rails_pro/docs/updating.md +209 -15
  41. data/react_on_rails_pro/lib/react_on_rails_pro/concerns/stream.rb +58 -4
  42. data/react_on_rails_pro/lib/react_on_rails_pro/configuration.rb +17 -3
  43. data/react_on_rails_pro/lib/react_on_rails_pro/license_public_key.rb +9 -9
  44. data/react_on_rails_pro/lib/react_on_rails_pro/request.rb +41 -25
  45. data/react_on_rails_pro/lib/react_on_rails_pro/stream_request.rb +27 -7
  46. data/react_on_rails_pro/lib/react_on_rails_pro/utils.rb +3 -3
  47. data/react_on_rails_pro/lib/react_on_rails_pro/version.rb +1 -1
  48. data/react_on_rails_pro/package-scripts.yml +1 -1
  49. data/react_on_rails_pro/package.json +5 -8
  50. data/react_on_rails_pro/packages/node-renderer/src/integrations/api.ts +1 -1
  51. data/react_on_rails_pro/packages/node-renderer/src/master/restartWorkers.ts +39 -17
  52. data/react_on_rails_pro/packages/node-renderer/src/master.ts +15 -4
  53. data/react_on_rails_pro/packages/node-renderer/src/shared/configBuilder.ts +44 -5
  54. data/react_on_rails_pro/packages/node-renderer/src/shared/utils.ts +4 -2
  55. data/react_on_rails_pro/packages/node-renderer/src/worker/handleGracefulShutdown.ts +49 -0
  56. data/react_on_rails_pro/packages/node-renderer/src/worker/vm.ts +3 -3
  57. data/react_on_rails_pro/packages/node-renderer/src/worker.ts +5 -2
  58. data/react_on_rails_pro/packages/node-renderer/tests/helper.ts +8 -8
  59. data/react_on_rails_pro/packages/node-renderer/tests/testingNodeRendererConfigs.js +1 -1
  60. data/react_on_rails_pro/packages/node-renderer/tests/worker.test.ts +19 -19
  61. data/react_on_rails_pro/rakelib/public_key_management.rake +6 -5
  62. data/react_on_rails_pro/rakelib/rbs.rake +47 -0
  63. data/react_on_rails_pro/react_on_rails_pro.gemspec +1 -0
  64. data/react_on_rails_pro/sig/react_on_rails_pro/cache.rbs +13 -0
  65. data/react_on_rails_pro/sig/react_on_rails_pro/configuration.rbs +100 -0
  66. data/react_on_rails_pro/sig/react_on_rails_pro/error.rbs +4 -0
  67. data/react_on_rails_pro/sig/react_on_rails_pro/utils.rbs +7 -0
  68. data/react_on_rails_pro/sig/react_on_rails_pro.rbs +5 -0
  69. data/react_on_rails_pro/spec/dummy/Gemfile.lock +23 -3
  70. data/react_on_rails_pro/spec/dummy/app/controllers/pages_controller.rb +3 -3
  71. data/react_on_rails_pro/spec/dummy/bin/dev +4 -8
  72. data/react_on_rails_pro/spec/dummy/client/node-renderer.js +4 -4
  73. data/react_on_rails_pro/spec/dummy/config/environments/production.rb +1 -1
  74. data/react_on_rails_pro/spec/dummy/config/initializers/react_on_rails.rb +28 -12
  75. data/react_on_rails_pro/spec/dummy/config.ru +1 -1
  76. data/react_on_rails_pro/spec/dummy/package.json +2 -2
  77. data/react_on_rails_pro/spec/dummy/spec/helpers/react_on_rails_pro_helper_spec.rb +40 -11
  78. data/react_on_rails_pro/spec/dummy/spec/rails_helper.rb +1 -1
  79. data/react_on_rails_pro/spec/dummy/spec/requests/renderer_console_logging_spec.rb +5 -5
  80. data/react_on_rails_pro/spec/dummy/spec/system/integration_spec.rb +15 -10
  81. data/react_on_rails_pro/spec/dummy/spec/system/renderer_integration_spec.rb +3 -3
  82. data/react_on_rails_pro/spec/dummy/yarn.lock +4 -4
  83. data/react_on_rails_pro/spec/execjs-compatible-dummy/config/environments/production.rb +1 -1
  84. data/react_on_rails_pro/spec/execjs-compatible-dummy/config/initializers/react_on_rails.rb +16 -43
  85. data/react_on_rails_pro/spec/react_on_rails_pro/assets_precompile_spec.rb +15 -18
  86. data/react_on_rails_pro/spec/react_on_rails_pro/cache_spec.rb +1 -1
  87. data/react_on_rails_pro/spec/react_on_rails_pro/configuration_spec.rb +5 -3
  88. data/react_on_rails_pro/spec/react_on_rails_pro/license_validator_spec.rb +27 -12
  89. data/react_on_rails_pro/spec/react_on_rails_pro/request_spec.rb +0 -27
  90. data/react_on_rails_pro/spec/react_on_rails_pro/spec_helper.rb +1 -1
  91. data/react_on_rails_pro/spec/react_on_rails_pro/stream_decorator_spec.rb +89 -0
  92. data/react_on_rails_pro/spec/react_on_rails_pro/stream_spec.rb +144 -0
  93. data/react_on_rails_pro/spec/react_on_rails_pro/support/caching.rb +1 -1
  94. data/react_on_rails_pro/spec/react_on_rails_pro/support/mock_block_helper.rb +4 -2
  95. data/sig/react_on_rails/controller.rbs +1 -1
  96. data/sig/react_on_rails/error.rbs +4 -0
  97. data/sig/react_on_rails/helper.rbs +2 -2
  98. data/sig/react_on_rails/json_parse_error.rbs +10 -0
  99. data/sig/react_on_rails/prerender_error.rbs +21 -0
  100. data/sig/react_on_rails/smart_error.rbs +28 -0
  101. data/sig/react_on_rails.rbs +3 -24
  102. metadata +14 -4
  103. data/lib/react_on_rails/pro_utils.rb +0 -37
  104. data/react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/TestingStreamableComponent.jsx +0 -15
@@ -1,67 +1,47 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # See https://github.com/shakacode/react_on_rails/blob/master/docs/guides/configuration.md
4
- # for many more options.
3
+ # React on Rails configuration
4
+ # See https://github.com/shakacode/react_on_rails/blob/master/docs/api-reference/configuration.md
5
+ # for complete documentation of all configuration options.
5
6
 
6
7
  ReactOnRails.configure do |config|
7
- # This configures the script to run to build the production assets by webpack. Set this to nil
8
- # if you don't want react_on_rails building this file for you.
9
- # If nil, then the standard shakacode/shakapacker assets:precompile will run
10
- # config.build_production_command = nil
11
-
12
- ################################################################################
13
8
  ################################################################################
14
- # TEST CONFIGURATION OPTIONS
15
- # Below options are used with the use of this test helper:
16
- # ReactOnRails::TestHelper.configure_rspec_to_compile_assets(config)
9
+ # Server Rendering (Recommended)
17
10
  ################################################################################
18
-
19
- # If you are using this in your spec_helper.rb (or rails_helper.rb):
20
- #
21
- # ReactOnRails::TestHelper.configure_rspec_to_compile_assets(config)
22
- #
23
- # with rspec then this controls what npm command is run
24
- # to automatically refresh your webpack assets on every test run.
25
- #
26
- # Alternately, you can remove the `ReactOnRails::TestHelper.configure_rspec_to_compile_assets`
27
- # and set the config/shakapacker.yml option for test to true.
28
- config.build_test_command = "RAILS_ENV=test bin/shakapacker"
11
+ # Configure server bundle for server-side rendering with `prerender: true`
12
+ # Set to "" if you're not using server rendering
13
+ config.server_bundle_js_file = "server-bundle.js"
29
14
 
30
15
  ################################################################################
16
+ # Test Configuration (Optional)
31
17
  ################################################################################
32
- # SERVER RENDERING OPTIONS
33
- ################################################################################
34
- # This is the file used for server rendering of React when using `(prerender: true)`
35
- # If you are never using server rendering, you should set this to "".
36
- # Note, there is only one server bundle, unlike JavaScript where you want to minimize the size
37
- # of the JS sent to the client. For the server rendering, React on Rails creates a pool of
38
- # JavaScript execution instances which should handle any component requested.
18
+ # ⚠️ IMPORTANT: Two mutually exclusive approaches - use ONLY ONE:
39
19
  #
40
- # While you may configure this to be the same as your client bundle file, this file is typically
41
- # different. You should have ONE server bundle which can create all of your server rendered
42
- # React components.
20
+ # RECOMMENDED APPROACH: Set `compile: true` in config/shakapacker.yml test section
21
+ # - Simpler configuration (no additional setup needed)
22
+ # - Handled automatically by Shakapacker
43
23
  #
44
- config.server_bundle_js_file = "server-bundle.js"
45
-
46
- # Configure where server bundles are output. Defaults to "ssr-generated".
47
- # This should match your webpack configuration for server bundles.
48
- config.server_bundle_output_path = "ssr-generated"
49
-
50
- # Enforce that server bundles are only loaded from private (non-public) directories.
51
- # When true, server bundles will only be loaded from the configured server_bundle_output_path.
52
- # This is recommended for production to prevent server-side code from being exposed.
53
- config.enforce_private_server_bundles = true
24
+ # ALTERNATIVE APPROACH: Uncomment below AND configure ReactOnRails::TestHelper
25
+ # - Provides explicit control over test asset compilation timing
26
+ # - Requires adding ReactOnRails::TestHelper to spec/rails_helper.rb
27
+ # - See: https://github.com/shakacode/react_on_rails/blob/master/docs/guides/testing-configuration.md
28
+ #
29
+ config.build_test_command = "RAILS_ENV=test bin/shakapacker"
54
30
 
55
- ################################################################################
56
- ################################################################################
57
- # FILE SYSTEM BASED COMPONENT REGISTRY
58
- ################################################################################
59
- # `components_subdirectory` is the name of the matching directories that contain automatically registered components
60
- # for use in the Rails views. The default is nil, you can enable the feature by updating it in the next line.
31
+ config.auto_load_bundle = true
61
32
  config.components_subdirectory = "ror_components"
33
+ ################################################################################
34
+ # Advanced Configuration
35
+ ################################################################################
36
+ # Most configuration options have sensible defaults and don't need to be set.
37
+ # For advanced options including:
38
+ # - File-based component registry (components_subdirectory, auto_load_bundle)
39
+ # - Component loading strategies (async/defer/sync)
40
+ # - Server bundle security and organization
41
+ # - I18n configuration
42
+ # - Server rendering pool configuration
43
+ # - Custom rendering extensions
44
+ # - And more...
62
45
  #
63
- # For automated component registry, `render_component` view helper method tries to load bundle for component from
64
- # generated directory. default is false, you can pass option at the time of individual usage or update the default
65
- # in the following line
66
- config.auto_load_bundle = true
46
+ # See: https://github.com/shakacode/react_on_rails/blob/master/docs/api-reference/configuration.md
67
47
  end
@@ -55,6 +55,11 @@ default: &default
55
55
  # https://webpack.js.org/guides/build-performance/#avoid-production-specific-tooling
56
56
  useContentHash: false
57
57
 
58
+ # On-demand compilation of packs when modified. Defaults to false.
59
+ # Set to false if using bin/shakapacker-dev-server or bin/shakapacker --watch via Procfiles.
60
+ # Set to true only in test environment for on-demand compilation.
61
+ compile: false
62
+
58
63
  # Setting the asset host here will override Rails.application.config.asset_host.
59
64
  # Here, you can set different asset_host per environment. Note that
60
65
  # SHAKAPACKER_ASSET_HOST will override both configurations.
@@ -72,7 +77,6 @@ default: &default
72
77
 
73
78
  development:
74
79
  <<: *default
75
- compile: true
76
80
  compiler_strategy: mtime
77
81
 
78
82
  # Reference: https://webpack.js.org/configuration/dev-server/
@@ -41,8 +41,6 @@ module ReactOnRails
41
41
  components_subdirectory: nil,
42
42
  make_generated_server_bundle_the_entrypoint: false,
43
43
  defer_generated_component_packs: false,
44
- # React on Rails Pro (licensed) feature - enables immediate hydration of React components
45
- immediate_hydration: false,
46
44
  # Maximum time in milliseconds to wait for client-side component registration after page load.
47
45
  # If exceeded, an error will be thrown for server-side rendered components not registered on the client.
48
46
  # Set to 0 to disable the timeout and wait indefinitely for component registration.
@@ -64,10 +62,55 @@ module ReactOnRails
64
62
  :server_render_method, :random_dom_id, :auto_load_bundle,
65
63
  :same_bundle_for_client_and_server, :rendering_props_extension,
66
64
  :make_generated_server_bundle_the_entrypoint,
67
- :generated_component_packs_loading_strategy, :immediate_hydration,
65
+ :generated_component_packs_loading_strategy,
68
66
  :component_registry_timeout,
69
67
  :server_bundle_output_path, :enforce_private_server_bundles
70
68
 
69
+ # Class instance variable and mutex to track if deprecation warning has been shown
70
+ # Using mutex to ensure thread-safety in multi-threaded environments
71
+ @immediate_hydration_warned = false
72
+ @immediate_hydration_mutex = Mutex.new
73
+
74
+ class << self
75
+ attr_accessor :immediate_hydration_warned, :immediate_hydration_mutex
76
+ end
77
+
78
+ # Deprecated: immediate_hydration configuration has been removed
79
+ def immediate_hydration=(value)
80
+ warned = false
81
+ self.class.immediate_hydration_mutex.synchronize do
82
+ warned = self.class.immediate_hydration_warned
83
+ self.class.immediate_hydration_warned = true unless warned
84
+ end
85
+
86
+ return if warned
87
+
88
+ Rails.logger.warn <<~WARNING
89
+ [REACT ON RAILS] The 'config.immediate_hydration' configuration option is deprecated and no longer used.
90
+ Immediate hydration is now automatically enabled for React on Rails Pro users.
91
+ Please remove 'config.immediate_hydration = #{value}' from your config/initializers/react_on_rails.rb file.
92
+ See CHANGELOG.md for migration instructions.
93
+ WARNING
94
+ end
95
+
96
+ def immediate_hydration
97
+ warned = false
98
+ self.class.immediate_hydration_mutex.synchronize do
99
+ warned = self.class.immediate_hydration_warned
100
+ self.class.immediate_hydration_warned = true unless warned
101
+ end
102
+
103
+ return nil if warned
104
+
105
+ Rails.logger.warn <<~WARNING
106
+ [REACT ON RAILS] The 'config.immediate_hydration' configuration option is deprecated and no longer used.
107
+ Immediate hydration is now automatically enabled for React on Rails Pro users.
108
+ Please remove any references to 'config.immediate_hydration' from your config/initializers/react_on_rails.rb file.
109
+ See CHANGELOG.md for migration instructions.
110
+ WARNING
111
+ nil
112
+ end
113
+
71
114
  # rubocop:disable Metrics/AbcSize
72
115
  def initialize(node_modules_location: nil, server_bundle_js_file: nil, prerender: nil,
73
116
  replay_console: nil, make_generated_server_bundle_the_entrypoint: nil,
@@ -81,7 +124,7 @@ module ReactOnRails
81
124
  same_bundle_for_client_and_server: nil,
82
125
  i18n_dir: nil, i18n_yml_dir: nil, i18n_output_format: nil, i18n_yml_safe_load_options: nil,
83
126
  random_dom_id: nil, server_render_method: nil, rendering_props_extension: nil,
84
- components_subdirectory: nil, auto_load_bundle: nil, immediate_hydration: nil,
127
+ components_subdirectory: nil, auto_load_bundle: nil,
85
128
  component_registry_timeout: nil, server_bundle_output_path: nil, enforce_private_server_bundles: nil)
86
129
  self.node_modules_location = node_modules_location.present? ? node_modules_location : Rails.root
87
130
  self.generated_assets_dirs = generated_assets_dirs
@@ -122,7 +165,6 @@ module ReactOnRails
122
165
  self.auto_load_bundle = auto_load_bundle
123
166
  self.make_generated_server_bundle_the_entrypoint = make_generated_server_bundle_the_entrypoint
124
167
  self.defer_generated_component_packs = defer_generated_component_packs
125
- self.immediate_hydration = immediate_hydration
126
168
  self.generated_component_packs_loading_strategy = generated_component_packs_loading_strategy
127
169
  self.server_bundle_output_path = server_bundle_output_path
128
170
  self.enforce_private_server_bundles = enforce_private_server_bundles
@@ -154,9 +196,9 @@ module ReactOnRails
154
196
  raise ReactOnRails::Error, "component_registry_timeout must be a positive integer"
155
197
  end
156
198
 
157
- # rubocop:disable Metrics/CyclomaticComplexity
199
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
158
200
  def validate_generated_component_packs_loading_strategy
159
- # rubocop:enable Metrics/CyclomaticComplexity
201
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
160
202
 
161
203
  if defer_generated_component_packs
162
204
  if %i[async sync].include?(generated_component_packs_loading_strategy)
@@ -176,11 +218,13 @@ module ReactOnRails
176
218
  1. Use :defer or :sync loading strategy instead of :async
177
219
  2. Upgrade to Shakapacker v8.2.0 or above to enable async script loading
178
220
  MSG
179
- if generated_component_packs_loading_strategy.nil?
180
- # Use defer as the default to ensure generated component packs load and register
181
- # components before main bundle executes, avoiding race conditions with async loading
182
- self.generated_component_packs_loading_strategy = :defer
183
- elsif generated_component_packs_loading_strategy == :async && !PackerUtils.supports_async_loading?
221
+ if PackerUtils.supports_async_loading?
222
+ # Default based on Pro license: Pro users get :async, non-Pro users get :defer
223
+ self.generated_component_packs_loading_strategy ||= (Utils.react_on_rails_pro? ? :async : :defer)
224
+ elsif generated_component_packs_loading_strategy.nil?
225
+ Rails.logger.warn("**WARNING** #{msg}")
226
+ self.generated_component_packs_loading_strategy = :sync
227
+ elsif generated_component_packs_loading_strategy == :async
184
228
  raise ReactOnRails::Error, "**ERROR** #{msg}"
185
229
  end
186
230
 
@@ -9,13 +9,13 @@ module ReactOnRails
9
9
  # JavaScript code.
10
10
  # props: Named parameter props which is a Ruby Hash or JSON string which contains the properties
11
11
  # to pass to the redux store.
12
- # immediate_hydration: React on Rails Pro (licensed) feature. Pass as true if you wish to hydrate this
13
- # store immediately instead of waiting for the page to load.
12
+ # immediate_hydration: React on Rails Pro (licensed) feature. When nil (default), Pro users get
13
+ # immediate hydration, non-Pro users don't. Can be explicitly overridden.
14
14
  #
15
15
  # Be sure to include view helper `redux_store_hydration_data` at the end of your layout or view
16
16
  # or else there will be no client side hydration of your stores.
17
17
  def redux_store(store_name, props: {}, immediate_hydration: nil)
18
- immediate_hydration = ReactOnRails.configuration.immediate_hydration if immediate_hydration.nil?
18
+ immediate_hydration = ReactOnRails::Utils.normalize_immediate_hydration(immediate_hydration, store_name, "Store")
19
19
  redux_store_data = { store_name: store_name,
20
20
  props: props,
21
21
  immediate_hydration: immediate_hydration }
@@ -144,10 +144,11 @@ module ReactOnRails
144
144
  puts help_troubleshooting
145
145
  end
146
146
 
147
+ # rubocop:disable Metrics/AbcSize
147
148
  def run_from_command_line(args = ARGV)
148
149
  require "optparse"
149
150
 
150
- options = { route: nil, rails_env: nil }
151
+ options = { route: nil, rails_env: nil, verbose: false }
151
152
 
152
153
  OptionParser.new do |opts|
153
154
  opts.banner = "Usage: dev [command] [options]"
@@ -160,6 +161,10 @@ module ReactOnRails
160
161
  options[:rails_env] = env
161
162
  end
162
163
 
164
+ opts.on("-v", "--verbose", "Enable verbose output for pack generation") do
165
+ options[:verbose] = true
166
+ end
167
+
163
168
  opts.on("-h", "--help", "Prints this help") do
164
169
  show_help
165
170
  exit
@@ -172,21 +177,23 @@ module ReactOnRails
172
177
  # Main execution
173
178
  case command
174
179
  when "production-assets", "prod"
175
- start(:production_like, nil, verbose: false, route: options[:route], rails_env: options[:rails_env])
180
+ start(:production_like, nil, verbose: options[:verbose], route: options[:route],
181
+ rails_env: options[:rails_env])
176
182
  when "static"
177
- start(:static, "Procfile.dev-static-assets", verbose: false, route: options[:route])
183
+ start(:static, "Procfile.dev-static-assets", verbose: options[:verbose], route: options[:route])
178
184
  when "kill"
179
185
  kill_processes
180
186
  when "help", "--help", "-h"
181
187
  show_help
182
188
  when "hmr", nil
183
- start(:development, "Procfile.dev", verbose: false, route: options[:route])
189
+ start(:development, "Procfile.dev", verbose: options[:verbose], route: options[:route])
184
190
  else
185
191
  puts "Unknown argument: #{command}"
186
192
  puts "Run 'dev help' for usage information"
187
193
  exit 1
188
194
  end
189
195
  end
196
+ # rubocop:enable Metrics/AbcSize
190
197
 
191
198
  private
192
199
 
@@ -154,6 +154,7 @@ module ReactOnRails
154
154
  def check_configuration_details
155
155
  check_shakapacker_configuration_details
156
156
  check_react_on_rails_configuration_details
157
+ check_server_bundle_prerender_consistency
157
158
  end
158
159
 
159
160
  def check_bin_dev_launcher
@@ -166,6 +167,7 @@ module ReactOnRails
166
167
 
167
168
  def check_testing_setup
168
169
  check_rspec_helper_setup
170
+ check_build_test_configuration
169
171
  end
170
172
 
171
173
  def check_development
@@ -173,6 +175,7 @@ module ReactOnRails
173
175
  check_procfile_dev
174
176
  check_bin_dev_script
175
177
  check_gitignore
178
+ check_async_usage
176
179
  end
177
180
 
178
181
  def check_javascript_bundles
@@ -732,10 +735,12 @@ module ReactOnRails
732
735
  auto_load_match = content.match(/config\.auto_load_bundle\s*=\s*([^\s\n,]+)/)
733
736
  checker.add_info(" auto_load_bundle: #{auto_load_match[1]}") if auto_load_match
734
737
 
735
- # Immediate hydration (Pro feature)
738
+ # Deprecated immediate_hydration setting
736
739
  immediate_hydration_match = content.match(/config\.immediate_hydration\s*=\s*([^\s\n,]+)/)
737
740
  if immediate_hydration_match
738
- checker.add_info(" immediate_hydration: #{immediate_hydration_match[1]} (React on Rails Pro)")
741
+ checker.add_warning(" ⚠️ immediate_hydration: #{immediate_hydration_match[1]} (DEPRECATED)")
742
+ checker.add_info(" 💡 This setting is no longer used. Immediate hydration is now automatic for Pro users.")
743
+ checker.add_info(" 💡 Remove this line from your config/initializers/react_on_rails.rb file.")
739
744
  end
740
745
 
741
746
  # Component registry timeout
@@ -1108,6 +1113,124 @@ module ReactOnRails
1108
1113
  end
1109
1114
  end
1110
1115
 
1116
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
1117
+ def check_server_bundle_prerender_consistency
1118
+ config_path = "config/initializers/react_on_rails.rb"
1119
+ return unless File.exist?(config_path)
1120
+
1121
+ checker.add_info("\n🔍 Server Rendering Consistency:")
1122
+
1123
+ begin
1124
+ content = File.read(config_path)
1125
+
1126
+ # Check for server bundle configuration
1127
+ server_bundle_match = content.match(/config\.server_bundle_js_file\s*=\s*["']([^"']+)["']/)
1128
+ server_bundle_set = server_bundle_match && server_bundle_match[1].present?
1129
+
1130
+ # Check for global prerender setting
1131
+ prerender_match = content.match(/config\.prerender\s*=\s*(true)/)
1132
+ prerender_set = prerender_match
1133
+
1134
+ # Check if prerender is used in views
1135
+ uses_prerender = uses_prerender_in_views?
1136
+
1137
+ # Analyze the configuration
1138
+ if (prerender_set || uses_prerender) && !server_bundle_set
1139
+ checker.add_warning(" ⚠️ Server rendering is enabled but server_bundle_js_file is not configured")
1140
+ checker.add_info(" 💡 Set config.server_bundle_js_file = 'server-bundle.js' to enable SSR")
1141
+ checker.add_info(" 💡 See: https://www.shakacode.com/react-on-rails/docs/guides/server-rendering")
1142
+ elsif server_bundle_set && !prerender_set && !uses_prerender
1143
+ checker.add_info(" ℹ️ server_bundle_js_file is configured but prerender doesn't appear to be used")
1144
+ checker.add_info(" 💡 Either use prerender: true in react_component calls or remove server_bundle_js_file")
1145
+ else
1146
+ checker.add_success(" ✅ Server rendering configuration is consistent")
1147
+ end
1148
+ rescue StandardError => e
1149
+ checker.add_warning(" ⚠️ Could not analyze server rendering configuration: #{e.message}")
1150
+ end
1151
+ end
1152
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
1153
+
1154
+ def uses_prerender_in_views?
1155
+ # Check view files for prerender: true
1156
+ view_files = Dir.glob("app/views/**/*.{erb,haml,slim}")
1157
+ view_files.any? do |file|
1158
+ next unless File.exist?(file)
1159
+
1160
+ File.read(file).match?(/prerender:\s*true/)
1161
+ end
1162
+ rescue StandardError
1163
+ false
1164
+ end
1165
+
1166
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
1167
+ def check_build_test_configuration
1168
+ config_path = "config/initializers/react_on_rails.rb"
1169
+ shakapacker_yml = "config/shakapacker.yml"
1170
+
1171
+ return unless File.exist?(config_path)
1172
+
1173
+ checker.add_info("\n🧪 Test Asset Compilation:")
1174
+
1175
+ begin
1176
+ config_content = File.read(config_path)
1177
+ has_build_test_command = config_content.match(/^\s*config\.build_test_command\s*=\s*["']([^"']+)["']/)
1178
+ uses_test_helper = uses_react_on_rails_test_helper?
1179
+
1180
+ if File.exist?(shakapacker_yml)
1181
+ shakapacker_content = File.read(shakapacker_yml)
1182
+ # Match test section and look for compile: true
1183
+ has_compile_true = shakapacker_content.match(/^test:.*?^\s+compile:\s*true/m)
1184
+
1185
+ if has_build_test_command && has_compile_true
1186
+ checker.add_warning(" ⚠️ Both build_test_command and shakapacker compile: true are configured")
1187
+ checker.add_info(" 💡 These are mutually exclusive - use only one approach")
1188
+ checker.add_info(" 💡 Recommended: Use compile: true in shakapacker.yml (simpler)")
1189
+ checker.add_info(" 💡 Alternative: Use build_test_command with ReactOnRails::TestHelper (explicit control)")
1190
+ checker.add_info(" 📖 See: https://github.com/shakacode/react_on_rails/blob/master/docs/guides/testing-configuration.md")
1191
+ elsif has_build_test_command && !uses_test_helper
1192
+ checker.add_warning(" ⚠️ build_test_command is set but ReactOnRails::TestHelper is not configured")
1193
+ checker.add_info(" 💡 Add to spec/rails_helper.rb:")
1194
+ checker.add_info(" ReactOnRails::TestHelper.configure_rspec_to_compile_assets(config)")
1195
+ checker.add_info(" 💡 Or remove build_test_command and use compile: true in shakapacker.yml")
1196
+ elsif !has_build_test_command && uses_test_helper
1197
+ checker.add_error(" 🚫 ReactOnRails::TestHelper is configured but build_test_command is not set")
1198
+ checker.add_info(" 💡 Add to config/initializers/react_on_rails.rb:")
1199
+ checker.add_info(" config.build_test_command = 'RAILS_ENV=test bin/shakapacker'")
1200
+ checker.add_info(" 💡 Or remove TestHelper and use compile: true in shakapacker.yml")
1201
+ elsif !has_build_test_command && !has_compile_true && !uses_test_helper
1202
+ checker.add_warning(" ⚠️ No test asset compilation configured")
1203
+ checker.add_info(" 💡 Recommended: Add to shakapacker.yml test section:")
1204
+ checker.add_info(" compile: true")
1205
+ checker.add_info(" 📖 See: https://github.com/shakacode/react_on_rails/blob/master/docs/guides/testing-configuration.md")
1206
+ elsif has_compile_true
1207
+ checker.add_success(" ✅ Test assets configured via Shakapacker auto-compilation")
1208
+ checker.add_info(" (compile: true in shakapacker.yml)")
1209
+ elsif has_build_test_command && uses_test_helper
1210
+ checker.add_success(" ✅ Test assets configured via React on Rails test helper")
1211
+ checker.add_info(" (build_test_command + ReactOnRails::TestHelper)")
1212
+ end
1213
+ else
1214
+ checker.add_warning(" ⚠️ config/shakapacker.yml not found")
1215
+ end
1216
+ rescue StandardError => e
1217
+ checker.add_warning(" ⚠️ Could not analyze test configuration: #{e.message}")
1218
+ end
1219
+ end
1220
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
1221
+
1222
+ def uses_react_on_rails_test_helper?
1223
+ spec_helpers = ["spec/rails_helper.rb", "spec/spec_helper.rb", "test/test_helper.rb"]
1224
+ spec_helpers.any? do |helper|
1225
+ next unless File.exist?(helper)
1226
+
1227
+ content = File.read(helper)
1228
+ content.include?("configure_rspec_to_compile_assets") || content.include?("ensure_assets_compiled")
1229
+ end
1230
+ rescue StandardError
1231
+ false
1232
+ end
1233
+
1111
1234
  def relativize_path(absolute_path)
1112
1235
  return absolute_path unless absolute_path.is_a?(String)
1113
1236
 
@@ -1144,6 +1267,130 @@ module ReactOnRails
1144
1267
  checker.add_info(" #{label}: <error reading value: #{e.message}>")
1145
1268
  end
1146
1269
  end
1270
+
1271
+ # Comment patterns used for filtering out commented async usage
1272
+ ERB_COMMENT_PATTERN = /<%\s*#.*javascript_pack_tag/
1273
+ HAML_COMMENT_PATTERN = /^\s*-#.*javascript_pack_tag/
1274
+ SLIM_COMMENT_PATTERN = %r{^\s*/.*javascript_pack_tag}
1275
+ HTML_COMMENT_PATTERN = /<!--.*javascript_pack_tag/
1276
+
1277
+ def check_async_usage
1278
+ # When Pro is installed, async is fully supported and is the default behavior
1279
+ # No need to check for async usage in this case
1280
+ return if ReactOnRails::Utils.react_on_rails_pro?
1281
+
1282
+ async_issues = []
1283
+
1284
+ # Check 1: javascript_pack_tag with :async in view files
1285
+ view_files_with_async = scan_view_files_for_async_pack_tag
1286
+ unless view_files_with_async.empty?
1287
+ async_issues << "javascript_pack_tag with :async found in view files:"
1288
+ view_files_with_async.each do |file|
1289
+ async_issues << " • #{file}"
1290
+ end
1291
+ end
1292
+
1293
+ # Check 2: generated_component_packs_loading_strategy = :async
1294
+ if config_has_async_loading_strategy?
1295
+ async_issues << "config.generated_component_packs_loading_strategy = :async in initializer"
1296
+ end
1297
+
1298
+ return if async_issues.empty?
1299
+
1300
+ # Report errors if async usage is found without Pro
1301
+ checker.add_error("🚫 :async usage detected without React on Rails Pro")
1302
+ async_issues.each { |issue| checker.add_error(" #{issue}") }
1303
+ checker.add_info(" 💡 :async can cause race conditions. Options:")
1304
+ checker.add_info(" 1. Upgrade to React on Rails Pro (recommended for :async support)")
1305
+ checker.add_info(" 2. Change to :defer or :sync loading strategy")
1306
+ checker.add_info(" 📖 https://www.shakacode.com/react-on-rails/docs/guides/configuration/")
1307
+ end
1308
+
1309
+ def scan_view_files_for_async_pack_tag
1310
+ view_patterns = ["app/views/**/*.erb", "app/views/**/*.haml", "app/views/**/*.slim"]
1311
+ files_with_async = view_patterns.flat_map { |pattern| scan_pattern_for_async(pattern) }
1312
+ files_with_async.compact
1313
+ rescue Errno::ENOENT, Encoding::InvalidByteSequenceError, Encoding::UndefinedConversionError => e
1314
+ # Log the error if Rails logger is available
1315
+ log_debug("Error scanning view files for async: #{e.message}")
1316
+ []
1317
+ end
1318
+
1319
+ def scan_pattern_for_async(pattern)
1320
+ Dir.glob(pattern).filter_map do |file|
1321
+ next unless File.exist?(file)
1322
+
1323
+ content = File.read(file)
1324
+ next if content_has_only_commented_async?(content)
1325
+ next unless file_has_async_pack_tag?(content)
1326
+
1327
+ relativize_path(file)
1328
+ end
1329
+ end
1330
+
1331
+ def file_has_async_pack_tag?(content)
1332
+ # Match javascript_pack_tag with :async symbol or async: true hash syntax
1333
+ # Examples that should match:
1334
+ # - javascript_pack_tag "app", :async
1335
+ # - javascript_pack_tag "app", async: true
1336
+ # - javascript_pack_tag "app", :async, other_option: value
1337
+ # Examples that should NOT match:
1338
+ # - javascript_pack_tag "app", defer: "async" (async is a string value, not the option)
1339
+ # - javascript_pack_tag "app", :defer
1340
+ # Note: Theoretical edge case `data: { async: true }` would match but is extremely unlikely
1341
+ # in real code and represents a harmless false positive (showing a warning when not needed)
1342
+ # Use word boundary \b to ensure :async is not part of a longer symbol like :async_mode
1343
+ # [^<]* allows matching across newlines within ERB tags but stops at closing ERB tag
1344
+ content.match?(/javascript_pack_tag[^<]*(?::async\b|async:\s*true)/)
1345
+ end
1346
+
1347
+ def content_has_only_commented_async?(content)
1348
+ # Check if all occurrences of javascript_pack_tag with :async are in comments
1349
+ # Returns true if ONLY commented async usage exists (no active async usage)
1350
+
1351
+ # First check if there's any javascript_pack_tag with :async in the full content
1352
+ return true unless file_has_async_pack_tag?(content)
1353
+
1354
+ # Strategy: Remove all commented lines, then check if any :async remains
1355
+ # This handles both single-line and multi-line tags correctly
1356
+ uncommented_lines = content.each_line.reject do |line|
1357
+ line.match?(ERB_COMMENT_PATTERN) ||
1358
+ line.match?(HAML_COMMENT_PATTERN) ||
1359
+ line.match?(SLIM_COMMENT_PATTERN) ||
1360
+ line.match?(HTML_COMMENT_PATTERN)
1361
+ end
1362
+
1363
+ uncommented_content = uncommented_lines.join
1364
+ # If no async found in uncommented content, all async usage was commented
1365
+ !file_has_async_pack_tag?(uncommented_content)
1366
+ end
1367
+
1368
+ def config_has_async_loading_strategy?
1369
+ config_path = "config/initializers/react_on_rails.rb"
1370
+ return false unless File.exist?(config_path)
1371
+
1372
+ content = File.read(config_path)
1373
+ # Check if generated_component_packs_loading_strategy is set to :async
1374
+ # Filter out commented lines (lines starting with # after optional whitespace)
1375
+ content.each_line.any? do |line|
1376
+ # Skip lines that start with # (after optional whitespace)
1377
+ next if line.match?(/^\s*#/)
1378
+
1379
+ # Match: config.generated_component_packs_loading_strategy = :async
1380
+ # Use word boundary \b to ensure :async is the complete symbol, not part of :async_mode etc.
1381
+ line.match?(/config\.generated_component_packs_loading_strategy\s*=\s*:async\b/)
1382
+ end
1383
+ rescue Errno::ENOENT, Encoding::InvalidByteSequenceError, Encoding::UndefinedConversionError => e
1384
+ # Log the error if Rails logger is available
1385
+ log_debug("Error checking async loading strategy: #{e.message}")
1386
+ false
1387
+ end
1388
+
1389
+ def log_debug(message)
1390
+ return unless defined?(Rails.logger) && Rails.logger
1391
+
1392
+ Rails.logger.debug(message)
1393
+ end
1147
1394
  end
1148
1395
  # rubocop:enable Metrics/ClassLength
1149
1396
  end
@@ -155,10 +155,10 @@ module ReactOnRails
155
155
  # props: Ruby Hash or JSON string which contains the properties to pass to the redux store.
156
156
  # Options
157
157
  # defer: false -- pass as true if you wish to render this below your component.
158
- # immediate_hydration: false -- React on Rails Pro (licensed) feature. Pass as true if you wish to
159
- # hydrate this store immediately instead of waiting for the page to load.
158
+ # immediate_hydration: nil -- React on Rails Pro (licensed) feature. When nil (default), Pro users
159
+ # get immediate hydration, non-Pro users don't. Can be explicitly overridden.
160
160
  def redux_store(store_name, props: {}, defer: false, immediate_hydration: nil)
161
- immediate_hydration = ReactOnRails.configuration.immediate_hydration if immediate_hydration.nil?
161
+ immediate_hydration = ReactOnRails::Utils.normalize_immediate_hydration(immediate_hydration, store_name, "Store")
162
162
 
163
163
  redux_store_data = { store_name: store_name,
164
164
  props: props,
@@ -587,6 +587,15 @@ module ReactOnRails
587
587
  # It doesn't make any transformation, it listens and raises error if a chunk has errors
588
588
  chunk_json_result
589
589
  end
590
+
591
+ result.rescue do |err|
592
+ # This error came from the renderer
593
+ raise ReactOnRails::PrerenderError.new(component_name: react_component_name,
594
+ # Sanitize as this might be browser logged
595
+ props: sanitized_props_string(props),
596
+ err: err,
597
+ js_code: js_code)
598
+ end
590
599
  elsif result["hasErrors"] && render_options.raise_on_prerender_error
591
600
  raise_prerender_error(result, react_component_name, props, js_code)
592
601
  end