react_on_rails 16.6.0 → 16.7.0.rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -0
- data/Gemfile.development_dependencies +2 -2
- data/Gemfile.lock +2 -14
- data/Rakefile +0 -6
- data/Steepfile +4 -0
- data/lib/generators/react_on_rails/base_generator.rb +4 -4
- data/lib/generators/react_on_rails/demo_page_config.rb +3 -3
- data/lib/generators/react_on_rails/dev_tests_generator.rb +1 -1
- data/lib/generators/react_on_rails/generator_helper.rb +6 -65
- data/lib/generators/react_on_rails/generator_messages/ci_section.rb +42 -0
- data/lib/generators/react_on_rails/generator_messages/package_manager_detection.rb +194 -0
- data/lib/generators/react_on_rails/generator_messages/shakapacker_status_section.rb +61 -0
- data/lib/generators/react_on_rails/generator_messages.rb +22 -79
- data/lib/generators/react_on_rails/install_generator.rb +243 -28
- data/lib/generators/react_on_rails/js_dependency_manager.rb +7 -4
- data/lib/generators/react_on_rails/pro/USAGE +1 -1
- data/lib/generators/react_on_rails/pro_generator.rb +206 -183
- data/lib/generators/react_on_rails/pro_setup.rb +102 -26
- data/lib/generators/react_on_rails/react_with_redux_generator.rb +3 -2
- data/lib/generators/react_on_rails/templates/base/base/.env.example +25 -0
- data/lib/generators/react_on_rails/templates/base/base/.github/workflows/ci.yml.tt +86 -0
- data/lib/generators/react_on_rails/templates/base/base/Procfile.dev +4 -3
- data/lib/generators/react_on_rails/templates/base/base/babel.config.js.tt +1 -1
- data/lib/generators/react_on_rails/templates/base/base/bin/switch-bundler +2 -2
- data/lib/generators/react_on_rails/templates/base/base/config/webpack/ServerClientOrBoth.js.tt +1 -1
- data/lib/generators/react_on_rails/templates/base/base/config/webpack/clientWebpackConfig.js.tt +1 -1
- data/lib/generators/react_on_rails/templates/base/base/config/webpack/commonWebpackConfig.js.tt +2 -2
- data/lib/generators/react_on_rails/templates/base/base/config/webpack/development.js.tt +1 -1
- data/lib/generators/react_on_rails/templates/base/base/config/webpack/production.js.tt +1 -1
- data/lib/generators/react_on_rails/templates/base/base/config/webpack/serverWebpackConfig.js.tt +6 -5
- data/lib/generators/react_on_rails/templates/base/base/config/webpack/test.js.tt +1 -1
- data/lib/generators/react_on_rails/templates/pro/base/config/initializers/react_on_rails_pro.rb.tt +1 -1
- data/lib/generators/react_on_rails/templates/pro/base/{client → renderer}/node-renderer.js +1 -0
- data/lib/react_on_rails/config_path_resolver.rb +101 -4
- data/lib/react_on_rails/configuration.rb +22 -0
- data/lib/react_on_rails/dev/file_manager.rb +135 -8
- data/lib/react_on_rails/dev/port_selector.rb +259 -7
- data/lib/react_on_rails/dev/process_manager.rb +29 -2
- data/lib/react_on_rails/dev/server_manager.rb +607 -39
- data/lib/react_on_rails/doctor.rb +513 -45
- data/lib/react_on_rails/helper.rb +3 -11
- data/lib/react_on_rails/js_code_builder.rb +66 -0
- data/lib/react_on_rails/length_prefixed_parser.rb +142 -0
- data/lib/react_on_rails/packs_generator.rb +65 -12
- data/lib/react_on_rails/pro_migration.rb +175 -0
- data/lib/react_on_rails/render_request.rb +74 -0
- data/lib/react_on_rails/rendering_strategy/exec_js_strategy.rb +29 -0
- data/lib/react_on_rails/rendering_strategy.rb +44 -0
- data/lib/react_on_rails/server_rendering_pool/ruby_embedded_java_script.rb +33 -22
- data/lib/react_on_rails/system_checker.rb +44 -23
- data/lib/react_on_rails/utils.rb +5 -0
- data/lib/react_on_rails/version.rb +1 -1
- data/lib/react_on_rails.rb +3 -0
- data/rakelib/run_rspec.rake +0 -5
- data/rakelib/shakapacker_examples.rake +66 -23
- data/react_on_rails.gemspec +18 -8
- data/sig/react_on_rails/js_code_builder.rbs +11 -0
- data/sig/react_on_rails/render_request.rbs +28 -0
- data/sig/react_on_rails/rendering_strategy/exec_js_strategy.rbs +11 -0
- data/sig/react_on_rails/rendering_strategy.rbs +7 -0
- data/sig/react_on_rails.rbs +6 -0
- metadata +31 -10
|
@@ -3,12 +3,14 @@
|
|
|
3
3
|
require "json"
|
|
4
4
|
require "erb"
|
|
5
5
|
require "stringio"
|
|
6
|
+
require "timeout"
|
|
6
7
|
require "yaml"
|
|
7
8
|
require_relative "utils"
|
|
8
9
|
require_relative "config_path_resolver"
|
|
9
10
|
require_relative "version_syntax_converter"
|
|
10
11
|
require_relative "version_synchronizer"
|
|
11
12
|
require_relative "system_checker"
|
|
13
|
+
require_relative "pro_migration"
|
|
12
14
|
|
|
13
15
|
begin
|
|
14
16
|
require "rainbow"
|
|
@@ -55,6 +57,59 @@ module ReactOnRails
|
|
|
55
57
|
MINITEST_HELPER_FILE = "test/test_helper.rb"
|
|
56
58
|
DEFAULT_BUILD_TEST_COMMAND = 'config.build_test_command = "RAILS_ENV=test bin/shakapacker"'
|
|
57
59
|
DEFAULT_SHAKAPACKER_CONFIG_PATH = "config/shakapacker.yml"
|
|
60
|
+
SERVER_BUNDLE_SOURCE_EXTENSIONS = %w[.js .jsx .ts .tsx .mjs .cjs].freeze
|
|
61
|
+
CUSTOM_LAUNCHER_INDICATOR_FILES = %w[dev].freeze
|
|
62
|
+
|
|
63
|
+
# Deprecated-renderer-cache scan (used by check_deprecated_renderer_cache_task):
|
|
64
|
+
# look for references to the old pre_stage_bundle_for_node_renderer task in
|
|
65
|
+
# common deploy-script locations so users on older Procfile/Dockerfile entries
|
|
66
|
+
# get a migration nudge before the task is removed.
|
|
67
|
+
DEPRECATED_RENDERER_CACHE_TASK = "pre_stage_bundle_for_node_renderer"
|
|
68
|
+
# Fixed allowlist of single-file deploy-script paths. Each entry is a literal
|
|
69
|
+
# path that may host a deploy hook referencing the deprecated task. Directory
|
|
70
|
+
# globs (e.g., per-stage Capistrano files or per-workflow GitHub Actions YAML)
|
|
71
|
+
# live in RENDERER_CACHE_DEPLOY_SCRIPT_GLOBS so they stay bounded.
|
|
72
|
+
RENDERER_CACHE_DEPLOY_SCRIPT_PATHS = [
|
|
73
|
+
"Procfile",
|
|
74
|
+
"Procfile.dev",
|
|
75
|
+
"Procfile.dev-static-assets",
|
|
76
|
+
"Procfile.production",
|
|
77
|
+
"Dockerfile",
|
|
78
|
+
"Dockerfile.production",
|
|
79
|
+
"Dockerfile.staging",
|
|
80
|
+
"Dockerfile.review",
|
|
81
|
+
"docker-compose.yml",
|
|
82
|
+
"docker-compose.yaml",
|
|
83
|
+
"compose.yml",
|
|
84
|
+
"compose.yaml",
|
|
85
|
+
"bin/deploy",
|
|
86
|
+
"bin/release",
|
|
87
|
+
"bin/docker-entrypoint",
|
|
88
|
+
"config/deploy.rb",
|
|
89
|
+
"config/deploy/production.rb",
|
|
90
|
+
"config/deploy/staging.rb",
|
|
91
|
+
".kamal/deploy.yml",
|
|
92
|
+
"scripts/deploy.sh",
|
|
93
|
+
".circleci/config.yml",
|
|
94
|
+
".gitlab-ci.yml",
|
|
95
|
+
"bitbucket-pipelines.yml"
|
|
96
|
+
].freeze
|
|
97
|
+
# Bounded glob allowlist for deploy manifests that live in a known directory
|
|
98
|
+
# but use per-environment or per-workflow filenames. Each pattern matches
|
|
99
|
+
# only one directory level (no `**`) so the scan never recurses into the
|
|
100
|
+
# project tree, and the expansion is capped by
|
|
101
|
+
# RENDERER_CACHE_DEPLOY_SCRIPT_GLOB_MAX_MATCHES.
|
|
102
|
+
RENDERER_CACHE_DEPLOY_SCRIPT_GLOBS = [
|
|
103
|
+
".github/workflows/*.yml",
|
|
104
|
+
".github/workflows/*.yaml",
|
|
105
|
+
"config/deploy/*.rb"
|
|
106
|
+
].freeze
|
|
107
|
+
# Per-file safety gate to bound IO during the scan, not a meaningful size limit.
|
|
108
|
+
RENDERER_CACHE_DEPLOY_SCRIPT_MAX_BYTES = 1_048_576
|
|
109
|
+
# Defense-in-depth cap on how many files a single glob may contribute.
|
|
110
|
+
# Realistic repos have a handful of workflow / deploy-stage files; far more
|
|
111
|
+
# than this is a sign of an unexpectedly broad pattern, not legitimate config.
|
|
112
|
+
RENDERER_CACHE_DEPLOY_SCRIPT_GLOB_MAX_MATCHES = 100
|
|
58
113
|
|
|
59
114
|
def initialize(verbose: false, fix: false)
|
|
60
115
|
@verbose = verbose
|
|
@@ -180,7 +235,7 @@ module ReactOnRails
|
|
|
180
235
|
|
|
181
236
|
def check_bin_dev_launcher
|
|
182
237
|
checker.add_info("🚀 bin/dev Launcher:")
|
|
183
|
-
check_bin_dev_launcher_setup
|
|
238
|
+
return unless check_bin_dev_launcher_setup
|
|
184
239
|
|
|
185
240
|
checker.add_info("\n📄 Launcher Procfiles:")
|
|
186
241
|
check_launcher_procfiles
|
|
@@ -489,8 +544,8 @@ module ReactOnRails
|
|
|
489
544
|
end
|
|
490
545
|
|
|
491
546
|
def check_npm_package_version
|
|
492
|
-
package_json_path =
|
|
493
|
-
return unless
|
|
547
|
+
package_json_path = package_json_path_for("react-on-rails npm package version")
|
|
548
|
+
return unless package_json_path
|
|
494
549
|
|
|
495
550
|
begin
|
|
496
551
|
package_json = JSON.parse(File.read(package_json_path))
|
|
@@ -605,8 +660,8 @@ module ReactOnRails
|
|
|
605
660
|
end
|
|
606
661
|
|
|
607
662
|
def check_npm_wildcards
|
|
608
|
-
package_json_path =
|
|
609
|
-
return unless
|
|
663
|
+
package_json_path = package_json_path_for("npm package version constraints")
|
|
664
|
+
return unless package_json_path
|
|
610
665
|
|
|
611
666
|
begin
|
|
612
667
|
package_json = JSON.parse(File.read(package_json_path))
|
|
@@ -719,8 +774,8 @@ module ReactOnRails
|
|
|
719
774
|
end
|
|
720
775
|
|
|
721
776
|
def auto_fix_versions
|
|
722
|
-
package_json_path =
|
|
723
|
-
return unless
|
|
777
|
+
package_json_path = package_json_path_for("package version auto-sync")
|
|
778
|
+
return unless package_json_path
|
|
724
779
|
|
|
725
780
|
synchronizer = ReactOnRails::VersionSynchronizer.new(package_json_path: package_json_path, io: StringIO.new)
|
|
726
781
|
result = synchronizer.sync(write: true)
|
|
@@ -764,8 +819,8 @@ module ReactOnRails
|
|
|
764
819
|
end
|
|
765
820
|
|
|
766
821
|
def check_pro_package_consistency
|
|
767
|
-
package_json_path =
|
|
768
|
-
return unless
|
|
822
|
+
package_json_path = package_json_path_for("Pro package consistency")
|
|
823
|
+
return unless package_json_path
|
|
769
824
|
|
|
770
825
|
package_json = JSON.parse(File.read(package_json_path))
|
|
771
826
|
all_deps = (package_json["dependencies"] || {}).merge(package_json["devDependencies"] || {})
|
|
@@ -1423,12 +1478,24 @@ module ReactOnRails
|
|
|
1423
1478
|
checker.add_info(" • Modern React patterns recommended")
|
|
1424
1479
|
end
|
|
1425
1480
|
|
|
1481
|
+
# Returns true if bin/dev exists, false otherwise.
|
|
1482
|
+
# Used by check_bin_dev_launcher to decide whether to check Procfiles.
|
|
1426
1483
|
def check_bin_dev_launcher_setup
|
|
1427
1484
|
bin_dev_path = "bin/dev"
|
|
1428
1485
|
|
|
1429
1486
|
unless File.exist?(bin_dev_path)
|
|
1430
|
-
checker.
|
|
1431
|
-
|
|
1487
|
+
checker.add_warning(" ⚠️ Official React on Rails bin/dev launcher not found")
|
|
1488
|
+
custom_launchers = detected_custom_launcher_paths
|
|
1489
|
+
if custom_launchers.any?
|
|
1490
|
+
checker.add_info(
|
|
1491
|
+
" ℹ️ Custom launcher detected (#{custom_launchers.join(', ')}). " \
|
|
1492
|
+
"This is OK if your project intentionally manages its own dev workflow."
|
|
1493
|
+
)
|
|
1494
|
+
checker.add_info(" 💡 To use the official launcher instead, run: rails generate react_on_rails:install")
|
|
1495
|
+
else
|
|
1496
|
+
checker.add_info(" 💡 Generate the official launcher with: rails generate react_on_rails:install")
|
|
1497
|
+
end
|
|
1498
|
+
return false
|
|
1432
1499
|
end
|
|
1433
1500
|
|
|
1434
1501
|
content = File.read(bin_dev_path)
|
|
@@ -1441,6 +1508,8 @@ module ReactOnRails
|
|
|
1441
1508
|
checker.add_warning(" ⚠️ bin/dev exists but doesn't use ReactOnRails Launcher")
|
|
1442
1509
|
checker.add_info(" 💡 Consider upgrading: rails generate react_on_rails:install")
|
|
1443
1510
|
end
|
|
1511
|
+
|
|
1512
|
+
true
|
|
1444
1513
|
end
|
|
1445
1514
|
|
|
1446
1515
|
def check_launcher_procfiles
|
|
@@ -1468,6 +1537,14 @@ module ReactOnRails
|
|
|
1468
1537
|
end
|
|
1469
1538
|
end
|
|
1470
1539
|
|
|
1540
|
+
def detected_custom_launcher_paths
|
|
1541
|
+
CUSTOM_LAUNCHER_INDICATOR_FILES.filter_map do |path|
|
|
1542
|
+
next unless File.file?(path)
|
|
1543
|
+
|
|
1544
|
+
path == "dev" ? "./dev" : path
|
|
1545
|
+
end
|
|
1546
|
+
end
|
|
1547
|
+
|
|
1471
1548
|
def check_test_helper_setup
|
|
1472
1549
|
framework_status = test_helper_status_by_framework
|
|
1473
1550
|
|
|
@@ -1535,11 +1612,12 @@ module ReactOnRails
|
|
|
1535
1612
|
source_entry_path = source_entry_path.sub("#{source_path}/", "")
|
|
1536
1613
|
end
|
|
1537
1614
|
|
|
1538
|
-
File.join(source_path, source_entry_path, bundle_filename)
|
|
1615
|
+
bundle_path = File.join(source_path, source_entry_path, bundle_filename)
|
|
1616
|
+
resolve_server_bundle_source_path(bundle_path)
|
|
1539
1617
|
rescue LoadError, StandardError
|
|
1540
1618
|
# Handle missing Shakapacker gem or other configuration errors
|
|
1541
1619
|
bundle_filename = server_bundle_filename
|
|
1542
|
-
"app/javascript/packs/#{bundle_filename}"
|
|
1620
|
+
resolve_server_bundle_source_path("app/javascript/packs/#{bundle_filename}")
|
|
1543
1621
|
end
|
|
1544
1622
|
|
|
1545
1623
|
def server_bundle_filename
|
|
@@ -1562,6 +1640,27 @@ module ReactOnRails
|
|
|
1562
1640
|
"server-bundle.js"
|
|
1563
1641
|
end
|
|
1564
1642
|
|
|
1643
|
+
def resolve_server_bundle_source_path(bundle_path)
|
|
1644
|
+
return bundle_path if File.exist?(bundle_path)
|
|
1645
|
+
|
|
1646
|
+
base_path = bundle_path.sub(%r{\.[^./]+\z}, "")
|
|
1647
|
+
|
|
1648
|
+
candidate_extensions = server_bundle_source_extensions_for(bundle_path)
|
|
1649
|
+
candidate_extensions.each do |extension|
|
|
1650
|
+
candidate_path = "#{base_path}#{extension}"
|
|
1651
|
+
return candidate_path if File.exist?(candidate_path)
|
|
1652
|
+
end
|
|
1653
|
+
|
|
1654
|
+
bundle_path
|
|
1655
|
+
end
|
|
1656
|
+
|
|
1657
|
+
def server_bundle_source_extensions_for(bundle_path)
|
|
1658
|
+
extension = File.extname(bundle_path)
|
|
1659
|
+
return SERVER_BUNDLE_SOURCE_EXTENSIONS if extension.empty?
|
|
1660
|
+
|
|
1661
|
+
SERVER_BUNDLE_SOURCE_EXTENSIONS.reject { |candidate_extension| candidate_extension == extension }
|
|
1662
|
+
end
|
|
1663
|
+
|
|
1565
1664
|
def exit_with_status
|
|
1566
1665
|
if checker.errors?
|
|
1567
1666
|
puts Rainbow("❌ Doctor found critical issues. Please address errors above.").red.bold
|
|
@@ -2670,7 +2769,9 @@ module ReactOnRails
|
|
|
2670
2769
|
check_pro_initializer_existence
|
|
2671
2770
|
ensure_rails_environment_loaded
|
|
2672
2771
|
check_pro_renderer_mode
|
|
2673
|
-
|
|
2772
|
+
check_base_package_references
|
|
2773
|
+
check_deprecated_renderer_cache_task
|
|
2774
|
+
check_rolling_deploy_adapter
|
|
2674
2775
|
end
|
|
2675
2776
|
|
|
2676
2777
|
def check_pro_initializer_existence
|
|
@@ -2700,45 +2801,389 @@ module ReactOnRails
|
|
|
2700
2801
|
checker.add_warning("⚠️ Could not detect Pro renderer mode: #{e.message}")
|
|
2701
2802
|
end
|
|
2702
2803
|
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2804
|
+
def check_deprecated_renderer_cache_task
|
|
2805
|
+
# Resolve against Rails.root (not Dir.pwd) so the scan still fires when
|
|
2806
|
+
# doctor is invoked from a subdirectory — otherwise the checks silently
|
|
2807
|
+
# find nothing and the deprecation warning never surfaces.
|
|
2808
|
+
#
|
|
2809
|
+
# Read in binary mode (the task name is pure ASCII) so a non-UTF-8 byte
|
|
2810
|
+
# in a deploy script does not raise Encoding::InvalidByteSequenceError
|
|
2811
|
+
# and mask the file via the rescue below.
|
|
2812
|
+
#
|
|
2813
|
+
# Skip leading-comment lines (`#` is the comment prefix for all scanned
|
|
2814
|
+
# file types: Procfile, Dockerfile, shell scripts, YAML, and Ruby) so
|
|
2815
|
+
# files that mention the old task only inside a comment do not trip the
|
|
2816
|
+
# migration nudge.
|
|
2817
|
+
# Per-file rescue so a transient failure on one path (e.g. Errno::EACCES)
|
|
2818
|
+
# does not abort the whole scan and silently skip the rest. The outer
|
|
2819
|
+
# rescue catches anything that escapes the per-file guard. Globs are
|
|
2820
|
+
# expanded under their own rescue so a failure expanding one pattern
|
|
2821
|
+
# cannot stop other patterns or fixed paths from being scanned.
|
|
2822
|
+
candidate_paths = (
|
|
2823
|
+
RENDERER_CACHE_DEPLOY_SCRIPT_PATHS + expand_renderer_cache_deploy_script_globs
|
|
2824
|
+
).uniq
|
|
2825
|
+
|
|
2826
|
+
matches = candidate_paths.select do |path|
|
|
2827
|
+
deploy_script_path_references_deprecated_task?(path)
|
|
2828
|
+
end
|
|
2829
|
+
|
|
2830
|
+
return if matches.empty?
|
|
2709
2831
|
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
js_patterns = js_extensions.map { |ext| "#{source_path}/**/*.#{ext}" }
|
|
2714
|
-
files_with_base_import = []
|
|
2832
|
+
checker.add_warning(<<~MSG.strip)
|
|
2833
|
+
⚠️ Deprecated rake task '#{DEPRECATED_RENDERER_CACHE_TASK}' referenced in:
|
|
2834
|
+
#{matches.map { |p| format_renderer_cache_migration_bullet(p) }.join("\n")}
|
|
2715
2835
|
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2836
|
+
The unified 'pre_seed_renderer_cache' task uses MODE=copy by default (for
|
|
2837
|
+
Docker/image builds) and MODE=symlink for same-filesystem workflows.
|
|
2838
|
+
MSG
|
|
2839
|
+
rescue StandardError => e
|
|
2840
|
+
checker.add_warning("⚠️ Could not complete scan for deprecated renderer-cache task references: #{e.message}")
|
|
2841
|
+
end
|
|
2842
|
+
|
|
2843
|
+
def deploy_script_references_deprecated_task?(full_path)
|
|
2844
|
+
# Only `#` comments matter for the scanned file types: Procfile, Dockerfile*,
|
|
2845
|
+
# and bin/* scripts all use `#`. None use `//`, so we don't filter it.
|
|
2846
|
+
# The trailing-comment strip requires whitespace before `#`, so a fragment
|
|
2847
|
+
# like `task#name` stays intact while `cmd # was: <deprecated>` is filtered.
|
|
2848
|
+
full_path.binread.each_line.any? do |line|
|
|
2849
|
+
stripped = line.lstrip
|
|
2850
|
+
next false if stripped.start_with?("#")
|
|
2851
|
+
|
|
2852
|
+
without_inline_comment = stripped.sub(/ +#.*/, "")
|
|
2853
|
+
without_inline_comment.include?(DEPRECATED_RENDERER_CACHE_TASK)
|
|
2854
|
+
end
|
|
2855
|
+
end
|
|
2720
2856
|
|
|
2721
|
-
|
|
2857
|
+
def deploy_script_path_references_deprecated_task?(path)
|
|
2858
|
+
full_path = Rails.root.join(path)
|
|
2859
|
+
|
|
2860
|
+
return false unless full_path.file?
|
|
2861
|
+
# Skip files larger than RENDERER_CACHE_DEPLOY_SCRIPT_MAX_BYTES;
|
|
2862
|
+
# deploy scripts and CI manifests should be tiny.
|
|
2863
|
+
return false if full_path.size > RENDERER_CACHE_DEPLOY_SCRIPT_MAX_BYTES
|
|
2864
|
+
|
|
2865
|
+
deploy_script_references_deprecated_task?(full_path)
|
|
2866
|
+
rescue StandardError => e
|
|
2867
|
+
checker.add_warning(
|
|
2868
|
+
"⚠️ Could not scan #{path} for deprecated renderer-cache task references: #{e.message}"
|
|
2869
|
+
)
|
|
2870
|
+
false
|
|
2871
|
+
end
|
|
2872
|
+
|
|
2873
|
+
def expand_renderer_cache_deploy_script_globs
|
|
2874
|
+
# File::FNM_PATHNAME stops `*` from crossing slashes even though none of
|
|
2875
|
+
# the patterns use `**`. base: scopes the expansion to the project root
|
|
2876
|
+
# and yields paths relative to it. Each pattern is rescued individually
|
|
2877
|
+
# so a permission error on one glob (e.g. an unreadable .github/) does
|
|
2878
|
+
# not silence the rest.
|
|
2879
|
+
root = Rails.root.to_s
|
|
2880
|
+
RENDERER_CACHE_DEPLOY_SCRIPT_GLOBS.flat_map do |pattern|
|
|
2881
|
+
Dir.glob(pattern, File::FNM_PATHNAME, base: root)
|
|
2882
|
+
.sort
|
|
2883
|
+
.first(RENDERER_CACHE_DEPLOY_SCRIPT_GLOB_MAX_MATCHES)
|
|
2884
|
+
rescue StandardError => e
|
|
2885
|
+
checker.add_warning(
|
|
2886
|
+
"⚠️ Could not expand renderer-cache deploy-script glob #{pattern}: #{e.message}"
|
|
2887
|
+
)
|
|
2888
|
+
[]
|
|
2889
|
+
end
|
|
2890
|
+
end
|
|
2891
|
+
|
|
2892
|
+
def format_renderer_cache_migration_bullet(path)
|
|
2893
|
+
suggestion = renderer_cache_migration_suggestion(path)
|
|
2894
|
+
lines = suggestion.split("\n")
|
|
2895
|
+
return " • #{path} → #{suggestion}" if lines.length == 1
|
|
2896
|
+
|
|
2897
|
+
indented = lines.map { |line| " #{line}" }.join("\n")
|
|
2898
|
+
" • #{path} →\n#{indented}"
|
|
2899
|
+
end
|
|
2900
|
+
|
|
2901
|
+
def renderer_cache_migration_suggestion(path)
|
|
2902
|
+
# Dockerfile* entries are RUN steps during image build, so copy mode bakes the cache into the layer.
|
|
2903
|
+
# Runtime hooks (Procfile, bin/*, .kamal/deploy.yml, Capistrano config) run after the app is deployed,
|
|
2904
|
+
# where both the app and renderer share the same filesystem, so symlink mode is correct.
|
|
2905
|
+
if path.start_with?("Dockerfile")
|
|
2906
|
+
# /app/.node-renderer-bundles is a placeholder matching the docs' Dockerfile examples;
|
|
2907
|
+
# the user must adjust it to match their image's WORKDIR. Hardcoding Rails.root here would
|
|
2908
|
+
# leak the developer host path (e.g. /Users/alice/myapp/...) into the Dockerfile, which
|
|
2909
|
+
# does not exist inside the container.
|
|
2910
|
+
"ENV RENDERER_SERVER_BUNDLE_CACHE_PATH=/app/.node-renderer-bundles\n" \
|
|
2911
|
+
"RUN bundle exec rake react_on_rails_pro:pre_seed_renderer_cache"
|
|
2912
|
+
elsif path.start_with?(".kamal/")
|
|
2913
|
+
# Kamal hooks run in two contexts: post-deploy hooks on the live server (symlink),
|
|
2914
|
+
# and image-build hooks invoked during the Docker build (copy). The trailing comments
|
|
2915
|
+
# disambiguate so users pick the mode that matches the hook they're editing.
|
|
2916
|
+
"rake react_on_rails_pro:pre_seed_renderer_cache MODE=symlink\n" \
|
|
2917
|
+
"# For Kamal deploy hooks (post-deploy, on the live server): MODE=symlink\n" \
|
|
2918
|
+
"# For Kamal image-build hooks (hook/pre-build inside the Docker build): MODE=copy"
|
|
2919
|
+
else
|
|
2920
|
+
# docker-compose.yml / compose.yaml / bin/* / config/deploy.rb / scripts/deploy.sh
|
|
2921
|
+
# default to symlink (correct for local dev + same-filesystem deploys); call out the
|
|
2922
|
+
# copy alternative for users driving production container builds via Compose.
|
|
2923
|
+
"rake react_on_rails_pro:pre_seed_renderer_cache MODE=symlink # use MODE=copy for Docker/container image builds"
|
|
2924
|
+
end
|
|
2925
|
+
end
|
|
2926
|
+
|
|
2927
|
+
# ── Rolling Deploy Adapter ────────────────────────────────────────
|
|
2928
|
+
|
|
2929
|
+
ROLLING_DEPLOY_REQUIRED_METHODS = %i[previous_bundle_hashes fetch upload].freeze
|
|
2930
|
+
|
|
2931
|
+
def check_rolling_deploy_adapter
|
|
2932
|
+
adapter = ReactOnRailsPro.configuration.rolling_deploy_adapter
|
|
2933
|
+
|
|
2934
|
+
if adapter.nil?
|
|
2935
|
+
env_override = ENV.fetch("PREVIOUS_BUNDLE_HASHES", nil)
|
|
2936
|
+
if env_override && !env_override.empty?
|
|
2937
|
+
checker.add_warning(
|
|
2938
|
+
"⚠️ PREVIOUS_BUNDLE_HASHES=#{truncate_for_warning(env_override).inspect} is set but no " \
|
|
2939
|
+
"rolling_deploy_adapter is configured. Rolling-deploy seeding needs both — the env var " \
|
|
2940
|
+
"overrides *discovery* but the adapter is still required to fetch bundle files. " \
|
|
2941
|
+
"Set config.rolling_deploy_adapter or unset PREVIOUS_BUNDLE_HASHES."
|
|
2942
|
+
)
|
|
2943
|
+
else
|
|
2944
|
+
checker.add_info("ℹ️ No rolling_deploy_adapter configured (rolling-deploy seeding disabled).")
|
|
2722
2945
|
end
|
|
2946
|
+
return
|
|
2947
|
+
end
|
|
2948
|
+
|
|
2949
|
+
return unless report_adapter_protocol(adapter)
|
|
2950
|
+
|
|
2951
|
+
env_override = ENV.fetch("PREVIOUS_BUNDLE_HASHES", nil)
|
|
2952
|
+
if env_override && !env_override.empty?
|
|
2953
|
+
# PREVIOUS_BUNDLE_HASHES is a full discovery override at runtime, so
|
|
2954
|
+
# probing adapter#previous_bundle_hashes here would surface timeout/error
|
|
2955
|
+
# noise for a code path the deploy will never invoke. Skip the probe and
|
|
2956
|
+
# state the override explicitly so operators see what's happening.
|
|
2957
|
+
checker.add_info(
|
|
2958
|
+
"ℹ️ PREVIOUS_BUNDLE_HASHES=#{truncate_for_warning(env_override).inspect} is set; " \
|
|
2959
|
+
"skipping rolling_deploy_adapter#previous_bundle_hashes probe (env var overrides discovery)."
|
|
2960
|
+
)
|
|
2961
|
+
report_resolved_cache_dir
|
|
2962
|
+
return
|
|
2723
2963
|
end
|
|
2724
2964
|
|
|
2725
|
-
|
|
2726
|
-
|
|
2965
|
+
report_previous_bundle_hashes_probe(adapter)
|
|
2966
|
+
report_resolved_cache_dir
|
|
2967
|
+
rescue StandardError => e
|
|
2968
|
+
checker.add_warning("⚠️ Could not evaluate rolling_deploy_adapter: #{e.message}")
|
|
2969
|
+
end
|
|
2970
|
+
|
|
2971
|
+
# Cap echoed env-var values so a malformed (or accidentally large)
|
|
2972
|
+
# PREVIOUS_BUNDLE_HASHES value doesn't dump kilobytes into operator output.
|
|
2973
|
+
PREVIOUS_BUNDLE_HASHES_DISPLAY_LIMIT = 80
|
|
2974
|
+
private_constant :PREVIOUS_BUNDLE_HASHES_DISPLAY_LIMIT
|
|
2975
|
+
|
|
2976
|
+
def truncate_for_warning(value)
|
|
2977
|
+
return value if value.length <= PREVIOUS_BUNDLE_HASHES_DISPLAY_LIMIT
|
|
2978
|
+
|
|
2979
|
+
"#{value[0, PREVIOUS_BUNDLE_HASHES_DISPLAY_LIMIT]}… (#{value.length} chars total)"
|
|
2980
|
+
end
|
|
2981
|
+
|
|
2982
|
+
def report_adapter_protocol(adapter)
|
|
2983
|
+
missing = ROLLING_DEPLOY_REQUIRED_METHODS.reject { |m| adapter.respond_to?(m) }
|
|
2984
|
+
if missing.empty?
|
|
2985
|
+
checker.add_success(
|
|
2986
|
+
"✅ rolling_deploy_adapter responds to all required methods " \
|
|
2987
|
+
"(#{ROLLING_DEPLOY_REQUIRED_METHODS.join(', ')})"
|
|
2988
|
+
)
|
|
2989
|
+
true
|
|
2990
|
+
else
|
|
2991
|
+
checker.add_warning(
|
|
2992
|
+
"⚠️ rolling_deploy_adapter is missing required methods: #{missing.join(', ')}. " \
|
|
2993
|
+
"See docs/pro/rolling-deploy-adapters.md."
|
|
2994
|
+
)
|
|
2995
|
+
false
|
|
2996
|
+
end
|
|
2997
|
+
end
|
|
2998
|
+
|
|
2999
|
+
def report_previous_bundle_hashes_probe(adapter)
|
|
3000
|
+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
3001
|
+
timeout_seconds = rolling_deploy_discovery_timeout_seconds
|
|
3002
|
+
hashes = Timeout.timeout(timeout_seconds) { Array(adapter.previous_bundle_hashes) }
|
|
3003
|
+
latency_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000).round
|
|
3004
|
+
|
|
3005
|
+
if hashes.empty?
|
|
3006
|
+
checker.add_warning(
|
|
3007
|
+
"⚠️ rolling_deploy_adapter#previous_bundle_hashes returned []. " \
|
|
3008
|
+
"Usually indicates the upload side has never run on a prior deploy."
|
|
3009
|
+
)
|
|
3010
|
+
else
|
|
3011
|
+
checker.add_success(
|
|
3012
|
+
"✅ rolling_deploy_adapter#previous_bundle_hashes returned #{hashes.length} hash(es) in #{latency_ms}ms"
|
|
3013
|
+
)
|
|
3014
|
+
end
|
|
3015
|
+
rescue Timeout::Error
|
|
3016
|
+
checker.add_warning(
|
|
3017
|
+
"⚠️ rolling_deploy_adapter#previous_bundle_hashes timed out after " \
|
|
3018
|
+
"#{timeout_seconds}s"
|
|
3019
|
+
)
|
|
3020
|
+
rescue StandardError => e
|
|
3021
|
+
checker.add_warning("⚠️ rolling_deploy_adapter#previous_bundle_hashes raised #{e.class}: #{e.message}")
|
|
3022
|
+
end
|
|
3023
|
+
|
|
3024
|
+
def rolling_deploy_discovery_timeout_seconds
|
|
3025
|
+
if defined?(ReactOnRailsPro::RollingDeployCacheStager::DISCOVERY_TIMEOUT_SECONDS)
|
|
3026
|
+
ReactOnRailsPro::RollingDeployCacheStager::DISCOVERY_TIMEOUT_SECONDS
|
|
3027
|
+
else
|
|
3028
|
+
# Must match the canonical Pro constant. Bidirectional pointers:
|
|
3029
|
+
# Pro file: react_on_rails_pro/lib/react_on_rails_pro/rolling_deploy_cache_stager.rb
|
|
3030
|
+
# Pro constant: ReactOnRailsPro::RollingDeployCacheStager::DISCOVERY_TIMEOUT_SECONDS
|
|
3031
|
+
# Pro guard: react_on_rails_pro/spec/dummy/spec/rolling_deploy_cache_stager_spec.rb
|
|
3032
|
+
# (describe "DISCOVERY_TIMEOUT_SECONDS" → expects this fallback to equal Pro constant)
|
|
3033
|
+
# The Pro spec catches drift in the Pro→OSS direction. If you change
|
|
3034
|
+
# the value here without updating the Pro constant + spec, doctor will
|
|
3035
|
+
# silently use a different timeout from the live stager.
|
|
3036
|
+
10
|
|
3037
|
+
end
|
|
3038
|
+
end
|
|
3039
|
+
|
|
3040
|
+
# Fallback used when the Pro gem isn't loaded. Must match
|
|
3041
|
+
# ReactOnRailsPro::RollingDeployCacheStager::TEMPORARY_DIRECTORY_PATTERN so
|
|
3042
|
+
# doctor still filters staging/backup dirs out of the bundle-hash count.
|
|
3043
|
+
# Drift is caught by:
|
|
3044
|
+
# react_on_rails_pro/spec/dummy/spec/rolling_deploy_cache_stager_spec.rb
|
|
3045
|
+
# describe "TEMPORARY_DIRECTORY_PATTERN"
|
|
3046
|
+
# PID is `\d+` to match container deployments (Docker/Kubernetes) where
|
|
3047
|
+
# seeding runs as PID 1.
|
|
3048
|
+
ROLLING_DEPLOY_TEMP_DIR_PATTERN = /\.(?:staging|previous)-\d+-[0-9a-f]{8,}\z/
|
|
3049
|
+
|
|
3050
|
+
def report_resolved_cache_dir
|
|
3051
|
+
cache_dir = ReactOnRailsPro::Utils.resolve_renderer_cache_dir
|
|
3052
|
+
if File.directory?(cache_dir)
|
|
3053
|
+
temp_dir_pattern = rolling_deploy_temp_dir_pattern
|
|
3054
|
+
subdirs = Dir.children(cache_dir).select do |entry|
|
|
3055
|
+
File.directory?(File.join(cache_dir, entry)) && !entry.match?(temp_dir_pattern)
|
|
3056
|
+
end
|
|
3057
|
+
checker.add_info("ℹ️ Resolved renderer cache dir: #{cache_dir} (#{subdirs.length} bundle-hash subdir(s))")
|
|
3058
|
+
else
|
|
3059
|
+
checker.add_info("ℹ️ Resolved renderer cache dir: #{cache_dir} (does not exist yet)")
|
|
3060
|
+
end
|
|
3061
|
+
end
|
|
3062
|
+
|
|
3063
|
+
def rolling_deploy_temp_dir_pattern
|
|
3064
|
+
if defined?(ReactOnRailsPro::RollingDeployCacheStager::TEMPORARY_DIRECTORY_PATTERN)
|
|
3065
|
+
ReactOnRailsPro::RollingDeployCacheStager::TEMPORARY_DIRECTORY_PATTERN
|
|
3066
|
+
else
|
|
3067
|
+
ROLLING_DEPLOY_TEMP_DIR_PATTERN
|
|
3068
|
+
end
|
|
3069
|
+
end
|
|
3070
|
+
|
|
3071
|
+
# The base 'react-on-rails' npm package is a transitive dependency of 'react-on-rails-pro',
|
|
3072
|
+
# so references to 'react-on-rails' resolve silently, loading the base package instead of Pro.
|
|
3073
|
+
# Components registered through the base package won't have Pro features (streaming, caching,
|
|
3074
|
+
# RSC), and may cause "component not registered" errors at runtime.
|
|
3075
|
+
BASE_PACKAGE_IMPORT_PATTERN = %r{\bfrom\s+['"]react-on-rails(?:/[^'"]*)?['"]}
|
|
3076
|
+
BASE_PACKAGE_REQUIRE_PATTERN = %r{\brequire\s*\(\s*['"]react-on-rails(?:/[^'"]*)?['"]\s*\)}
|
|
3077
|
+
BASE_PACKAGE_DYNAMIC_IMPORT_PATTERN = %r{\bimport\s*\(\s*['"]react-on-rails(?:/[^'"]*)?['"]\s*\)}
|
|
3078
|
+
BASE_PACKAGE_SIDE_EFFECT_IMPORT_PATTERN = %r{^\s*import\s+['"]react-on-rails(?:/[^'"]*)?['"]}
|
|
3079
|
+
BASE_PACKAGE_REFERENCE_SOURCE_ROOTS = ReactOnRails::ProMigration::JS_SOURCE_ROOTS
|
|
3080
|
+
BASE_PACKAGE_REFERENCE_EXTENSIONS = ReactOnRails::ProMigration::JS_SOURCE_EXTENSIONS
|
|
3081
|
+
# Explicit allowlist of documented Jest/Vitest APIs whose first argument is a module specifier.
|
|
3082
|
+
BASE_PACKAGE_JEST_MODULE_SPECIFIER_METHOD_PATTERN =
|
|
3083
|
+
ReactOnRails::ProMigration::JEST_MODULE_SPECIFIER_METHOD_PATTERN
|
|
3084
|
+
BASE_PACKAGE_VITEST_MODULE_SPECIFIER_METHOD_PATTERN =
|
|
3085
|
+
ReactOnRails::ProMigration::VITEST_MODULE_SPECIFIER_METHOD_PATTERN
|
|
3086
|
+
# Match known Jest/Vitest module-specifier helpers. Aliased or nested receivers
|
|
3087
|
+
# are intentionally out of scope to avoid warning on arbitrary application methods.
|
|
3088
|
+
#
|
|
3089
|
+
# importActual/importMock exist only as vi.* methods; there is no
|
|
3090
|
+
# `import { importActual } from 'vitest'` form. The bare branch below is a
|
|
3091
|
+
# deliberately broad detector heuristic (the rewriter omits it because
|
|
3092
|
+
# rewriting is destructive while detection is advisory) and accepts that a
|
|
3093
|
+
# user-defined helper of that name taking a 'react-on-rails' string matches.
|
|
3094
|
+
BASE_PACKAGE_MOCK_PATTERN = %r{
|
|
3095
|
+
\b(?:
|
|
3096
|
+
(?:
|
|
3097
|
+
jest\.(?:#{BASE_PACKAGE_JEST_MODULE_SPECIFIER_METHOD_PATTERN})
|
|
3098
|
+
|
|
|
3099
|
+
vi\.(?:#{BASE_PACKAGE_VITEST_MODULE_SPECIFIER_METHOD_PATTERN})
|
|
3100
|
+
)
|
|
3101
|
+
\s*
|
|
3102
|
+
(?:<[^;\n]*>\s*)?
|
|
3103
|
+
|
|
|
3104
|
+
(?:importActual|importMock)
|
|
3105
|
+
)
|
|
3106
|
+
\s*\(\s*['"]react-on-rails(?:/[^'"]*)?['"]
|
|
3107
|
+
}x
|
|
3108
|
+
# In Ruby, ^ matches the start of any line, so this catches declarations anywhere in the file.
|
|
3109
|
+
BASE_PACKAGE_DECLARE_MODULE_PATTERN = %r{^\s*(?:export\s+)?declare\s+module\s+['"]react-on-rails(?:/[^'"]*)?['"]}
|
|
3110
|
+
BASE_PACKAGE_REFERENCE_PATTERNS = [
|
|
3111
|
+
BASE_PACKAGE_IMPORT_PATTERN,
|
|
3112
|
+
BASE_PACKAGE_REQUIRE_PATTERN,
|
|
3113
|
+
BASE_PACKAGE_DYNAMIC_IMPORT_PATTERN,
|
|
3114
|
+
BASE_PACKAGE_SIDE_EFFECT_IMPORT_PATTERN,
|
|
3115
|
+
BASE_PACKAGE_MOCK_PATTERN,
|
|
3116
|
+
BASE_PACKAGE_DECLARE_MODULE_PATTERN
|
|
3117
|
+
].freeze
|
|
3118
|
+
|
|
3119
|
+
def check_base_package_references
|
|
3120
|
+
files_with_base_reference = files_with_base_package_references(resolve_js_source_path)
|
|
3121
|
+
|
|
3122
|
+
if files_with_base_reference.empty?
|
|
3123
|
+
checker.add_success("✅ No base 'react-on-rails' references found (Pro package used correctly)")
|
|
2727
3124
|
else
|
|
2728
3125
|
checker.add_warning(<<~MSG.strip)
|
|
2729
|
-
⚠️ Found
|
|
2730
|
-
#{
|
|
3126
|
+
⚠️ Found references to 'react-on-rails' instead of 'react-on-rails-pro':
|
|
3127
|
+
#{files_with_base_reference.map { |f| " • #{f}" }.join("\n")}
|
|
2731
3128
|
|
|
2732
|
-
|
|
3129
|
+
Look for static imports, side-effect imports, CommonJS requires, dynamic imports,
|
|
3130
|
+
Jest/Vitest mock helpers, or TypeScript module augmentations.
|
|
3131
|
+
Note: this includes commented-out references; review each file before updating.
|
|
3132
|
+
|
|
3133
|
+
The base package is a transitive dependency of Pro, so these references resolve
|
|
2733
3134
|
silently but load the base version without Pro features.
|
|
2734
3135
|
|
|
2735
|
-
Fix:
|
|
2736
|
-
import ReactOnRails from 'react-on-rails-pro';
|
|
2737
|
-
import ReactOnRails from 'react-on-rails-pro/client'; // client
|
|
3136
|
+
Fix: Replace base-package references with their Pro equivalents:
|
|
3137
|
+
import ReactOnRails from 'react-on-rails-pro'; // ES import (server)
|
|
3138
|
+
import ReactOnRails from 'react-on-rails-pro/client'; // ES import (client)
|
|
3139
|
+
import 'react-on-rails-pro'; // Side-effect import
|
|
3140
|
+
const ReactOnRails = require('react-on-rails-pro'); // CommonJS require
|
|
3141
|
+
const ReactOnRails = await import('react-on-rails-pro'); // Dynamic import
|
|
3142
|
+
jest.mock('react-on-rails-pro', ...); // Jest mock helper
|
|
3143
|
+
vi.mock('react-on-rails-pro', ...); // Vitest mock helper
|
|
3144
|
+
declare module 'react-on-rails-pro' { ... } // TypeScript augmentation
|
|
2738
3145
|
MSG
|
|
2739
3146
|
end
|
|
2740
3147
|
rescue StandardError => e
|
|
2741
|
-
checker.add_warning("⚠️ Could not scan for base package
|
|
3148
|
+
checker.add_warning("⚠️ Could not scan for base package references: #{e.message}")
|
|
3149
|
+
end
|
|
3150
|
+
|
|
3151
|
+
def files_with_base_package_references(source_path)
|
|
3152
|
+
# Scan every file type the Pro migration rewriter can modify.
|
|
3153
|
+
# **/*.ts naturally matches *.d.ts declaration files because they end in .ts.
|
|
3154
|
+
js_patterns = base_package_reference_source_paths(source_path).flat_map do |source_root|
|
|
3155
|
+
BASE_PACKAGE_REFERENCE_EXTENSIONS.map { |ext| "#{source_root}/**/*.#{ext}" }
|
|
3156
|
+
end
|
|
3157
|
+
|
|
3158
|
+
js_patterns.flat_map do |pattern|
|
|
3159
|
+
Dir.glob(pattern)
|
|
3160
|
+
.reject { |file| file.include?("/node_modules/") }
|
|
3161
|
+
.select { |file| base_package_reference_file?(file) }
|
|
3162
|
+
end.uniq.sort
|
|
3163
|
+
end
|
|
3164
|
+
|
|
3165
|
+
def base_package_reference_source_paths(source_path)
|
|
3166
|
+
([source_path] + BASE_PACKAGE_REFERENCE_SOURCE_ROOTS)
|
|
3167
|
+
.compact
|
|
3168
|
+
.map(&:to_s)
|
|
3169
|
+
.reject(&:empty?)
|
|
3170
|
+
.uniq
|
|
3171
|
+
.select { |path| Dir.exist?(path) }
|
|
3172
|
+
end
|
|
3173
|
+
|
|
3174
|
+
def base_package_reference_file?(file)
|
|
3175
|
+
content = File.binread(file).force_encoding("UTF-8")
|
|
3176
|
+
return false unless content.valid_encoding?
|
|
3177
|
+
|
|
3178
|
+
base_package_reference?(content)
|
|
3179
|
+
rescue SystemCallError, IOError
|
|
3180
|
+
false
|
|
3181
|
+
end
|
|
3182
|
+
|
|
3183
|
+
def base_package_reference?(content)
|
|
3184
|
+
# Content-based matching intentionally catches comments and string literals
|
|
3185
|
+
# so stale migration references stay visible.
|
|
3186
|
+
BASE_PACKAGE_REFERENCE_PATTERNS.any? { |reference_pattern| content.match?(reference_pattern) }
|
|
2742
3187
|
end
|
|
2743
3188
|
|
|
2744
3189
|
# ── React Server Components ────────────────────────────────────
|
|
@@ -2871,7 +3316,15 @@ module ReactOnRails
|
|
|
2871
3316
|
# Prefer the actually installed version from node_modules over the declared
|
|
2872
3317
|
# range in package.json. Declared ranges like "^19.0.0" would be misleading
|
|
2873
3318
|
# (stripped to "19.0.0" even though 19.0.4+ may be installed).
|
|
2874
|
-
|
|
3319
|
+
package_root = resolved_package_root
|
|
3320
|
+
if package_root_missing?(package_root)
|
|
3321
|
+
# This check only needs the directory before Node chdirs into it; an
|
|
3322
|
+
# installed React version can be resolved without package.json.
|
|
3323
|
+
warn_missing_package_root(package_root)
|
|
3324
|
+
return nil
|
|
3325
|
+
end
|
|
3326
|
+
|
|
3327
|
+
installed = installed_react_version(package_root)
|
|
2875
3328
|
return installed if installed
|
|
2876
3329
|
|
|
2877
3330
|
declared_react_version
|
|
@@ -2879,11 +3332,12 @@ module ReactOnRails
|
|
|
2879
3332
|
nil
|
|
2880
3333
|
end
|
|
2881
3334
|
|
|
2882
|
-
def installed_react_version
|
|
3335
|
+
def installed_react_version(package_root)
|
|
2883
3336
|
# Use Node's own module resolution to find the actually installed React,
|
|
2884
3337
|
# which handles hoisted dependencies in monorepos and pnpm workspaces.
|
|
2885
|
-
|
|
2886
|
-
|
|
3338
|
+
# Resolve from the configured package root so nested client/ layouts work.
|
|
3339
|
+
script = "console.log(require.resolve('react/package.json'))"
|
|
3340
|
+
stdout, _stderr, status = Open3.capture3("node", "-e", script, chdir: package_root)
|
|
2887
3341
|
return nil unless status.success?
|
|
2888
3342
|
|
|
2889
3343
|
resolved_path = stdout.strip
|
|
@@ -2895,10 +3349,24 @@ module ReactOnRails
|
|
|
2895
3349
|
nil
|
|
2896
3350
|
end
|
|
2897
3351
|
|
|
3352
|
+
def add_warning(message)
|
|
3353
|
+
checker.add_warning(message)
|
|
3354
|
+
end
|
|
3355
|
+
|
|
3356
|
+
# Delegates the protected registry to checker so warnings emitted from
|
|
3357
|
+
# Doctor share the same de-dupe state as warnings emitted from the checker.
|
|
3358
|
+
# The cross-class call is permitted because both Doctor and SystemChecker
|
|
3359
|
+
# include ConfigPathResolver, which satisfies Ruby's protected-visibility
|
|
3360
|
+
# rule (caller and receiver share an ancestor that defines the method).
|
|
3361
|
+
def config_path_warning_registry
|
|
3362
|
+
checker.config_path_warning_registry
|
|
3363
|
+
end
|
|
3364
|
+
|
|
2898
3365
|
def declared_react_version
|
|
2899
|
-
|
|
3366
|
+
package_json_path = package_json_path_for("declared React version")
|
|
3367
|
+
return nil unless package_json_path
|
|
2900
3368
|
|
|
2901
|
-
package_json = JSON.parse(File.read(
|
|
3369
|
+
package_json = JSON.parse(File.read(package_json_path))
|
|
2902
3370
|
all_deps = (package_json["dependencies"] || {}).merge(package_json["devDependencies"] || {})
|
|
2903
3371
|
version_str = all_deps["react"]
|
|
2904
3372
|
return nil unless version_str
|