Skip to content

Commit d3a4ba2

Browse files
justin808claude
andcommitted
Enhance React on Rails doctor with comprehensive diagnostics
- Add exact version reporting for Shakapacker (shows "8.0.0" vs ">= 6.0") - Display package manager actually in use (yarn) vs available options - Remove empty section headers - only show headers when content exists - Make React dependencies more concise ("React ^19.0.0, React DOM ^19.0.0") - Add structured bin/dev Launcher analysis with headers and groupings - Fix Shakapacker configuration to avoid undefined method errors - Implement relative path display helper for future Shakapacker integration - Remove confusing "Could include" messages for cleaner output - Add version threshold warnings (8.2+ needed for auto-registration) - Improve wildcard version detection in Gemfile and package.json 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 984256e commit d3a4ba2

File tree

2 files changed

+387
-24
lines changed

2 files changed

+387
-24
lines changed

lib/react_on_rails/doctor.rb

Lines changed: 313 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -73,17 +73,28 @@ def print_header
7373
def run_all_checks
7474
checks = [
7575
["Environment Prerequisites", :check_environment],
76+
["React on Rails Versions", :check_react_on_rails_versions],
7677
["React on Rails Packages", :check_packages],
7778
["Dependencies", :check_dependencies],
79+
["Key Configuration Files", :check_key_files],
80+
["Configuration Analysis", :check_configuration_details],
81+
["bin/dev Launcher Setup", :check_bin_dev_launcher],
7882
["Rails Integration", :check_rails],
7983
["Webpack Configuration", :check_webpack],
84+
["Testing Setup", :check_testing_setup],
8085
["Development Environment", :check_development]
8186
]
8287

8388
checks.each do |section_name, check_method|
84-
print_section_header(section_name)
89+
initial_message_count = checker.messages.length
8590
send(check_method)
86-
puts
91+
92+
# Only print header if messages were added
93+
if checker.messages.length > initial_message_count
94+
print_section_header(section_name)
95+
print_recent_messages(initial_message_count)
96+
puts
97+
end
8798
end
8899
end
89100

@@ -92,11 +103,24 @@ def print_section_header(section_name)
92103
puts Rainbow("-" * (section_name.length + 1)).blue
93104
end
94105

106+
def print_recent_messages(start_index)
107+
checker.messages[start_index..-1].each do |message|
108+
color = MESSAGE_COLORS[message[:type]] || :blue
109+
puts Rainbow(message[:content]).send(color)
110+
end
111+
end
112+
95113
def check_environment
96114
checker.check_node_installation
97115
checker.check_package_manager
98116
end
99117

118+
def check_react_on_rails_versions
119+
check_gem_version
120+
check_npm_package_version
121+
check_version_wildcards
122+
end
123+
100124
def check_packages
101125
checker.check_react_on_rails_packages
102126
checker.check_shakapacker_configuration
@@ -114,6 +138,27 @@ def check_webpack
114138
checker.check_webpack_configuration
115139
end
116140

141+
def check_key_files
142+
check_key_configuration_files
143+
end
144+
145+
def check_configuration_details
146+
check_shakapacker_configuration_details
147+
check_react_on_rails_configuration_details
148+
end
149+
150+
def check_bin_dev_launcher
151+
checker.add_info("🚀 bin/dev Launcher:")
152+
check_bin_dev_launcher_setup
153+
154+
checker.add_info("\n📄 Launcher Procfiles:")
155+
check_launcher_procfiles
156+
end
157+
158+
def check_testing_setup
159+
check_rspec_helper_setup
160+
end
161+
117162
def check_development
118163
check_javascript_bundles
119164
check_procfile_dev
@@ -177,13 +222,12 @@ def check_individual_procfile(filename, config)
177222
if File.exist?(filename)
178223
checker.add_success("✅ #{filename} exists (#{config[:description]})")
179224

225+
# Only check for critical missing components, not optional suggestions
180226
content = File.read(filename)
181-
config[:should_contain].each do |expected_content|
182-
if content.include?(expected_content)
183-
checker.add_success(" ✓ Contains #{expected_content}")
184-
else
185-
checker.add_info(" ℹ️ Could include #{expected_content} for #{config[:description]}")
186-
end
227+
if filename == "Procfile.dev" && !content.include?("shakapacker-dev-server")
228+
checker.add_warning(" ⚠️ Missing shakapacker-dev-server for HMR development")
229+
elsif filename == "Procfile.dev-static-assets" && !content.include?("shakapacker")
230+
checker.add_warning(" ⚠️ Missing shakapacker for static asset compilation")
187231
end
188232
else
189233
checker.add_info("ℹ️ #{filename} not found (needed for #{config[:required_for]})")
@@ -274,9 +318,11 @@ def print_summary_message(counts)
274318
end
275319

276320
def print_detailed_results_if_needed(counts)
277-
return unless verbose || counts[:error].positive? || counts[:warning].positive?
321+
# Skip detailed results since messages are now printed under section headers
322+
# Only show detailed results in verbose mode for debugging
323+
return unless verbose
278324

279-
puts "\nDetailed Results:"
325+
puts "\nDetailed Results (Verbose Mode):"
280326
print_all_messages
281327
end
282328

@@ -378,6 +424,226 @@ def print_next_steps
378424
end
379425
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
380426

427+
def check_gem_version
428+
gem_version = ReactOnRails::VERSION
429+
checker.add_success("✅ React on Rails gem version: #{gem_version}")
430+
rescue StandardError
431+
checker.add_error("🚫 Unable to determine React on Rails gem version")
432+
end
433+
434+
def check_npm_package_version
435+
return unless File.exist?("package.json")
436+
437+
begin
438+
package_json = JSON.parse(File.read("package.json"))
439+
all_deps = package_json["dependencies"]&.merge(package_json["devDependencies"] || {}) || {}
440+
441+
npm_version = all_deps["react-on-rails"]
442+
if npm_version
443+
checker.add_success("✅ react-on-rails npm package version: #{npm_version}")
444+
else
445+
checker.add_warning("⚠️ react-on-rails npm package not found in package.json")
446+
end
447+
rescue JSON::ParserError
448+
checker.add_error("🚫 Unable to parse package.json")
449+
rescue StandardError
450+
checker.add_error("🚫 Error reading package.json")
451+
end
452+
end
453+
454+
def check_version_wildcards
455+
check_gem_wildcards
456+
check_npm_wildcards
457+
end
458+
459+
# rubocop:disable Metrics/CyclomaticComplexity
460+
def check_gem_wildcards
461+
gemfile_path = ENV["BUNDLE_GEMFILE"] || "Gemfile"
462+
return unless File.exist?(gemfile_path)
463+
464+
begin
465+
content = File.read(gemfile_path)
466+
react_line = content.lines.find { |line| line.match(/^\s*gem\s+['"]react_on_rails['"]/) }
467+
468+
if react_line
469+
if /['"][~^]/.match?(react_line)
470+
checker.add_warning("⚠️ Gemfile uses wildcard version pattern (~, ^) for react_on_rails")
471+
elsif />=\s*/.match?(react_line)
472+
checker.add_warning("⚠️ Gemfile uses version range (>=) for react_on_rails")
473+
else
474+
checker.add_success("✅ Gemfile uses exact version for react_on_rails")
475+
end
476+
end
477+
rescue StandardError
478+
# Ignore errors reading Gemfile
479+
end
480+
end
481+
# rubocop:enable Metrics/CyclomaticComplexity
482+
483+
# rubocop:disable Metrics/CyclomaticComplexity
484+
def check_npm_wildcards
485+
return unless File.exist?("package.json")
486+
487+
begin
488+
package_json = JSON.parse(File.read("package.json"))
489+
all_deps = package_json["dependencies"]&.merge(package_json["devDependencies"] || {}) || {}
490+
491+
npm_version = all_deps["react-on-rails"]
492+
if npm_version
493+
if /[~^]/.match?(npm_version)
494+
checker.add_warning("⚠️ package.json uses wildcard version pattern (~, ^) for react-on-rails")
495+
else
496+
checker.add_success("✅ package.json uses exact version for react-on-rails")
497+
end
498+
end
499+
rescue JSON::ParserError
500+
# Ignore JSON parsing errors
501+
rescue StandardError
502+
# Ignore other errors
503+
end
504+
end
505+
# rubocop:enable Metrics/CyclomaticComplexity
506+
507+
def check_key_configuration_files
508+
files_to_check = {
509+
"config/shakapacker.yml" => "Shakapacker configuration",
510+
"config/initializers/react_on_rails.rb" => "React on Rails initializer",
511+
"bin/shakapacker" => "Shakapacker binary",
512+
"bin/shakapacker-dev-server" => "Shakapacker dev server binary",
513+
"config/webpack/webpack.config.js" => "Webpack configuration"
514+
}
515+
516+
files_to_check.each do |file_path, description|
517+
if File.exist?(file_path)
518+
checker.add_success("✅ #{description}: #{file_path}")
519+
else
520+
checker.add_warning("⚠️ Missing #{description}: #{file_path}")
521+
end
522+
end
523+
end
524+
525+
def check_shakapacker_configuration_details
526+
return unless File.exist?("config/shakapacker.yml")
527+
528+
# For now, just indicate that the configuration file exists
529+
# TODO: Parse YAML directly or improve Shakapacker integration
530+
checker.add_info("📋 Shakapacker Configuration:")
531+
checker.add_info(" Configuration file: config/shakapacker.yml")
532+
checker.add_info(" ℹ️ Run 'rake shakapacker:info' for detailed configuration")
533+
end
534+
535+
def check_react_on_rails_configuration_details
536+
config_path = "config/initializers/react_on_rails.rb"
537+
return unless File.exist?(config_path)
538+
539+
begin
540+
content = File.read(config_path)
541+
542+
checker.add_info("📋 React on Rails Configuration:")
543+
544+
# Extract key configuration values
545+
config_patterns = {
546+
"server_bundle_js_file" => /config\.server_bundle_js_file\s*=\s*["']([^"']+)["']/,
547+
"prerender" => /config\.prerender\s*=\s*([^\s\n]+)/,
548+
"trace" => /config\.trace\s*=\s*([^\s\n]+)/,
549+
"development_mode" => /config\.development_mode\s*=\s*([^\s\n]+)/,
550+
"logging_on_server" => /config\.logging_on_server\s*=\s*([^\s\n]+)/
551+
}
552+
553+
config_patterns.each do |setting, pattern|
554+
match = content.match(pattern)
555+
checker.add_info(" #{setting}: #{match[1]}") if match
556+
end
557+
rescue StandardError => e
558+
checker.add_warning("⚠️ Unable to read react_on_rails.rb: #{e.message}")
559+
end
560+
end
561+
562+
def check_bin_dev_launcher_setup
563+
bin_dev_path = "bin/dev"
564+
565+
unless File.exist?(bin_dev_path)
566+
checker.add_error(" 🚫 bin/dev script not found")
567+
return
568+
end
569+
570+
content = File.read(bin_dev_path)
571+
572+
if content.include?("ReactOnRails::Dev::ServerManager")
573+
checker.add_success(" ✅ bin/dev uses ReactOnRails Launcher (ReactOnRails::Dev::ServerManager)")
574+
elsif content.include?("run_from_command_line")
575+
checker.add_success(" ✅ bin/dev uses ReactOnRails Launcher (run_from_command_line)")
576+
else
577+
checker.add_warning(" ⚠️ bin/dev exists but doesn't use ReactOnRails Launcher")
578+
checker.add_info(" 💡 Consider upgrading: rails generate react_on_rails:install")
579+
end
580+
end
581+
582+
def check_launcher_procfiles
583+
procfiles = {
584+
"Procfile.dev" => "HMR development (bin/dev default)",
585+
"Procfile.dev-static-assets" => "Static development (bin/dev static)",
586+
"Procfile.dev-prod-assets" => "Production assets (bin/dev prod)"
587+
}
588+
589+
missing_count = 0
590+
591+
procfiles.each do |filename, description|
592+
if File.exist?(filename)
593+
checker.add_success(" ✅ #{filename} - #{description}")
594+
else
595+
checker.add_warning(" ⚠️ Missing #{filename} - #{description}")
596+
missing_count += 1
597+
end
598+
end
599+
600+
if missing_count.zero?
601+
checker.add_success(" ✅ All Launcher Procfiles available")
602+
else
603+
checker.add_info(" 💡 Run: rails generate react_on_rails:install")
604+
end
605+
end
606+
607+
# rubocop:disable Metrics/CyclomaticComplexity
608+
def check_rspec_helper_setup
609+
spec_helper_paths = [
610+
"spec/rails_helper.rb",
611+
"spec/spec_helper.rb"
612+
]
613+
614+
react_on_rails_test_helper_found = false
615+
616+
spec_helper_paths.each do |helper_path|
617+
next unless File.exist?(helper_path)
618+
619+
content = File.read(helper_path)
620+
621+
unless content.include?("ReactOnRails::TestHelper") || content.include?("configure_rspec_to_compile_assets")
622+
next
623+
end
624+
625+
checker.add_success("✅ ReactOnRails RSpec helper configured in #{helper_path}")
626+
react_on_rails_test_helper_found = true
627+
628+
# Check specific configurations
629+
checker.add_success(" ✓ Assets compilation enabled for tests") if content.include?("ensure_assets_compiled")
630+
631+
checker.add_success(" ✓ RSpec configuration present") if content.include?("RSpec.configure")
632+
end
633+
634+
return if react_on_rails_test_helper_found
635+
636+
if File.exist?("spec")
637+
checker.add_warning("⚠️ ReactOnRails RSpec helper not found")
638+
checker.add_info(" Add to spec/rails_helper.rb:")
639+
checker.add_info(" require 'react_on_rails/test_helper'")
640+
checker.add_info(" ReactOnRails::TestHelper.configure_rspec_to_compile_assets(config)")
641+
else
642+
checker.add_info("ℹ️ No RSpec directory found - skipping RSpec helper check")
643+
end
644+
end
645+
# rubocop:enable Metrics/CyclomaticComplexity
646+
381647
def npm_test_script?
382648
return false unless File.exist?("package.json")
383649

@@ -452,6 +718,43 @@ def exit_with_status
452718
exit(0)
453719
end
454720
end
721+
722+
def relativize_path(absolute_path)
723+
return absolute_path unless absolute_path.is_a?(String)
724+
725+
project_root = Dir.pwd
726+
if absolute_path.start_with?(project_root)
727+
# Remove project root and leading slash to make it relative
728+
relative = absolute_path.sub(project_root, "").sub(/^\//, "")
729+
relative.empty? ? "." : relative
730+
else
731+
absolute_path
732+
end
733+
end
734+
735+
def safe_display_config_path(label, path_value)
736+
return unless path_value
737+
738+
begin
739+
# Convert to string and relativize
740+
path_str = path_value.to_s
741+
relative_path = relativize_path(path_str)
742+
checker.add_info(" #{label}: #{relative_path}")
743+
rescue StandardError => e
744+
checker.add_info(" #{label}: <error reading path: #{e.message}>")
745+
end
746+
end
747+
748+
def safe_display_config_value(label, config, method_name)
749+
return unless config.respond_to?(method_name)
750+
751+
begin
752+
value = config.send(method_name)
753+
checker.add_info(" #{label}: #{value}")
754+
rescue StandardError => e
755+
checker.add_info(" #{label}: <error reading value: #{e.message}>")
756+
end
757+
end
455758
end
456759
# rubocop:enable Metrics/ClassLength, Metrics/AbcSize
457760
end

0 commit comments

Comments
 (0)