react_on_rails 16.1.2 → 16.2.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 (106) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +2 -0
  3. data/.rubocop.yml +85 -0
  4. data/Gemfile.development_dependencies +8 -7
  5. data/Gemfile.lock +158 -119
  6. data/Steepfile +56 -0
  7. data/lib/generators/react_on_rails/base_generator.rb +43 -120
  8. data/lib/generators/react_on_rails/dev_tests_generator.rb +2 -1
  9. data/lib/generators/react_on_rails/generator_helper.rb +102 -2
  10. data/lib/generators/react_on_rails/install_generator.rb +36 -156
  11. data/lib/generators/react_on_rails/js_dependency_manager.rb +383 -0
  12. data/lib/generators/react_on_rails/templates/base/base/.dev-services.yml.example +76 -0
  13. data/lib/generators/react_on_rails/templates/base/base/bin/shakapacker-precompile-hook +30 -0
  14. data/lib/generators/react_on_rails/templates/base/base/bin/switch-bundler +141 -0
  15. data/lib/generators/react_on_rails/templates/base/base/config/initializers/react_on_rails.rb.tt +44 -45
  16. data/lib/generators/react_on_rails/templates/base/base/config/{shakapacker.yml → shakapacker.yml.tt} +28 -3
  17. data/lib/generators/react_on_rails/templates/base/base/config/webpack/development.js.tt +15 -9
  18. data/lib/generators/react_on_rails/templates/base/base/config/webpack/serverWebpackConfig.js.tt +42 -6
  19. data/lib/react_on_rails/configuration.rb +149 -32
  20. data/lib/react_on_rails/controller.rb +3 -3
  21. data/lib/react_on_rails/dev/pack_generator.rb +168 -2
  22. data/lib/react_on_rails/dev/process_manager.rb +136 -14
  23. data/lib/react_on_rails/dev/server_manager.rb +194 -26
  24. data/lib/react_on_rails/dev/service_checker.rb +200 -0
  25. data/lib/react_on_rails/doctor.rb +341 -12
  26. data/lib/react_on_rails/engine.rb +75 -1
  27. data/lib/react_on_rails/git_utils.rb +3 -1
  28. data/lib/react_on_rails/helper.rb +70 -192
  29. data/lib/react_on_rails/locales/base.rb +17 -5
  30. data/lib/react_on_rails/packer_utils.rb +79 -2
  31. data/lib/react_on_rails/packs_generator.rb +57 -39
  32. data/lib/react_on_rails/prerender_error.rb +74 -17
  33. data/lib/react_on_rails/pro_helper.rb +64 -0
  34. data/lib/react_on_rails/react_component/render_options.rb +7 -7
  35. data/lib/react_on_rails/server_rendering_pool/ruby_embedded_java_script.rb +2 -5
  36. data/lib/react_on_rails/smart_error.rb +326 -0
  37. data/lib/react_on_rails/system_checker.rb +8 -9
  38. data/lib/react_on_rails/test_helper/webpack_assets_status_checker.rb +16 -7
  39. data/lib/react_on_rails/utils.rb +241 -55
  40. data/lib/react_on_rails/version.rb +1 -1
  41. data/lib/react_on_rails/version_checker.rb +383 -35
  42. data/lib/tasks/generate_packs.rake +12 -6
  43. data/lib/tasks/locale.rake +6 -1
  44. data/rakelib/docker.rake +26 -0
  45. data/rakelib/dummy_apps.rake +30 -0
  46. data/rakelib/example_type.rb +121 -0
  47. data/rakelib/examples_config.yml +52 -0
  48. data/rakelib/lint.rake +52 -0
  49. data/rakelib/node_package.rake +15 -0
  50. data/rakelib/rbs.rake +70 -0
  51. data/rakelib/run_rspec.rake +223 -0
  52. data/rakelib/shakapacker_examples.rake +171 -0
  53. data/rakelib/task_helpers.rb +134 -0
  54. data/rakelib/update_changelog.rake +73 -0
  55. data/react_on_rails.gemspec +4 -3
  56. data/sig/README.md +52 -0
  57. data/sig/react_on_rails/configuration.rbs +96 -0
  58. data/sig/react_on_rails/controller.rbs +15 -0
  59. data/sig/react_on_rails/dev/file_manager.rbs +15 -0
  60. data/sig/react_on_rails/dev/pack_generator.rbs +19 -0
  61. data/sig/react_on_rails/dev/process_manager.rbs +22 -0
  62. data/sig/react_on_rails/dev/server_manager.rbs +39 -0
  63. data/sig/react_on_rails/dev/service_checker.rbs +22 -0
  64. data/sig/react_on_rails/error.rbs +4 -0
  65. data/sig/react_on_rails/generators/js_dependency_manager.rbs +123 -0
  66. data/sig/react_on_rails/git_utils.rbs +8 -0
  67. data/sig/react_on_rails/helper.rbs +65 -0
  68. data/sig/react_on_rails/json_parse_error.rbs +10 -0
  69. data/sig/react_on_rails/locales.rbs +46 -0
  70. data/sig/react_on_rails/packer_utils.rbs +15 -0
  71. data/sig/react_on_rails/prerender_error.rbs +21 -0
  72. data/sig/react_on_rails/server_rendering_pool.rbs +12 -0
  73. data/sig/react_on_rails/smart_error.rbs +28 -0
  74. data/sig/react_on_rails/test_helper.rbs +11 -0
  75. data/sig/react_on_rails/utils.rbs +34 -0
  76. data/sig/react_on_rails/version_checker.rbs +12 -0
  77. data/sig/react_on_rails.rbs +17 -0
  78. metadata +49 -32
  79. data/AI_AGENT_INSTRUCTIONS.md +0 -63
  80. data/CHANGELOG.md +0 -1836
  81. data/CLAUDE.md +0 -135
  82. data/CODING_AGENTS.md +0 -313
  83. data/CONTRIBUTING.md +0 -668
  84. data/Dockerfile_tests +0 -12
  85. data/KUDOS.md +0 -114
  86. data/LICENSE.md +0 -47
  87. data/LICENSES/README.md +0 -14
  88. data/NEWS.md +0 -62
  89. data/PROJECTS.md +0 -63
  90. data/REACT-ON-RAILS-PRO-LICENSE.md +0 -129
  91. data/README.md +0 -217
  92. data/SUMMARY.md +0 -88
  93. data/TODO.md +0 -135
  94. data/bin/lefthook/check-trailing-newlines +0 -38
  95. data/bin/lefthook/get-changed-files +0 -26
  96. data/bin/lefthook/prettier-format +0 -26
  97. data/bin/lefthook/ruby-autofix +0 -26
  98. data/bin/lefthook/ruby-lint +0 -27
  99. data/docker-compose.yml +0 -11
  100. data/eslint.config.ts +0 -232
  101. data/knip.ts +0 -114
  102. data/lib/react_on_rails/pro/NOTICE +0 -21
  103. data/lib/react_on_rails/pro/helper.rb +0 -122
  104. data/lib/react_on_rails/pro/utils.rb +0 -53
  105. data/tsconfig.eslint.json +0 -6
  106. data/tsconfig.json +0 -19
@@ -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
@@ -465,7 +468,6 @@ module ReactOnRails
465
468
  check_npm_wildcards
466
469
  end
467
470
 
468
- # rubocop:disable Metrics/CyclomaticComplexity
469
471
  def check_gem_wildcards
470
472
  gemfile_path = ENV["BUNDLE_GEMFILE"] || "Gemfile"
471
473
  return unless File.exist?(gemfile_path)
@@ -487,9 +489,7 @@ module ReactOnRails
487
489
  # Ignore errors reading Gemfile
488
490
  end
489
491
  end
490
- # rubocop:enable Metrics/CyclomaticComplexity
491
492
 
492
- # rubocop:disable Metrics/CyclomaticComplexity
493
493
  def check_npm_wildcards
494
494
  return unless File.exist?("package.json")
495
495
 
@@ -511,7 +511,6 @@ module ReactOnRails
511
511
  # Ignore other errors
512
512
  end
513
513
  end
514
- # rubocop:enable Metrics/CyclomaticComplexity
515
514
 
516
515
  def check_key_configuration_files
517
516
  files_to_check = {
@@ -535,7 +534,6 @@ module ReactOnRails
535
534
  check_server_rendering_engine
536
535
  end
537
536
 
538
- # rubocop:disable Metrics/CyclomaticComplexity
539
537
  def check_layout_files
540
538
  layout_files = Dir.glob("app/views/layouts/**/*.erb")
541
539
  return if layout_files.empty?
@@ -562,7 +560,6 @@ module ReactOnRails
562
560
  end
563
561
  end
564
562
  end
565
- # rubocop:enable Metrics/CyclomaticComplexity
566
563
 
567
564
  # rubocop:disable Metrics/CyclomaticComplexity
568
565
  def check_server_rendering_engine
@@ -600,7 +597,6 @@ module ReactOnRails
600
597
  end
601
598
  # rubocop:enable Metrics/CyclomaticComplexity
602
599
 
603
- # rubocop:disable Metrics/CyclomaticComplexity
604
600
  def check_shakapacker_configuration_details
605
601
  return unless File.exist?("config/shakapacker.yml")
606
602
 
@@ -629,7 +625,6 @@ module ReactOnRails
629
625
  checker.add_warning(" ⚠️ Could not run 'rake shakapacker:info': #{e.message}")
630
626
  end
631
627
  end
632
- # rubocop:enable Metrics/CyclomaticComplexity
633
628
 
634
629
  def check_react_on_rails_configuration_details
635
630
  check_react_on_rails_initializer
@@ -664,6 +659,7 @@ module ReactOnRails
664
659
  end
665
660
  end
666
661
 
662
+ # rubocop:disable Metrics/CyclomaticComplexity
667
663
  def analyze_server_rendering_config(content)
668
664
  checker.add_info("\n🖥️ Server Rendering:")
669
665
 
@@ -675,6 +671,19 @@ module ReactOnRails
675
671
  checker.add_info(" server_bundle_js_file: server-bundle.js (default)")
676
672
  end
677
673
 
674
+ # Server bundle output path
675
+ server_bundle_path_match = content.match(/config\.server_bundle_output_path\s*=\s*["']([^"']+)["']/)
676
+ default_path = ReactOnRails::DEFAULT_SERVER_BUNDLE_OUTPUT_PATH
677
+ rails_bundle_path = server_bundle_path_match ? server_bundle_path_match[1] : default_path
678
+ checker.add_info(" server_bundle_output_path: #{rails_bundle_path}")
679
+
680
+ # Enforce private server bundles
681
+ enforce_private_match = content.match(/config\.enforce_private_server_bundles\s*=\s*([^\s\n,]+)/)
682
+ checker.add_info(" enforce_private_server_bundles: #{enforce_private_match[1]}") if enforce_private_match
683
+
684
+ # Check Shakapacker integration and provide recommendations
685
+ check_shakapacker_private_output_path(rails_bundle_path)
686
+
678
687
  # RSC bundle file (Pro feature)
679
688
  rsc_bundle_match = content.match(/config\.rsc_bundle_js_file\s*=\s*["']([^"']+)["']/)
680
689
  if rsc_bundle_match
@@ -699,9 +708,9 @@ module ReactOnRails
699
708
 
700
709
  checker.add_info(" raise_on_prerender_error: #{raise_on_error_match[1]}")
701
710
  end
702
- # rubocop:enable Metrics/AbcSize
711
+ # rubocop:enable Metrics/CyclomaticComplexity
703
712
 
704
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
713
+ # rubocop:disable Metrics/CyclomaticComplexity
705
714
  def analyze_performance_config(content)
706
715
  checker.add_info("\n⚡ Performance & Loading:")
707
716
 
@@ -732,10 +741,12 @@ module ReactOnRails
732
741
  auto_load_match = content.match(/config\.auto_load_bundle\s*=\s*([^\s\n,]+)/)
733
742
  checker.add_info(" auto_load_bundle: #{auto_load_match[1]}") if auto_load_match
734
743
 
735
- # Immediate hydration (Pro feature)
744
+ # Deprecated immediate_hydration setting
736
745
  immediate_hydration_match = content.match(/config\.immediate_hydration\s*=\s*([^\s\n,]+)/)
737
746
  if immediate_hydration_match
738
- checker.add_info(" immediate_hydration: #{immediate_hydration_match[1]} (React on Rails Pro)")
747
+ checker.add_warning(" ⚠️ immediate_hydration: #{immediate_hydration_match[1]} (DEPRECATED)")
748
+ checker.add_info(" 💡 This setting is no longer used. Immediate hydration is now automatic for Pro users.")
749
+ checker.add_info(" 💡 Remove this line from your config/initializers/react_on_rails.rb file.")
739
750
  end
740
751
 
741
752
  # Component registry timeout
@@ -1108,6 +1119,124 @@ module ReactOnRails
1108
1119
  end
1109
1120
  end
1110
1121
 
1122
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
1123
+ def check_server_bundle_prerender_consistency
1124
+ config_path = "config/initializers/react_on_rails.rb"
1125
+ return unless File.exist?(config_path)
1126
+
1127
+ checker.add_info("\n🔍 Server Rendering Consistency:")
1128
+
1129
+ begin
1130
+ content = File.read(config_path)
1131
+
1132
+ # Check for server bundle configuration
1133
+ server_bundle_match = content.match(/config\.server_bundle_js_file\s*=\s*["']([^"']+)["']/)
1134
+ server_bundle_set = server_bundle_match && server_bundle_match[1].present?
1135
+
1136
+ # Check for global prerender setting
1137
+ prerender_match = content.match(/config\.prerender\s*=\s*(true)/)
1138
+ prerender_set = prerender_match
1139
+
1140
+ # Check if prerender is used in views
1141
+ uses_prerender = uses_prerender_in_views?
1142
+
1143
+ # Analyze the configuration
1144
+ if (prerender_set || uses_prerender) && !server_bundle_set
1145
+ checker.add_warning(" ⚠️ Server rendering is enabled but server_bundle_js_file is not configured")
1146
+ checker.add_info(" 💡 Set config.server_bundle_js_file = 'server-bundle.js' to enable SSR")
1147
+ checker.add_info(" 💡 See: https://www.shakacode.com/react-on-rails/docs/guides/server-rendering")
1148
+ elsif server_bundle_set && !prerender_set && !uses_prerender
1149
+ checker.add_info(" ℹ️ server_bundle_js_file is configured but prerender doesn't appear to be used")
1150
+ checker.add_info(" 💡 Either use prerender: true in react_component calls or remove server_bundle_js_file")
1151
+ else
1152
+ checker.add_success(" ✅ Server rendering configuration is consistent")
1153
+ end
1154
+ rescue StandardError => e
1155
+ checker.add_warning(" ⚠️ Could not analyze server rendering configuration: #{e.message}")
1156
+ end
1157
+ end
1158
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
1159
+
1160
+ def uses_prerender_in_views?
1161
+ # Check view files for prerender: true
1162
+ view_files = Dir.glob("app/views/**/*.{erb,haml,slim}")
1163
+ view_files.any? do |file|
1164
+ next unless File.exist?(file)
1165
+
1166
+ File.read(file).match?(/prerender:\s*true/)
1167
+ end
1168
+ rescue StandardError
1169
+ false
1170
+ end
1171
+
1172
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
1173
+ def check_build_test_configuration
1174
+ config_path = "config/initializers/react_on_rails.rb"
1175
+ shakapacker_yml = "config/shakapacker.yml"
1176
+
1177
+ return unless File.exist?(config_path)
1178
+
1179
+ checker.add_info("\n🧪 Test Asset Compilation:")
1180
+
1181
+ begin
1182
+ config_content = File.read(config_path)
1183
+ has_build_test_command = config_content.match(/^\s*config\.build_test_command\s*=\s*["']([^"']+)["']/)
1184
+ uses_test_helper = uses_react_on_rails_test_helper?
1185
+
1186
+ if File.exist?(shakapacker_yml)
1187
+ shakapacker_content = File.read(shakapacker_yml)
1188
+ # Match test section and look for compile: true
1189
+ has_compile_true = shakapacker_content.match(/^test:.*?^\s+compile:\s*true/m)
1190
+
1191
+ if has_build_test_command && has_compile_true
1192
+ checker.add_warning(" ⚠️ Both build_test_command and shakapacker compile: true are configured")
1193
+ checker.add_info(" 💡 These are mutually exclusive - use only one approach")
1194
+ checker.add_info(" 💡 Recommended: Use compile: true in shakapacker.yml (simpler)")
1195
+ checker.add_info(" 💡 Alternative: Use build_test_command with ReactOnRails::TestHelper (explicit control)")
1196
+ checker.add_info(" 📖 See: https://github.com/shakacode/react_on_rails/blob/master/docs/guides/testing-configuration.md")
1197
+ elsif has_build_test_command && !uses_test_helper
1198
+ checker.add_warning(" ⚠️ build_test_command is set but ReactOnRails::TestHelper is not configured")
1199
+ checker.add_info(" 💡 Add to spec/rails_helper.rb:")
1200
+ checker.add_info(" ReactOnRails::TestHelper.configure_rspec_to_compile_assets(config)")
1201
+ checker.add_info(" 💡 Or remove build_test_command and use compile: true in shakapacker.yml")
1202
+ elsif !has_build_test_command && uses_test_helper
1203
+ checker.add_error(" 🚫 ReactOnRails::TestHelper is configured but build_test_command is not set")
1204
+ checker.add_info(" 💡 Add to config/initializers/react_on_rails.rb:")
1205
+ checker.add_info(" config.build_test_command = 'RAILS_ENV=test bin/shakapacker'")
1206
+ checker.add_info(" 💡 Or remove TestHelper and use compile: true in shakapacker.yml")
1207
+ elsif !has_build_test_command && !has_compile_true && !uses_test_helper
1208
+ checker.add_warning(" ⚠️ No test asset compilation configured")
1209
+ checker.add_info(" 💡 Recommended: Add to shakapacker.yml test section:")
1210
+ checker.add_info(" compile: true")
1211
+ checker.add_info(" 📖 See: https://github.com/shakacode/react_on_rails/blob/master/docs/guides/testing-configuration.md")
1212
+ elsif has_compile_true
1213
+ checker.add_success(" ✅ Test assets configured via Shakapacker auto-compilation")
1214
+ checker.add_info(" (compile: true in shakapacker.yml)")
1215
+ elsif has_build_test_command && uses_test_helper
1216
+ checker.add_success(" ✅ Test assets configured via React on Rails test helper")
1217
+ checker.add_info(" (build_test_command + ReactOnRails::TestHelper)")
1218
+ end
1219
+ else
1220
+ checker.add_warning(" ⚠️ config/shakapacker.yml not found")
1221
+ end
1222
+ rescue StandardError => e
1223
+ checker.add_warning(" ⚠️ Could not analyze test configuration: #{e.message}")
1224
+ end
1225
+ end
1226
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
1227
+
1228
+ def uses_react_on_rails_test_helper?
1229
+ spec_helpers = ["spec/rails_helper.rb", "spec/spec_helper.rb", "test/test_helper.rb"]
1230
+ spec_helpers.any? do |helper|
1231
+ next unless File.exist?(helper)
1232
+
1233
+ content = File.read(helper)
1234
+ content.include?("configure_rspec_to_compile_assets") || content.include?("ensure_assets_compiled")
1235
+ end
1236
+ rescue StandardError
1237
+ false
1238
+ end
1239
+
1111
1240
  def relativize_path(absolute_path)
1112
1241
  return absolute_path unless absolute_path.is_a?(String)
1113
1242
 
@@ -1144,6 +1273,206 @@ module ReactOnRails
1144
1273
  checker.add_info(" #{label}: <error reading value: #{e.message}>")
1145
1274
  end
1146
1275
  end
1276
+
1277
+ # Comment patterns used for filtering out commented async usage
1278
+ ERB_COMMENT_PATTERN = /<%\s*#.*javascript_pack_tag/
1279
+ HAML_COMMENT_PATTERN = /^\s*-#.*javascript_pack_tag/
1280
+ SLIM_COMMENT_PATTERN = %r{^\s*/.*javascript_pack_tag}
1281
+ HTML_COMMENT_PATTERN = /<!--.*javascript_pack_tag/
1282
+
1283
+ def check_async_usage
1284
+ # When Pro is installed, async is fully supported and is the default behavior
1285
+ # No need to check for async usage in this case
1286
+ return if ReactOnRails::Utils.react_on_rails_pro?
1287
+
1288
+ async_issues = []
1289
+
1290
+ # Check 1: javascript_pack_tag with :async in view files
1291
+ view_files_with_async = scan_view_files_for_async_pack_tag
1292
+ unless view_files_with_async.empty?
1293
+ async_issues << "javascript_pack_tag with :async found in view files:"
1294
+ view_files_with_async.each do |file|
1295
+ async_issues << " • #{file}"
1296
+ end
1297
+ end
1298
+
1299
+ # Check 2: generated_component_packs_loading_strategy = :async
1300
+ if config_has_async_loading_strategy?
1301
+ async_issues << "config.generated_component_packs_loading_strategy = :async in initializer"
1302
+ end
1303
+
1304
+ return if async_issues.empty?
1305
+
1306
+ # Report errors if async usage is found without Pro
1307
+ checker.add_error("🚫 :async usage detected without React on Rails Pro")
1308
+ async_issues.each { |issue| checker.add_error(" #{issue}") }
1309
+ checker.add_info(" 💡 :async can cause race conditions. Options:")
1310
+ checker.add_info(" 1. Upgrade to React on Rails Pro (recommended for :async support)")
1311
+ checker.add_info(" 2. Change to :defer or :sync loading strategy")
1312
+ checker.add_info(" 📖 https://www.shakacode.com/react-on-rails/docs/guides/configuration/")
1313
+ end
1314
+
1315
+ def scan_view_files_for_async_pack_tag
1316
+ view_patterns = ["app/views/**/*.erb", "app/views/**/*.haml", "app/views/**/*.slim"]
1317
+ files_with_async = view_patterns.flat_map { |pattern| scan_pattern_for_async(pattern) }
1318
+ files_with_async.compact
1319
+ rescue Errno::ENOENT, Encoding::InvalidByteSequenceError, Encoding::UndefinedConversionError => e
1320
+ # Log the error if Rails logger is available
1321
+ log_debug("Error scanning view files for async: #{e.message}")
1322
+ []
1323
+ end
1324
+
1325
+ def scan_pattern_for_async(pattern)
1326
+ Dir.glob(pattern).filter_map do |file|
1327
+ next unless File.exist?(file)
1328
+
1329
+ content = File.read(file)
1330
+ next if content_has_only_commented_async?(content)
1331
+ next unless file_has_async_pack_tag?(content)
1332
+
1333
+ relativize_path(file)
1334
+ end
1335
+ end
1336
+
1337
+ def file_has_async_pack_tag?(content)
1338
+ # Match javascript_pack_tag with :async symbol or async: true hash syntax
1339
+ # Examples that should match:
1340
+ # - javascript_pack_tag "app", :async
1341
+ # - javascript_pack_tag "app", async: true
1342
+ # - javascript_pack_tag "app", :async, other_option: value
1343
+ # Examples that should NOT match:
1344
+ # - javascript_pack_tag "app", defer: "async" (async is a string value, not the option)
1345
+ # - javascript_pack_tag "app", :defer
1346
+ # Note: Theoretical edge case `data: { async: true }` would match but is extremely unlikely
1347
+ # in real code and represents a harmless false positive (showing a warning when not needed)
1348
+ # Use word boundary \b to ensure :async is not part of a longer symbol like :async_mode
1349
+ # [^<]* allows matching across newlines within ERB tags but stops at closing ERB tag
1350
+ content.match?(/javascript_pack_tag[^<]*(?::async\b|async:\s*true)/)
1351
+ end
1352
+
1353
+ def content_has_only_commented_async?(content)
1354
+ # Check if all occurrences of javascript_pack_tag with :async are in comments
1355
+ # Returns true if ONLY commented async usage exists (no active async usage)
1356
+
1357
+ # First check if there's any javascript_pack_tag with :async in the full content
1358
+ return true unless file_has_async_pack_tag?(content)
1359
+
1360
+ # Strategy: Remove all commented lines, then check if any :async remains
1361
+ # This handles both single-line and multi-line tags correctly
1362
+ uncommented_lines = content.each_line.reject do |line|
1363
+ line.match?(ERB_COMMENT_PATTERN) ||
1364
+ line.match?(HAML_COMMENT_PATTERN) ||
1365
+ line.match?(SLIM_COMMENT_PATTERN) ||
1366
+ line.match?(HTML_COMMENT_PATTERN)
1367
+ end
1368
+
1369
+ uncommented_content = uncommented_lines.join
1370
+ # If no async found in uncommented content, all async usage was commented
1371
+ !file_has_async_pack_tag?(uncommented_content)
1372
+ end
1373
+
1374
+ def config_has_async_loading_strategy?
1375
+ config_path = "config/initializers/react_on_rails.rb"
1376
+ return false unless File.exist?(config_path)
1377
+
1378
+ content = File.read(config_path)
1379
+ # Check if generated_component_packs_loading_strategy is set to :async
1380
+ # Filter out commented lines (lines starting with # after optional whitespace)
1381
+ content.each_line.any? do |line|
1382
+ # Skip lines that start with # (after optional whitespace)
1383
+ next if line.match?(/^\s*#/)
1384
+
1385
+ # Match: config.generated_component_packs_loading_strategy = :async
1386
+ # Use word boundary \b to ensure :async is the complete symbol, not part of :async_mode etc.
1387
+ line.match?(/config\.generated_component_packs_loading_strategy\s*=\s*:async\b/)
1388
+ end
1389
+ rescue Errno::ENOENT, Encoding::InvalidByteSequenceError, Encoding::UndefinedConversionError => e
1390
+ # Log the error if Rails logger is available
1391
+ log_debug("Error checking async loading strategy: #{e.message}")
1392
+ false
1393
+ end
1394
+
1395
+ def log_debug(message)
1396
+ Rails.logger&.debug(message)
1397
+ end
1398
+
1399
+ # Check Shakapacker private_output_path integration and provide recommendations
1400
+ def check_shakapacker_private_output_path(rails_bundle_path)
1401
+ return report_no_shakapacker unless defined?(::Shakapacker)
1402
+ return report_upgrade_shakapacker unless ::Shakapacker.config.respond_to?(:private_output_path)
1403
+
1404
+ check_shakapacker_9_private_output_path(rails_bundle_path)
1405
+ rescue StandardError => e
1406
+ checker.add_info("\n ℹ️ Could not check Shakapacker config: #{e.message}")
1407
+ end
1408
+
1409
+ def report_no_shakapacker
1410
+ checker.add_info("\n ℹ️ Shakapacker not detected - using manual configuration")
1411
+ end
1412
+
1413
+ def report_upgrade_shakapacker
1414
+ checker.add_info(<<~MSG.strip)
1415
+ \n 💡 Recommendation: Upgrade to Shakapacker 9.0+
1416
+
1417
+ Shakapacker 9.0+ adds 'private_output_path' in shakapacker.yml for server bundles.
1418
+ This eliminates the need to configure server_bundle_output_path separately.
1419
+
1420
+ Benefits:
1421
+ - Single source of truth in shakapacker.yml
1422
+ - Automatic detection by React on Rails
1423
+ - No configuration duplication
1424
+ MSG
1425
+ end
1426
+
1427
+ def check_shakapacker_9_private_output_path(rails_bundle_path)
1428
+ private_path = ::Shakapacker.config.private_output_path
1429
+
1430
+ if private_path
1431
+ report_shakapacker_path_status(private_path, rails_bundle_path)
1432
+ else
1433
+ report_configure_private_output_path(rails_bundle_path)
1434
+ end
1435
+ end
1436
+
1437
+ def report_shakapacker_path_status(private_path, rails_bundle_path)
1438
+ relative_path = ReactOnRails::Utils.normalize_to_relative_path(private_path)
1439
+ # Normalize both paths for comparison (remove trailing slashes)
1440
+ normalized_relative = relative_path.to_s.chomp("/")
1441
+ normalized_rails = rails_bundle_path.to_s.chomp("/")
1442
+
1443
+ if normalized_relative == normalized_rails
1444
+ checker.add_success("\n ✅ Using Shakapacker 9.0+ private_output_path: '#{relative_path}'")
1445
+ checker.add_info(" Auto-detected from shakapacker.yml - no manual config needed")
1446
+ else
1447
+ report_configuration_mismatch(relative_path, rails_bundle_path)
1448
+ end
1449
+ end
1450
+
1451
+ def report_configuration_mismatch(relative_path, rails_bundle_path)
1452
+ checker.add_warning(<<~MSG.strip)
1453
+ \n ⚠️ Configuration mismatch detected!
1454
+
1455
+ Shakapacker private_output_path: '#{relative_path}'
1456
+ React on Rails server_bundle_output_path: '#{rails_bundle_path}'
1457
+
1458
+ Recommendation: Remove server_bundle_output_path from your React on Rails
1459
+ initializer and let it auto-detect from shakapacker.yml private_output_path.
1460
+ MSG
1461
+ end
1462
+
1463
+ def report_configure_private_output_path(rails_bundle_path)
1464
+ checker.add_info(<<~MSG.strip)
1465
+ \n 💡 Recommendation: Configure private_output_path in shakapacker.yml
1466
+
1467
+ Add to config/shakapacker.yml:
1468
+ private_output_path: #{rails_bundle_path}
1469
+
1470
+ This will:
1471
+ - Keep webpack and Rails configs in sync automatically
1472
+ - Enable auto-detection by React on Rails
1473
+ - Serve as single source of truth for server bundle location
1474
+ MSG
1475
+ end
1147
1476
  end
1148
1477
  # rubocop:enable Metrics/ClassLength
1149
1478
  end
@@ -4,8 +4,82 @@ require "rails/railtie"
4
4
 
5
5
  module ReactOnRails
6
6
  class Engine < ::Rails::Engine
7
+ # Validate package versions and compatibility on Rails startup
8
+ # This ensures the application fails fast if versions don't match or packages are misconfigured
9
+ # Skip validation during installation tasks (e.g., shakapacker:install) or generator runtime
10
+ initializer "react_on_rails.validate_version_and_package_compatibility" do
11
+ config.after_initialize do
12
+ next if Engine.skip_version_validation?
13
+
14
+ Rails.logger.info "[React on Rails] Validating package version and compatibility..."
15
+ VersionChecker.build.validate_version_and_package_compatibility!
16
+ Rails.logger.info "[React on Rails] Package validation successful"
17
+ end
18
+ end
19
+
20
+ # Determine if version validation should be skipped
21
+ #
22
+ # This method checks multiple conditions to determine if package version validation
23
+ # should be skipped. Validation is skipped during setup scenarios where the npm
24
+ # package isn't installed yet (e.g., during generator execution).
25
+ #
26
+ # @return [Boolean] true if validation should be skipped
27
+ #
28
+ # @note Thread Safety: ENV variables are process-global. In practice, Rails generators
29
+ # run in a single process, so concurrent execution is not a concern. If running
30
+ # generators concurrently (e.g., in parallel tests), ensure tests run in separate
31
+ # processes to avoid ENV variable conflicts.
32
+ #
33
+ # @example Testing with parallel processes
34
+ # # In RSpec configuration:
35
+ # config.before(:each) do |example|
36
+ # ENV.delete("REACT_ON_RAILS_SKIP_VALIDATION")
37
+ # end
38
+ #
39
+ # @note Manual ENV Setting: While this ENV variable is designed to be set by generators,
40
+ # users can manually set it (e.g., `REACT_ON_RAILS_SKIP_VALIDATION=true rails server`)
41
+ # to bypass validation. This should only be done temporarily during debugging or
42
+ # setup scenarios. The validation helps catch version mismatches early, so bypassing
43
+ # it in production is not recommended.
44
+ def self.skip_version_validation?
45
+ # Skip if explicitly disabled via environment variable (set by generators)
46
+ # Using ENV variable instead of ARGV because Rails can modify/clear ARGV during
47
+ # initialization, making ARGV unreliable for detecting generator context. The ENV
48
+ # variable persists through the entire Rails initialization process.
49
+ if ENV["REACT_ON_RAILS_SKIP_VALIDATION"] == "true"
50
+ Rails.logger.debug "[React on Rails] Skipping validation - disabled via environment variable"
51
+ return true
52
+ end
53
+
54
+ # Check package.json first as it's cheaper and handles more cases
55
+ if package_json_missing?
56
+ Rails.logger.debug "[React on Rails] Skipping validation - package.json not found"
57
+ return true
58
+ end
59
+
60
+ # Skip during generator runtime since packages are installed during execution
61
+ # This is a fallback check in case ENV wasn't set, though ENV is the primary mechanism
62
+ if running_generator?
63
+ Rails.logger.debug "[React on Rails] Skipping validation during generator runtime"
64
+ return true
65
+ end
66
+
67
+ false
68
+ end
69
+
70
+ # Check if we're running a Rails generator
71
+ # @return [Boolean] true if running a generator
72
+ def self.running_generator?
73
+ !ARGV.empty? && ARGV.first&.in?(%w[generate g])
74
+ end
75
+
76
+ # Check if package.json doesn't exist yet
77
+ # @return [Boolean] true if package.json is missing
78
+ def self.package_json_missing?
79
+ !File.exist?(VersionChecker::NodePackageVersion.package_json_path)
80
+ end
81
+
7
82
  config.to_prepare do
8
- VersionChecker.build.log_if_gem_and_node_package_versions_differ
9
83
  ReactOnRails::ServerRenderingPool.reset_pool
10
84
  end
11
85
 
@@ -5,7 +5,9 @@ require "English"
5
5
  module ReactOnRails
6
6
  module GitUtils
7
7
  def self.uncommitted_changes?(message_handler, git_installed: true)
8
- return false if ENV["COVERAGE"] == "true"
8
+ # Skip check in CI environments - CI often makes temporary modifications
9
+ # (e.g., script/convert for minimum version testing) before running generators
10
+ return false if ENV["CI"] == "true" || ENV["COVERAGE"] == "true"
9
11
 
10
12
  status = `git status --porcelain`
11
13
  return false if git_installed && status&.empty?