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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -0
  3. data/Gemfile.development_dependencies +2 -2
  4. data/Gemfile.lock +2 -14
  5. data/Rakefile +0 -6
  6. data/Steepfile +4 -0
  7. data/lib/generators/react_on_rails/base_generator.rb +4 -4
  8. data/lib/generators/react_on_rails/demo_page_config.rb +3 -3
  9. data/lib/generators/react_on_rails/dev_tests_generator.rb +1 -1
  10. data/lib/generators/react_on_rails/generator_helper.rb +6 -65
  11. data/lib/generators/react_on_rails/generator_messages/ci_section.rb +42 -0
  12. data/lib/generators/react_on_rails/generator_messages/package_manager_detection.rb +194 -0
  13. data/lib/generators/react_on_rails/generator_messages/shakapacker_status_section.rb +61 -0
  14. data/lib/generators/react_on_rails/generator_messages.rb +22 -79
  15. data/lib/generators/react_on_rails/install_generator.rb +243 -28
  16. data/lib/generators/react_on_rails/js_dependency_manager.rb +7 -4
  17. data/lib/generators/react_on_rails/pro/USAGE +1 -1
  18. data/lib/generators/react_on_rails/pro_generator.rb +206 -183
  19. data/lib/generators/react_on_rails/pro_setup.rb +102 -26
  20. data/lib/generators/react_on_rails/react_with_redux_generator.rb +3 -2
  21. data/lib/generators/react_on_rails/templates/base/base/.env.example +25 -0
  22. data/lib/generators/react_on_rails/templates/base/base/.github/workflows/ci.yml.tt +86 -0
  23. data/lib/generators/react_on_rails/templates/base/base/Procfile.dev +4 -3
  24. data/lib/generators/react_on_rails/templates/base/base/babel.config.js.tt +1 -1
  25. data/lib/generators/react_on_rails/templates/base/base/bin/switch-bundler +2 -2
  26. data/lib/generators/react_on_rails/templates/base/base/config/webpack/ServerClientOrBoth.js.tt +1 -1
  27. data/lib/generators/react_on_rails/templates/base/base/config/webpack/clientWebpackConfig.js.tt +1 -1
  28. data/lib/generators/react_on_rails/templates/base/base/config/webpack/commonWebpackConfig.js.tt +2 -2
  29. data/lib/generators/react_on_rails/templates/base/base/config/webpack/development.js.tt +1 -1
  30. data/lib/generators/react_on_rails/templates/base/base/config/webpack/production.js.tt +1 -1
  31. data/lib/generators/react_on_rails/templates/base/base/config/webpack/serverWebpackConfig.js.tt +6 -5
  32. data/lib/generators/react_on_rails/templates/base/base/config/webpack/test.js.tt +1 -1
  33. data/lib/generators/react_on_rails/templates/pro/base/config/initializers/react_on_rails_pro.rb.tt +1 -1
  34. data/lib/generators/react_on_rails/templates/pro/base/{client → renderer}/node-renderer.js +1 -0
  35. data/lib/react_on_rails/config_path_resolver.rb +101 -4
  36. data/lib/react_on_rails/configuration.rb +22 -0
  37. data/lib/react_on_rails/dev/file_manager.rb +135 -8
  38. data/lib/react_on_rails/dev/port_selector.rb +259 -7
  39. data/lib/react_on_rails/dev/process_manager.rb +29 -2
  40. data/lib/react_on_rails/dev/server_manager.rb +607 -39
  41. data/lib/react_on_rails/doctor.rb +513 -45
  42. data/lib/react_on_rails/helper.rb +3 -11
  43. data/lib/react_on_rails/js_code_builder.rb +66 -0
  44. data/lib/react_on_rails/length_prefixed_parser.rb +142 -0
  45. data/lib/react_on_rails/packs_generator.rb +65 -12
  46. data/lib/react_on_rails/pro_migration.rb +175 -0
  47. data/lib/react_on_rails/render_request.rb +74 -0
  48. data/lib/react_on_rails/rendering_strategy/exec_js_strategy.rb +29 -0
  49. data/lib/react_on_rails/rendering_strategy.rb +44 -0
  50. data/lib/react_on_rails/server_rendering_pool/ruby_embedded_java_script.rb +33 -22
  51. data/lib/react_on_rails/system_checker.rb +44 -23
  52. data/lib/react_on_rails/utils.rb +5 -0
  53. data/lib/react_on_rails/version.rb +1 -1
  54. data/lib/react_on_rails.rb +3 -0
  55. data/rakelib/run_rspec.rake +0 -5
  56. data/rakelib/shakapacker_examples.rake +66 -23
  57. data/react_on_rails.gemspec +18 -8
  58. data/sig/react_on_rails/js_code_builder.rbs +11 -0
  59. data/sig/react_on_rails/render_request.rbs +28 -0
  60. data/sig/react_on_rails/rendering_strategy/exec_js_strategy.rbs +11 -0
  61. data/sig/react_on_rails/rendering_strategy.rbs +7 -0
  62. data/sig/react_on_rails.rbs +6 -0
  63. 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
- killed_any = kill_running_processes || kill_port_processes([3000, 3001]) || cleanup_socket_files
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
- files = [".overmind.sock", "tmp/sockets/overmind.sock", "tmp/pids/server.pid"]
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('http://localhost:3000/<route>').cyan.underline}
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('http://localhost:3000/<route>').cyan.underline}
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('http://localhost:3001/<route>').cyan.underline}
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
- # Set PORT before foreman starts — foreman injects its own PORT=5000
600
- # into child processes when ENV["PORT"] is unset, overriding the
601
- # ${PORT:-3001} fallback in the Procfile. Scan from 3001 (not 3000)
602
- # so prod-assets doesn't collide with the normal dev server.
603
- ENV["PORT"] ||= PortSelector.find_available_port(procfile_port(procfile)).to_s
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
- selected = PortSelector.select_ports
840
- ENV["PORT"] ||= selected[:rails].to_s
841
- ENV["SHAKAPACKER_DEV_SERVER_PORT"] ||= selected[:webpack].to_s
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 and --open-browser-once share scheduling, but only the latter writes
865
- # the marker so explicit --open-browser continues to open on each invocation.
866
- schedule_browser_open(procfile_port(procfile), route: route, once: open_browser_once)
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
- Thread.current.report_on_exception = false if Thread.current.respond_to?(:report_on_exception=)
894
- next unless wait_for_app_route(port, request_path)
895
-
896
- marker_state = prepare_browser_open_once_marker(once)
897
- next if marker_state == :already_opened
898
-
899
- if open_browser(url)
900
- nil
901
- else
902
- clear_browser_open_once_marker_if_claimed(marker_state)
903
- warn("[react_on_rails] Could not open browser automatically. Visit #{url} manually.")
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
- def browser_auto_open_allowed?
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 StandardError
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) } && command_available?("xdg-open")
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(OPEN_BROWSER_ONCE_MARKER))
981
- File.open(OPEN_BROWSER_ONCE_MARKER, File::WRONLY | File::CREAT | File::EXCL) do |marker|
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(OPEN_BROWSER_ONCE_MARKER)
1563
+ File.delete(open_browser_once_marker)
996
1564
  rescue Errno::ENOENT
997
1565
  nil
998
1566
  rescue StandardError => e