react_on_rails 16.4.0 → 16.5.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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/lib/generators/USAGE +1 -1
  4. data/lib/generators/react_on_rails/generator_messages.rb +20 -18
  5. data/lib/generators/react_on_rails/install_generator.rb +128 -12
  6. data/lib/generators/react_on_rails/pro_generator.rb +1 -1
  7. data/lib/generators/react_on_rails/pro_setup.rb +3 -3
  8. data/lib/generators/react_on_rails/react_with_redux_generator.rb +2 -1
  9. data/lib/generators/react_on_rails/rsc_generator.rb +1 -1
  10. data/lib/generators/react_on_rails/templates/base/base/bin/dev +1 -1
  11. data/lib/generators/react_on_rails/templates/base/base/config/initializers/react_on_rails.rb.tt +3 -3
  12. data/lib/generators/react_on_rails/templates/pro/base/client/node-renderer.js +8 -4
  13. data/lib/generators/react_on_rails/templates/pro/base/config/initializers/react_on_rails_pro.rb.tt +1 -1
  14. data/lib/generators/react_on_rails/templates/rsc/base/app/controllers/hello_server_controller.rb.tt +1 -1
  15. data/lib/generators/react_on_rails/templates/rsc/base/app/javascript/src/HelloServer/components/HelloServer.jsx +1 -1
  16. data/lib/generators/react_on_rails/templates/rsc/base/app/javascript/src/HelloServer/components/HelloServer.tsx +1 -1
  17. data/lib/generators/react_on_rails/templates/rsc/base/app/views/hello_server/index.html.erb +1 -1
  18. data/lib/generators/react_on_rails/templates/rsc/base/config/webpack/rscWebpackConfig.js.tt +1 -1
  19. data/lib/react_on_rails/config_path_resolver.rb +50 -0
  20. data/lib/react_on_rails/configuration.rb +1 -1
  21. data/lib/react_on_rails/dev/process_manager.rb +16 -1
  22. data/lib/react_on_rails/dev/server_manager.rb +2 -2
  23. data/lib/react_on_rails/doctor.rb +389 -25
  24. data/lib/react_on_rails/git_utils.rb +95 -23
  25. data/lib/react_on_rails/helper.rb +3 -3
  26. data/lib/react_on_rails/packer_utils.rb +1 -1
  27. data/lib/react_on_rails/packs_generator.rb +3 -3
  28. data/lib/react_on_rails/react_component/render_options.rb +48 -0
  29. data/lib/react_on_rails/server_rendering_js_code.rb +1 -1
  30. data/lib/react_on_rails/system_checker.rb +42 -19
  31. data/lib/react_on_rails/test_helper/webpack_assets_compiler.rb +1 -2
  32. data/lib/react_on_rails/utils.rb +1 -1
  33. data/lib/react_on_rails/version.rb +1 -1
  34. data/lib/react_on_rails/version_synchronizer.rb +250 -0
  35. data/lib/tasks/generate_packs.rake +4 -4
  36. data/lib/tasks/sync_versions.rake +23 -0
  37. data/rakelib/examples_config.yml +1 -1
  38. data/rakelib/update_changelog.rake +3 -3
  39. data/react_on_rails.gemspec +1 -1
  40. data/sig/react_on_rails/dev/process_manager.rbs +2 -0
  41. metadata +6 -3
@@ -4,6 +4,7 @@ require "json"
4
4
  require "erb"
5
5
  require "yaml"
6
6
  require_relative "utils"
7
+ require_relative "config_path_resolver"
7
8
  require_relative "version_syntax_converter"
8
9
  require_relative "system_checker"
9
10
 
@@ -39,6 +40,8 @@ end
39
40
  module ReactOnRails
40
41
  # rubocop:disable Metrics/ClassLength, Metrics/AbcSize
41
42
  class Doctor
43
+ include ConfigPathResolver
44
+
42
45
  MESSAGE_COLORS = {
43
46
  error: :red,
44
47
  warning: :yellow,
@@ -56,6 +59,7 @@ module ReactOnRails
56
59
  @fix = fix
57
60
  @checker = SystemChecker.new
58
61
  @test_output_path_strategy = :unknown
62
+ @rails_environment_loaded = false
59
63
  end
60
64
 
61
65
  def run_diagnosis
@@ -86,7 +90,7 @@ module ReactOnRails
86
90
  puts " • This diagnostic tool is available in React on Rails v16.0.0+"
87
91
  puts " • For older versions, upgrade your gem to access this feature"
88
92
  puts " • Run: bundle update react_on_rails"
89
- puts " • Documentation: https://www.shakacode.com/react-on-rails/docs/"
93
+ puts " • Documentation: https://reactonrails.com/docs/"
90
94
  end
91
95
 
92
96
  def run_all_checks
@@ -101,7 +105,9 @@ module ReactOnRails
101
105
  ["Rails Integration", :check_rails],
102
106
  ["Webpack Configuration", :check_webpack],
103
107
  ["Testing Setup", :check_testing_setup],
104
- ["Development Environment", :check_development]
108
+ ["Development Environment", :check_development],
109
+ ["React on Rails Pro Setup", :check_pro_setup],
110
+ ["React Server Components", :check_rsc_setup]
105
111
  ]
106
112
 
107
113
  checks.each do |section_name, check_method|
@@ -473,10 +479,11 @@ module ReactOnRails
473
479
  end
474
480
 
475
481
  def check_npm_package_version
476
- return unless File.exist?("package.json")
482
+ package_json_path = resolved_package_json_path
483
+ return unless File.exist?(package_json_path)
477
484
 
478
485
  begin
479
- package_json = JSON.parse(File.read("package.json"))
486
+ package_json = JSON.parse(File.read(package_json_path))
480
487
  all_deps = package_json["dependencies"]&.merge(package_json["devDependencies"] || {}) || {}
481
488
 
482
489
  npm_version = all_deps["react-on-rails"]
@@ -520,10 +527,11 @@ module ReactOnRails
520
527
  end
521
528
 
522
529
  def check_npm_wildcards
523
- return unless File.exist?("package.json")
530
+ package_json_path = resolved_package_json_path
531
+ return unless File.exist?(package_json_path)
524
532
 
525
533
  begin
526
- package_json = JSON.parse(File.read("package.json"))
534
+ package_json = JSON.parse(File.read(package_json_path))
527
535
  all_deps = package_json["dependencies"]&.merge(package_json["devDependencies"] || {}) || {}
528
536
 
529
537
  npm_version = all_deps["react-on-rails"]
@@ -542,9 +550,10 @@ module ReactOnRails
542
550
  end
543
551
 
544
552
  def check_pro_package_consistency
545
- return unless File.exist?("package.json")
553
+ package_json_path = resolved_package_json_path
554
+ return unless File.exist?(package_json_path)
546
555
 
547
- package_json = JSON.parse(File.read("package.json"))
556
+ package_json = JSON.parse(File.read(package_json_path))
548
557
  all_deps = (package_json["dependencies"] || {}).merge(package_json["devDependencies"] || {})
549
558
  has_base = all_deps.key?("react-on-rails")
550
559
  has_pro = all_deps.key?("react-on-rails-pro")
@@ -606,8 +615,7 @@ module ReactOnRails
606
615
  "config/initializers/react_on_rails.rb" => "React on Rails initializer",
607
616
  "bin/dev" => "Development server launcher",
608
617
  "bin/shakapacker" => "Shakapacker binary",
609
- "bin/shakapacker-dev-server" => "Shakapacker dev server binary",
610
- "config/webpack/webpack.config.js" => "Webpack configuration"
618
+ "bin/shakapacker-dev-server" => "Shakapacker dev server binary"
611
619
  }
612
620
 
613
621
  files_to_check.each do |file_path, description|
@@ -618,6 +626,16 @@ module ReactOnRails
618
626
  end
619
627
  end
620
628
 
629
+ webpack_config_path = resolved_webpack_config_path
630
+ if webpack_config_path
631
+ checker.add_success("✅ Webpack configuration: #{webpack_config_path}")
632
+ else
633
+ checker.add_warning("⚠️ Missing Webpack configuration: config/webpack/webpack.config.js")
634
+ checker.add_info(
635
+ "ℹ️ If your app uses a custom webpack config location, this warning may be informational."
636
+ )
637
+ end
638
+
621
639
  check_layout_files
622
640
  check_server_rendering_engine
623
641
  end
@@ -649,20 +667,29 @@ module ReactOnRails
649
667
  end
650
668
  end
651
669
 
652
- # rubocop:disable Metrics/CyclomaticComplexity
670
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
653
671
  def check_server_rendering_engine
654
672
  return unless defined?(ReactOnRails)
655
673
 
656
674
  checker.add_info("\n🖥️ Server Rendering Engine:")
657
675
 
658
676
  begin
659
- # Check if ExecJS is available and what runtime is being used
660
- if defined?(ExecJS)
677
+ uses_node_renderer = ReactOnRails::Utils.react_on_rails_pro? &&
678
+ pro_initializer_has_node_renderer?
679
+
680
+ if uses_node_renderer
681
+ checker.add_info(" Pro uses NodeRenderer for server rendering")
682
+ if defined?(ExecJS) && ExecJS.runtime
683
+ checker.add_info(" ExecJS available as fallback: #{ExecJS.runtime.name}")
684
+ else
685
+ checker.add_warning(" ⚠️ ExecJS fallback is enabled but ExecJS is not available")
686
+ checker.add_info(" 💡 Install mini_racer or set renderer_use_fallback_exec_js = false")
687
+ end
688
+ elsif defined?(ExecJS)
661
689
  runtime_name = ExecJS.runtime.name if ExecJS.runtime
662
690
  if runtime_name
663
691
  checker.add_info(" ExecJS Runtime: #{runtime_name}")
664
692
 
665
- # Provide more specific information about the runtime
666
693
  case runtime_name
667
694
  when /MiniRacer/
668
695
  checker.add_info(" ℹ️ Using V8 via mini_racer gem (fast, isolated)")
@@ -683,7 +710,7 @@ module ReactOnRails
683
710
  checker.add_warning(" ⚠️ Could not determine server rendering engine: #{e.message}")
684
711
  end
685
712
  end
686
- # rubocop:enable Metrics/CyclomaticComplexity
713
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
687
714
 
688
715
  def check_shakapacker_configuration_details
689
716
  return unless File.exist?("config/shakapacker.yml")
@@ -733,7 +760,7 @@ module ReactOnRails
733
760
  content = File.read(config_path)
734
761
 
735
762
  checker.add_info("📋 React on Rails Configuration:")
736
- checker.add_info("📍 Documentation: https://www.shakacode.com/react-on-rails/docs/guides/configuration/")
763
+ checker.add_info("📍 Documentation: https://reactonrails.com/docs/configuration/")
737
764
 
738
765
  # Analyze configuration settings
739
766
  analyze_server_rendering_config(content)
@@ -930,7 +957,7 @@ module ReactOnRails
930
957
  if /config\.rendering_extension\s*=\s*([^\s\n,]+)/.match?(content)
931
958
  checker.add_info("\n🔌 Custom Extensions:")
932
959
  checker.add_info(" rendering_extension: Custom rendering logic detected")
933
- checker.add_info(" ℹ️ See: https://www.shakacode.com/react-on-rails/docs/guides/rendering-extensions")
960
+ checker.add_info(" ℹ️ See: https://reactonrails.com/docs/configuration/#rendering_extension")
934
961
  end
935
962
 
936
963
  # Check for rendering props extension
@@ -968,7 +995,7 @@ module ReactOnRails
968
995
  deprecated_settings.each do |setting|
969
996
  checker.add_warning(" #{setting}")
970
997
  end
971
- checker.add_info("📖 Migration guide: https://www.shakacode.com/react-on-rails/docs/guides/upgrading-react-on-rails")
998
+ checker.add_info("📖 Migration guide: https://reactonrails.com/docs/upgrading/upgrading-react-on-rails")
972
999
  end
973
1000
 
974
1001
  def check_breaking_changes_warnings
@@ -1036,7 +1063,7 @@ module ReactOnRails
1036
1063
 
1037
1064
  checker.add_info("\n🚨 React on Rails v16+ Breaking Changes Detected:")
1038
1065
  issues_found.each { |issue| checker.add_warning(" #{issue}") }
1039
- checker.add_info("📖 Full migration guide: https://www.shakacode.com/react-on-rails/docs/guides/upgrading-react-on-rails#upgrading-to-version-16")
1066
+ checker.add_info("📖 Full migration guide: https://reactonrails.com/docs/upgrading/upgrading-react-on-rails#upgrading-to-version-16")
1040
1067
  end
1041
1068
  # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
1042
1069
 
@@ -1117,10 +1144,11 @@ module ReactOnRails
1117
1144
  end
1118
1145
 
1119
1146
  def npm_test_script?
1120
- return false unless File.exist?("package.json")
1147
+ package_json_path = resolved_package_json_path
1148
+ return false unless File.exist?(package_json_path)
1121
1149
 
1122
1150
  begin
1123
- package_json = JSON.parse(File.read("package.json"))
1151
+ package_json = JSON.parse(File.read(package_json_path))
1124
1152
  test_script = package_json.dig("scripts", "test")
1125
1153
  test_script && !test_script.empty?
1126
1154
  rescue StandardError
@@ -1216,7 +1244,7 @@ module ReactOnRails
1216
1244
  if (prerender_set || uses_prerender) && !server_bundle_set
1217
1245
  checker.add_warning(" ⚠️ Server rendering is enabled but server_bundle_js_file is not configured")
1218
1246
  checker.add_info(" 💡 Set config.server_bundle_js_file = 'server-bundle.js' to enable SSR")
1219
- checker.add_info(" 💡 See: https://www.shakacode.com/react-on-rails/docs/guides/server-rendering")
1247
+ checker.add_info(" 💡 See: https://reactonrails.com/docs/core-concepts/react-server-rendering/")
1220
1248
  elsif server_bundle_set && !prerender_set && !uses_prerender
1221
1249
  checker.add_info(" ℹ️ server_bundle_js_file is configured but prerender doesn't appear to be used")
1222
1250
  checker.add_info(" 💡 Either use prerender: true in react_component calls or remove server_bundle_js_file")
@@ -1230,17 +1258,28 @@ module ReactOnRails
1230
1258
  # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
1231
1259
 
1232
1260
  def uses_prerender_in_views?
1233
- # Check view files for prerender: true
1234
1261
  view_files = Dir.glob("app/views/**/*.{erb,haml,slim}")
1235
1262
  view_files.any? do |file|
1236
1263
  next unless File.exist?(file)
1237
1264
 
1238
- File.read(file).match?(/prerender:\s*true/)
1265
+ content = File.read(file)
1266
+ # Match explicit prerender: true OR Pro streaming helpers that implicitly prerender
1267
+ content.match?(/prerender:\s*true/) ||
1268
+ content.match?(/stream_react_component|cached_stream_react_component|rsc_payload_react_component/)
1239
1269
  end
1240
1270
  rescue StandardError
1241
1271
  false
1242
1272
  end
1243
1273
 
1274
+ def pro_initializer_has_node_renderer?
1275
+ config_path = "config/initializers/react_on_rails_pro.rb"
1276
+ return false unless File.exist?(config_path)
1277
+
1278
+ File.read(config_path).match?(/server_renderer\s*=\s*["']NodeRenderer["']/)
1279
+ rescue StandardError
1280
+ false
1281
+ end
1282
+
1244
1283
  # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength, Metrics/BlockNesting
1245
1284
  def check_build_test_configuration
1246
1285
  config_path = "config/initializers/react_on_rails.rb"
@@ -1990,7 +2029,7 @@ module ReactOnRails
1990
2029
  checker.add_info(" 💡 :async can cause race conditions. Options:")
1991
2030
  checker.add_info(" 1. Upgrade to React on Rails Pro (recommended for :async support)")
1992
2031
  checker.add_info(" 2. Change to :defer or :sync loading strategy")
1993
- checker.add_info(" 📖 https://www.shakacode.com/react-on-rails/docs/guides/configuration/")
2032
+ checker.add_info(" 📖 https://reactonrails.com/docs/configuration/")
1994
2033
  end
1995
2034
 
1996
2035
  def scan_view_files_for_async_pack_tag
@@ -2154,6 +2193,331 @@ module ReactOnRails
2154
2193
  - Serve as single source of truth for server bundle location
2155
2194
  MSG
2156
2195
  end
2196
+ # ── Helpers for Pro/RSC checks ────────────────────────────────────
2197
+
2198
+ # Lazily load the Rails environment so that initializers (which configure
2199
+ # ReactOnRailsPro) have run before we read Pro/RSC config values.
2200
+ # Safe to call multiple times — only loads once.
2201
+ # Returns true if environment was loaded successfully, false otherwise.
2202
+ def ensure_rails_environment_loaded
2203
+ return true if @rails_environment_loaded
2204
+ return false if @rails_environment_attempted
2205
+
2206
+ @rails_environment_attempted = true
2207
+
2208
+ env_file = "config/environment.rb"
2209
+ return false unless File.exist?(env_file)
2210
+
2211
+ require File.expand_path(env_file)
2212
+ @rails_environment_loaded = true
2213
+ rescue StandardError, LoadError => e
2214
+ checker.add_warning(<<~MSG.strip)
2215
+ ⚠️ Could not load Rails environment: #{e.message}
2216
+
2217
+ Pro/RSC diagnostics may reflect default values instead of your app's configuration.
2218
+ MSG
2219
+ false
2220
+ end
2221
+
2222
+ # Resolve the JavaScript source path from Shakapacker config.
2223
+ # Falls back to "app/javascript" if Shakapacker is not available.
2224
+ def resolve_js_source_path
2225
+ require "shakapacker"
2226
+ Shakapacker.config.source_path.to_s
2227
+ rescue LoadError, StandardError
2228
+ shakapacker_yml_source_path || "app/javascript"
2229
+ end
2230
+
2231
+ def shakapacker_yml_source_path
2232
+ config_path = "config/shakapacker.yml"
2233
+ return nil unless File.exist?(config_path)
2234
+
2235
+ config = parse_shakapacker_config(File.read(config_path))
2236
+ return nil unless config.is_a?(Hash)
2237
+
2238
+ default_config = config["default"] || {}
2239
+ normalize_yaml_scalar(default_config["source_path"]) if default_config.key?("source_path")
2240
+ rescue StandardError
2241
+ nil
2242
+ end
2243
+
2244
+ # ── React on Rails Pro Setup ──────────────────────────────────────
2245
+
2246
+ def check_pro_setup
2247
+ return unless ReactOnRails::Utils.react_on_rails_pro?
2248
+
2249
+ check_pro_initializer_existence
2250
+ ensure_rails_environment_loaded
2251
+ check_pro_renderer_mode
2252
+ check_base_package_imports
2253
+ end
2254
+
2255
+ def check_pro_initializer_existence
2256
+ initializer_path = "config/initializers/react_on_rails_pro.rb"
2257
+ if File.exist?(initializer_path)
2258
+ checker.add_success("✅ Pro initializer exists (#{initializer_path})")
2259
+ else
2260
+ checker.add_warning(<<~MSG.strip)
2261
+ ⚠️ Pro initializer not found at #{initializer_path}.
2262
+
2263
+ Without this file, React on Rails Pro runs with all default settings.
2264
+ Run the Pro generator to create it:
2265
+ rails g react_on_rails:pro
2266
+ MSG
2267
+ end
2268
+ end
2269
+
2270
+ def check_pro_renderer_mode
2271
+ renderer = ReactOnRailsPro.configuration.server_renderer
2272
+ if renderer == "NodeRenderer"
2273
+ checker.add_success("✅ Pro renderer: NodeRenderer (dedicated Node.js process)")
2274
+ else
2275
+ checker.add_info("ℹ️ Pro renderer: #{renderer}")
2276
+ checker.add_info(" 💡 NodeRenderer provides better performance and is required for RSC")
2277
+ end
2278
+ rescue StandardError => e
2279
+ checker.add_warning("⚠️ Could not detect Pro renderer mode: #{e.message}")
2280
+ end
2281
+
2282
+ # The base 'react-on-rails' npm package is a transitive dependency of 'react-on-rails-pro',
2283
+ # so `import ... from 'react-on-rails'` resolves silently — loading the base package instead
2284
+ # of Pro. Components registered through the base package won't have Pro features (streaming,
2285
+ # caching, RSC), and may cause "component not registered" errors at runtime.
2286
+ BASE_PACKAGE_IMPORT_PATTERN = %r{\bfrom\s+['"]react-on-rails(?:/[^'"]*)?['"]}
2287
+ BASE_PACKAGE_REQUIRE_PATTERN = %r{\brequire\s*\(\s*['"]react-on-rails(?:/[^'"]*)?['"]\s*\)}
2288
+
2289
+ def check_base_package_imports # rubocop:disable Metrics/CyclomaticComplexity
2290
+ source_path = resolve_js_source_path
2291
+ js_extensions = %w[js jsx ts tsx]
2292
+ js_patterns = js_extensions.map { |ext| "#{source_path}/**/*.#{ext}" }
2293
+ files_with_base_import = []
2294
+
2295
+ js_patterns.each do |pattern|
2296
+ Dir.glob(pattern).each do |file|
2297
+ content = File.read(file)
2298
+ next unless content.match?(BASE_PACKAGE_IMPORT_PATTERN) || content.match?(BASE_PACKAGE_REQUIRE_PATTERN)
2299
+
2300
+ files_with_base_import << file
2301
+ end
2302
+ end
2303
+
2304
+ if files_with_base_import.empty?
2305
+ checker.add_success("✅ No base 'react-on-rails' imports found (Pro package used correctly)")
2306
+ else
2307
+ checker.add_warning(<<~MSG.strip)
2308
+ ⚠️ Found imports from 'react-on-rails' instead of 'react-on-rails-pro':
2309
+ #{files_with_base_import.map { |f| " • #{f}" }.join("\n")}
2310
+
2311
+ The base package is a transitive dependency of Pro, so these imports resolve
2312
+ silently but load the base version without Pro features.
2313
+
2314
+ Fix: Update imports to use 'react-on-rails-pro':
2315
+ import ReactOnRails from 'react-on-rails-pro'; // server
2316
+ import ReactOnRails from 'react-on-rails-pro/client'; // client
2317
+ MSG
2318
+ end
2319
+ rescue StandardError => e
2320
+ checker.add_warning("⚠️ Could not scan for base package imports: #{e.message}")
2321
+ end
2322
+
2323
+ # ── React Server Components ────────────────────────────────────
2324
+
2325
+ # Candidate paths for RSC bundler configuration (webpack and rspack variants)
2326
+ RSC_BUNDLER_CONFIG_PATHS = %w[
2327
+ config/webpack/rscWebpackConfig.js
2328
+ config/rspack/rscWebpackConfig.js
2329
+ ].freeze
2330
+
2331
+ def check_rsc_setup
2332
+ return unless ReactOnRails::Utils.react_on_rails_pro?
2333
+
2334
+ ensure_rails_environment_loaded
2335
+ pro_config = ReactOnRailsPro.configuration
2336
+ return unless pro_config.enable_rsc_support
2337
+
2338
+ checker.add_info("🔬 React Server Components: enabled")
2339
+ checker.add_info(" rsc_bundle_js_file: #{pro_config.rsc_bundle_js_file}")
2340
+ checker.add_info(" rsc_payload_generation_url_path: #{pro_config.rsc_payload_generation_url_path}")
2341
+
2342
+ check_rsc_renderer_mode(pro_config)
2343
+ check_rsc_payload_route
2344
+ check_rsc_bundler_config
2345
+ check_rsc_react_version
2346
+ check_rsc_procfile_watcher
2347
+ rescue StandardError => e
2348
+ checker.add_warning("⚠️ RSC setup check encountered an error: #{e.message}")
2349
+ end
2350
+
2351
+ def check_rsc_renderer_mode(pro_config)
2352
+ return if pro_config.server_renderer == "NodeRenderer"
2353
+
2354
+ checker.add_error(<<~MSG.strip)
2355
+ 🚫 RSC requires NodeRenderer but current renderer is '#{pro_config.server_renderer}'.
2356
+
2357
+ React Server Components need a dedicated Node.js process for server rendering.
2358
+
2359
+ Fix: Set server_renderer to "NodeRenderer" in config/initializers/react_on_rails_pro.rb:
2360
+ config.server_renderer = "NodeRenderer"
2361
+ MSG
2362
+ end
2363
+
2364
+ def check_rsc_payload_route
2365
+ routes_file = "config/routes.rb"
2366
+
2367
+ unless File.exist?(routes_file)
2368
+ checker.add_warning("⚠️ config/routes.rb not found — cannot verify RSC payload route")
2369
+ return
2370
+ end
2371
+
2372
+ routes_content = File.read(routes_file)
2373
+ uncommented_route = routes_content.each_line.any? do |line|
2374
+ next if line.match?(/^\s*#/)
2375
+
2376
+ line.include?("rsc_payload_route")
2377
+ end
2378
+ if uncommented_route
2379
+ checker.add_success("✅ RSC payload route configured")
2380
+ else
2381
+ checker.add_error(<<~MSG.strip)
2382
+ 🚫 RSC payload route not found in config/routes.rb.
2383
+
2384
+ Without this route, React Server Component payload requests will 404.
2385
+
2386
+ Fix: Add to config/routes.rb inside the Rails.application.routes.draw block:
2387
+ rsc_payload_route
2388
+ MSG
2389
+ end
2390
+ end
2391
+
2392
+ def check_rsc_bundler_config
2393
+ found_path = RSC_BUNDLER_CONFIG_PATHS.find { |path| File.exist?(path) }
2394
+
2395
+ if found_path
2396
+ checker.add_success("✅ RSC bundler config exists (#{found_path})")
2397
+ else
2398
+ checker.add_error(<<~MSG.strip)
2399
+ 🚫 RSC bundler config not found.
2400
+
2401
+ Expected one of: #{RSC_BUNDLER_CONFIG_PATHS.join(' or ')}
2402
+
2403
+ This file defines the webpack/rspack configuration for the RSC bundle.
2404
+
2405
+ Fix: Run the RSC generator to create it:
2406
+ rails g react_on_rails:rsc
2407
+ MSG
2408
+ end
2409
+ end
2410
+
2411
+ # rubocop:disable Metrics/CyclomaticComplexity
2412
+ def check_rsc_react_version
2413
+ react_version = detect_react_version_from_deps
2414
+ unless react_version
2415
+ checker.add_info("ℹ️ Could not detect React version — skipping RSC version check")
2416
+ return
2417
+ end
2418
+
2419
+ major, minor, patch = react_version.split(".").map(&:to_i)
2420
+
2421
+ if major == 19 && minor.zero? && patch >= 4
2422
+ checker.add_success("✅ React #{react_version} is compatible with RSC")
2423
+ elsif major == 19 && minor.zero?
2424
+ checker.add_warning(<<~MSG.strip)
2425
+ ⚠️ React #{react_version} has known security vulnerabilities fixed in 19.0.4+.
2426
+
2427
+ Upgrade to at least React 19.0.4:
2428
+ npm install react@~19.0.4 react-dom@~19.0.4
2429
+ MSG
2430
+ elsif major >= 19
2431
+ checker.add_warning(<<~MSG.strip)
2432
+ ⚠️ React #{react_version} has not been verified with React on Rails Pro RSC.
2433
+
2434
+ RSC support currently targets React 19.0.x. React #{major}.#{minor}.x may work
2435
+ but has not been tested. Verified compatibility: React 19.0.4+.
2436
+ MSG
2437
+ else
2438
+ checker.add_error(<<~MSG.strip)
2439
+ 🚫 React #{react_version} is not compatible with RSC.
2440
+
2441
+ React Server Components in React on Rails Pro requires React 19.x or higher.
2442
+
2443
+ Fix: npm install react@~19.0.4 react-dom@~19.0.4
2444
+ MSG
2445
+ end
2446
+ end
2447
+ # rubocop:enable Metrics/CyclomaticComplexity
2448
+
2449
+ def detect_react_version_from_deps
2450
+ # Prefer the actually installed version from node_modules over the declared
2451
+ # range in package.json. Declared ranges like "^19.0.0" would be misleading
2452
+ # (stripped to "19.0.0" even though 19.0.4+ may be installed).
2453
+ installed = installed_react_version
2454
+ return installed if installed
2455
+
2456
+ declared_react_version
2457
+ rescue StandardError
2458
+ nil
2459
+ end
2460
+
2461
+ def installed_react_version
2462
+ # Use Node's own module resolution to find the actually installed React,
2463
+ # which handles hoisted dependencies in monorepos and pnpm workspaces.
2464
+ stdout, _stderr, status = Open3.capture3("node", "-e",
2465
+ "console.log(require.resolve('react/package.json'))")
2466
+ return nil unless status.success?
2467
+
2468
+ resolved_path = stdout.strip
2469
+ return nil if resolved_path.empty? || !File.exist?(resolved_path)
2470
+
2471
+ version = JSON.parse(File.read(resolved_path))["version"]
2472
+ version if version&.match?(/\A\d+\.\d+\.\d+/)
2473
+ rescue StandardError
2474
+ nil
2475
+ end
2476
+
2477
+ def declared_react_version
2478
+ return nil unless File.exist?("package.json")
2479
+
2480
+ package_json = JSON.parse(File.read("package.json"))
2481
+ all_deps = (package_json["dependencies"] || {}).merge(package_json["devDependencies"] || {})
2482
+ version_str = all_deps["react"]
2483
+ return nil unless version_str
2484
+
2485
+ clean_version = version_str.gsub(/\A[^0-9]*/, "")
2486
+ clean_version if clean_version.match?(/\A\d+\.\d+\.\d+\z/)
2487
+ rescue StandardError
2488
+ nil
2489
+ end
2490
+
2491
+ def check_rsc_procfile_watcher
2492
+ procfile_path = "Procfile.dev"
2493
+
2494
+ unless File.exist?(procfile_path)
2495
+ checker.add_warning("⚠️ Procfile.dev not found — cannot verify RSC bundle watcher")
2496
+ checker.add_info(" 💡 If using a custom process manager, ensure RSC bundle is built separately")
2497
+ return
2498
+ end
2499
+
2500
+ uncommented_watcher = File.readlines(procfile_path).any? do |line|
2501
+ next if line.match?(/^\s*#/)
2502
+
2503
+ line.include?("RSC_BUNDLE_ONLY")
2504
+ end
2505
+ if uncommented_watcher
2506
+ checker.add_success("✅ RSC bundle watcher configured in Procfile.dev")
2507
+ else
2508
+ checker.add_warning(<<~MSG.strip)
2509
+ ⚠️ RSC bundle watcher not found in Procfile.dev.
2510
+
2511
+ The RSC bundle needs to be built separately from client/server bundles.
2512
+
2513
+ If using Procfile.dev, add:
2514
+ rsc-bundle: RSC_BUNDLE_ONLY=yes bin/shakapacker --watch
2515
+
2516
+ If using a custom process manager, ensure the RSC bundle is built with
2517
+ the RSC_BUNDLE_ONLY=yes environment variable.
2518
+ MSG
2519
+ end
2520
+ end
2157
2521
  end
2158
2522
  # rubocop:enable Metrics/ClassLength
2159
2523
  end