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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +124 -77
- data/CLAUDE.md +46 -2
- data/CONTRIBUTING.md +12 -6
- data/Gemfile.development_dependencies +1 -0
- data/Gemfile.lock +3 -1
- data/LICENSE.md +15 -1
- data/README.md +68 -18
- data/bin/lefthook/check-trailing-newlines +38 -0
- data/bin/lefthook/get-changed-files +26 -0
- data/bin/lefthook/prettier-format +26 -0
- data/bin/lefthook/ruby-autofix +26 -0
- data/bin/lefthook/ruby-lint +27 -0
- data/eslint.config.ts +10 -0
- data/knip.ts +20 -9
- data/lib/generators/react_on_rails/USAGE +65 -0
- data/lib/generators/react_on_rails/base_generator.rb +7 -7
- data/lib/generators/react_on_rails/generator_helper.rb +4 -0
- data/lib/generators/react_on_rails/generator_messages.rb +2 -2
- data/lib/generators/react_on_rails/install_generator.rb +115 -7
- data/lib/generators/react_on_rails/react_no_redux_generator.rb +16 -4
- data/lib/generators/react_on_rails/react_with_redux_generator.rb +83 -14
- data/lib/generators/react_on_rails/templates/base/base/app/javascript/src/HelloWorld/ror_components/HelloWorld.client.tsx +25 -0
- data/lib/generators/react_on_rails/templates/base/base/app/javascript/src/HelloWorld/ror_components/HelloWorld.server.tsx +5 -0
- data/lib/generators/react_on_rails/templates/base/base/bin/dev +12 -24
- data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/actions/helloWorldActionCreators.ts +18 -0
- data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/components/HelloWorld.tsx +24 -0
- data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/constants/helloWorldConstants.ts +6 -0
- data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/containers/HelloWorldContainer.ts +20 -0
- data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/reducers/helloWorldReducer.ts +22 -0
- data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/startup/HelloWorldApp.client.tsx +23 -0
- data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/startup/HelloWorldApp.server.tsx +5 -0
- data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/store/helloWorldStore.ts +18 -0
- data/lib/react_on_rails/configuration.rb +10 -6
- data/lib/react_on_rails/dev/server_manager.rb +185 -28
- data/lib/react_on_rails/doctor.rb +1149 -0
- data/lib/react_on_rails/helper.rb +9 -78
- data/lib/react_on_rails/pro/NOTICE +21 -0
- data/lib/react_on_rails/pro/helper.rb +122 -0
- data/lib/react_on_rails/pro/utils.rb +53 -0
- data/lib/react_on_rails/react_component/render_options.rb +6 -2
- data/lib/react_on_rails/system_checker.rb +659 -0
- data/lib/react_on_rails/version.rb +1 -1
- data/lib/tasks/doctor.rake +48 -0
- data/lib/tasks/generate_packs.rake +127 -4
- data/package-lock.json +11984 -0
- metadata +26 -6
- 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
|