diff --git a/docs/building-features/process-managers.md b/docs/building-features/process-managers.md index 5c61afe4e7..fe50a084a0 100644 --- a/docs/building-features/process-managers.md +++ b/docs/building-features/process-managers.md @@ -320,6 +320,23 @@ overmind start -f Procfile.dev foreman start -f Procfile.dev ``` +> [!WARNING] +> When running Foreman directly (not via `bin/dev`), Foreman injects its own `PORT` +> environment variable (starting at 5000) into every subprocess. This causes +> `${PORT:-3000}` in `Procfile.dev` to evaluate to Foreman's injected value rather +> than the fallback 3000. +> +> To avoid this, set `PORT` explicitly in your shell or `.env` file before running +> Foreman: +> +> ```sh +> PORT=3000 foreman start -f Procfile.dev +> # or add PORT=3000 to your .env file +> ``` +> +> `bin/dev` handles this automatically — port detection runs before Foreman starts, +> so `PORT` is always set correctly when Foreman launches. + ## Customizing Your Setup Edit `Procfile.dev` in your project root to customize which processes run and their configuration. @@ -327,11 +344,33 @@ Edit `Procfile.dev` in your project root to customize which processes run and th The default `Procfile.dev` includes: ```procfile -rails: bundle exec rails s -p 3000 -wp-client: bin/shakapacker-dev-server -wp-server: SERVER_BUNDLE_ONLY=true bin/shakapacker --watch +rails: bundle exec rails s -p ${PORT:-3000} +dev-server: bin/shakapacker-dev-server +server-bundle: SERVER_BUNDLE_ONLY=yes bin/shakapacker --watch +``` + +## Running Multiple Worktrees Simultaneously + +If you use [git worktrees](https://git-scm.com/docs/git-worktree) to work on multiple branches +in parallel, `bin/dev` automatically detects and avoids port conflicts — no configuration needed. + +When the default ports (3000 for Rails, 3035 for webpack-dev-server) are occupied, `bin/dev` +scans for the next free pair and prints: + +```text +Default ports in use. Using Rails :3001, webpack :3036 ``` +**To override ports manually**, create a `.env` file in the worktree (gitignored by default). +A `.env.example` is generated by `rails g react_on_rails:install` as a reference: + +```sh +PORT=3001 +SHAKAPACKER_DEV_SERVER_PORT=3036 +``` + +When `PORT` or `SHAKAPACKER_DEV_SERVER_PORT` are set, auto-detection is skipped entirely. + ## See Also - [HMR and Hot Reloading](./hmr-and-hot-reloading-with-the-webpack-dev-server.md) diff --git a/react_on_rails/lib/generators/react_on_rails/base_generator.rb b/react_on_rails/lib/generators/react_on_rails/base_generator.rb index ae034a7dfc..3343571b07 100644 --- a/react_on_rails/lib/generators/react_on_rails/base_generator.rb +++ b/react_on_rails/lib/generators/react_on_rails/base_generator.rb @@ -59,6 +59,7 @@ def copy_base_files Procfile.dev-static-assets Procfile.dev-prod-assets .dev-services.yml.example + .env.example bin/shakapacker-precompile-hook] # HelloServer uses the hello_world layout so React on Rails can inject generated @@ -145,11 +146,12 @@ def update_gitignore_for_auto_registration additions = [] additions << "**/generated/**" unless gitignore_content.include?("**/generated/**") additions << "ssr-generated" unless gitignore_content.include?("ssr-generated") + additions << ".env" unless gitignore_content.match?(/^\.env$/) return if additions.empty? append_to_file ".gitignore" do - lines = ["\n# Generated React on Rails packs"] + lines = ["\n# React on Rails"] lines.concat(additions) "#{lines.join("\n")}\n" end diff --git a/react_on_rails/lib/generators/react_on_rails/templates/base/base/.env.example b/react_on_rails/lib/generators/react_on_rails/templates/base/base/.env.example new file mode 100644 index 0000000000..647763998e --- /dev/null +++ b/react_on_rails/lib/generators/react_on_rails/templates/base/base/.env.example @@ -0,0 +1,18 @@ +# Development environment configuration +# Copy this file to .env to override default ports. +# +# This is especially useful when running multiple git worktrees simultaneously. +# Each worktree needs its own .env with different ports to avoid conflicts. +# +# Shakapacker reads SHAKAPACKER_DEV_SERVER_PORT on both the Ruby (proxy) and +# JS (webpack-dev-server) sides, so no shakapacker.yml changes are needed. +# +# Example for a second worktree: +# PORT=3001 +# SHAKAPACKER_DEV_SERVER_PORT=3036 + +# Rails server port (default: 3000 for Procfile.dev / 3001 for Procfile.dev-prod-assets) +# PORT=3000 + +# Webpack dev server port (default: 3035, used by shakapacker) +# SHAKAPACKER_DEV_SERVER_PORT=3035 diff --git a/react_on_rails/lib/generators/react_on_rails/templates/base/base/Procfile.dev b/react_on_rails/lib/generators/react_on_rails/templates/base/base/Procfile.dev index c82348cf80..16b8ccd8fe 100644 --- a/react_on_rails/lib/generators/react_on_rails/templates/base/base/Procfile.dev +++ b/react_on_rails/lib/generators/react_on_rails/templates/base/base/Procfile.dev @@ -1,5 +1,9 @@ # Procfile for development # You can run these commands in separate shells -rails: bundle exec rails s -p 3000 +# +# To run multiple worktrees simultaneously, set different ports in each worktree's .env: +# PORT=3001 +# SHAKAPACKER_DEV_SERVER_PORT=3036 +rails: bundle exec rails s -p ${PORT:-3000} dev-server: bin/shakapacker-dev-server server-bundle: SERVER_BUNDLE_ONLY=yes bin/shakapacker --watch diff --git a/react_on_rails/lib/generators/react_on_rails/templates/base/base/Procfile.dev-prod-assets b/react_on_rails/lib/generators/react_on_rails/templates/base/base/Procfile.dev-prod-assets index 5e97047291..6e15b93f22 100644 --- a/react_on_rails/lib/generators/react_on_rails/templates/base/base/Procfile.dev-prod-assets +++ b/react_on_rails/lib/generators/react_on_rails/templates/base/base/Procfile.dev-prod-assets @@ -2,7 +2,7 @@ # Uses production-optimized, precompiled assets with development environment # Uncomment additional processes as needed for your app -rails: bundle exec rails s -p 3001 +rails: bundle exec rails s -p ${PORT:-3001} # sidekiq: bundle exec sidekiq -C config/sidekiq.yml # redis: redis-server # mailcatcher: mailcatcher --foreground diff --git a/react_on_rails/lib/generators/react_on_rails/templates/base/base/Procfile.dev-static-assets b/react_on_rails/lib/generators/react_on_rails/templates/base/base/Procfile.dev-static-assets index 75152f0e2f..10ff743acf 100644 --- a/react_on_rails/lib/generators/react_on_rails/templates/base/base/Procfile.dev-static-assets +++ b/react_on_rails/lib/generators/react_on_rails/templates/base/base/Procfile.dev-static-assets @@ -1,2 +1,2 @@ -web: bin/rails server -p 3000 +web: bin/rails server -p ${PORT:-3000} js: bin/shakapacker --watch diff --git a/react_on_rails/lib/react_on_rails/dev.rb b/react_on_rails/lib/react_on_rails/dev.rb index 6ccd4c2433..9d3dcb7729 100644 --- a/react_on_rails/lib/react_on_rails/dev.rb +++ b/react_on_rails/lib/react_on_rails/dev.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require_relative "dev/port_selector" require_relative "dev/server_manager" require_relative "dev/process_manager" require_relative "dev/pack_generator" diff --git a/react_on_rails/lib/react_on_rails/dev/port_selector.rb b/react_on_rails/lib/react_on_rails/dev/port_selector.rb new file mode 100644 index 0000000000..bbddf884fd --- /dev/null +++ b/react_on_rails/lib/react_on_rails/dev/port_selector.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +require "socket" + +module ReactOnRails + module Dev + class PortSelector + DEFAULT_RAILS_PORT = 3000 + DEFAULT_WEBPACK_PORT = 3035 + MAX_ATTEMPTS = 100 + + class NoPortAvailable < StandardError; end + + class << self + # Returns { rails: Integer, webpack: Integer }. + # Respects existing ENV['PORT'] / ENV['SHAKAPACKER_DEV_SERVER_PORT']. + # Probes for free ports when either or both env vars are unset. + def select_ports + rails_port = explicit_rails_port + webpack_port = explicit_webpack_port + + # If both are explicitly set, trust the user completely + return { rails: rails_port, webpack: webpack_port } if rails_port && webpack_port + + # If only one is set, anchor it and probe for a free port on the other side + if rails_port + return { rails: rails_port, + webpack: find_available_port(DEFAULT_WEBPACK_PORT, exclude: rails_port) } + end + + if webpack_port + return { rails: find_available_port(DEFAULT_RAILS_PORT, exclude: webpack_port), + webpack: webpack_port } + end + + # Neither set — auto-detect a free pair + find_free_pair + end + + # Public so it can be stubbed in tests. + # NOTE: Inherent TOCTOU race — another process can claim the port between + # server.close and the caller binding to it. This is unavoidable with the + # probe-then-use pattern and acceptable for the worktree port-selection use case. + def port_available?(port, host = "127.0.0.1") + server = TCPServer.new(host, port) + server.close + true + rescue Errno::EADDRINUSE, Errno::EACCES + false + end + + private + + def explicit_rails_port + ENV["PORT"]&.to_i&.then { |p| p.between?(1, 65_535) ? p : nil } + end + + def explicit_webpack_port + ENV["SHAKAPACKER_DEV_SERVER_PORT"]&.to_i&.then { |p| p.between?(1, 65_535) ? p : nil } + end + + def find_available_port(start_port, exclude: nil) + MAX_ATTEMPTS.times do |i| + port = start_port + i + next if port == exclude + + return port if port_available?(port) + end + + raise NoPortAvailable, "No available port found starting at #{start_port}." + end + + def find_free_pair + rails_port = find_available_port(DEFAULT_RAILS_PORT) + webpack_port = find_available_port(DEFAULT_WEBPACK_PORT, exclude: rails_port) + + if rails_port != DEFAULT_RAILS_PORT || webpack_port != DEFAULT_WEBPACK_PORT + puts "Default ports in use. Using Rails :#{rails_port}, webpack :#{webpack_port}" + end + + { rails: rails_port, webpack: webpack_port } + end + end + end + end +end diff --git a/react_on_rails/lib/react_on_rails/dev/server_manager.rb b/react_on_rails/lib/react_on_rails/dev/server_manager.rb index ba3195ebf5..957edd6268 100644 --- a/react_on_rails/lib/react_on_rails/dev/server_manager.rb +++ b/react_on_rails/lib/react_on_rails/dev/server_manager.rb @@ -478,7 +478,7 @@ def run_production_like(_verbose: false, route: nil, rails_env: nil, skip_databa features = [ "Precompiling assets with production optimizations", - "Running Rails server on port 3001", + "Running Rails server on port #{procfile_port(procfile)}", "No HMR (Hot Module Replacement)", "CSS extracted to separate files (no FOUC)" ] @@ -497,7 +497,7 @@ def run_production_like(_verbose: false, route: nil, rails_env: nil, skip_databa print_server_info( "🏭 Starting production-like development server...", features, - 3001, + procfile_port(procfile), route: route ) @@ -614,14 +614,16 @@ def run_production_like(_verbose: false, route: nil, rails_env: nil, skip_databa # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity def run_static_development(procfile, verbose: false, route: nil, skip_database_check: false) - print_procfile_info(procfile, route: route) - # Check database setup before starting exit 1 unless DatabaseChecker.check_database(skip: skip_database_check) # Check required services before starting exit 1 unless ServiceChecker.check_services + # Configure ports before printing so the banner shows the correct URL + configure_ports + print_procfile_info(procfile, route: route) + features = [ "Using shakapacker --watch (no HMR)", "CSS extracted to separate files (no FOUC)", @@ -637,6 +639,7 @@ def run_static_development(procfile, verbose: false, route: nil, skip_database_c print_server_info( "⚡ Starting development server with static assets...", features, + procfile_port(procfile), route: route ) @@ -646,14 +649,16 @@ def run_static_development(procfile, verbose: false, route: nil, skip_database_c end def run_development(procfile, verbose: false, route: nil, skip_database_check: false) - print_procfile_info(procfile, route: route) - # Check database setup before starting exit 1 unless DatabaseChecker.check_database(skip: skip_database_check) # Check required services before starting exit 1 unless ServiceChecker.check_services + # Configure ports before printing so the banner shows the correct URL + configure_ports + print_procfile_info(procfile, route: route) + PackGenerator.generate(verbose: verbose) ProcessManager.ensure_procfile(procfile) ProcessManager.run_with_process_manager(procfile) @@ -687,8 +692,21 @@ def print_procfile_info(procfile, route: nil) puts "" end + def configure_ports + selected = PortSelector.select_ports + ENV["PORT"] = selected[:rails].to_s + ENV["SHAKAPACKER_DEV_SERVER_PORT"] = selected[:webpack].to_s + rescue PortSelector::NoPortAvailable => e + warn e.message + exit 1 + end + def procfile_port(procfile) - procfile == "Procfile.dev-prod-assets" ? 3001 : 3000 + if procfile == "Procfile.dev-prod-assets" + ENV.fetch("PORT", 3001).to_i + else + ENV.fetch("PORT", 3000).to_i + end end def box_border(width) diff --git a/react_on_rails/spec/react_on_rails/dev/port_selector_spec.rb b/react_on_rails/spec/react_on_rails/dev/port_selector_spec.rb new file mode 100644 index 0000000000..2e06b96d5f --- /dev/null +++ b/react_on_rails/spec/react_on_rails/dev/port_selector_spec.rb @@ -0,0 +1,206 @@ +# frozen_string_literal: true + +require_relative "../spec_helper" +require "react_on_rails/dev/port_selector" +require "socket" + +RSpec.describe ReactOnRails::Dev::PortSelector do + describe ".select_ports" do + context "when default ports are free" do + it "returns the default Rails port 3000" do + allow(described_class).to receive(:port_available?).and_return(true) + result = described_class.select_ports + expect(result[:rails]).to eq(3000) + end + + it "returns the default webpack port 3035" do + allow(described_class).to receive(:port_available?).and_return(true) + result = described_class.select_ports + expect(result[:webpack]).to eq(3035) + end + + it "does not print a shift message" do + allow(described_class).to receive(:port_available?).and_return(true) + expect { described_class.select_ports }.not_to output(/shifted/i).to_stdout + end + end + + context "when default ports are occupied" do + it "finds the next free Rails port" do + allow(described_class).to receive(:port_available?) do |port| + port != 3000 # only 3000 is occupied + end + result = described_class.select_ports + expect(result[:rails]).to eq(3001) + end + + it "keeps webpack at its default when only the Rails default port is occupied" do + allow(described_class).to receive(:port_available?) do |port| + port != 3000 # 3035 is still free + end + result = described_class.select_ports + expect(result[:webpack]).to eq(3035) + end + + it "increments webpack independently when both default ports are occupied" do + allow(described_class).to receive(:port_available?) do |port| + port != 3000 && port != 3035 + end + result = described_class.select_ports + expect(result[:rails]).to eq(3001) + expect(result[:webpack]).to eq(3036) + end + + it "prints a message when ports are shifted" do + allow(described_class).to receive(:port_available?) do |port| + port != 3000 && port != 3035 + end + expect { described_class.select_ports }.to output(/3001.*3036|shifted|in use/i).to_stdout + end + end + + context "when ENV['PORT'] is already set" do + around do |example| + old = ENV.fetch("PORT", nil) + ENV["PORT"] = "4000" + example.run + ENV["PORT"] = old + end + + it "respects the existing PORT env var" do + allow(described_class).to receive(:port_available?).and_return(true) + result = described_class.select_ports + expect(result[:rails]).to eq(4000) + end + + it "defaults webpack to 3035 when SHAKAPACKER_DEV_SERVER_PORT is not set and 3035 is free" do + allow(described_class).to receive(:port_available?).and_return(true) + result = described_class.select_ports + expect(result[:webpack]).to eq(3035) + end + + it "finds next free webpack port when 3035 is occupied" do + call_count = 0 + allow(described_class).to receive(:port_available?) do + call_count += 1 + call_count > 1 # first check (3035) fails + end + result = described_class.select_ports + expect(result[:webpack]).to eq(3036) + end + + it "does not return PORT value as webpack port when they would be equal" do + ENV["PORT"] = "3035" + allow(described_class).to receive(:port_available?).and_return(true) + result = described_class.select_ports + expect(result[:rails]).to eq(3035) + expect(result[:webpack]).not_to eq(3035) + end + end + + context "when ENV['SHAKAPACKER_DEV_SERVER_PORT'] is already set" do + around do |example| + old = ENV.fetch("SHAKAPACKER_DEV_SERVER_PORT", nil) + ENV["SHAKAPACKER_DEV_SERVER_PORT"] = "4035" + example.run + ENV["SHAKAPACKER_DEV_SERVER_PORT"] = old + end + + it "respects the existing SHAKAPACKER_DEV_SERVER_PORT env var" do + allow(described_class).to receive(:port_available?).and_return(true) + result = described_class.select_ports + expect(result[:webpack]).to eq(4035) + end + + it "defaults Rails to 3000 when PORT is not set and 3000 is free" do + allow(described_class).to receive(:port_available?).and_return(true) + result = described_class.select_ports + expect(result[:rails]).to eq(3000) + end + + it "finds next free rails port when 3000 is occupied" do + call_count = 0 + allow(described_class).to receive(:port_available?) do + call_count += 1 + call_count > 1 # first check (3000) fails + end + result = described_class.select_ports + expect(result[:rails]).to eq(3001) + end + + it "does not return SHAKAPACKER_DEV_SERVER_PORT value as rails port when they would be equal" do + ENV["SHAKAPACKER_DEV_SERVER_PORT"] = "3000" + allow(described_class).to receive(:port_available?).and_return(true) + result = described_class.select_ports + expect(result[:webpack]).to eq(3000) + expect(result[:rails]).not_to eq(3000) + end + end + + context "when both PORT and SHAKAPACKER_DEV_SERVER_PORT are set" do + around do |example| + old_port = ENV.fetch("PORT", nil) + old_wp = ENV.fetch("SHAKAPACKER_DEV_SERVER_PORT", nil) + ENV["PORT"] = "4000" + ENV["SHAKAPACKER_DEV_SERVER_PORT"] = "4035" + example.run + ENV["PORT"] = old_port + ENV["SHAKAPACKER_DEV_SERVER_PORT"] = old_wp + end + + it "returns both explicit ports" do + result = described_class.select_ports + expect(result[:rails]).to eq(4000) + expect(result[:webpack]).to eq(4035) + end + + it "does not probe for free ports" do + expect(described_class).not_to receive(:port_available?) + described_class.select_ports + end + end + + context "when no port is available within max attempts" do + it "raises an error" do + allow(described_class).to receive(:port_available?).and_return(false) + expect { described_class.select_ports }.to raise_error(described_class::NoPortAvailable, /No available port/) + end + end + + context "when PORT contains an out-of-range value" do + around do |example| + old = ENV.fetch("PORT", nil) + ENV["PORT"] = "99999" + example.run + ENV["PORT"] = old + end + + it "treats out-of-range PORT as unset and falls back to auto-detection" do + allow(described_class).to receive(:port_available?).and_return(true) + result = described_class.select_ports + expect(result[:rails]).to eq(3000) + expect(result[:webpack]).to eq(3035) + end + end + end + + describe ".port_available?" do + it "returns true for a port that nothing is listening on" do + # Find a definitely-free port using OS assignment, then close it and check + server = TCPServer.new("127.0.0.1", 0) + free_port = server.addr[1] + server.close + expect(described_class.port_available?(free_port)).to be true + end + + it "returns false for a port that is already in use" do + server = TCPServer.new("127.0.0.1", 0) + occupied_port = server.addr[1] + begin + expect(described_class.port_available?(occupied_port)).to be false + ensure + server.close + end + end + end +end diff --git a/react_on_rails/spec/react_on_rails/dev/server_manager_spec.rb b/react_on_rails/spec/react_on_rails/dev/server_manager_spec.rb index baf4f4dbc4..04554d89a6 100644 --- a/react_on_rails/spec/react_on_rails/dev/server_manager_spec.rb +++ b/react_on_rails/spec/react_on_rails/dev/server_manager_spec.rb @@ -25,6 +25,8 @@ def mock_system_calls allow(ReactOnRails::Dev::ProcessManager).to receive(:ensure_procfile) allow(ReactOnRails::Dev::ProcessManager).to receive(:run_with_process_manager) allow(ReactOnRails::Dev::DatabaseChecker).to receive(:check_database).and_return(true) + allow(ReactOnRails::Dev::PortSelector).to receive(:select_ports) + .and_return({ rails: 3000, webpack: 3035 }) end describe ".start" do @@ -65,6 +67,26 @@ def mock_system_calls described_class.start(:production_like) end + it "passes procfile_port to print_server_info in production-like mode" do + ENV["PORT"] = "4000" + env = { "NODE_ENV" => "production" } + argv = ["bundle", "exec", "rails", "assets:precompile"] + status_double = instance_double(Process::Status, success?: true) + allow(Open3).to receive(:capture3).with(env, *argv).and_return(["output", "", status_double]) + + port_at_server_info_time = nil + allow(described_class).to receive(:print_server_info).and_wrap_original do |m, *args, **kwargs| + port_at_server_info_time = args[2] + m.call(*args, **kwargs) + end + + described_class.start(:production_like) + + expect(port_at_server_info_time).to eq(4000) + ensure + ENV.delete("PORT") + end + it "starts production-like mode with custom rails_env" do env = { "NODE_ENV" => "production", "RAILS_ENV" => "staging" } argv = ["bundle", "exec", "rails", "assets:precompile"] @@ -88,6 +110,100 @@ def mock_system_calls it "raises error for unknown mode" do expect { described_class.start(:unknown) }.to raise_error(ArgumentError, "Unknown mode: unknown") end + + context "when configuring ports" do + before do + mock_system_calls + allow(ReactOnRails::Dev::PortSelector).to receive(:select_ports) + .and_return({ rails: 3000, webpack: 3035 }) + end + + around do |example| + old_port = ENV.fetch("PORT", nil) + old_webpack_port = ENV.fetch("SHAKAPACKER_DEV_SERVER_PORT", nil) + ENV.delete("PORT") + ENV.delete("SHAKAPACKER_DEV_SERVER_PORT") + example.run + ENV["PORT"] = old_port + ENV["SHAKAPACKER_DEV_SERVER_PORT"] = old_webpack_port + end + + it "sets PORT env var before starting development mode" do + described_class.start(:development) + expect(ENV.fetch("PORT", nil)).to eq("3000") + end + + it "sets SHAKAPACKER_DEV_SERVER_PORT env var before starting development mode" do + described_class.start(:development) + expect(ENV.fetch("SHAKAPACKER_DEV_SERVER_PORT", nil)).to eq("3035") + end + + it "sets PORT env var before starting static mode" do + described_class.start(:static) + expect(ENV.fetch("PORT", nil)).to eq("3000") + end + + it "uses auto-detected ports when defaults are occupied" do + allow(ReactOnRails::Dev::PortSelector).to receive(:select_ports) + .and_return({ rails: 3001, webpack: 3036 }) + described_class.start(:development) + expect(ENV.fetch("PORT", nil)).to eq("3001") + expect(ENV.fetch("SHAKAPACKER_DEV_SERVER_PORT", nil)).to eq("3036") + end + + it "has PORT set when print_procfile_info is called in development mode" do + allow(ReactOnRails::Dev::PortSelector).to receive(:select_ports) + .and_return({ rails: 3001, webpack: 3036 }) + + port_at_print_time = nil + allow(described_class).to receive(:print_procfile_info).and_wrap_original do |m, *args, **kwargs| + port_at_print_time = ENV.fetch("PORT", nil) + m.call(*args, **kwargs) + end + + described_class.start(:development) + + expect(port_at_print_time).to eq("3001") + end + + it "passes the auto-detected port to print_server_info in static mode" do + allow(ReactOnRails::Dev::PortSelector).to receive(:select_ports) + .and_return({ rails: 3001, webpack: 3036 }) + + port_at_server_info_time = nil + allow(described_class).to receive(:print_server_info).and_wrap_original do |m, *args, **kwargs| + port_at_server_info_time = args[2] + m.call(*args, **kwargs) + end + + described_class.start(:static) + + expect(port_at_server_info_time).to eq(3001) + end + + it "has PORT set when print_procfile_info is called in static mode" do + allow(ReactOnRails::Dev::PortSelector).to receive(:select_ports) + .and_return({ rails: 3001, webpack: 3036 }) + + port_at_print_time = nil + allow(described_class).to receive(:print_procfile_info).and_wrap_original do |m, *args, **kwargs| + port_at_print_time = ENV.fetch("PORT", nil) + m.call(*args, **kwargs) + end + + described_class.start(:static) + + expect(port_at_print_time).to eq("3001") + end + + it "exits cleanly when no port pair is available" do + allow(ReactOnRails::Dev::PortSelector).to receive(:select_ports) + .and_raise(ReactOnRails::Dev::PortSelector::NoPortAvailable, "No available port pair found") + + expect_any_instance_of(Kernel).to receive(:exit).with(1) + expect { described_class.start(:development) }.not_to raise_error + end + end end describe ".kill_processes" do @@ -264,6 +380,33 @@ def mock_system_calls end end + describe ".procfile_port" do + around do |example| + old_port = ENV.fetch("PORT", nil) + ENV.delete("PORT") + example.run + ENV["PORT"] = old_port + end + + it "returns 3000 for Procfile.dev when PORT is unset" do + expect(described_class.send(:procfile_port, "Procfile.dev")).to eq(3000) + end + + it "returns 3001 for Procfile.dev-prod-assets when PORT is unset" do + expect(described_class.send(:procfile_port, "Procfile.dev-prod-assets")).to eq(3001) + end + + it "returns the auto-detected port for Procfile.dev when PORT is set" do + ENV["PORT"] = "3001" + expect(described_class.send(:procfile_port, "Procfile.dev")).to eq(3001) + end + + it "returns the PORT value for Procfile.dev-prod-assets when PORT is set" do + ENV["PORT"] = "4000" + expect(described_class.send(:procfile_port, "Procfile.dev-prod-assets")).to eq(4000) + end + end + describe ".show_help" do it "displays help information" do expect { described_class.show_help }.to output(%r{Usage: bin/dev \[command\]}).to_stdout_from_any_process diff --git a/react_on_rails/spec/react_on_rails/support/shared_examples/base_generator_examples.rb b/react_on_rails/spec/react_on_rails/support/shared_examples/base_generator_examples.rb index 803ba8a641..bef13c83a7 100644 --- a/react_on_rails/spec/react_on_rails/support/shared_examples/base_generator_examples.rb +++ b/react_on_rails/spec/react_on_rails/support/shared_examples/base_generator_examples.rb @@ -15,7 +15,20 @@ config/initializers/react_on_rails.rb Procfile.dev Procfile.dev-static-assets - Procfile.dev-prod-assets].each { |file| assert_file(file) } + Procfile.dev-prod-assets + .env.example].each { |file| assert_file(file) } + end + + it "uses env-var-driven port in Procfile.dev" do + assert_file "Procfile.dev", /\$\{PORT:-3000\}/ + end + + it "uses env-var-driven port in Procfile.dev-static-assets" do + assert_file "Procfile.dev-static-assets", /\$\{PORT:-3000\}/ + end + + it "uses env-var-driven port in Procfile.dev-prod-assets" do + assert_file "Procfile.dev-prod-assets", /\$\{PORT:-3001\}/ end end