react_on_rails 16.0.0 → 16.0.1.rc.2

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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +124 -77
  3. data/CLAUDE.md +46 -2
  4. data/CONTRIBUTING.md +12 -6
  5. data/Gemfile.development_dependencies +1 -0
  6. data/Gemfile.lock +3 -1
  7. data/LICENSE.md +15 -1
  8. data/README.md +68 -18
  9. data/bin/lefthook/check-trailing-newlines +38 -0
  10. data/bin/lefthook/get-changed-files +26 -0
  11. data/bin/lefthook/prettier-format +26 -0
  12. data/bin/lefthook/ruby-autofix +26 -0
  13. data/bin/lefthook/ruby-lint +27 -0
  14. data/eslint.config.ts +10 -0
  15. data/knip.ts +20 -9
  16. data/lib/generators/react_on_rails/USAGE +65 -0
  17. data/lib/generators/react_on_rails/base_generator.rb +7 -7
  18. data/lib/generators/react_on_rails/generator_helper.rb +4 -0
  19. data/lib/generators/react_on_rails/generator_messages.rb +2 -2
  20. data/lib/generators/react_on_rails/install_generator.rb +115 -7
  21. data/lib/generators/react_on_rails/react_no_redux_generator.rb +16 -4
  22. data/lib/generators/react_on_rails/react_with_redux_generator.rb +83 -14
  23. data/lib/generators/react_on_rails/templates/base/base/app/javascript/src/HelloWorld/ror_components/HelloWorld.client.tsx +25 -0
  24. data/lib/generators/react_on_rails/templates/base/base/app/javascript/src/HelloWorld/ror_components/HelloWorld.server.tsx +5 -0
  25. data/lib/generators/react_on_rails/templates/base/base/bin/dev +12 -24
  26. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/actions/helloWorldActionCreators.ts +18 -0
  27. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/components/HelloWorld.tsx +24 -0
  28. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/constants/helloWorldConstants.ts +6 -0
  29. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/containers/HelloWorldContainer.ts +20 -0
  30. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/reducers/helloWorldReducer.ts +22 -0
  31. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/startup/HelloWorldApp.client.tsx +23 -0
  32. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/startup/HelloWorldApp.server.tsx +5 -0
  33. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/store/helloWorldStore.ts +18 -0
  34. data/lib/react_on_rails/configuration.rb +10 -6
  35. data/lib/react_on_rails/dev/server_manager.rb +185 -28
  36. data/lib/react_on_rails/doctor.rb +1149 -0
  37. data/lib/react_on_rails/helper.rb +9 -78
  38. data/lib/react_on_rails/pro/NOTICE +21 -0
  39. data/lib/react_on_rails/pro/helper.rb +122 -0
  40. data/lib/react_on_rails/pro/utils.rb +53 -0
  41. data/lib/react_on_rails/react_component/render_options.rb +6 -2
  42. data/lib/react_on_rails/system_checker.rb +659 -0
  43. data/lib/react_on_rails/version.rb +1 -1
  44. data/lib/tasks/doctor.rake +48 -0
  45. data/lib/tasks/generate_packs.rake +127 -4
  46. data/package-lock.json +11984 -0
  47. metadata +26 -6
  48. data/lib/generators/react_on_rails/bin/dev +0 -46
@@ -0,0 +1,1149 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require_relative "utils"
5
+ require_relative "system_checker"
6
+
7
+ begin
8
+ require "rainbow"
9
+ rescue LoadError
10
+ # Fallback if Rainbow is not available - define Kernel-level Rainbow method
11
+ # rubocop:disable Naming/MethodName
12
+ def Rainbow(text)
13
+ SimpleColorWrapper.new(text)
14
+ end
15
+ # rubocop:enable Naming/MethodName
16
+
17
+ class SimpleColorWrapper
18
+ def initialize(text)
19
+ @text = text
20
+ end
21
+
22
+ def method_missing(_method, *_args)
23
+ self
24
+ end
25
+
26
+ def respond_to_missing?(_method, _include_private = false)
27
+ true
28
+ end
29
+
30
+ def to_s
31
+ @text
32
+ end
33
+ end
34
+ end
35
+
36
+ module ReactOnRails
37
+ # rubocop:disable Metrics/ClassLength, Metrics/AbcSize
38
+ class Doctor
39
+ MESSAGE_COLORS = {
40
+ error: :red,
41
+ warning: :yellow,
42
+ success: :green,
43
+ info: :blue
44
+ }.freeze
45
+
46
+ def initialize(verbose: false, fix: false)
47
+ @verbose = verbose
48
+ @fix = fix
49
+ @checker = SystemChecker.new
50
+ end
51
+
52
+ def run_diagnosis
53
+ print_header
54
+ run_all_checks
55
+ print_summary
56
+ print_recommendations if should_show_recommendations?
57
+
58
+ exit_with_status
59
+ end
60
+
61
+ private
62
+
63
+ attr_reader :verbose, :fix, :checker
64
+
65
+ def print_header
66
+ puts Rainbow("\n#{'=' * 80}").cyan
67
+ puts Rainbow("🩺 REACT ON RAILS DOCTOR").cyan.bold
68
+ puts Rainbow("Diagnosing your React on Rails setup...").cyan
69
+ puts Rainbow("=" * 80).cyan
70
+ puts
71
+ print_doctor_feature_info
72
+ puts
73
+ end
74
+
75
+ def print_doctor_feature_info
76
+ puts Rainbow("ℹ️ Doctor Feature Information:").blue
77
+ puts " • This diagnostic tool is available in React on Rails v16.0.0+"
78
+ puts " • For older versions, upgrade your gem to access this feature"
79
+ puts " • Run: bundle update react_on_rails"
80
+ puts " • Documentation: https://www.shakacode.com/react-on-rails/docs/"
81
+ end
82
+
83
+ def run_all_checks
84
+ checks = [
85
+ ["Environment Prerequisites", :check_environment],
86
+ ["React on Rails Versions", :check_react_on_rails_versions],
87
+ ["React on Rails Packages", :check_packages],
88
+ ["JavaScript Package Dependencies", :check_dependencies],
89
+ ["Key Configuration Files", :check_key_files],
90
+ ["Configuration Analysis", :check_configuration_details],
91
+ ["bin/dev Launcher Setup", :check_bin_dev_launcher],
92
+ ["Rails Integration", :check_rails],
93
+ ["Webpack Configuration", :check_webpack],
94
+ ["Testing Setup", :check_testing_setup],
95
+ ["Development Environment", :check_development]
96
+ ]
97
+
98
+ checks.each do |section_name, check_method|
99
+ initial_message_count = checker.messages.length
100
+ send(check_method)
101
+
102
+ # Only print header if messages were added
103
+ next unless checker.messages.length > initial_message_count
104
+
105
+ print_section_header(section_name)
106
+ print_recent_messages(initial_message_count)
107
+ puts
108
+ end
109
+ end
110
+
111
+ def print_section_header(section_name)
112
+ puts Rainbow("#{section_name}:").blue.bold
113
+ puts Rainbow("-" * (section_name.length + 1)).blue
114
+ end
115
+
116
+ def print_recent_messages(start_index)
117
+ checker.messages[start_index..].each do |message|
118
+ color = MESSAGE_COLORS[message[:type]] || :blue
119
+ puts Rainbow(message[:content]).send(color)
120
+ end
121
+ end
122
+
123
+ def check_environment
124
+ checker.check_node_installation
125
+ checker.check_package_manager
126
+ end
127
+
128
+ def check_react_on_rails_versions
129
+ # Use system_checker for comprehensive package validation instead of duplicating
130
+ checker.check_react_on_rails_packages
131
+ check_version_wildcards
132
+ end
133
+
134
+ def check_packages
135
+ checker.check_shakapacker_configuration
136
+ end
137
+
138
+ def check_dependencies
139
+ checker.check_react_dependencies
140
+ end
141
+
142
+ def check_rails
143
+ checker.check_react_on_rails_initializer
144
+ end
145
+
146
+ def check_webpack
147
+ checker.check_webpack_configuration
148
+ end
149
+
150
+ def check_key_files
151
+ check_key_configuration_files
152
+ end
153
+
154
+ def check_configuration_details
155
+ check_shakapacker_configuration_details
156
+ check_react_on_rails_configuration_details
157
+ end
158
+
159
+ def check_bin_dev_launcher
160
+ checker.add_info("🚀 bin/dev Launcher:")
161
+ check_bin_dev_launcher_setup
162
+
163
+ checker.add_info("\n📄 Launcher Procfiles:")
164
+ check_launcher_procfiles
165
+ end
166
+
167
+ def check_testing_setup
168
+ check_rspec_helper_setup
169
+ end
170
+
171
+ def check_development
172
+ check_javascript_bundles
173
+ check_procfile_dev
174
+ check_bin_dev_script
175
+ check_gitignore
176
+ end
177
+
178
+ def check_javascript_bundles
179
+ server_bundle_path = determine_server_bundle_path
180
+ if File.exist?(server_bundle_path)
181
+ checker.add_success("✅ Server bundle file exists at #{server_bundle_path}")
182
+ else
183
+ checker.add_warning(<<~MSG.strip)
184
+ ⚠️ Server bundle not found: #{server_bundle_path}
185
+
186
+ This is required for server-side rendering.
187
+ Check your Shakapacker configuration and ensure the bundle is compiled.
188
+ MSG
189
+ end
190
+ end
191
+
192
+ def check_procfile_dev
193
+ check_procfiles
194
+ end
195
+
196
+ def check_procfiles
197
+ procfiles = {
198
+ "Procfile.dev" => {
199
+ description: "HMR development with webpack-dev-server",
200
+ required_for: "bin/dev (default/hmr mode)",
201
+ should_contain: ["shakapacker-dev-server", "rails server"]
202
+ },
203
+ "Procfile.dev-static-assets" => {
204
+ description: "Static development with webpack --watch",
205
+ required_for: "bin/dev static",
206
+ should_contain: ["shakapacker", "rails server"]
207
+ },
208
+ "Procfile.dev-prod-assets" => {
209
+ description: "Production-optimized assets development",
210
+ required_for: "bin/dev prod",
211
+ should_contain: ["rails server"]
212
+ }
213
+ }
214
+
215
+ procfiles.each do |filename, config|
216
+ check_individual_procfile(filename, config)
217
+ end
218
+
219
+ # Check if at least Procfile.dev exists
220
+ if File.exist?("Procfile.dev")
221
+ checker.add_success("✅ Essential Procfiles available for bin/dev script")
222
+ else
223
+ checker.add_warning(<<~MSG.strip)
224
+ ⚠️ Procfile.dev missing - required for bin/dev development server
225
+ Run 'rails generate react_on_rails:install' to generate required Procfiles
226
+ MSG
227
+ end
228
+ end
229
+
230
+ def check_individual_procfile(filename, config)
231
+ if File.exist?(filename)
232
+ checker.add_success("✅ #{filename} exists (#{config[:description]})")
233
+
234
+ # Only check for critical missing components, not optional suggestions
235
+ content = File.read(filename)
236
+ if filename == "Procfile.dev" && !content.include?("shakapacker-dev-server")
237
+ checker.add_warning(" ⚠️ Missing shakapacker-dev-server for HMR development")
238
+ elsif filename == "Procfile.dev-static-assets" && !content.include?("shakapacker")
239
+ checker.add_warning(" ⚠️ Missing shakapacker for static asset compilation")
240
+ end
241
+ else
242
+ checker.add_info("ℹ️ #{filename} not found (needed for #{config[:required_for]})")
243
+ end
244
+ end
245
+
246
+ def check_bin_dev_script
247
+ bin_dev_path = "bin/dev"
248
+ if File.exist?(bin_dev_path)
249
+ checker.add_success("✅ bin/dev script exists")
250
+ check_bin_dev_content(bin_dev_path)
251
+ else
252
+ checker.add_warning(<<~MSG.strip)
253
+ ⚠️ bin/dev script missing
254
+ This script provides an enhanced development workflow with HMR, static, and production modes.
255
+ Run 'rails generate react_on_rails:install' to generate the script.
256
+ MSG
257
+ end
258
+ end
259
+
260
+ def check_bin_dev_content(bin_dev_path)
261
+ return unless File.exist?(bin_dev_path)
262
+
263
+ content = File.read(bin_dev_path)
264
+
265
+ # Check if it's using the new ReactOnRails::Dev::ServerManager
266
+ if content.include?("ReactOnRails::Dev::ServerManager")
267
+ checker.add_success(" ✓ Uses enhanced ReactOnRails development server")
268
+ elsif content.include?("foreman") || content.include?("overmind")
269
+ checker.add_info(" ℹ️ Using basic foreman/overmind - consider upgrading to ReactOnRails enhanced dev script")
270
+ else
271
+ checker.add_info(" ℹ️ Custom bin/dev script detected")
272
+ end
273
+
274
+ # Check if it's executable
275
+ if File.executable?(bin_dev_path)
276
+ checker.add_success(" ✓ Script is executable")
277
+ else
278
+ checker.add_warning(" ⚠️ Script is not executable - run 'chmod +x bin/dev'")
279
+ end
280
+ end
281
+
282
+ def check_gitignore
283
+ gitignore_path = ".gitignore"
284
+ return unless File.exist?(gitignore_path)
285
+
286
+ content = File.read(gitignore_path)
287
+ if content.include?("**/generated/**")
288
+ checker.add_success("✅ .gitignore excludes generated files")
289
+ else
290
+ checker.add_info("ℹ️ Consider adding '**/generated/**' to .gitignore")
291
+ end
292
+ end
293
+
294
+ def print_summary
295
+ print_summary_header
296
+ counts = calculate_message_counts
297
+ print_summary_message(counts)
298
+ print_detailed_results_if_needed(counts)
299
+ end
300
+
301
+ def print_summary_header
302
+ puts Rainbow("DIAGNOSIS COMPLETE").cyan.bold
303
+ puts Rainbow("=" * 80).cyan
304
+ puts
305
+ end
306
+
307
+ def calculate_message_counts
308
+ {
309
+ error: checker.messages.count { |msg| msg[:type] == :error },
310
+ warning: checker.messages.count { |msg| msg[:type] == :warning },
311
+ success: checker.messages.count { |msg| msg[:type] == :success }
312
+ }
313
+ end
314
+
315
+ def print_summary_message(counts)
316
+ if counts[:error].zero? && counts[:warning].zero?
317
+ puts Rainbow("🎉 Excellent! Your React on Rails setup looks perfect!").green.bold
318
+ elsif counts[:error].zero?
319
+ puts Rainbow("✅ Good! Your setup is functional with #{counts[:warning]} minor issue(s).").yellow
320
+ else
321
+ puts Rainbow("❌ Issues found: #{counts[:error]} error(s), #{counts[:warning]} warning(s)").red
322
+ end
323
+
324
+ summary_text = "📊 Summary: #{counts[:success]} checks passed, " \
325
+ "#{counts[:warning]} warnings, #{counts[:error]} errors"
326
+ puts Rainbow(summary_text).blue
327
+ end
328
+
329
+ def print_detailed_results_if_needed(_counts)
330
+ # Skip detailed results since messages are now printed under section headers
331
+ # Only show detailed results in verbose mode for debugging
332
+ return unless verbose
333
+
334
+ puts "\nDetailed Results (Verbose Mode):"
335
+ print_all_messages
336
+ end
337
+
338
+ def print_all_messages
339
+ checker.messages.each do |message|
340
+ color = MESSAGE_COLORS[message[:type]] || :blue
341
+
342
+ puts Rainbow(message[:content]).send(color)
343
+ puts
344
+ end
345
+ end
346
+
347
+ def print_recommendations
348
+ puts Rainbow("RECOMMENDATIONS").cyan.bold
349
+ puts Rainbow("=" * 80).cyan
350
+
351
+ if checker.errors?
352
+ puts Rainbow("Critical Issues:").red.bold
353
+ puts "• Fix the errors above before proceeding"
354
+ puts "• Run 'rails generate react_on_rails:install' to set up missing components"
355
+ puts "• Ensure all prerequisites (Node.js, package manager) are installed"
356
+ puts
357
+ end
358
+
359
+ if checker.warnings?
360
+ puts Rainbow("Suggested Improvements:").yellow.bold
361
+ puts "• Review warnings above for optimization opportunities"
362
+
363
+ # Enhanced development workflow recommendations
364
+ unless File.exist?("bin/dev") && File.read("bin/dev").include?("ReactOnRails::Dev::ServerManager")
365
+ puts "• #{Rainbow('Upgrade to enhanced bin/dev script').yellow}:"
366
+ puts " - Run #{Rainbow('rails generate react_on_rails:install').cyan} for latest development tools"
367
+ puts " - Provides HMR, static, and production-like asset modes"
368
+ puts " - Better error handling and debugging capabilities"
369
+ end
370
+
371
+ missing_procfiles = ["Procfile.dev-static-assets", "Procfile.dev-prod-assets"].reject { |f| File.exist?(f) }
372
+ unless missing_procfiles.empty?
373
+ puts "• #{Rainbow('Complete development workflow setup').yellow}:"
374
+ puts " - Missing: #{missing_procfiles.join(', ')}"
375
+ puts " - Run #{Rainbow('rails generate react_on_rails:install').cyan} to generate missing files"
376
+ end
377
+
378
+ puts "• Consider updating packages to latest compatible versions"
379
+ puts "• Check documentation for best practices"
380
+ puts
381
+ end
382
+
383
+ print_next_steps
384
+ end
385
+
386
+ def should_show_recommendations?
387
+ # Only show recommendations if there are actual issues or actionable improvements
388
+ checker.errors? || checker.warnings?
389
+ end
390
+
391
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
392
+ def print_next_steps
393
+ puts Rainbow("Next Steps:").blue.bold
394
+
395
+ if checker.errors?
396
+ puts "• Fix critical errors above before proceeding"
397
+ puts "• Run doctor again to verify fixes: rake react_on_rails:doctor"
398
+ elsif checker.warnings?
399
+ puts "• Address warnings above for optimal setup"
400
+ puts "• Run doctor again to verify improvements: rake react_on_rails:doctor"
401
+ else
402
+ puts "• Your setup is healthy! Consider these development workflow steps:"
403
+ end
404
+
405
+ # Enhanced contextual suggestions based on what exists
406
+ if File.exist?("bin/dev") && File.exist?("Procfile.dev")
407
+ puts "• Start development with HMR: #{Rainbow('./bin/dev').cyan}"
408
+ puts "• Try static mode: #{Rainbow('./bin/dev static').cyan}"
409
+ puts "• Test production assets: #{Rainbow('./bin/dev prod').cyan}"
410
+ puts "• See all options: #{Rainbow('./bin/dev help').cyan}"
411
+ elsif File.exist?("Procfile.dev")
412
+ puts "• Start development with: #{Rainbow('./bin/dev').cyan} (or foreman start -f Procfile.dev)"
413
+ else
414
+ puts "• Start Rails server: bin/rails server"
415
+ puts "• Start webpack dev server: bin/shakapacker-dev-server (in separate terminal)"
416
+ end
417
+
418
+ # Test suggestions based on what's available
419
+ test_suggestions = []
420
+ test_suggestions << "bundle exec rspec" if File.exist?("spec")
421
+ test_suggestions << "npm test" if npm_test_script?
422
+ test_suggestions << "yarn test" if yarn_test_script?
423
+
424
+ puts "• Run tests: #{test_suggestions.join(' or ')}" if test_suggestions.any?
425
+
426
+ # Build suggestions
427
+ if checker.messages.any? { |msg| msg[:content].include?("server bundle") }
428
+ puts "• Build assets: bin/shakapacker or npm run build"
429
+ end
430
+
431
+ puts "• Documentation: https://github.com/shakacode/react_on_rails"
432
+ puts
433
+ end
434
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
435
+
436
+ def check_gem_version
437
+ gem_version = ReactOnRails::VERSION
438
+ checker.add_success("✅ React on Rails gem version: #{gem_version}")
439
+ rescue StandardError
440
+ checker.add_error("🚫 Unable to determine React on Rails gem version")
441
+ end
442
+
443
+ def check_npm_package_version
444
+ return unless File.exist?("package.json")
445
+
446
+ begin
447
+ package_json = JSON.parse(File.read("package.json"))
448
+ all_deps = package_json["dependencies"]&.merge(package_json["devDependencies"] || {}) || {}
449
+
450
+ npm_version = all_deps["react-on-rails"]
451
+ if npm_version
452
+ checker.add_success("✅ react-on-rails npm package version: #{npm_version}")
453
+ else
454
+ checker.add_warning("⚠️ react-on-rails npm package not found in package.json")
455
+ end
456
+ rescue JSON::ParserError
457
+ checker.add_error("🚫 Unable to parse package.json")
458
+ rescue StandardError
459
+ checker.add_error("🚫 Error reading package.json")
460
+ end
461
+ end
462
+
463
+ def check_version_wildcards
464
+ check_gem_wildcards
465
+ check_npm_wildcards
466
+ end
467
+
468
+ # rubocop:disable Metrics/CyclomaticComplexity
469
+ def check_gem_wildcards
470
+ gemfile_path = ENV["BUNDLE_GEMFILE"] || "Gemfile"
471
+ return unless File.exist?(gemfile_path)
472
+
473
+ begin
474
+ content = File.read(gemfile_path)
475
+ react_line = content.lines.find { |line| line.match(/^\s*gem\s+['"]react_on_rails['"]/) }
476
+
477
+ if react_line
478
+ if /['"][~^]/.match?(react_line)
479
+ checker.add_warning("⚠️ Gemfile uses wildcard version pattern (~, ^) for react_on_rails")
480
+ elsif />=\s*/.match?(react_line)
481
+ checker.add_warning("⚠️ Gemfile uses version range (>=) for react_on_rails")
482
+ else
483
+ checker.add_success("✅ Gemfile uses exact version for react_on_rails")
484
+ end
485
+ end
486
+ rescue StandardError
487
+ # Ignore errors reading Gemfile
488
+ end
489
+ end
490
+ # rubocop:enable Metrics/CyclomaticComplexity
491
+
492
+ # rubocop:disable Metrics/CyclomaticComplexity
493
+ def check_npm_wildcards
494
+ return unless File.exist?("package.json")
495
+
496
+ begin
497
+ package_json = JSON.parse(File.read("package.json"))
498
+ all_deps = package_json["dependencies"]&.merge(package_json["devDependencies"] || {}) || {}
499
+
500
+ npm_version = all_deps["react-on-rails"]
501
+ if npm_version
502
+ if /[~^]/.match?(npm_version)
503
+ checker.add_warning("⚠️ package.json uses wildcard version pattern (~, ^) for react-on-rails")
504
+ else
505
+ checker.add_success("✅ package.json uses exact version for react-on-rails")
506
+ end
507
+ end
508
+ rescue JSON::ParserError
509
+ # Ignore JSON parsing errors
510
+ rescue StandardError
511
+ # Ignore other errors
512
+ end
513
+ end
514
+ # rubocop:enable Metrics/CyclomaticComplexity
515
+
516
+ def check_key_configuration_files
517
+ files_to_check = {
518
+ "config/shakapacker.yml" => "Shakapacker configuration",
519
+ "config/initializers/react_on_rails.rb" => "React on Rails initializer",
520
+ "bin/dev" => "Development server launcher",
521
+ "bin/shakapacker" => "Shakapacker binary",
522
+ "bin/shakapacker-dev-server" => "Shakapacker dev server binary",
523
+ "config/webpack/webpack.config.js" => "Webpack configuration"
524
+ }
525
+
526
+ files_to_check.each do |file_path, description|
527
+ if File.exist?(file_path)
528
+ checker.add_success("✅ #{description}: #{file_path}")
529
+ else
530
+ checker.add_warning("⚠️ Missing #{description}: #{file_path}")
531
+ end
532
+ end
533
+
534
+ check_layout_files
535
+ check_server_rendering_engine
536
+ end
537
+
538
+ # rubocop:disable Metrics/CyclomaticComplexity
539
+ def check_layout_files
540
+ layout_files = Dir.glob("app/views/layouts/**/*.erb")
541
+ return if layout_files.empty?
542
+
543
+ checker.add_info("\n📄 Layout Files Analysis:")
544
+
545
+ layout_files.each do |layout_file|
546
+ next unless File.exist?(layout_file)
547
+
548
+ content = File.read(layout_file)
549
+ has_stylesheet = content.include?("stylesheet_pack_tag")
550
+ has_javascript = content.include?("javascript_pack_tag")
551
+
552
+ layout_name = File.basename(layout_file, ".html.erb")
553
+
554
+ if has_stylesheet && has_javascript
555
+ checker.add_info(" ✅ #{layout_name}: has both stylesheet_pack_tag and javascript_pack_tag")
556
+ elsif has_stylesheet
557
+ checker.add_warning(" ⚠️ #{layout_name}: has stylesheet_pack_tag but missing javascript_pack_tag")
558
+ elsif has_javascript
559
+ checker.add_warning(" ⚠️ #{layout_name}: has javascript_pack_tag but missing stylesheet_pack_tag")
560
+ else
561
+ checker.add_info(" ℹ️ #{layout_name}: no pack tags found")
562
+ end
563
+ end
564
+ end
565
+ # rubocop:enable Metrics/CyclomaticComplexity
566
+
567
+ # rubocop:disable Metrics/CyclomaticComplexity
568
+ def check_server_rendering_engine
569
+ return unless defined?(ReactOnRails)
570
+
571
+ checker.add_info("\n🖥️ Server Rendering Engine:")
572
+
573
+ begin
574
+ # Check if ExecJS is available and what runtime is being used
575
+ if defined?(ExecJS)
576
+ runtime_name = ExecJS.runtime.name if ExecJS.runtime
577
+ if runtime_name
578
+ checker.add_info(" ExecJS Runtime: #{runtime_name}")
579
+
580
+ # Provide more specific information about the runtime
581
+ case runtime_name
582
+ when /MiniRacer/
583
+ checker.add_info(" ℹ️ Using V8 via mini_racer gem (fast, isolated)")
584
+ when /Node/
585
+ checker.add_info(" ℹ️ Using Node.js runtime (requires Node.js)")
586
+ when /Duktape/
587
+ checker.add_info(" ℹ️ Using Duktape runtime (pure Ruby, slower)")
588
+ else
589
+ checker.add_info(" ℹ️ JavaScript runtime: #{runtime_name}")
590
+ end
591
+ else
592
+ checker.add_warning(" ⚠️ ExecJS runtime not detected")
593
+ end
594
+ else
595
+ checker.add_warning(" ⚠️ ExecJS not available")
596
+ end
597
+ rescue StandardError => e
598
+ checker.add_warning(" ⚠️ Could not determine server rendering engine: #{e.message}")
599
+ end
600
+ end
601
+ # rubocop:enable Metrics/CyclomaticComplexity
602
+
603
+ # rubocop:disable Metrics/CyclomaticComplexity
604
+ def check_shakapacker_configuration_details
605
+ return unless File.exist?("config/shakapacker.yml")
606
+
607
+ checker.add_info("📋 Shakapacker Configuration:")
608
+
609
+ begin
610
+ # Run shakapacker:info to get detailed configuration
611
+ stdout, stderr, status = Open3.capture3("bundle", "exec", "rake", "shakapacker:info")
612
+
613
+ if status.success?
614
+ # Parse and display relevant info from shakapacker:info
615
+ lines = stdout.lines.map(&:strip)
616
+
617
+ lines.each do |line|
618
+ next if line.empty?
619
+
620
+ # Show only Shakapacker-specific configuration lines, not general environment info
621
+ checker.add_info(" #{line}") if line.match?(%r{^Is bin/shakapacker})
622
+ end
623
+ else
624
+ checker.add_info(" Configuration file: config/shakapacker.yml")
625
+ checker.add_warning(" ⚠️ Could not run 'rake shakapacker:info': #{stderr.strip}")
626
+ end
627
+ rescue StandardError => e
628
+ checker.add_info(" Configuration file: config/shakapacker.yml")
629
+ checker.add_warning(" ⚠️ Could not run 'rake shakapacker:info': #{e.message}")
630
+ end
631
+ end
632
+ # rubocop:enable Metrics/CyclomaticComplexity
633
+
634
+ def check_react_on_rails_configuration_details
635
+ check_react_on_rails_initializer
636
+ check_deprecated_configuration_settings
637
+ check_breaking_changes_warnings
638
+ end
639
+
640
+ def check_react_on_rails_initializer
641
+ config_path = "config/initializers/react_on_rails.rb"
642
+
643
+ unless File.exist?(config_path)
644
+ checker.add_warning("⚠️ React on Rails configuration file not found: #{config_path}")
645
+ checker.add_info("💡 Run 'rails generate react_on_rails:install' to create configuration file")
646
+ return
647
+ end
648
+
649
+ begin
650
+ content = File.read(config_path)
651
+
652
+ checker.add_info("📋 React on Rails Configuration:")
653
+ checker.add_info("📍 Documentation: https://www.shakacode.com/react-on-rails/docs/guides/configuration/")
654
+
655
+ # Analyze configuration settings
656
+ analyze_server_rendering_config(content)
657
+ analyze_performance_config(content)
658
+ analyze_development_config(content)
659
+ analyze_i18n_config(content)
660
+ analyze_component_loading_config(content)
661
+ analyze_custom_extensions(content)
662
+ rescue StandardError => e
663
+ checker.add_warning("⚠️ Unable to read react_on_rails.rb: #{e.message}")
664
+ end
665
+ end
666
+
667
+ def analyze_server_rendering_config(content)
668
+ checker.add_info("\n🖥️ Server Rendering:")
669
+
670
+ # Server bundle file
671
+ server_bundle_match = content.match(/config\.server_bundle_js_file\s*=\s*["']([^"']+)["']/)
672
+ if server_bundle_match
673
+ checker.add_info(" server_bundle_js_file: #{server_bundle_match[1]}")
674
+ else
675
+ checker.add_info(" server_bundle_js_file: server-bundle.js (default)")
676
+ end
677
+
678
+ # RSC bundle file (Pro feature)
679
+ rsc_bundle_match = content.match(/config\.rsc_bundle_js_file\s*=\s*["']([^"']+)["']/)
680
+ if rsc_bundle_match
681
+ checker.add_info(" rsc_bundle_js_file: #{rsc_bundle_match[1]} (React Server Components - Pro)")
682
+ end
683
+
684
+ # Prerender setting
685
+ prerender_match = content.match(/config\.prerender\s*=\s*([^\s\n,]+)/)
686
+ prerender_value = prerender_match ? prerender_match[1] : "false (default)"
687
+ checker.add_info(" prerender: #{prerender_value}")
688
+
689
+ # Server renderer pool settings
690
+ pool_size_match = content.match(/config\.server_renderer_pool_size\s*=\s*([^\s\n,]+)/)
691
+ checker.add_info(" server_renderer_pool_size: #{pool_size_match[1]}") if pool_size_match
692
+
693
+ timeout_match = content.match(/config\.server_renderer_timeout\s*=\s*([^\s\n,]+)/)
694
+ checker.add_info(" server_renderer_timeout: #{timeout_match[1]} seconds") if timeout_match
695
+
696
+ # Error handling
697
+ raise_on_error_match = content.match(/config\.raise_on_prerender_error\s*=\s*([^\s\n,]+)/)
698
+ return unless raise_on_error_match
699
+
700
+ checker.add_info(" raise_on_prerender_error: #{raise_on_error_match[1]}")
701
+ end
702
+ # rubocop:enable Metrics/AbcSize
703
+
704
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
705
+ def analyze_performance_config(content)
706
+ checker.add_info("\n⚡ Performance & Loading:")
707
+
708
+ # Component loading strategy
709
+ loading_strategy_match = content.match(/config\.generated_component_packs_loading_strategy\s*=\s*:([^\s\n,]+)/)
710
+ if loading_strategy_match
711
+ strategy = loading_strategy_match[1]
712
+ checker.add_info(" generated_component_packs_loading_strategy: :#{strategy}")
713
+
714
+ case strategy
715
+ when "async"
716
+ checker.add_info(" ℹ️ Async loading requires Shakapacker >= 8.2.0")
717
+ when "defer"
718
+ checker.add_info(" ℹ️ Deferred loading provides good performance balance")
719
+ when "sync"
720
+ checker.add_info(" ℹ️ Synchronous loading ensures immediate availability")
721
+ end
722
+ end
723
+
724
+ # Deprecated defer setting
725
+ defer_match = content.match(/config\.defer_generated_component_packs\s*=\s*([^\s\n,]+)/)
726
+ if defer_match
727
+ checker.add_warning(" ⚠️ defer_generated_component_packs: #{defer_match[1]} (DEPRECATED)")
728
+ checker.add_info(" 💡 Use generated_component_packs_loading_strategy = :defer instead")
729
+ end
730
+
731
+ # Auto load bundle
732
+ auto_load_match = content.match(/config\.auto_load_bundle\s*=\s*([^\s\n,]+)/)
733
+ checker.add_info(" auto_load_bundle: #{auto_load_match[1]}") if auto_load_match
734
+
735
+ # Immediate hydration (Pro feature)
736
+ immediate_hydration_match = content.match(/config\.immediate_hydration\s*=\s*([^\s\n,]+)/)
737
+ if immediate_hydration_match
738
+ checker.add_info(" immediate_hydration: #{immediate_hydration_match[1]} (React on Rails Pro)")
739
+ end
740
+
741
+ # Component registry timeout
742
+ timeout_match = content.match(/config\.component_registry_timeout\s*=\s*([^\s\n,]+)/)
743
+ return unless timeout_match
744
+
745
+ checker.add_info(" component_registry_timeout: #{timeout_match[1]}ms")
746
+ end
747
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
748
+
749
+ # rubocop:disable Metrics/AbcSize
750
+ def analyze_development_config(content)
751
+ checker.add_info("\n🔧 Development & Debugging:")
752
+
753
+ # Development mode
754
+ dev_mode_match = content.match(/config\.development_mode\s*=\s*([^\s\n,]+)/)
755
+ if dev_mode_match
756
+ checker.add_info(" development_mode: #{dev_mode_match[1]}")
757
+ else
758
+ checker.add_info(" development_mode: Rails.env.development? (default)")
759
+ end
760
+
761
+ # Trace setting
762
+ trace_match = content.match(/config\.trace\s*=\s*([^\s\n,]+)/)
763
+ if trace_match
764
+ checker.add_info(" trace: #{trace_match[1]}")
765
+ else
766
+ checker.add_info(" trace: Rails.env.development? (default)")
767
+ end
768
+
769
+ # Logging
770
+ logging_match = content.match(/config\.logging_on_server\s*=\s*([^\s\n,]+)/)
771
+ logging_value = logging_match ? logging_match[1] : "true (default)"
772
+ checker.add_info(" logging_on_server: #{logging_value}")
773
+
774
+ # Console replay
775
+ replay_match = content.match(/config\.replay_console\s*=\s*([^\s\n,]+)/)
776
+ replay_value = replay_match ? replay_match[1] : "true (default)"
777
+ checker.add_info(" replay_console: #{replay_value}")
778
+
779
+ # Build commands
780
+ build_test_match = content.match(/config\.build_test_command\s*=\s*["']([^"']+)["']/)
781
+ checker.add_info(" build_test_command: #{build_test_match[1]}") if build_test_match
782
+
783
+ build_prod_match = content.match(/config\.build_production_command\s*=\s*["']([^"']+)["']/)
784
+ return unless build_prod_match
785
+
786
+ checker.add_info(" build_production_command: #{build_prod_match[1]}")
787
+ end
788
+ # rubocop:enable Metrics/AbcSize
789
+
790
+ def analyze_i18n_config(content)
791
+ i18n_configs = []
792
+
793
+ i18n_dir_match = content.match(/config\.i18n_dir\s*=\s*["']([^"']+)["']/)
794
+ i18n_configs << "i18n_dir: #{i18n_dir_match[1]}" if i18n_dir_match
795
+
796
+ i18n_yml_dir_match = content.match(/config\.i18n_yml_dir\s*=\s*["']([^"']+)["']/)
797
+ i18n_configs << "i18n_yml_dir: #{i18n_yml_dir_match[1]}" if i18n_yml_dir_match
798
+
799
+ i18n_format_match = content.match(/config\.i18n_output_format\s*=\s*["']([^"']+)["']/)
800
+ i18n_configs << "i18n_output_format: #{i18n_format_match[1]}" if i18n_format_match
801
+
802
+ return unless i18n_configs.any?
803
+
804
+ checker.add_info("\n🌍 Internationalization:")
805
+ i18n_configs.each { |config| checker.add_info(" #{config}") }
806
+ end
807
+
808
+ def analyze_component_loading_config(content)
809
+ component_configs = []
810
+
811
+ components_subdir_match = content.match(/config\.components_subdirectory\s*=\s*["']([^"']+)["']/)
812
+ if components_subdir_match
813
+ component_configs << "components_subdirectory: #{components_subdir_match[1]}"
814
+ checker.add_info(" ℹ️ File-system based component registry enabled")
815
+ end
816
+
817
+ same_bundle_match = content.match(/config\.same_bundle_for_client_and_server\s*=\s*([^\s\n,]+)/)
818
+ component_configs << "same_bundle_for_client_and_server: #{same_bundle_match[1]}" if same_bundle_match
819
+
820
+ random_dom_match = content.match(/config\.random_dom_id\s*=\s*([^\s\n,]+)/)
821
+ component_configs << "random_dom_id: #{random_dom_match[1]}" if random_dom_match
822
+
823
+ return unless component_configs.any?
824
+
825
+ checker.add_info("\n📦 Component Loading:")
826
+ component_configs.each { |config| checker.add_info(" #{config}") }
827
+ end
828
+
829
+ def analyze_custom_extensions(content)
830
+ # Check for rendering extension
831
+ if /config\.rendering_extension\s*=\s*([^\s\n,]+)/.match?(content)
832
+ checker.add_info("\n🔌 Custom Extensions:")
833
+ checker.add_info(" rendering_extension: Custom rendering logic detected")
834
+ checker.add_info(" ℹ️ See: https://www.shakacode.com/react-on-rails/docs/guides/rendering-extensions")
835
+ end
836
+
837
+ # Check for rendering props extension
838
+ if /config\.rendering_props_extension\s*=\s*([^\s\n,]+)/.match?(content)
839
+ checker.add_info(" rendering_props_extension: Custom props logic detected")
840
+ end
841
+
842
+ # Check for server render method
843
+ server_method_match = content.match(/config\.server_render_method\s*=\s*["']([^"']+)["']/)
844
+ return unless server_method_match
845
+
846
+ checker.add_info(" server_render_method: #{server_method_match[1]}")
847
+ end
848
+
849
+ def check_deprecated_configuration_settings
850
+ return unless File.exist?("config/initializers/react_on_rails.rb")
851
+
852
+ content = File.read("config/initializers/react_on_rails.rb")
853
+ deprecated_settings = []
854
+
855
+ # Check for deprecated settings
856
+ if content.include?("config.generated_assets_dirs")
857
+ deprecated_settings << "generated_assets_dirs (use generated_assets_dir)"
858
+ end
859
+ if content.include?("config.skip_display_none")
860
+ deprecated_settings << "skip_display_none (remove from configuration)"
861
+ end
862
+ if content.include?("config.defer_generated_component_packs")
863
+ deprecated_settings << "defer_generated_component_packs (use generated_component_packs_loading_strategy)"
864
+ end
865
+
866
+ return unless deprecated_settings.any?
867
+
868
+ checker.add_info("\n⚠️ Deprecated Configuration Settings:")
869
+ deprecated_settings.each do |setting|
870
+ checker.add_warning(" #{setting}")
871
+ end
872
+ checker.add_info("📖 Migration guide: https://www.shakacode.com/react-on-rails/docs/guides/upgrading-react-on-rails")
873
+ end
874
+
875
+ def check_breaking_changes_warnings
876
+ return unless defined?(ReactOnRails::VERSION)
877
+
878
+ # Parse version - handle pre-release versions like "16.0.0.beta.1"
879
+ current_version = ReactOnRails::VERSION.split(".").map(&:to_i)
880
+ major_version = current_version[0]
881
+
882
+ # Check for major version breaking changes
883
+ if major_version >= 16
884
+ check_v16_breaking_changes
885
+ elsif major_version >= 14
886
+ check_v14_breaking_changes
887
+ end
888
+ end
889
+
890
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
891
+ def check_v16_breaking_changes
892
+ issues_found = []
893
+
894
+ # Check for Webpacker usage (breaking change: removed in v16)
895
+ if File.exist?("config/webpacker.yml") || File.exist?("bin/webpacker")
896
+ issues_found << "• Webpacker support removed - migrate to Shakapacker >= 6.0"
897
+ end
898
+
899
+ # Check for CommonJS require() usage (breaking change: ESM-only)
900
+ commonjs_files = []
901
+ begin
902
+ # Check JavaScript/TypeScript files for require() usage
903
+ js_files = Dir.glob(%w[app/javascript/**/*.{js,ts,jsx,tsx} client/**/*.{js,ts,jsx,tsx}])
904
+ js_files.each do |file|
905
+ next unless File.exist?(file)
906
+
907
+ content = File.read(file)
908
+ commonjs_files << file if content.match?(/require\s*\(\s*['"]react-on-rails['"]/)
909
+ end
910
+ rescue StandardError
911
+ # Ignore file read errors
912
+ end
913
+
914
+ unless commonjs_files.empty?
915
+ issues_found << "• CommonJS require() found - update to ESM imports"
916
+ issues_found << " Files: #{commonjs_files.take(3).join(', ')}#{'...' if commonjs_files.length > 3}"
917
+ end
918
+
919
+ # Check Node.js version (recommendation, not breaking)
920
+ begin
921
+ stdout, _stderr, status = Open3.capture3("node", "--version")
922
+ if status.success?
923
+ node_version = stdout.strip.gsub(/^v/, "")
924
+ version_parts = node_version.split(".").map(&:to_i)
925
+ major = version_parts[0]
926
+ minor = version_parts[1] || 0
927
+
928
+ if major < 20 || (major == 20 && minor < 19)
929
+ issues_found << "• Node.js #{node_version} detected - v20.19.0+ recommended for full ESM support"
930
+ end
931
+ end
932
+ rescue StandardError
933
+ # Ignore version check errors
934
+ end
935
+
936
+ return if issues_found.empty?
937
+
938
+ checker.add_info("\n🚨 React on Rails v16+ Breaking Changes Detected:")
939
+ issues_found.each { |issue| checker.add_warning(" #{issue}") }
940
+ checker.add_info("📖 Full migration guide: https://www.shakacode.com/react-on-rails/docs/guides/upgrading-react-on-rails#upgrading-to-version-16")
941
+ end
942
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
943
+
944
+ def check_v14_breaking_changes
945
+ checker.add_info("\n📋 React on Rails v14+ Notes:")
946
+ checker.add_info(" • Enhanced React Server Components (RSC) support available in Pro")
947
+ checker.add_info(" • Improved component loading strategies")
948
+ checker.add_info(" • Modern React patterns recommended")
949
+ end
950
+
951
+ def check_bin_dev_launcher_setup
952
+ bin_dev_path = "bin/dev"
953
+
954
+ unless File.exist?(bin_dev_path)
955
+ checker.add_error(" 🚫 bin/dev script not found")
956
+ return
957
+ end
958
+
959
+ content = File.read(bin_dev_path)
960
+
961
+ if content.include?("ReactOnRails::Dev::ServerManager")
962
+ checker.add_success(" ✅ bin/dev uses ReactOnRails Launcher (ReactOnRails::Dev::ServerManager)")
963
+ elsif content.include?("run_from_command_line")
964
+ checker.add_success(" ✅ bin/dev uses ReactOnRails Launcher (run_from_command_line)")
965
+ else
966
+ checker.add_warning(" ⚠️ bin/dev exists but doesn't use ReactOnRails Launcher")
967
+ checker.add_info(" 💡 Consider upgrading: rails generate react_on_rails:install")
968
+ end
969
+ end
970
+
971
+ def check_launcher_procfiles
972
+ procfiles = {
973
+ "Procfile.dev" => "HMR development (bin/dev default)",
974
+ "Procfile.dev-static-assets" => "Static development (bin/dev static)",
975
+ "Procfile.dev-prod-assets" => "Production assets (bin/dev prod)"
976
+ }
977
+
978
+ missing_count = 0
979
+
980
+ procfiles.each do |filename, description|
981
+ if File.exist?(filename)
982
+ checker.add_success(" ✅ #{filename} - #{description}")
983
+ else
984
+ checker.add_warning(" ⚠️ Missing #{filename} - #{description}")
985
+ missing_count += 1
986
+ end
987
+ end
988
+
989
+ if missing_count.zero?
990
+ checker.add_success(" ✅ All Launcher Procfiles available")
991
+ else
992
+ checker.add_info(" 💡 Run: rails generate react_on_rails:install")
993
+ end
994
+ end
995
+
996
+ # rubocop:disable Metrics/CyclomaticComplexity
997
+ def check_rspec_helper_setup
998
+ spec_helper_paths = [
999
+ "spec/rails_helper.rb",
1000
+ "spec/spec_helper.rb"
1001
+ ]
1002
+
1003
+ react_on_rails_test_helper_found = false
1004
+
1005
+ spec_helper_paths.each do |helper_path|
1006
+ next unless File.exist?(helper_path)
1007
+
1008
+ content = File.read(helper_path)
1009
+
1010
+ unless content.include?("ReactOnRails::TestHelper") || content.include?("configure_rspec_to_compile_assets")
1011
+ next
1012
+ end
1013
+
1014
+ checker.add_success("✅ ReactOnRails RSpec helper configured in #{helper_path}")
1015
+ react_on_rails_test_helper_found = true
1016
+
1017
+ # Check specific configurations
1018
+ checker.add_success(" ✓ Assets compilation enabled for tests") if content.include?("ensure_assets_compiled")
1019
+
1020
+ checker.add_success(" ✓ RSpec configuration present") if content.include?("RSpec.configure")
1021
+ end
1022
+
1023
+ return if react_on_rails_test_helper_found
1024
+
1025
+ if File.exist?("spec")
1026
+ checker.add_warning("⚠️ ReactOnRails RSpec helper not found")
1027
+ checker.add_info(" Add to spec/rails_helper.rb:")
1028
+ checker.add_info(" require 'react_on_rails/test_helper'")
1029
+ checker.add_info(" ReactOnRails::TestHelper.configure_rspec_to_compile_assets(config)")
1030
+ else
1031
+ checker.add_info("ℹ️ No RSpec directory found - skipping RSpec helper check")
1032
+ end
1033
+ end
1034
+ # rubocop:enable Metrics/CyclomaticComplexity
1035
+
1036
+ def npm_test_script?
1037
+ return false unless File.exist?("package.json")
1038
+
1039
+ begin
1040
+ package_json = JSON.parse(File.read("package.json"))
1041
+ test_script = package_json.dig("scripts", "test")
1042
+ test_script && !test_script.empty?
1043
+ rescue StandardError
1044
+ false
1045
+ end
1046
+ end
1047
+
1048
+ def yarn_test_script?
1049
+ npm_test_script? && system("which yarn > /dev/null 2>&1")
1050
+ end
1051
+
1052
+ def determine_server_bundle_path
1053
+ # Try to use Shakapacker gem API to get configuration
1054
+
1055
+ require "shakapacker"
1056
+
1057
+ # Get the source path relative to Rails root
1058
+ source_path = Shakapacker.config.source_path.to_s
1059
+ source_entry_path = Shakapacker.config.source_entry_path.to_s
1060
+ bundle_filename = server_bundle_filename
1061
+ rails_root = Dir.pwd
1062
+
1063
+ # Convert absolute paths to relative paths
1064
+ if source_path.start_with?("/") && source_path.start_with?(rails_root)
1065
+ source_path = source_path.sub("#{rails_root}/", "")
1066
+ end
1067
+
1068
+ if source_entry_path.start_with?("/") && source_entry_path.start_with?(rails_root)
1069
+ source_entry_path = source_entry_path.sub("#{rails_root}/", "")
1070
+ end
1071
+
1072
+ # If source_entry_path is already within source_path, just use the relative part
1073
+ if source_entry_path.start_with?(source_path)
1074
+ # Extract just the entry path part (e.g., "packs" from "client/app/packs")
1075
+ source_entry_path = source_entry_path.sub("#{source_path}/", "")
1076
+ end
1077
+
1078
+ File.join(source_path, source_entry_path, bundle_filename)
1079
+ rescue StandardError
1080
+ # Handle missing Shakapacker gem or other configuration errors
1081
+ bundle_filename = server_bundle_filename
1082
+ "app/javascript/packs/#{bundle_filename}"
1083
+ end
1084
+
1085
+ def server_bundle_filename
1086
+ # Try to read from React on Rails initializer
1087
+ initializer_path = "config/initializers/react_on_rails.rb"
1088
+ if File.exist?(initializer_path)
1089
+ content = File.read(initializer_path)
1090
+ match = content.match(/config\.server_bundle_js_file\s*=\s*["']([^"']+)["']/)
1091
+ return match[1] if match
1092
+ end
1093
+
1094
+ # Default filename
1095
+ "server-bundle.js"
1096
+ end
1097
+
1098
+ def exit_with_status
1099
+ if checker.errors?
1100
+ puts Rainbow("❌ Doctor found critical issues. Please address errors above.").red.bold
1101
+ exit(1)
1102
+ elsif checker.warnings?
1103
+ puts Rainbow("⚠️ Doctor found some issues. Consider addressing warnings above.").yellow
1104
+ exit(0)
1105
+ else
1106
+ puts Rainbow("🎉 All checks passed! Your React on Rails setup is healthy.").green.bold
1107
+ exit(0)
1108
+ end
1109
+ end
1110
+
1111
+ def relativize_path(absolute_path)
1112
+ return absolute_path unless absolute_path.is_a?(String)
1113
+
1114
+ project_root = Dir.pwd
1115
+ if absolute_path.start_with?(project_root)
1116
+ # Remove project root and leading slash to make it relative
1117
+ relative = absolute_path.sub(project_root, "").sub(%r{^/}, "")
1118
+ relative.empty? ? "." : relative
1119
+ else
1120
+ absolute_path
1121
+ end
1122
+ end
1123
+
1124
+ def safe_display_config_path(label, path_value)
1125
+ return unless path_value
1126
+
1127
+ begin
1128
+ # Convert to string and relativize
1129
+ path_str = path_value.to_s
1130
+ relative_path = relativize_path(path_str)
1131
+ checker.add_info(" #{label}: #{relative_path}")
1132
+ rescue StandardError => e
1133
+ checker.add_info(" #{label}: <error reading path: #{e.message}>")
1134
+ end
1135
+ end
1136
+
1137
+ def safe_display_config_value(label, config, method_name)
1138
+ return unless config.respond_to?(method_name)
1139
+
1140
+ begin
1141
+ value = config.send(method_name)
1142
+ checker.add_info(" #{label}: #{value}")
1143
+ rescue StandardError => e
1144
+ checker.add_info(" #{label}: <error reading value: #{e.message}>")
1145
+ end
1146
+ end
1147
+ end
1148
+ # rubocop:enable Metrics/ClassLength
1149
+ end