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
|
@@ -10,6 +10,7 @@ require "erb"
|
|
|
10
10
|
require "rbconfig"
|
|
11
11
|
require "socket"
|
|
12
12
|
require "time"
|
|
13
|
+
require "uri"
|
|
13
14
|
require "yaml"
|
|
14
15
|
require_relative "../packer_utils"
|
|
15
16
|
require_relative "database_checker"
|
|
@@ -22,8 +23,6 @@ module ReactOnRails
|
|
|
22
23
|
TEST_WATCH_MODES = %w[auto full client-only].freeze
|
|
23
24
|
OPEN_BROWSER_WAIT_TIMEOUT = 60
|
|
24
25
|
OPEN_BROWSER_POLL_INTERVAL = 0.5
|
|
25
|
-
# Relative to Dir.pwd; bin/dev is expected to run from the Rails app root.
|
|
26
|
-
OPEN_BROWSER_ONCE_MARKER = File.join("tmp", "react_on_rails", "browser_opened_once").freeze
|
|
27
26
|
|
|
28
27
|
class << self
|
|
29
28
|
def start(mode = :development, procfile = nil, verbose: false, route: nil, rails_env: nil,
|
|
@@ -55,11 +54,105 @@ module ReactOnRails
|
|
|
55
54
|
puts "🔪 Killing all development processes..."
|
|
56
55
|
puts ""
|
|
57
56
|
|
|
58
|
-
|
|
57
|
+
# Run every cleanup step unconditionally so a successful first step
|
|
58
|
+
# (e.g. pattern-based kill) doesn't leave stale port-bound processes
|
|
59
|
+
# or socket/pid files behind. `.any?` still gives us the
|
|
60
|
+
# "anything actually got killed?" signal for the summary message.
|
|
61
|
+
killed_any = [
|
|
62
|
+
kill_running_processes,
|
|
63
|
+
kill_port_processes(killable_ports),
|
|
64
|
+
cleanup_socket_files
|
|
65
|
+
].any?
|
|
59
66
|
|
|
60
67
|
print_kill_summary(killed_any)
|
|
61
68
|
end
|
|
62
69
|
|
|
70
|
+
# Fallback port list for the port-scan kill path. Uses the base-port
|
|
71
|
+
# derived ports when REACT_ON_RAILS_BASE_PORT / CONDUCTOR_PORT is set,
|
|
72
|
+
# so `bin/dev kill` in a worktree on ports 5000/5001/5002 targets the
|
|
73
|
+
# right ports instead of the 3000/3001 default. Falls back to
|
|
74
|
+
# [3000, 3001] when no base port is configured, plus the renderer port
|
|
75
|
+
# when Pro renderer support is active. Uses PortSelector's pure
|
|
76
|
+
# #base_port_hash so no "Base port detected" banner prints during a kill.
|
|
77
|
+
#
|
|
78
|
+
# In base-port mode we include base[:renderer] whenever the Pro gem is
|
|
79
|
+
# loaded, even if the current shell has no renderer env vars set. The
|
|
80
|
+
# user has explicitly claimed this port range, and `bin/dev kill` is
|
|
81
|
+
# usually invoked from a fresh shell where RENDERER_PORT / *_URL aren't
|
|
82
|
+
# carried over from the dev session — so requiring env-var presence
|
|
83
|
+
# would let a stale renderer survive. Pattern-based killing
|
|
84
|
+
# (development_processes / node.*react[-_]on[-_]rails) does NOT catch
|
|
85
|
+
# the Pro renderer because it runs as `node renderer/node-renderer.js`
|
|
86
|
+
# with no "react_on_rails" substring in the command line. Port-based
|
|
87
|
+
# killing is the only reliable path. The default-port branch keeps the
|
|
88
|
+
# tighter renderer_env_signal? guard via configured_renderer_port_for_kill
|
|
89
|
+
# because 3800 is a shared default that could belong to an unrelated process.
|
|
90
|
+
def killable_ports
|
|
91
|
+
base = PortSelector.base_port_hash
|
|
92
|
+
return default_killable_ports unless base
|
|
93
|
+
|
|
94
|
+
ports = [base[:rails], base[:webpack]]
|
|
95
|
+
if pro_renderer_active?
|
|
96
|
+
# When the Pro gem is loaded but no renderer env var is set, the
|
|
97
|
+
# user may not realize base+2 is being scanned. Surface it so an
|
|
98
|
+
# unrelated process killed on that port isn't a silent surprise.
|
|
99
|
+
unless renderer_env_signal?
|
|
100
|
+
puts " ℹ️ Including renderer port #{base[:renderer]} (base+2): " \
|
|
101
|
+
"react_on_rails_pro is loaded but no renderer env var is set."
|
|
102
|
+
end
|
|
103
|
+
ports << base[:renderer]
|
|
104
|
+
end
|
|
105
|
+
ports
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def default_killable_ports
|
|
109
|
+
ports = [3000, 3001]
|
|
110
|
+
if pro_renderer_active?
|
|
111
|
+
renderer_port = configured_renderer_port_for_kill
|
|
112
|
+
ports << renderer_port if renderer_port
|
|
113
|
+
end
|
|
114
|
+
ports
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def configured_renderer_port_for_kill
|
|
118
|
+
raw_port = ENV.fetch("RENDERER_PORT", nil)
|
|
119
|
+
return raw_port.strip.to_i if valid_port_string?(raw_port)
|
|
120
|
+
|
|
121
|
+
local_url_port = local_renderer_url_port_for_kill
|
|
122
|
+
return local_url_port if local_url_port
|
|
123
|
+
return nil if remote_renderer_url_configured?
|
|
124
|
+
|
|
125
|
+
# Only fall back to the default renderer port when the user has set
|
|
126
|
+
# at least one renderer env var. Without that signal (Pro gem loaded
|
|
127
|
+
# but no renderer ever started), `bin/dev kill` would otherwise
|
|
128
|
+
# target an unrelated process bound to 3800 in OSS+Pro-gem apps.
|
|
129
|
+
renderer_env_signal? ? 3800 : nil
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def local_renderer_url_port_for_kill
|
|
133
|
+
%w[REACT_RENDERER_URL RENDERER_URL].each do |var|
|
|
134
|
+
url = ENV.fetch(var, nil)
|
|
135
|
+
next if url.nil? || url.strip.empty?
|
|
136
|
+
|
|
137
|
+
parsed = URI.parse(url)
|
|
138
|
+
next unless localhost_hostname?(parsed.hostname)
|
|
139
|
+
next unless url.match?(URL_WITH_EXPLICIT_PORT_RE)
|
|
140
|
+
|
|
141
|
+
return parsed.port
|
|
142
|
+
rescue URI::InvalidURIError
|
|
143
|
+
next
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
nil
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def remote_renderer_url_configured?
|
|
150
|
+
%w[REACT_RENDERER_URL RENDERER_URL].any? do |var|
|
|
151
|
+
url = ENV.fetch(var, nil)
|
|
152
|
+
!url.nil? && !url.strip.empty? && !localhost_renderer_url?(url)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
63
156
|
def development_processes
|
|
64
157
|
{
|
|
65
158
|
"rails" => "Rails server",
|
|
@@ -132,7 +225,11 @@ module ReactOnRails
|
|
|
132
225
|
end
|
|
133
226
|
|
|
134
227
|
def cleanup_socket_files
|
|
135
|
-
|
|
228
|
+
# Mirrors FileManager#cleanup_overmind_sockets so renamed/copied
|
|
229
|
+
# variants like overmind-4100.sock are removed during `bin/dev kill`,
|
|
230
|
+
# not just at startup.
|
|
231
|
+
overmind_sockets = Dir.glob("tmp/sockets/overmind*.sock")
|
|
232
|
+
files = [".overmind.sock", *overmind_sockets, "tmp/pids/server.pid"].uniq
|
|
136
233
|
killed_any = false
|
|
137
234
|
|
|
138
235
|
files.each do |file|
|
|
@@ -558,6 +655,12 @@ module ReactOnRails
|
|
|
558
655
|
|
|
559
656
|
# rubocop:disable Metrics/AbcSize
|
|
560
657
|
def help_mode_details
|
|
658
|
+
# Reflect base-port mode so help text advertises the port `bin/dev`
|
|
659
|
+
# will actually use. Without this, `bin/dev help` in a worktree with
|
|
660
|
+
# REACT_ON_RAILS_BASE_PORT=4000 still claims 3000/3001.
|
|
661
|
+
dev_url = "http://localhost:#{help_display_port(:dev)}/<route>"
|
|
662
|
+
prod_url = "http://localhost:#{help_display_port(:prod)}/<route>"
|
|
663
|
+
|
|
561
664
|
<<~MODES
|
|
562
665
|
#{Rainbow('🔥 HMR Development mode (default)').cyan.bold} - #{Rainbow('Procfile.dev').green}:
|
|
563
666
|
#{Rainbow('•').yellow} #{Rainbow('Hot Module Replacement (HMR) enabled').white}
|
|
@@ -566,7 +669,7 @@ module ReactOnRails
|
|
|
566
669
|
#{Rainbow('•').yellow} #{Rainbow('Source maps for debugging').white}
|
|
567
670
|
#{Rainbow('•').yellow} #{Rainbow('May have Flash of Unstyled Content (FOUC)').white}
|
|
568
671
|
#{Rainbow('•').yellow} #{Rainbow('Fast recompilation').white}
|
|
569
|
-
#{Rainbow('•').yellow} #{Rainbow('Access at:').white} #{Rainbow(
|
|
672
|
+
#{Rainbow('•').yellow} #{Rainbow('Access at:').white} #{Rainbow(dev_url).cyan.underline}
|
|
570
673
|
|
|
571
674
|
#{Rainbow('📦 Static development mode').cyan.bold} - #{Rainbow('Procfile.dev-static-assets').green}:
|
|
572
675
|
#{Rainbow('•').yellow} #{Rainbow('No HMR (static assets with auto-recompilation)').white}
|
|
@@ -576,7 +679,7 @@ module ReactOnRails
|
|
|
576
679
|
#{Rainbow('•').yellow} #{Rainbow('Development environment (faster builds than production)').white}
|
|
577
680
|
#{Rainbow('•').yellow} #{Rainbow('Source maps for debugging').white}
|
|
578
681
|
#{Rainbow('•').yellow} #{Rainbow('Optional advanced testing: share output path with tests only in this mode').white}
|
|
579
|
-
#{Rainbow('•').yellow} #{Rainbow('Access at:').white} #{Rainbow(
|
|
682
|
+
#{Rainbow('•').yellow} #{Rainbow('Access at:').white} #{Rainbow(dev_url).cyan.underline}
|
|
580
683
|
|
|
581
684
|
#{Rainbow('🏭 Production-assets mode').cyan.bold} - #{Rainbow('Procfile.dev-prod-assets').green}:
|
|
582
685
|
#{Rainbow('•').yellow} #{Rainbow('React on Rails pack generation (via precompile hook or assets:precompile)').white}
|
|
@@ -586,21 +689,80 @@ module ReactOnRails
|
|
|
586
689
|
#{Rainbow('•').yellow} #{Rainbow('Server processes controlled by Procfile.dev-prod-assets environment').white}
|
|
587
690
|
#{Rainbow('•').yellow} #{Rainbow('Optimized, minified bundles with CSS extraction').white}
|
|
588
691
|
#{Rainbow('•').yellow} #{Rainbow('No HMR (static assets)').white}
|
|
589
|
-
#{Rainbow('•').yellow} #{Rainbow('Access at:').white} #{Rainbow(
|
|
692
|
+
#{Rainbow('•').yellow} #{Rainbow('Access at:').white} #{Rainbow(prod_url).cyan.underline}
|
|
590
693
|
MODES
|
|
591
694
|
end
|
|
592
695
|
# rubocop:enable Metrics/AbcSize
|
|
593
696
|
|
|
697
|
+
# Returns the Rails port to advertise in `bin/dev help`. In base-port
|
|
698
|
+
# mode every mode uses `base + 0` (apply_base_port_env sets PORT
|
|
699
|
+
# uniformly across HMR/static/prod-assets); otherwise prod-assets
|
|
700
|
+
# defaults to 3001 and HMR/static default to 3000. Uses base_port_hash
|
|
701
|
+
# so help-rendering is silent (no banner) and read-only.
|
|
702
|
+
def help_display_port(mode)
|
|
703
|
+
base = PortSelector.base_port_hash
|
|
704
|
+
return base[:rails] if base
|
|
705
|
+
|
|
706
|
+
mode == :prod ? 3001 : 3000
|
|
707
|
+
end
|
|
708
|
+
|
|
594
709
|
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
595
710
|
def run_production_like(_verbose: false, route: nil, rails_env: nil, skip_database_check: false,
|
|
596
711
|
open_browser: false, open_browser_once: false)
|
|
597
712
|
procfile = "Procfile.dev-prod-assets"
|
|
598
713
|
|
|
599
|
-
#
|
|
600
|
-
#
|
|
601
|
-
#
|
|
602
|
-
#
|
|
603
|
-
|
|
714
|
+
# Honor base-port mode (REACT_ON_RAILS_BASE_PORT / CONDUCTOR_PORT)
|
|
715
|
+
# before falling through to the prod-specific 3001 auto-scan, so
|
|
716
|
+
# parallel worktrees running `bin/dev prod` don't silently collide
|
|
717
|
+
# on port 3001. warn_if_legacy_renderer_url_env_used fires here too
|
|
718
|
+
# so the RENDERER_URL rename warning surfaces in prod mode.
|
|
719
|
+
#
|
|
720
|
+
# The `unless apply_base_port_if_active` branch mirrors
|
|
721
|
+
# #configure_ports (the canonical per-mode env-setup) but intentionally
|
|
722
|
+
# differs in two ways: (1) PORT auto-scan starts at 3001 (via
|
|
723
|
+
# procfile_port) rather than 3000, and (2) SHAKAPACKER_DEV_SERVER_PORT
|
|
724
|
+
# is omitted because production-like mode runs static assets, not
|
|
725
|
+
# webpack-dev-server. sync_renderer_port_and_url still runs so Pro
|
|
726
|
+
# users who set RENDERER_PORT get the same auto-derivation and
|
|
727
|
+
# mismatch warnings as `bin/dev` and `bin/dev static`.
|
|
728
|
+
warn_if_legacy_renderer_url_env_used
|
|
729
|
+
unless apply_base_port_if_active
|
|
730
|
+
# Set PORT before foreman starts — foreman injects its own PORT=5000
|
|
731
|
+
# into child processes when ENV["PORT"] is unset, overriding the
|
|
732
|
+
# ${PORT:-3001} fallback in the Procfile. Scan from 3001 (not 3000)
|
|
733
|
+
# so prod-assets doesn't collide with the normal dev server.
|
|
734
|
+
#
|
|
735
|
+
# Also normalize invalid/out-of-range values: ${PORT:-3001} only
|
|
736
|
+
# falls back on empty/unset, so `PORT=abc` or `PORT=99999` would
|
|
737
|
+
# otherwise flow straight through to `rails s -p …` and fail to
|
|
738
|
+
# start.
|
|
739
|
+
existing_port = ENV.fetch("PORT", nil)
|
|
740
|
+
if valid_port_string?(existing_port)
|
|
741
|
+
# Strip whitespace so a value like " 3001 " doesn't leak into ENV
|
|
742
|
+
# unstripped — mirrors overwrite_invalid_port_env's normalization
|
|
743
|
+
# used by configure_ports so all bin/dev modes leave ENV["PORT"]
|
|
744
|
+
# in the same shape for downstream consumers (Procfile expansion,
|
|
745
|
+
# exact ENV string comparisons).
|
|
746
|
+
stripped = existing_port.strip
|
|
747
|
+
ENV["PORT"] = stripped if stripped != existing_port
|
|
748
|
+
else
|
|
749
|
+
unless existing_port.nil? || existing_port.strip.empty?
|
|
750
|
+
warn "WARNING: PORT=#{existing_port.inspect} is not a valid port; using auto-selected port."
|
|
751
|
+
end
|
|
752
|
+
# Clear the bad value first so procfile_port falls back to its default
|
|
753
|
+
# (3001) instead of `"abc".to_i == 0`, which would scan from port 0.
|
|
754
|
+
ENV.delete("PORT")
|
|
755
|
+
# Match configure_ports' clean-exit behavior on exhaustion so
|
|
756
|
+
# `bin/dev prod` surfaces a one-line error instead of a backtrace.
|
|
757
|
+
begin
|
|
758
|
+
ENV["PORT"] = PortSelector.find_available_port(procfile_port(procfile)).to_s
|
|
759
|
+
rescue PortSelector::NoPortAvailable => e
|
|
760
|
+
warn e.message
|
|
761
|
+
exit 1
|
|
762
|
+
end
|
|
763
|
+
end
|
|
764
|
+
sync_renderer_port_and_url
|
|
765
|
+
end
|
|
604
766
|
|
|
605
767
|
features = [
|
|
606
768
|
"Precompiling assets with production optimizations",
|
|
@@ -835,15 +997,376 @@ module ReactOnRails
|
|
|
835
997
|
puts ""
|
|
836
998
|
end
|
|
837
999
|
|
|
1000
|
+
# NOTE: `run_production_like` does NOT use this method — it calls
|
|
1001
|
+
# `apply_base_port_if_active` directly because (1) its PORT auto-scan
|
|
1002
|
+
# starts at 3001, not 3000, and (2) in non-base-port mode it must not
|
|
1003
|
+
# set SHAKAPACKER_DEV_SERVER_PORT (no webpack-dev-server in prod-assets).
|
|
1004
|
+
# Base-port mode still sets SHAKAPACKER_DEV_SERVER_PORT (= base + 1) for
|
|
1005
|
+
# tooling consistency — see apply_base_port_env. Future `run_*` methods
|
|
1006
|
+
# should choose between the two entry points rather than adding a third
|
|
1007
|
+
# path.
|
|
838
1008
|
def configure_ports
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
1009
|
+
warn_if_legacy_renderer_url_env_used
|
|
1010
|
+
# Single call: select_ports! internally consults base_port_ports and
|
|
1011
|
+
# returns the same hash when base-port mode is active, so we branch
|
|
1012
|
+
# on :base_port_mode instead of calling base_port_ports twice.
|
|
1013
|
+
# Pass pro_renderer so OSS apps don't get a "port base+2 (renderer)
|
|
1014
|
+
# is already in use" warning for a port they don't actually use.
|
|
1015
|
+
selected = PortSelector.select_ports!(pro_renderer: pro_renderer_active?)
|
|
1016
|
+
if selected[:base_port_mode]
|
|
1017
|
+
apply_base_port_env(selected)
|
|
1018
|
+
else
|
|
1019
|
+
apply_explicit_port_env(selected)
|
|
1020
|
+
end
|
|
842
1021
|
rescue PortSelector::NoPortAvailable => e
|
|
843
1022
|
warn e.message
|
|
844
1023
|
exit 1
|
|
845
1024
|
end
|
|
846
1025
|
|
|
1026
|
+
# Returns true if REACT_ON_RAILS_BASE_PORT / CONDUCTOR_PORT is active
|
|
1027
|
+
# and the derived env vars have been applied; false otherwise (env
|
|
1028
|
+
# untouched). Shared across development, static, and production-like
|
|
1029
|
+
# modes so all bin/dev entry points honor the same base-port contract.
|
|
1030
|
+
# Does not emit the legacy-RENDERER_URL warning — callers (or
|
|
1031
|
+
# configure_ports) do that so it fires in every mode regardless of
|
|
1032
|
+
# whether base-port mode is active.
|
|
1033
|
+
def apply_base_port_if_active
|
|
1034
|
+
selected = PortSelector.base_port_ports(pro_renderer: pro_renderer_active?)
|
|
1035
|
+
return false unless selected
|
|
1036
|
+
|
|
1037
|
+
apply_base_port_env(selected)
|
|
1038
|
+
true
|
|
1039
|
+
end
|
|
1040
|
+
|
|
1041
|
+
# The env var used to configure the Pro node renderer URL was renamed
|
|
1042
|
+
# from `RENDERER_URL` to `REACT_RENDERER_URL`. Two mid-migration states
|
|
1043
|
+
# are worth flagging:
|
|
1044
|
+
#
|
|
1045
|
+
# 1. Only `RENDERER_URL` is set — Rails falls back to the default
|
|
1046
|
+
# `http://localhost:3800` silently (which doesn't exist in most
|
|
1047
|
+
# container setups).
|
|
1048
|
+
# 2. Both are set but disagree — the Pro initializer and any tooling
|
|
1049
|
+
# that reads one but not the other will silently disagree. The
|
|
1050
|
+
# gem does not read either env var directly; the user's Pro
|
|
1051
|
+
# initializer picks one (`config.renderer_url = ENV[...]`).
|
|
1052
|
+
def warn_if_legacy_renderer_url_env_used
|
|
1053
|
+
legacy = ENV.fetch("RENDERER_URL", nil)
|
|
1054
|
+
current = ENV.fetch("REACT_RENDERER_URL", nil)
|
|
1055
|
+
return if legacy.nil? || legacy.strip.empty?
|
|
1056
|
+
|
|
1057
|
+
if current.nil? || current.strip.empty?
|
|
1058
|
+
warn "WARNING: RENDERER_URL is set but REACT_RENDERER_URL is not. " \
|
|
1059
|
+
"RENDERER_URL was renamed to REACT_RENDERER_URL; update your " \
|
|
1060
|
+
"env var to avoid silent fallback to the default renderer URL. " \
|
|
1061
|
+
"Note: RENDERER_URL alone still activates the Pro renderer code " \
|
|
1062
|
+
"path. Separately, if REACT_ON_RAILS_BASE_PORT or CONDUCTOR_PORT " \
|
|
1063
|
+
"is set, base-port mode will derive RENDERER_PORT/REACT_RENDERER_URL " \
|
|
1064
|
+
"from the base (overriding RENDERER_URL)."
|
|
1065
|
+
return
|
|
1066
|
+
end
|
|
1067
|
+
|
|
1068
|
+
return if legacy.strip == current.strip
|
|
1069
|
+
|
|
1070
|
+
warn "WARNING: RENDERER_URL=#{legacy.inspect} and REACT_RENDERER_URL=#{current.inspect} " \
|
|
1071
|
+
"are both set but disagree. RENDERER_URL was renamed to REACT_RENDERER_URL; " \
|
|
1072
|
+
"unset RENDERER_URL or align the two values so tooling and the Pro initializer " \
|
|
1073
|
+
"can't silently pick different renderer URLs."
|
|
1074
|
+
end
|
|
1075
|
+
|
|
1076
|
+
# Base port is active. Priority: base port > explicit per-service env vars.
|
|
1077
|
+
# Assign unconditionally so the effective ports match the "Base port
|
|
1078
|
+
# detected..." log line even when PORT/RENDERER_PORT were pre-set.
|
|
1079
|
+
#
|
|
1080
|
+
# Base-port mode is specifically for local, all-in-one dev setups (one
|
|
1081
|
+
# machine running Rails + webpack + node renderer together — typically
|
|
1082
|
+
# worktrees or coding-agent sandboxes). The derived renderer URL is
|
|
1083
|
+
# therefore hard-coded to http://localhost:<port>. If you run the node
|
|
1084
|
+
# renderer on a separate host/container (e.g. Docker `renderer:3800`),
|
|
1085
|
+
# do not use base-port mode — set REACT_RENDERER_URL explicitly and
|
|
1086
|
+
# rely on the explicit-ports path instead. warn_if_renderer_url_will_be_overridden
|
|
1087
|
+
# below surfaces the override whenever a pre-set URL doesn't match.
|
|
1088
|
+
#
|
|
1089
|
+
# SHAKAPACKER_DEV_SERVER_PORT is set even in production-like mode (which
|
|
1090
|
+
# runs static assets, not webpack-dev-server) for tooling consistency:
|
|
1091
|
+
# a subsequent `bin/dev` in the same shell sees the base-port-derived
|
|
1092
|
+
# value rather than a stale explicit one, and developers inspecting
|
|
1093
|
+
# their env after `bin/dev prod` see the full derived block.
|
|
1094
|
+
#
|
|
1095
|
+
# RENDERER_PORT / REACT_RENDERER_URL are gated on `pro_renderer_active?`
|
|
1096
|
+
# so OSS environments without the Pro node renderer don't get two
|
|
1097
|
+
# extra env vars in every child process. Pro users (gem loaded or env
|
|
1098
|
+
# vars already set) still get the derived block.
|
|
1099
|
+
def apply_base_port_env(selected)
|
|
1100
|
+
warn_if_port_will_be_overridden("PORT", selected[:rails])
|
|
1101
|
+
warn_if_port_will_be_overridden("SHAKAPACKER_DEV_SERVER_PORT", selected[:webpack])
|
|
1102
|
+
ENV["PORT"] = selected[:rails].to_s
|
|
1103
|
+
ENV["SHAKAPACKER_DEV_SERVER_PORT"] = selected[:webpack].to_s
|
|
1104
|
+
return unless pro_renderer_active?
|
|
1105
|
+
|
|
1106
|
+
derived_url = "http://localhost:#{selected[:renderer]}"
|
|
1107
|
+
warn_if_renderer_url_will_be_overridden("REACT_RENDERER_URL", derived_url)
|
|
1108
|
+
warn_if_port_will_be_overridden("RENDERER_PORT", selected[:renderer])
|
|
1109
|
+
ENV["RENDERER_PORT"] = selected[:renderer].to_s
|
|
1110
|
+
ENV["REACT_RENDERER_URL"] = derived_url
|
|
1111
|
+
# Keep legacy RENDERER_URL in sync only if the user already set it.
|
|
1112
|
+
# Pro initializers mid-migration may still read ENV["RENDERER_URL"];
|
|
1113
|
+
# leaving it pointed at the old port while the renderer process moves
|
|
1114
|
+
# to base+2 would silently route SSR calls to the wrong endpoint.
|
|
1115
|
+
# Skip when unset so we don't introduce the legacy name into envs
|
|
1116
|
+
# that have already migrated to REACT_RENDERER_URL.
|
|
1117
|
+
legacy_url = ENV.fetch("RENDERER_URL", nil)
|
|
1118
|
+
return if legacy_url.nil? || legacy_url.strip.empty?
|
|
1119
|
+
|
|
1120
|
+
warn_if_renderer_url_will_be_overridden("RENDERER_URL", derived_url)
|
|
1121
|
+
ENV["RENDERER_URL"] = derived_url
|
|
1122
|
+
end
|
|
1123
|
+
|
|
1124
|
+
# Heuristic for "this app has a Pro node renderer to point at": either
|
|
1125
|
+
# the react_on_rails_pro gem is loaded, or the user has already set one
|
|
1126
|
+
# of the renderer env vars (so they're configuring a renderer manually
|
|
1127
|
+
# without the Pro gem). Keeps OSS environments clean while not
|
|
1128
|
+
# silently dropping renderer env for any caller who actually wants it.
|
|
1129
|
+
#
|
|
1130
|
+
# The legacy `RENDERER_URL` (renamed to `REACT_RENDERER_URL`) is
|
|
1131
|
+
# intentionally included so users mid-migration who still export
|
|
1132
|
+
# `RENDERER_URL` keep base-port renderer-derivation behavior. The
|
|
1133
|
+
# rename reminder lives in `warn_if_legacy_renderer_url_env_used`,
|
|
1134
|
+
# which calls out that the legacy var still triggers this path.
|
|
1135
|
+
def pro_renderer_active?
|
|
1136
|
+
return true if Gem.loaded_specs.key?("react_on_rails_pro")
|
|
1137
|
+
|
|
1138
|
+
renderer_env_signal?
|
|
1139
|
+
end
|
|
1140
|
+
|
|
1141
|
+
# Returns true when at least one renderer-pointing env var is set to a
|
|
1142
|
+
# non-blank value. Used both by `pro_renderer_active?` (to detect
|
|
1143
|
+
# Pro-renderer intent without the gem) and by
|
|
1144
|
+
# `configured_renderer_port_for_kill` (to avoid widening the kill
|
|
1145
|
+
# scope to 3800 when the Pro gem is loaded but no renderer was ever
|
|
1146
|
+
# configured). The legacy `RENDERER_URL` is included intentionally —
|
|
1147
|
+
# see `pro_renderer_active?` for the migration rationale.
|
|
1148
|
+
def renderer_env_signal?
|
|
1149
|
+
%w[RENDERER_PORT REACT_RENDERER_URL RENDERER_URL].any? do |var|
|
|
1150
|
+
value = ENV.fetch(var, nil)
|
|
1151
|
+
!value.nil? && !value.strip.empty?
|
|
1152
|
+
end
|
|
1153
|
+
end
|
|
1154
|
+
|
|
1155
|
+
# Mirrors warn_if_renderer_url_will_be_overridden so users notice when a
|
|
1156
|
+
# pre-existing PORT or SHAKAPACKER_DEV_SERVER_PORT is replaced by the
|
|
1157
|
+
# base-port-derived value.
|
|
1158
|
+
#
|
|
1159
|
+
# Asymmetry vs. `overwrite_invalid_port_env` is intentional: base-port
|
|
1160
|
+
# mode replaces the user's explicit value with a derived one (so we
|
|
1161
|
+
# warn on any non-matching value), while explicit mode honors valid
|
|
1162
|
+
# user input and only warns when it must rewrite an invalid value.
|
|
1163
|
+
def warn_if_port_will_be_overridden(var_name, derived_port)
|
|
1164
|
+
existing = ENV.fetch(var_name, nil)
|
|
1165
|
+
return if existing.nil? || existing.strip.empty? || existing.strip == derived_port.to_s
|
|
1166
|
+
|
|
1167
|
+
warn "WARNING: Overriding #{var_name}=#{existing.inspect} with #{derived_port} " \
|
|
1168
|
+
"because base port mode is active."
|
|
1169
|
+
end
|
|
1170
|
+
|
|
1171
|
+
# Base port mode overrides REACT_RENDERER_URL (and the legacy
|
|
1172
|
+
# RENDERER_URL) to point at a local renderer derived from the base
|
|
1173
|
+
# port. Warn whenever an explicitly-set URL doesn't exactly match the
|
|
1174
|
+
# derived one so users notice — including the "localhost with a
|
|
1175
|
+
# different port" case, which is a real misconfiguration (Rails would
|
|
1176
|
+
# target one port, the renderer another).
|
|
1177
|
+
def warn_if_renderer_url_will_be_overridden(var_name, derived_url)
|
|
1178
|
+
existing = ENV.fetch(var_name, nil)
|
|
1179
|
+
return if existing.nil? || existing.strip.empty? || existing.strip == derived_url
|
|
1180
|
+
|
|
1181
|
+
warn "WARNING: Overriding #{var_name}=#{existing.inspect} with #{derived_url} " \
|
|
1182
|
+
"because base port mode is active."
|
|
1183
|
+
end
|
|
1184
|
+
|
|
1185
|
+
def apply_explicit_port_env(selected)
|
|
1186
|
+
# Overwrite whenever the current value is blank OR not a usable port
|
|
1187
|
+
# string. PortSelector.read_and_sanitize_port_env! has already
|
|
1188
|
+
# cleared invalid PORT / SHAKAPACKER_DEV_SERVER_PORT values upstream
|
|
1189
|
+
# (and emitted the "not a valid integer" warning there), so when
|
|
1190
|
+
# overwrite_invalid_port_env sees nil it silently writes the derived
|
|
1191
|
+
# port — the user-facing warning isn't missed, it fired at the
|
|
1192
|
+
# source. The Procfile's `${PORT:-3000}` fallback must not see a
|
|
1193
|
+
# stale `PORT=99999` or `PORT=abc` that would reach `rails s -p …`.
|
|
1194
|
+
overwrite_invalid_port_env("PORT", selected[:rails])
|
|
1195
|
+
overwrite_invalid_port_env("SHAKAPACKER_DEV_SERVER_PORT", selected[:webpack])
|
|
1196
|
+
sync_renderer_port_and_url
|
|
1197
|
+
end
|
|
1198
|
+
|
|
1199
|
+
# Replace an invalid env value with the derived port, surfacing the
|
|
1200
|
+
# override so a user who set (e.g.) PORT=abc can see why it was ignored.
|
|
1201
|
+
# Silent when the env var is unset or already valid — explicit mode
|
|
1202
|
+
# honors a valid user-supplied port and only rewrites bad input.
|
|
1203
|
+
# Inverse of `warn_if_port_will_be_overridden`, which always rewrites
|
|
1204
|
+
# under base-port mode and warns on any non-matching value.
|
|
1205
|
+
def overwrite_invalid_port_env(var_name, derived_port)
|
|
1206
|
+
existing = ENV.fetch(var_name, nil)
|
|
1207
|
+
if valid_port_string?(existing)
|
|
1208
|
+
# Strip and write back so a whitespace-padded `" 3000 "` does not
|
|
1209
|
+
# leak into the Procfile's `${PORT:-3000}` expansion (which would
|
|
1210
|
+
# forward the spaces verbatim to `rails s -p`). Matches the
|
|
1211
|
+
# normalization already done for RENDERER_PORT in
|
|
1212
|
+
# sync_renderer_port_and_url.
|
|
1213
|
+
stripped = existing.strip
|
|
1214
|
+
ENV[var_name] = stripped if stripped != existing
|
|
1215
|
+
return
|
|
1216
|
+
end
|
|
1217
|
+
|
|
1218
|
+
unless existing.nil? || existing.strip.empty?
|
|
1219
|
+
warn "WARNING: #{var_name}=#{existing.inspect} is not a valid port; " \
|
|
1220
|
+
"using #{derived_port}."
|
|
1221
|
+
end
|
|
1222
|
+
ENV[var_name] = derived_port.to_s
|
|
1223
|
+
end
|
|
1224
|
+
|
|
1225
|
+
def valid_port_string?(value)
|
|
1226
|
+
PortSelector.valid_port_string?(value)
|
|
1227
|
+
end
|
|
1228
|
+
|
|
1229
|
+
def sync_renderer_port_and_url
|
|
1230
|
+
raw_port = ENV.fetch("RENDERER_PORT", nil)
|
|
1231
|
+
url = ENV.fetch("REACT_RENDERER_URL", nil)
|
|
1232
|
+
if raw_port.nil? || raw_port.strip.empty?
|
|
1233
|
+
warn_url_without_port(url)
|
|
1234
|
+
return
|
|
1235
|
+
end
|
|
1236
|
+
|
|
1237
|
+
# Reuse the canonical port-string predicate so whitespace handling and
|
|
1238
|
+
# range checks match PortSelector exactly (`" 3800 "` is accepted
|
|
1239
|
+
# there; the inline regex here previously rejected it).
|
|
1240
|
+
unless valid_port_string?(raw_port)
|
|
1241
|
+
warn "WARNING: RENDERER_PORT=#{raw_port.inspect} is not a valid port (1..65535); ignoring."
|
|
1242
|
+
# Delete so the Procfile's `${RENDERER_PORT:-3800}` fallback applies
|
|
1243
|
+
# instead of passing the bad value through to the node renderer.
|
|
1244
|
+
ENV.delete("RENDERER_PORT")
|
|
1245
|
+
clear_local_renderer_url_after_invalid_port(url)
|
|
1246
|
+
return
|
|
1247
|
+
end
|
|
1248
|
+
|
|
1249
|
+
# Normalize for downstream URL construction and mismatch checks so
|
|
1250
|
+
# `RENDERER_PORT=" 3800 "` doesn't leak whitespace into the derived
|
|
1251
|
+
# URL or the warning body. Also write the stripped value back to ENV
|
|
1252
|
+
# so the Procfile's `${RENDERER_PORT:-3800}` expansion propagates the
|
|
1253
|
+
# clean value to the node renderer subprocess instead of forwarding
|
|
1254
|
+
# the whitespace-padded original.
|
|
1255
|
+
port = raw_port.strip
|
|
1256
|
+
ENV["RENDERER_PORT"] = port if port != raw_port
|
|
1257
|
+
|
|
1258
|
+
if url.nil? || url.strip.empty?
|
|
1259
|
+
# Only RENDERER_PORT set: derive the URL so Rails reaches the right port.
|
|
1260
|
+
# Use warn (stderr) so `bin/dev 2>/dev/null` silences this env-mutation
|
|
1261
|
+
# log together with every other "I changed your env" warning in this
|
|
1262
|
+
# file — stdout would leak through the same silencing attempt.
|
|
1263
|
+
derived = "http://localhost:#{port}"
|
|
1264
|
+
warn "WARNING: RENDERER_PORT=#{port} set without REACT_RENDERER_URL; " \
|
|
1265
|
+
"deriving REACT_RENDERER_URL=#{derived}."
|
|
1266
|
+
ENV["REACT_RENDERER_URL"] = derived
|
|
1267
|
+
elsif url_port_mismatch?(url, port)
|
|
1268
|
+
# Both set but inconsistent — SSR will silently break otherwise.
|
|
1269
|
+
warn "WARNING: RENDERER_PORT=#{port} does not match REACT_RENDERER_URL=#{url}; " \
|
|
1270
|
+
"Rails will use REACT_RENDERER_URL to reach the renderer. " \
|
|
1271
|
+
"Unset one of them or ensure they agree."
|
|
1272
|
+
end
|
|
1273
|
+
end
|
|
1274
|
+
|
|
1275
|
+
# URL without a port is a silent misconfig: the node renderer process
|
|
1276
|
+
# binds to the Procfile default (3800) while Rails targets whatever
|
|
1277
|
+
# port is in the URL. Warn so the mismatch is visible, but only for
|
|
1278
|
+
# local URLs where this process actually controls the renderer.
|
|
1279
|
+
#
|
|
1280
|
+
# Remote portless URLs (e.g. `REACT_RENDERER_URL=http://renderer.internal`)
|
|
1281
|
+
# are intentionally excluded: this process doesn't launch remote
|
|
1282
|
+
# renderers, so scheme-default ports (80/443) may be the correct target
|
|
1283
|
+
# behind a reverse proxy. Remote-side port mismatches are a deployment
|
|
1284
|
+
# concern, not something bin/dev can diagnose safely.
|
|
1285
|
+
def warn_url_without_port(url)
|
|
1286
|
+
return if url.nil? || url.strip.empty? || !localhost_renderer_url?(url)
|
|
1287
|
+
|
|
1288
|
+
warn "WARNING: REACT_RENDERER_URL=#{url} is set without RENDERER_PORT. " \
|
|
1289
|
+
"The node renderer process may bind to a different port than Rails " \
|
|
1290
|
+
"expects. Set RENDERER_PORT to match the URL port."
|
|
1291
|
+
end
|
|
1292
|
+
|
|
1293
|
+
# When a local renderer URL is paired with an invalid RENDERER_PORT,
|
|
1294
|
+
# keeping the URL would leave Rails targeting the stale port while the
|
|
1295
|
+
# Procfile falls back to 3800. Clear the URL so the initializer's
|
|
1296
|
+
# default localhost URL matches the renderer's fallback port.
|
|
1297
|
+
#
|
|
1298
|
+
# We clear unconditionally even when the user's URL port happens to
|
|
1299
|
+
# match the current Procfile default (e.g. http://localhost:3800):
|
|
1300
|
+
# - The user expressed deliberate intent via RENDERER_PORT, which we
|
|
1301
|
+
# could not honor. Falling through to the Procfile-default-driven
|
|
1302
|
+
# URL keeps Rails and the renderer in sync regardless of which
|
|
1303
|
+
# port the Procfile chooses now or later.
|
|
1304
|
+
# - Any future change to the Procfile default would otherwise re-
|
|
1305
|
+
# introduce a Rails/renderer mismatch silently for those users.
|
|
1306
|
+
def clear_local_renderer_url_after_invalid_port(url)
|
|
1307
|
+
return if url.nil? || url.strip.empty? || !localhost_renderer_url?(url)
|
|
1308
|
+
|
|
1309
|
+
warn "WARNING: Clearing REACT_RENDERER_URL=#{url} because invalid " \
|
|
1310
|
+
"RENDERER_PORT was ignored; falling back to the default " \
|
|
1311
|
+
"localhost renderer port."
|
|
1312
|
+
ENV.delete("REACT_RENDERER_URL")
|
|
1313
|
+
end
|
|
1314
|
+
|
|
1315
|
+
# Matches a URL with an explicit `:port` after the authority. Used by
|
|
1316
|
+
# `#url_port_mismatch?` to distinguish "URL has a port that disagrees"
|
|
1317
|
+
# from "URL has no port at all" (treated as a mismatch separately).
|
|
1318
|
+
#
|
|
1319
|
+
# Anatomy:
|
|
1320
|
+
# - `(?:[^@/]*@)?` — optional userinfo prefix (`user:pass@`) so a URL
|
|
1321
|
+
# like `http://user:3800@localhost` does not match the password as
|
|
1322
|
+
# a host port via backtracking.
|
|
1323
|
+
# - `(?:\[[^\]]+\]|[^@/:]+)` — host alternatives:
|
|
1324
|
+
# * `\[[^\]]+\]` for bracketed IPv6 literals (`http://[::1]:3800`)
|
|
1325
|
+
# * `[^@/:]+` for a regular hostname/IPv4 whose charset excludes
|
|
1326
|
+
# `/` and `:` so the `:\d+` port anchor lands on the authority
|
|
1327
|
+
# separator without backtracking into the host.
|
|
1328
|
+
URL_WITH_EXPLICIT_PORT_RE = %r{://(?:[^@/]*@)?(?:\[[^\]]+\]|[^@/:]+):\d+(?=[/?#]|$)}
|
|
1329
|
+
private_constant :URL_WITH_EXPLICIT_PORT_RE
|
|
1330
|
+
|
|
1331
|
+
# Uses URI.parse so a short port isn't matched as a substring of a
|
|
1332
|
+
# longer one (e.g. ":80" inside ":3800"). Malformed URLs fall back to
|
|
1333
|
+
# "no mismatch detected" rather than crashing; the warn-path is only
|
|
1334
|
+
# advisory.
|
|
1335
|
+
#
|
|
1336
|
+
# Treats a URL without an explicit port as a mismatch: URI.parse would
|
|
1337
|
+
# otherwise return the scheme default (80 for http, 443 for https),
|
|
1338
|
+
# which would silently match `RENDERER_PORT=80` / `=443` — a misconfig
|
|
1339
|
+
# worth flagging rather than hiding.
|
|
1340
|
+
def url_port_mismatch?(url, port)
|
|
1341
|
+
return true unless url.match?(URL_WITH_EXPLICIT_PORT_RE)
|
|
1342
|
+
|
|
1343
|
+
URI.parse(url).port != port.to_i
|
|
1344
|
+
rescue URI::InvalidURIError
|
|
1345
|
+
false
|
|
1346
|
+
end
|
|
1347
|
+
|
|
1348
|
+
def localhost_renderer_url?(url)
|
|
1349
|
+
localhost_hostname?(URI.parse(url).hostname)
|
|
1350
|
+
rescue URI::InvalidURIError
|
|
1351
|
+
false
|
|
1352
|
+
end
|
|
1353
|
+
|
|
1354
|
+
# Use `.hostname` not `.host`: for IPv6 URLs like `http://[::1]:3800`,
|
|
1355
|
+
# `.host` returns `"[::1]"` (with brackets) while `.hostname` returns
|
|
1356
|
+
# `"::1"` (bracket-stripped), matching the comparison list below.
|
|
1357
|
+
# Downcase: URI preserves host case, so `http://LOCALHOST:3900` would
|
|
1358
|
+
# otherwise be treated as non-local and skip the invalid-port URL
|
|
1359
|
+
# remediation path, leaving Rails targeting a stale port.
|
|
1360
|
+
def localhost_hostname?(hostname)
|
|
1361
|
+
%w[localhost 127.0.0.1 ::1].include?(hostname&.downcase)
|
|
1362
|
+
end
|
|
1363
|
+
|
|
1364
|
+
# Callers are expected to have normalized ENV["PORT"] beforehand:
|
|
1365
|
+
# run_production_like clears non-integer / out-of-range values before
|
|
1366
|
+
# calling here, and the development/static paths route through
|
|
1367
|
+
# PortSelector.read_and_sanitize_port_env! which does the same. That
|
|
1368
|
+
# makes the `.to_i` below safe — a stray "abc" would otherwise become
|
|
1369
|
+
# 0 and scan from port 0.
|
|
847
1370
|
def procfile_port(procfile)
|
|
848
1371
|
if procfile == "Procfile.dev-prod-assets"
|
|
849
1372
|
ENV.fetch("PORT", 3001).to_i
|
|
@@ -861,9 +1384,11 @@ module ReactOnRails
|
|
|
861
1384
|
def schedule_browser_open_if_requested(procfile, route:, open_browser:, open_browser_once:)
|
|
862
1385
|
return unless open_browser || open_browser_once
|
|
863
1386
|
|
|
864
|
-
# --open-browser
|
|
865
|
-
#
|
|
866
|
-
|
|
1387
|
+
# --open-browser is an explicit user request and bypasses TTY gating.
|
|
1388
|
+
# --open-browser-once is an auto-open and respects TTY/CI guards.
|
|
1389
|
+
explicit = open_browser && !open_browser_once
|
|
1390
|
+
schedule_browser_open(procfile_port(procfile), route: route, once: open_browser_once,
|
|
1391
|
+
explicit: explicit)
|
|
867
1392
|
end
|
|
868
1393
|
|
|
869
1394
|
def build_local_url(port, route)
|
|
@@ -884,30 +1409,38 @@ module ReactOnRails
|
|
|
884
1409
|
"/#{stripped}"
|
|
885
1410
|
end
|
|
886
1411
|
|
|
887
|
-
def schedule_browser_open(port, route:, once:)
|
|
888
|
-
return unless browser_auto_open_allowed?
|
|
1412
|
+
def schedule_browser_open(port, route:, once:, explicit: false)
|
|
1413
|
+
return unless browser_auto_open_allowed?(explicit: explicit)
|
|
889
1414
|
|
|
890
1415
|
url = build_local_url(port, route)
|
|
891
1416
|
request_path = build_request_path(route)
|
|
892
1417
|
Thread.new do
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
1418
|
+
if wait_for_app_route(port, request_path)
|
|
1419
|
+
marker_state = prepare_browser_open_once_marker(once)
|
|
1420
|
+
if marker_state != :already_opened && !open_browser(url)
|
|
1421
|
+
clear_browser_open_once_marker_if_claimed(marker_state)
|
|
1422
|
+
hint = if wsl?
|
|
1423
|
+
" On WSL, install wslu (for wslview), wsl-open, or xdg-open."
|
|
1424
|
+
elsif RbConfig::CONFIG["host_os"].include?("linux")
|
|
1425
|
+
" Install xdg-utils (provides xdg-open) to enable automatic browser opening."
|
|
1426
|
+
else
|
|
1427
|
+
""
|
|
1428
|
+
end
|
|
1429
|
+
warn("[react_on_rails] Could not open browser automatically.#{hint} Visit #{url} manually.")
|
|
1430
|
+
end
|
|
904
1431
|
end
|
|
905
1432
|
rescue StandardError => e
|
|
906
1433
|
warn("[react_on_rails] Browser auto-open failed: #{e.message}")
|
|
907
1434
|
end
|
|
908
1435
|
end
|
|
909
1436
|
|
|
910
|
-
|
|
1437
|
+
# Explicit user requests (--open-browser) bypass TTY/CI gating because the
|
|
1438
|
+
# developer deliberately asked for a browser open. Auto-opens
|
|
1439
|
+
# (--open-browser-once) respect the guards to avoid surprises in CI or
|
|
1440
|
+
# non-interactive shells.
|
|
1441
|
+
def browser_auto_open_allowed?(explicit: false)
|
|
1442
|
+
return true if explicit
|
|
1443
|
+
|
|
911
1444
|
!ENV.key?("CI") && $stdin.tty? && $stdout.tty?
|
|
912
1445
|
end
|
|
913
1446
|
|
|
@@ -930,13 +1463,24 @@ module ReactOnRails
|
|
|
930
1463
|
response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPRedirection)
|
|
931
1464
|
end
|
|
932
1465
|
|
|
1466
|
+
# Connection-level exceptions expected while the server is still booting.
|
|
1467
|
+
# Includes EOFError (premature close), Errno::EPIPE (dropped mid-request),
|
|
1468
|
+
# and Errno::EAFNOSUPPORT (IPv6 disabled) which are transient during startup.
|
|
1469
|
+
LOCALHOST_CONNECT_ERRORS = [
|
|
1470
|
+
Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::EHOSTUNREACH,
|
|
1471
|
+
Errno::ENETUNREACH, Errno::ETIMEDOUT, Errno::EADDRNOTAVAIL,
|
|
1472
|
+
Errno::EAFNOSUPPORT, Errno::EPIPE, EOFError,
|
|
1473
|
+
SocketError, Net::OpenTimeout, Net::ReadTimeout
|
|
1474
|
+
].freeze
|
|
1475
|
+
private_constant :LOCALHOST_CONNECT_ERRORS
|
|
1476
|
+
|
|
933
1477
|
def http_get_localhost(port, request_path)
|
|
934
1478
|
LOCALHOST_ADDRESSES.each do |host|
|
|
935
1479
|
response = Net::HTTP.start(host, port, open_timeout: 1, read_timeout: 1) do |http|
|
|
936
1480
|
http.get(request_path)
|
|
937
1481
|
end
|
|
938
1482
|
return response if response
|
|
939
|
-
rescue
|
|
1483
|
+
rescue *LOCALHOST_CONNECT_ERRORS
|
|
940
1484
|
next
|
|
941
1485
|
end
|
|
942
1486
|
nil
|
|
@@ -955,9 +1499,7 @@ module ReactOnRails
|
|
|
955
1499
|
host_os = RbConfig::CONFIG["host_os"]
|
|
956
1500
|
return ["open"] if host_os.include?("darwin")
|
|
957
1501
|
|
|
958
|
-
if %w[linux bsd].any? { |platform| host_os.include?(platform) }
|
|
959
|
-
return ["xdg-open"]
|
|
960
|
-
end
|
|
1502
|
+
return linux_browser_command if %w[linux bsd].any? { |platform| host_os.include?(platform) }
|
|
961
1503
|
|
|
962
1504
|
# "start" requires a window title before the URL; the empty string is the
|
|
963
1505
|
# conventional placeholder so Windows opens the browser instead of treating
|
|
@@ -967,6 +1509,26 @@ module ReactOnRails
|
|
|
967
1509
|
nil
|
|
968
1510
|
end
|
|
969
1511
|
|
|
1512
|
+
# WSL reports a Linux host_os but typically lacks xdg-open.
|
|
1513
|
+
# Try WSL-specific launchers first, then fall back to xdg-open.
|
|
1514
|
+
def linux_browser_command
|
|
1515
|
+
if wsl?
|
|
1516
|
+
return ["wslview"] if command_available?("wslview")
|
|
1517
|
+
return ["wsl-open"] if command_available?("wsl-open")
|
|
1518
|
+
end
|
|
1519
|
+
|
|
1520
|
+
return ["xdg-open"] if command_available?("xdg-open")
|
|
1521
|
+
|
|
1522
|
+
nil
|
|
1523
|
+
end
|
|
1524
|
+
|
|
1525
|
+
def wsl?
|
|
1526
|
+
# WSL_DISTRO_NAME is the authoritative indicator (set by the WSL kernel).
|
|
1527
|
+
# WSLENV is a weaker signal — it can appear in non-WSL contexts (e.g. Docker
|
|
1528
|
+
# images that inherit a Windows host env) but is included as a fallback.
|
|
1529
|
+
ENV.key?("WSL_DISTRO_NAME") || ENV.key?("WSLENV")
|
|
1530
|
+
end
|
|
1531
|
+
|
|
970
1532
|
def command_available?(command)
|
|
971
1533
|
ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).any? do |directory|
|
|
972
1534
|
executable = File.join(directory, command)
|
|
@@ -974,11 +1536,17 @@ module ReactOnRails
|
|
|
974
1536
|
end
|
|
975
1537
|
end
|
|
976
1538
|
|
|
1539
|
+
# Resolved lazily at call time so the path is correct even when this file
|
|
1540
|
+
# is required before the process has chdir'd into the Rails app root.
|
|
1541
|
+
def open_browser_once_marker
|
|
1542
|
+
File.join(Dir.pwd, "tmp", "react_on_rails", "browser_opened_once")
|
|
1543
|
+
end
|
|
1544
|
+
|
|
977
1545
|
def prepare_browser_open_once_marker(once)
|
|
978
1546
|
return :not_requested unless once
|
|
979
1547
|
|
|
980
|
-
FileUtils.mkdir_p(File.dirname(
|
|
981
|
-
File.open(
|
|
1548
|
+
FileUtils.mkdir_p(File.dirname(open_browser_once_marker))
|
|
1549
|
+
File.open(open_browser_once_marker, File::WRONLY | File::CREAT | File::EXCL) do |marker|
|
|
982
1550
|
marker.write("#{Time.now.utc.iso8601}\n")
|
|
983
1551
|
end
|
|
984
1552
|
:claimed
|
|
@@ -992,7 +1560,7 @@ module ReactOnRails
|
|
|
992
1560
|
def clear_browser_open_once_marker_if_claimed(marker_state)
|
|
993
1561
|
return unless marker_state == :claimed
|
|
994
1562
|
|
|
995
|
-
File.delete(
|
|
1563
|
+
File.delete(open_browser_once_marker)
|
|
996
1564
|
rescue Errno::ENOENT
|
|
997
1565
|
nil
|
|
998
1566
|
rescue StandardError => e
|