-
-
Notifications
You must be signed in to change notification settings - Fork 633
fix: use env-var-driven ports in Procfile templates to support multiple worktrees #2539
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 10 commits
3ebe213
7ce155b
5026b83
e717cb8
85abb9c
60573e7
f84641a
bcc9a59
f98bd7e
71c8bd3
ad62225
fd4806f
d09b9d8
0bc50cd
47d1844
d67ace7
f4eca17
ee004b6
5dd07fd
446ce06
9ecb4be
d120d53
f06b6ff
3305a64
bfec470
5fe353a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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, used by Procfile.dev) | ||
| # PORT=3000 | ||
ihabadham marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| # Webpack dev server port (default: 3035, used by shakapacker) | ||
| # SHAKAPACKER_DEV_SERVER_PORT=3035 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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} | ||
ihabadham marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| dev-server: bin/shakapacker-dev-server | ||
| server-bundle: SERVER_BUNDLE_ONLY=yes bin/shakapacker --watch | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The process names changed from
Since these are generated template files (not existing user files), only fresh installs are affected — but the name change should be documented in the CHANGELOG under "Breaking Changes" or "Changed" so teams upgrading their generator output know to update any tooling references. |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,2 @@ | ||
| web: bin/rails server -p 3000 | ||
| web: bin/rails server -p ${PORT:-3000} | ||
| js: bin/shakapacker --watch |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| require "socket" | ||
|
|
||
| module ReactOnRails | ||
| module Dev | ||
| class PortSelector | ||
| DEFAULT_RAILS_PORT = 3000 | ||
| DEFAULT_WEBPACK_PORT = 3035 | ||
| WEBPACK_OFFSET = DEFAULT_WEBPACK_PORT - DEFAULT_RAILS_PORT # 35 | ||
ihabadham marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| MAX_ATTEMPTS = 100 | ||
|
|
||
| class NoPortAvailable < StandardError; end | ||
|
|
||
| class << self | ||
| # Returns { rails: Integer, webpack: Integer }. | ||
ihabadham marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| # Respects existing ENV['PORT'] / ENV['SHAKAPACKER_DEV_SERVER_PORT']. | ||
| # Only probes when both are unset (i.e. user hasn't configured them). | ||
| 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, use it as the anchor and return defaults for the other | ||
| return { rails: rails_port, webpack: explicit_webpack_port || DEFAULT_WEBPACK_PORT } if rails_port | ||
ihabadham marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
ihabadham marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| return { rails: DEFAULT_RAILS_PORT, webpack: webpack_port } if webpack_port | ||
|
|
||
| # Neither set — auto-detect a free pair | ||
| find_free_pair | ||
| end | ||
|
|
||
| # Public so it can be stubbed in tests | ||
| def port_available?(port, host = "127.0.0.1") | ||
| server = TCPServer.new(host, port) | ||
| server.close | ||
| true | ||
ihabadham marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| rescue Errno::EADDRINUSE, Errno::EACCES | ||
| false | ||
| end | ||
|
|
||
| private | ||
|
|
||
| def explicit_rails_port | ||
| ENV["PORT"]&.to_i&.then { |p| p.positive? ? p : nil } | ||
| end | ||
|
|
||
| def explicit_webpack_port | ||
| ENV["SHAKAPACKER_DEV_SERVER_PORT"]&.to_i&.then { |p| p.positive? ? p : nil } | ||
| end | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| def find_free_pair | ||
ihabadham marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| MAX_ATTEMPTS.times do |i| | ||
| rails_port = DEFAULT_RAILS_PORT + i | ||
| webpack_port = DEFAULT_WEBPACK_PORT + i | ||
|
|
||
| next unless port_available?(rails_port) && port_available?(webpack_port) | ||
|
|
||
| puts "Default ports in use. Using Rails :#{rails_port}, webpack :#{webpack_port}" if i.positive? | ||
ihabadham marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| return { rails: rails_port, webpack: webpack_port } | ||
| end | ||
|
|
||
| raise NoPortAvailable, | ||
| "No available port pair found in range " \ | ||
| "#{DEFAULT_RAILS_PORT}--#{DEFAULT_RAILS_PORT + MAX_ATTEMPTS - 1}. " \ | ||
| "Run 'bin/dev kill' to free up ports." | ||
| end | ||
| end | ||
| end | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,157 @@ | ||
| # 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 | ||
| call_count = 0 | ||
| allow(described_class).to receive(:port_available?) do | ||
| call_count += 1 | ||
| call_count > 1 # first check (3000) fails; && short-circuits so 3035 is never checked | ||
| end | ||
| result = described_class.select_ports | ||
| expect(result[:rails]).to eq(3001) | ||
| end | ||
|
|
||
| it "keeps webpack port offset from Rails port" do | ||
| call_count = 0 | ||
| allow(described_class).to receive(:port_available?) do | ||
| call_count += 1 | ||
| call_count > 1 | ||
| end | ||
| result = described_class.select_ports | ||
| expect(result[:webpack]).to eq(3036) | ||
| end | ||
|
|
||
| it "prints a message when ports are shifted" do | ||
| call_count = 0 | ||
| allow(described_class).to receive(:port_available?) do | ||
| call_count += 1 | ||
| call_count > 1 | ||
| 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 | ||
ihabadham marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| end | ||
|
|
||
| it "respects the existing PORT env var" do | ||
| 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" do | ||
| result = described_class.select_ports | ||
| expect(result[:webpack]).to eq(3035) | ||
| 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 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 | ||
| result = described_class.select_ports | ||
| expect(result[:webpack]).to eq(4035) | ||
| end | ||
|
|
||
| it "defaults Rails to 3000 when PORT is not set" do | ||
| result = described_class.select_ports | ||
| expect(result[:rails]).to eq(3000) | ||
| 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 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 | ||
| 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 | ||
Uh oh!
There was an error while loading. Please reload this page.