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
|
@@ -1,8 +1,27 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "open3"
|
|
4
|
+
require "socket"
|
|
5
|
+
|
|
3
6
|
module ReactOnRails
|
|
4
7
|
module Dev
|
|
5
8
|
class FileManager
|
|
9
|
+
# Bounded probe so a stuck server with a full accept queue (rare for a
|
|
10
|
+
# local overmind socket but theoretically possible) cannot stall
|
|
11
|
+
# bin/dev startup indefinitely.
|
|
12
|
+
#
|
|
13
|
+
# Two paths consume this budget differently:
|
|
14
|
+
# - synchronous failure (typical): UNIX socket connect() to a dead
|
|
15
|
+
# listener fails in microseconds, so 150 ms is far more than needed.
|
|
16
|
+
# - async wait_writable: if connect() returns IO::WaitWritable, the
|
|
17
|
+
# full 150 ms can be consumed waiting for the kernel to mark the
|
|
18
|
+
# socket writable. This is the actual budget for slow-loopback or
|
|
19
|
+
# paused-PID cases — not "microseconds".
|
|
20
|
+
# Result: 150 ms is conservative for the common path and the cap for
|
|
21
|
+
# the worst case.
|
|
22
|
+
SOCKET_PROBE_TIMEOUT_SECS = 0.15
|
|
23
|
+
private_constant :SOCKET_PROBE_TIMEOUT_SECS
|
|
24
|
+
|
|
6
25
|
class << self
|
|
7
26
|
def cleanup_stale_files
|
|
8
27
|
socket_cleanup = cleanup_overmind_sockets
|
|
@@ -13,13 +32,18 @@ module ReactOnRails
|
|
|
13
32
|
|
|
14
33
|
private
|
|
15
34
|
|
|
35
|
+
# Targets overmind-named sockets only (`.overmind.sock` at the project root and
|
|
36
|
+
# `tmp/sockets/overmind*.sock` for copied/renamed variants). Inactive matches are
|
|
37
|
+
# removed on startup. Other apps' Unix sockets in `tmp/sockets/` (Puma, Action
|
|
38
|
+
# Cable, custom services) are left untouched even when stale, so a tight
|
|
39
|
+
# startup race window cannot delete a socket bin/dev does not own.
|
|
16
40
|
def cleanup_overmind_sockets
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
socket_files = [".overmind.sock", "tmp/sockets/overmind.sock"]
|
|
41
|
+
socket_files = [".overmind.sock", *Dir.glob("tmp/sockets/overmind*.sock")].uniq
|
|
20
42
|
cleaned_any = false
|
|
21
43
|
|
|
22
44
|
socket_files.each do |socket_file|
|
|
45
|
+
next if socket_active?(socket_file)
|
|
46
|
+
|
|
23
47
|
cleaned_any = true if remove_file_if_exists(socket_file, "stale socket")
|
|
24
48
|
end
|
|
25
49
|
|
|
@@ -43,13 +67,15 @@ module ReactOnRails
|
|
|
43
67
|
return true
|
|
44
68
|
end
|
|
45
69
|
|
|
46
|
-
|
|
70
|
+
unless process_running?(pid)
|
|
71
|
+
remove_file_if_exists(server_pid_file, "stale Rails pid file")
|
|
72
|
+
return true
|
|
73
|
+
end
|
|
47
74
|
|
|
48
|
-
|
|
49
|
-
|
|
75
|
+
pid_working_directory = working_directory_for_pid(pid)
|
|
76
|
+
return false if pid_working_directory.nil? || same_working_directory?(pid_working_directory, Dir.pwd)
|
|
50
77
|
|
|
51
|
-
|
|
52
|
-
!`pgrep -f "overmind" 2>/dev/null`.split("\n").empty?
|
|
78
|
+
remove_file_if_exists(server_pid_file, "stale Rails pid file from another app directory")
|
|
53
79
|
end
|
|
54
80
|
|
|
55
81
|
def process_running?(pid)
|
|
@@ -63,6 +89,107 @@ module ReactOnRails
|
|
|
63
89
|
true
|
|
64
90
|
end
|
|
65
91
|
|
|
92
|
+
def socket_active?(socket_path)
|
|
93
|
+
return false unless File.exist?(socket_path)
|
|
94
|
+
|
|
95
|
+
# Pre-pack the sockaddr in a narrow rescue so the only ArgumentError
|
|
96
|
+
# we swallow is the one Ruby raises for "too long unix socket path"
|
|
97
|
+
# (sun_path is capped at ~104/108 bytes). The wider connect block
|
|
98
|
+
# below intentionally does NOT catch ArgumentError, so a programming
|
|
99
|
+
# mistake in Socket.new or connect_nonblock surfaces instead of
|
|
100
|
+
# silently returning false.
|
|
101
|
+
begin
|
|
102
|
+
sockaddr = Socket.sockaddr_un(socket_path)
|
|
103
|
+
rescue ArgumentError
|
|
104
|
+
return false
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
socket = nil
|
|
108
|
+
begin
|
|
109
|
+
socket = Socket.new(Socket::AF_UNIX, Socket::SOCK_STREAM, 0)
|
|
110
|
+
socket.connect_nonblock(sockaddr)
|
|
111
|
+
true
|
|
112
|
+
rescue IO::WaitWritable
|
|
113
|
+
# connect is in progress — wait up to SOCKET_PROBE_TIMEOUT_SECS for
|
|
114
|
+
# writability, then check SO_ERROR to distinguish accepted vs.
|
|
115
|
+
# refused/timed-out connections. Uses socket.wait_writable rather
|
|
116
|
+
# than IO.select so a Fiber scheduler can interleave properly.
|
|
117
|
+
ready = socket.wait_writable(SOCKET_PROBE_TIMEOUT_SECS)
|
|
118
|
+
return false unless ready
|
|
119
|
+
|
|
120
|
+
socket.getsockopt(Socket::SOL_SOCKET, Socket::SO_ERROR).int.zero?
|
|
121
|
+
rescue Errno::EISCONN
|
|
122
|
+
true
|
|
123
|
+
rescue SystemCallError, IOError
|
|
124
|
+
false
|
|
125
|
+
ensure
|
|
126
|
+
socket&.close
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Two-stage lookup: try the zero-dependency `/proc/PID/cwd` readlink first
|
|
131
|
+
# (Linux), then fall back to `lsof` (macOS, BSD, Linux without /proc visibility).
|
|
132
|
+
# The /proc path matters for minimal Alpine/CI containers where `lsof`
|
|
133
|
+
# is not installed by default — without it, this method would silently
|
|
134
|
+
# return nil on every container and leave stale PID files behind.
|
|
135
|
+
#
|
|
136
|
+
# The lsof call uses Open3.capture2 with a word-list argv (matching
|
|
137
|
+
# ServerManager#find_port_pids) so the no-shell-injection invariant is
|
|
138
|
+
# structural rather than caller-enforced.
|
|
139
|
+
#
|
|
140
|
+
# Returns nil when both probes fail:
|
|
141
|
+
# - /proc not present (macOS, BSD) and `lsof` absent (Errno::ENOENT) — minimal containers,
|
|
142
|
+
# - the kernel withholds info (permission denied, process exited mid-probe),
|
|
143
|
+
# - any other unexpected error.
|
|
144
|
+
# The nil return causes `cleanup_rails_pid_file` to keep the PID file as a safe
|
|
145
|
+
# fallback. Set DEBUG=1 to surface the failure on stderr.
|
|
146
|
+
def working_directory_for_pid(pid)
|
|
147
|
+
proc_cwd = working_directory_via_proc(pid)
|
|
148
|
+
return proc_cwd if proc_cwd
|
|
149
|
+
|
|
150
|
+
working_directory_via_lsof(pid)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def working_directory_via_proc(pid)
|
|
154
|
+
# File.readlink either returns a non-empty String or raises.
|
|
155
|
+
File.readlink("/proc/#{pid}/cwd")
|
|
156
|
+
rescue Errno::ENOENT, Errno::EACCES, Errno::EPERM, NotImplementedError
|
|
157
|
+
# /proc not present (macOS, BSD), no permission to read this PID's cwd,
|
|
158
|
+
# or readlink unsupported on this platform. Fall through to lsof.
|
|
159
|
+
nil
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def working_directory_via_lsof(pid)
|
|
163
|
+
stdout, = Open3.capture2("lsof", "-a", "-p", pid.to_s, "-d", "cwd", "-Fn", err: File::NULL)
|
|
164
|
+
path_line = stdout.lines.find { |line| line.start_with?("n") }
|
|
165
|
+
path = path_line&.delete_prefix("n")&.strip
|
|
166
|
+
return nil if path.nil? || path.empty?
|
|
167
|
+
|
|
168
|
+
path
|
|
169
|
+
rescue Errno::ENOENT => e
|
|
170
|
+
# `lsof` binary missing — expected on minimal images. DEBUG-gated so
|
|
171
|
+
# users on Linux/macOS without the package don't see noise.
|
|
172
|
+
log_lsof_missing(e) if ENV["DEBUG"]
|
|
173
|
+
nil
|
|
174
|
+
rescue StandardError => e
|
|
175
|
+
# Genuinely unexpected: resource limits (Errno::EMFILE), interrupted
|
|
176
|
+
# syscall (Errno::EINTR), permission errors, etc. Always-warn so the
|
|
177
|
+
# cross-directory PID check can't silently misfire under load.
|
|
178
|
+
warn "bin/dev: could not determine working directory for PID #{pid} " \
|
|
179
|
+
"(#{e.class}: #{e.message})."
|
|
180
|
+
nil
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def log_lsof_missing(error)
|
|
184
|
+
warn "bin/dev: `lsof` not found; cross-directory PID check skipped (#{error.message})."
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def same_working_directory?(left, right)
|
|
188
|
+
File.realpath(left) == File.realpath(right)
|
|
189
|
+
rescue SystemCallError
|
|
190
|
+
File.expand_path(left) == File.expand_path(right)
|
|
191
|
+
end
|
|
192
|
+
|
|
66
193
|
def remove_file_if_exists(file_path, description)
|
|
67
194
|
return false unless File.exist?(file_path)
|
|
68
195
|
|
|
@@ -9,13 +9,66 @@ module ReactOnRails
|
|
|
9
9
|
DEFAULT_WEBPACK_PORT = 3035
|
|
10
10
|
MAX_ATTEMPTS = 100
|
|
11
11
|
|
|
12
|
+
# Offsets from the base port when REACT_ON_RAILS_BASE_PORT (or a recognized
|
|
13
|
+
# tool-specific equivalent like CONDUCTOR_PORT) is set. The base port block
|
|
14
|
+
# is typically 10 consecutive ports allocated per workspace.
|
|
15
|
+
BASE_PORT_RAILS_OFFSET = 0
|
|
16
|
+
BASE_PORT_WEBPACK_OFFSET = 1
|
|
17
|
+
BASE_PORT_RENDERER_OFFSET = 2
|
|
18
|
+
TCP_PORT_MAX = 65_535
|
|
19
|
+
MAX_BASE_PORT = TCP_PORT_MAX - BASE_PORT_RENDERER_OFFSET
|
|
20
|
+
|
|
21
|
+
# Ports 1..1023 are privileged on Linux/macOS and require root to bind.
|
|
22
|
+
PRIVILEGED_PORT_MAX = 1023
|
|
23
|
+
|
|
24
|
+
# Env vars checked (in order) for a base port value.
|
|
25
|
+
#
|
|
26
|
+
# CONDUCTOR_PORT is an empirical interpretation based on Conductor.build
|
|
27
|
+
# (https://conductor.build) allocating a block of consecutive ports per
|
|
28
|
+
# workspace and exposing the block base via this env var. This contract
|
|
29
|
+
# is not in a public Conductor API, so treat CONDUCTOR_PORT support as
|
|
30
|
+
# best-effort until Conductor documents it. If a future release changes
|
|
31
|
+
# the meaning (e.g. CONDUCTOR_PORT becomes the Rails port itself rather
|
|
32
|
+
# than a block base), the derived offsets below will land on the wrong
|
|
33
|
+
# ports — users would see port-conflict failures at runtime rather than
|
|
34
|
+
# a clear misconfiguration error. A future "validate derived ports are
|
|
35
|
+
# reachable on startup" path could surface this earlier.
|
|
36
|
+
#
|
|
37
|
+
# Escape hatch: REACT_ON_RAILS_BASE_PORT takes precedence, so users can
|
|
38
|
+
# override the CONDUCTOR_PORT interpretation without code changes.
|
|
39
|
+
BASE_PORT_ENV_VARS = %w[REACT_ON_RAILS_BASE_PORT CONDUCTOR_PORT].freeze
|
|
40
|
+
|
|
12
41
|
class NoPortAvailable < StandardError; end
|
|
13
42
|
|
|
14
43
|
class << self
|
|
15
|
-
# Returns { rails: Integer, webpack: Integer
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
|
|
44
|
+
# Returns { rails: Integer, webpack: Integer, renderer: Integer|nil,
|
|
45
|
+
# base_port_mode: Boolean }.
|
|
46
|
+
#
|
|
47
|
+
# Priority:
|
|
48
|
+
# 1. Base port (REACT_ON_RAILS_BASE_PORT or CONDUCTOR_PORT) — all ports
|
|
49
|
+
# derived deterministically from the base; no probing.
|
|
50
|
+
# 2. Explicit per-service env vars (PORT, SHAKAPACKER_DEV_SERVER_PORT).
|
|
51
|
+
# 3. Auto-detect free ports starting from defaults.
|
|
52
|
+
#
|
|
53
|
+
# The :renderer key is populated only when a base port is set (it is a
|
|
54
|
+
# Pro-only service and does not participate in auto-detection).
|
|
55
|
+
# :base_port_mode is true only in case 1.
|
|
56
|
+
#
|
|
57
|
+
# NOTE: This method mutates ENV.
|
|
58
|
+
# @side_effect Deletes invalid PORT / SHAKAPACKER_DEV_SERVER_PORT
|
|
59
|
+
# values via `read_and_sanitize_port_env!` so ServerManager's
|
|
60
|
+
# apply_explicit_port_env path doesn't re-warn on the same bad
|
|
61
|
+
# value. Intended for `bin/dev` startup; do not call from
|
|
62
|
+
# read-only contexts that expect ENV to survive the call. See
|
|
63
|
+
# `read_and_sanitize_port_env!` (which uses the `!` suffix to make
|
|
64
|
+
# the mutation explicit at the inner call site).
|
|
65
|
+
# @param pro_renderer [Boolean] when false, suppresses the renderer
|
|
66
|
+
# port-in-use warning so OSS apps without a node renderer don't
|
|
67
|
+
# see "port X (renderer)" noise on a coincidentally-bound base+2.
|
|
68
|
+
def select_ports!(pro_renderer: true)
|
|
69
|
+
base = base_port_ports(pro_renderer: pro_renderer)
|
|
70
|
+
return base if base
|
|
71
|
+
|
|
19
72
|
rails_port = explicit_rails_port
|
|
20
73
|
webpack_port = explicit_webpack_port
|
|
21
74
|
|
|
@@ -30,7 +83,16 @@ module ReactOnRails
|
|
|
30
83
|
puts "Default ports in use. Using Rails :#{rails_port}, webpack :#{webpack_port}"
|
|
31
84
|
end
|
|
32
85
|
|
|
33
|
-
{ rails: rails_port, webpack: webpack_port }
|
|
86
|
+
{ rails: rails_port, webpack: webpack_port, renderer: nil, base_port_mode: false }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Deprecated alias for the pre-bang name. Kept as a safety net for any
|
|
90
|
+
# external caller (generator extension, host-app rake task) that wired
|
|
91
|
+
# to `select_ports` before the rename. The bang form is preferred — it
|
|
92
|
+
# surfaces the ENV-mutation side effect at the call site, which was
|
|
93
|
+
# the whole point of the rename. Remove in a future major release.
|
|
94
|
+
def select_ports(**kwargs)
|
|
95
|
+
select_ports!(**kwargs)
|
|
34
96
|
end
|
|
35
97
|
|
|
36
98
|
# Public so it can be stubbed in tests.
|
|
@@ -52,6 +114,48 @@ module ReactOnRails
|
|
|
52
114
|
end
|
|
53
115
|
end
|
|
54
116
|
|
|
117
|
+
# Returns the base-port-derived port hash when a base port env var is
|
|
118
|
+
# set (with the same shape as #select_ports!), otherwise nil. Does not
|
|
119
|
+
# fall back to per-service env vars or auto-detect, so callers can
|
|
120
|
+
# branch on "is base-port mode active?" without triggering probing.
|
|
121
|
+
# Used by ServerManager so all bin/dev modes (development, static,
|
|
122
|
+
# production-like) honor the base-port contract consistently.
|
|
123
|
+
#
|
|
124
|
+
# Logs the detected base port and warns on derived-port collisions.
|
|
125
|
+
# Callers that need the derived ports without user-facing output
|
|
126
|
+
# (e.g. ServerManager#kill_processes, which shouldn't print a banner
|
|
127
|
+
# while killing) should use #base_port_hash instead.
|
|
128
|
+
def base_port_ports(pro_renderer: true)
|
|
129
|
+
bp, source = base_port_with_source
|
|
130
|
+
return nil unless bp
|
|
131
|
+
|
|
132
|
+
ports = derive_ports_from_base(bp)
|
|
133
|
+
source_note = if source == "CONDUCTOR_PORT"
|
|
134
|
+
" (unofficial contract; set REACT_ON_RAILS_BASE_PORT to override)"
|
|
135
|
+
else
|
|
136
|
+
""
|
|
137
|
+
end
|
|
138
|
+
renderer_segment = pro_renderer ? ", renderer :#{ports[:renderer]}" : ""
|
|
139
|
+
puts "Base port #{bp} detected via #{source}#{source_note}. Using Rails :#{ports[:rails]}, " \
|
|
140
|
+
"webpack :#{ports[:webpack]}#{renderer_segment}"
|
|
141
|
+
warn_if_derived_ports_in_use(bp, ports, source: source, pro_renderer: pro_renderer)
|
|
142
|
+
ports
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Pure derivation: returns the same port hash as #base_port_ports but
|
|
146
|
+
# without the "Base port X detected" log line or the derived-port
|
|
147
|
+
# collision warnings. Safe to call from any context where logging is
|
|
148
|
+
# undesirable (e.g. kill flows). Still delegates to
|
|
149
|
+
# #base_port_with_source, which surfaces invalid-value warnings — those
|
|
150
|
+
# describe the env input, not the port output, and are desirable even
|
|
151
|
+
# in silent callers.
|
|
152
|
+
def base_port_hash
|
|
153
|
+
bp, _source = base_port_with_source
|
|
154
|
+
return nil unless bp
|
|
155
|
+
|
|
156
|
+
derive_ports_from_base(bp)
|
|
157
|
+
end
|
|
158
|
+
|
|
55
159
|
def find_available_port(start_port, exclude: nil)
|
|
56
160
|
MAX_ATTEMPTS.times do |i|
|
|
57
161
|
port = start_port + i
|
|
@@ -63,14 +167,162 @@ module ReactOnRails
|
|
|
63
167
|
raise NoPortAvailable, "No available port found starting at #{start_port}."
|
|
64
168
|
end
|
|
65
169
|
|
|
170
|
+
# Strict port-string predicate shared with ServerManager so the two
|
|
171
|
+
# layers can't silently diverge. `String#to_i` would otherwise truncate
|
|
172
|
+
# `"3000abc"` to 3000 and slip it through here while ServerManager's
|
|
173
|
+
# overwrite path rejected it.
|
|
174
|
+
def valid_port_string?(value)
|
|
175
|
+
return false if value.nil?
|
|
176
|
+
|
|
177
|
+
stripped = value.to_s.strip
|
|
178
|
+
return false if stripped.empty?
|
|
179
|
+
return false unless stripped.match?(/\A\d+\z/)
|
|
180
|
+
|
|
181
|
+
stripped.to_i.between?(1, TCP_PORT_MAX)
|
|
182
|
+
end
|
|
183
|
+
|
|
66
184
|
private
|
|
67
185
|
|
|
186
|
+
def derive_ports_from_base(base)
|
|
187
|
+
{
|
|
188
|
+
rails: base + BASE_PORT_RAILS_OFFSET,
|
|
189
|
+
webpack: base + BASE_PORT_WEBPACK_OFFSET,
|
|
190
|
+
renderer: base + BASE_PORT_RENDERER_OFFSET,
|
|
191
|
+
base_port_mode: true
|
|
192
|
+
}
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Advisory: surface early conflicts when a base port's derived ports are
|
|
196
|
+
# already bound (e.g. two worktrees share a base). Does not fail — the
|
|
197
|
+
# actual bind at server start gives the definitive error.
|
|
198
|
+
#
|
|
199
|
+
# Skips the renderer port when `pro_renderer` is false: OSS apps don't
|
|
200
|
+
# run a node renderer, so "port base+2 (renderer) is already in use"
|
|
201
|
+
# would be confusing noise on a coincidental collision with an
|
|
202
|
+
# unrelated local service.
|
|
203
|
+
#
|
|
204
|
+
# When the base came from CONDUCTOR_PORT and the *Rails* port (base+0)
|
|
205
|
+
# is taken, append a hint that Conductor's contract is unofficial —
|
|
206
|
+
# this is the most likely failure mode if Conductor ever changes
|
|
207
|
+
# CONDUCTOR_PORT to mean "the Rails port" rather than "a block base"
|
|
208
|
+
# (the derived ports would silently land on whatever the user already
|
|
209
|
+
# has bound).
|
|
210
|
+
def warn_if_derived_ports_in_use(base, ports, source: nil, pro_renderer: true)
|
|
211
|
+
roles = pro_renderer ? %i[rails webpack renderer] : %i[rails webpack]
|
|
212
|
+
roles.each do |role|
|
|
213
|
+
port_num = ports[role]
|
|
214
|
+
next if port_available?(port_num)
|
|
215
|
+
|
|
216
|
+
hint = if role == :rails && source == "CONDUCTOR_PORT"
|
|
217
|
+
" If your Conductor workspace exposes CONDUCTOR_PORT as the Rails port " \
|
|
218
|
+
"rather than a block base, set REACT_ON_RAILS_BASE_PORT explicitly to override."
|
|
219
|
+
else
|
|
220
|
+
""
|
|
221
|
+
end
|
|
222
|
+
warn "WARNING: port #{port_num} (#{role}, derived from base #{base}) is already in use.#{hint}"
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Returns [val, source_var] when a valid base port env var is set,
|
|
227
|
+
# otherwise nil. The source var is included so callers can surface it
|
|
228
|
+
# in user-facing log lines (helpful when CONDUCTOR_PORT vs.
|
|
229
|
+
# REACT_ON_RAILS_BASE_PORT activated base-port mode).
|
|
230
|
+
def base_port_with_source
|
|
231
|
+
# Upper bound accounts for the largest derived offset so base + N stays
|
|
232
|
+
# within the valid TCP port range (1..65_535).
|
|
233
|
+
#
|
|
234
|
+
# Strip before validating so whitespace-padded values (common with
|
|
235
|
+
# copy-paste or env-file templating) parse the same way PORT and
|
|
236
|
+
# SHAKAPACKER_DEV_SERVER_PORT do via read_and_sanitize_port_env!.
|
|
237
|
+
BASE_PORT_ENV_VARS.each_with_index do |var, idx|
|
|
238
|
+
raw = ENV.fetch(var, nil)
|
|
239
|
+
next if raw.nil?
|
|
240
|
+
|
|
241
|
+
stripped = raw.strip
|
|
242
|
+
next if stripped.empty?
|
|
243
|
+
|
|
244
|
+
unless stripped.match?(/\A\d+\z/)
|
|
245
|
+
warn invalid_base_port_warning(var, raw, "not a valid integer", idx)
|
|
246
|
+
next
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
val = stripped.to_i
|
|
250
|
+
unless val.between?(1, MAX_BASE_PORT)
|
|
251
|
+
reason = "out of range (1..#{MAX_BASE_PORT}; must leave room for " \
|
|
252
|
+
"+#{BASE_PORT_RENDERER_OFFSET} renderer offset)"
|
|
253
|
+
warn invalid_base_port_warning(var, raw, reason, idx)
|
|
254
|
+
next
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
if val <= PRIVILEGED_PORT_MAX
|
|
258
|
+
warn "WARNING: #{var}=#{raw.inspect} is in the privileged range " \
|
|
259
|
+
"(1..#{PRIVILEGED_PORT_MAX}); binding will fail without root."
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
return [val, var]
|
|
263
|
+
end
|
|
264
|
+
nil
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# Invalid REACT_ON_RAILS_BASE_PORT silently falls through to CONDUCTOR_PORT
|
|
268
|
+
# (or any future later entry). Surface the fallthrough in the warning so
|
|
269
|
+
# users who set a non-integer to "disable" base port mode realize they
|
|
270
|
+
# also need to unset the next var.
|
|
271
|
+
#
|
|
272
|
+
# Filter `remaining` against the same validity rules as `base_port_ports`
|
|
273
|
+
# (numeric + 1..MAX_BASE_PORT) so we never promise activation from a
|
|
274
|
+
# var that the validator will also reject — e.g.
|
|
275
|
+
# `REACT_ON_RAILS_BASE_PORT="disabled"` + `CONDUCTOR_PORT="abc"` must
|
|
276
|
+
# not say "will still activate from CONDUCTOR_PORT".
|
|
277
|
+
def invalid_base_port_warning(var, raw, reason, idx)
|
|
278
|
+
msg = "WARNING: #{var}=#{raw.inspect} is #{reason}; ignoring."
|
|
279
|
+
remaining = BASE_PORT_ENV_VARS[(idx + 1)..].select do |v|
|
|
280
|
+
val = ENV.fetch(v, "").strip
|
|
281
|
+
val.match?(/\A\d+\z/) && val.to_i.between?(1, MAX_BASE_PORT)
|
|
282
|
+
end
|
|
283
|
+
return msg if remaining.empty?
|
|
284
|
+
|
|
285
|
+
msg + " Base port mode will still activate from #{remaining.join(', ')}; " \
|
|
286
|
+
"unset to disable entirely."
|
|
287
|
+
end
|
|
288
|
+
|
|
68
289
|
def explicit_rails_port
|
|
69
|
-
|
|
290
|
+
read_and_sanitize_port_env!("PORT")
|
|
70
291
|
end
|
|
71
292
|
|
|
72
293
|
def explicit_webpack_port
|
|
73
|
-
|
|
294
|
+
read_and_sanitize_port_env!("SHAKAPACKER_DEV_SERVER_PORT")
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# Reject values that aren't valid port strings and clear the env var
|
|
298
|
+
# so ServerManager's apply_explicit_port_env path (which also rejects
|
|
299
|
+
# them) doesn't emit a second warning for the same value.
|
|
300
|
+
#
|
|
301
|
+
# The `!` suffix signals the ENV-mutation side effect at the call site
|
|
302
|
+
# (explicit_rails_port / explicit_webpack_port); the "warn once + fall
|
|
303
|
+
# back" flow is shared with ServerManager via the cleared env, not via
|
|
304
|
+
# the return value. Kept in one place so the coupling is obvious.
|
|
305
|
+
def read_and_sanitize_port_env!(var_name)
|
|
306
|
+
raw = ENV.fetch(var_name, nil)
|
|
307
|
+
return nil if raw.nil?
|
|
308
|
+
|
|
309
|
+
stripped = raw.strip
|
|
310
|
+
return nil if stripped.empty?
|
|
311
|
+
|
|
312
|
+
unless stripped.match?(/\A\d+\z/)
|
|
313
|
+
warn "WARNING: #{var_name}=#{raw.inspect} is not a valid integer; ignoring."
|
|
314
|
+
ENV.delete(var_name)
|
|
315
|
+
return nil
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
n = stripped.to_i
|
|
319
|
+
unless n.between?(1, TCP_PORT_MAX)
|
|
320
|
+
warn "WARNING: #{var_name}=#{raw.inspect} is out of range (1..#{TCP_PORT_MAX}); ignoring."
|
|
321
|
+
ENV.delete(var_name)
|
|
322
|
+
return nil
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
n
|
|
74
326
|
end
|
|
75
327
|
end
|
|
76
328
|
end
|
|
@@ -14,7 +14,29 @@ module ReactOnRails
|
|
|
14
14
|
# before entering the block and pass them explicitly to system().
|
|
15
15
|
# This follows the same pattern used by Rails' bundle_command (railties),
|
|
16
16
|
# Spring's process spawning, and this codebase's own PackGenerator.
|
|
17
|
-
|
|
17
|
+
#
|
|
18
|
+
# REACT_ON_RAILS_BASE_PORT and CONDUCTOR_PORT are intentionally excluded:
|
|
19
|
+
# by the time sub-processes spawn, configure_ports has already derived
|
|
20
|
+
# concrete values into PORT / SHAKAPACKER_DEV_SERVER_PORT / RENDERER_PORT /
|
|
21
|
+
# REACT_RENDERER_URL. Sub-processes should use those fixed ports rather
|
|
22
|
+
# than re-deriving from the base.
|
|
23
|
+
# SHAKAPACKER_SKIP_PRECOMPILE_HOOK is also runtime-only and must survive
|
|
24
|
+
# Bundler's env reset so nested shakapacker commands don't rerun the hook.
|
|
25
|
+
# RENDERER_URL is the legacy name for REACT_RENDERER_URL; preserved for
|
|
26
|
+
# mid-migration users (see ServerManager#warn_if_legacy_renderer_url_env_used).
|
|
27
|
+
# Inclusion here also matters when base-port mode scrubs the legacy var:
|
|
28
|
+
# `preserve_runtime_env_vars` returns `nil` (not the string "nil") for unset
|
|
29
|
+
# keys, which `Process.spawn`/`system` use to explicitly unset the variable
|
|
30
|
+
# in the child — preventing `with_unbundled_env` from resurrecting a stale
|
|
31
|
+
# pre-Bundler value. See the comment on `preserve_runtime_env_vars` below.
|
|
32
|
+
ENV_KEYS_TO_PRESERVE = %w[
|
|
33
|
+
PORT
|
|
34
|
+
SHAKAPACKER_DEV_SERVER_PORT
|
|
35
|
+
RENDERER_PORT
|
|
36
|
+
REACT_RENDERER_URL
|
|
37
|
+
RENDERER_URL
|
|
38
|
+
SHAKAPACKER_SKIP_PRECOMPILE_HOOK
|
|
39
|
+
].freeze
|
|
18
40
|
|
|
19
41
|
class << self
|
|
20
42
|
# Check if a process is available and usable in the current execution context
|
|
@@ -180,9 +202,14 @@ module ReactOnRails
|
|
|
180
202
|
MSG
|
|
181
203
|
end
|
|
182
204
|
|
|
205
|
+
# Always include every key (nil when unset) so Process.spawn/system
|
|
206
|
+
# explicitly unsets it in the child. A non-nil-only hash would let
|
|
207
|
+
# `with_unbundled_env` restore a pre-Bundler value the parent had
|
|
208
|
+
# just deleted — e.g. an invalid `RENDERER_PORT=abc` that
|
|
209
|
+
# PortSelector scrubbed would resurrect in the renderer child.
|
|
183
210
|
def preserve_runtime_env_vars
|
|
184
211
|
ENV_KEYS_TO_PRESERVE.each_with_object({}) do |key, hash|
|
|
185
|
-
hash[key] = ENV
|
|
212
|
+
hash[key] = ENV.fetch(key, nil)
|
|
186
213
|
end
|
|
187
214
|
end
|
|
188
215
|
|