Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
5cdd939
feat: add REACT_ON_RAILS_BASE_PORT for concurrent worktree port manag…
justin808 Apr 15, 2026
a9f8bea
chore: add .gstack/ to .gitignore
justin808 Apr 18, 2026
001c19a
fix: address review feedback on base port feature
justin808 Apr 18, 2026
3675263
fix: second round of port selector review feedback
justin808 Apr 18, 2026
6589747
fix: third round of port selector review feedback
justin808 Apr 18, 2026
6cf3724
fix: fourth round of port selector review feedback
justin808 Apr 18, 2026
f430db1
fix: fifth round of port selector review feedback
justin808 Apr 18, 2026
889a150
fix: sixth round of port selector review feedback
justin808 Apr 18, 2026
c5a6f76
fix: seventh round of port selector review feedback
justin808 Apr 18, 2026
f2adcdb
fix: eighth round of port selector review feedback
justin808 Apr 18, 2026
e9418ec
fix: ninth round of port selector review feedback
justin808 Apr 18, 2026
c2b76ec
fix: keep renderer fallbacks aligned
justin808 Apr 18, 2026
5dd9456
fix: tenth round of port selector review feedback
justin808 Apr 18, 2026
268a2cc
fix: eleventh round of port selector review feedback
justin808 Apr 19, 2026
287d30e
fix: twelfth round of port selector review feedback
justin808 Apr 22, 2026
3f33b2e
Fix Pro generator expectations for renderer port env default
justin808 Apr 23, 2026
26ab092
fix: honor REACT_ON_RAILS_BASE_PORT in bin/dev prod mode
justin808 Apr 23, 2026
6a692b9
fix: thirteenth round of port selector review feedback
justin808 Apr 23, 2026
803970e
fix: accept IPv6 hosts in url_port_mismatch? regex
justin808 Apr 23, 2026
3535812
fix: six round-15 review fixes around base-port consistency
justin808 Apr 23, 2026
4434b44
fix: round 16 review fixes — 5 design/doc polish
justin808 Apr 23, 2026
e1c658a
fix: clean copied runtime files before bin/dev startup
justin808 Apr 25, 2026
7e56bd3
fix: round 17 review polish — 4 doc/UX fixes
justin808 Apr 29, 2026
d3cabf9
fix: round 18 review polish — 5 fixes from cursor + claude bots
justin808 Apr 29, 2026
5601be4
fix: round 19 review polish — 5 fixes from cursor + claude bots
justin808 Apr 30, 2026
7ca8054
fix: round 20 review polish — 5 fixes from claude bot
justin808 Apr 30, 2026
4e9504f
fix: round 21 review polish — case-insensitive localhost + whitespace…
justin808 May 3, 2026
4dce031
Merge main and address review blockers
justin808 May 4, 2026
53c62ee
Avoid killing OSS renderer-derived port
justin808 May 4, 2026
3d328f8
Harden dev cleanup review fixes
justin808 May 4, 2026
5ef6d50
Skip renderer kill fallback for remote URLs
justin808 May 4, 2026
db313f1
Cover local renderer URL port cleanup
justin808 May 4, 2026
a7d748a
Respect local renderer URL port during cleanup
justin808 May 4, 2026
b1cc0bb
fix: round 22 review polish — 16 fixes from claude/cursor/codex bots
justin808 May 5, 2026
ae374f7
Merge remote-tracking branch 'origin/main' into codex/pr-3142-work
justin808 May 8, 2026
31b63d1
fix: harden base-port review edge cases
justin808 May 9, 2026
98d221f
fix: round 23 review polish — 6 fixes from claude/codex bots
justin808 May 10, 2026
11877c6
fix: round 24 review polish — 8 fixes from claude[bot] review
justin808 May 10, 2026
2c67e07
fix: round 25 review polish — 8 fixes from claude/cursor bots
justin808 May 10, 2026
d90fcef
docs: revert container-deployment.md RENDERER_URL rename
justin808 May 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,4 @@ packages/**/node_modules/
junit.xml
.lycheecache
.playwright-cli/
.gstack/
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,25 @@ After a release, run `/update-changelog` in Claude Code to analyze commits, writ

### [Unreleased]

#### Added

- **`bin/dev` deterministic port allocation via `REACT_ON_RAILS_BASE_PORT` (and `CONDUCTOR_PORT`)**: `bin/dev` now derives Rails / webpack-dev-server / node-renderer ports from a single base port when `REACT_ON_RAILS_BASE_PORT` (or `CONDUCTOR_PORT`, for [Conductor.build](https://conductor.build) workspaces) is set: Rails = `base + 0`, webpack = `base + 1`, renderer = `base + 2`. This makes parallel worktrees and coding-agent sandboxes collision-free without per-service env vars. The priority chain is base port → explicit per-service env vars (`PORT`, `SHAKAPACKER_DEV_SERVER_PORT`) → auto-detection. **Behavior note:** when base-port mode is active, any pre-set `PORT`, `SHAKAPACKER_DEV_SERVER_PORT`, `RENDERER_PORT`, or non-matching `REACT_RENDERER_URL` (and the legacy `RENDERER_URL`, when already set) is unconditionally overwritten with the derived value (a warning is printed before each override). This applies in all `bin/dev` modes including `bin/dev prod`, where `SHAKAPACKER_DEV_SERVER_PORT` is also derived/overwritten for tooling consistency even though the production-like mode does not run webpack-dev-server. **Sub-process env preservation:** to keep these derived values consistent across spawned processes, `bin/dev` now also preserves `RENDERER_PORT`, `REACT_RENDERER_URL`, and `SHAKAPACKER_SKIP_PRECOMPILE_HOOK` across Bundler's env reset (previously only `PORT` and `SHAKAPACKER_DEV_SERVER_PORT` were preserved); this prevents nested `shakapacker` commands from silently re-running the precompile hook or losing the renderer URL. [PR 3142](https://github.com/shakacode/react_on_rails/pull/3142) by [justin808](https://github.com/justin808).
- **[Pro] `bin/dev` auto-derives `REACT_RENDERER_URL` from `RENDERER_PORT`**: When only `RENDERER_PORT` is set, `bin/dev` now sets `REACT_RENDERER_URL=http://localhost:RENDERER_PORT` so Rails reaches the right port by default. Users running a remote or non-localhost node renderer (Docker service, remote host) should set `REACT_RENDERER_URL` explicitly so it is not replaced with the localhost default. [PR 3142](https://github.com/shakacode/react_on_rails/pull/3142) by [justin808](https://github.com/justin808).

#### Removed

- **[Pro]** **Removed the `--rsc-pro` install generator flag**: `--rsc` already implies Pro, so the separate mode was unnecessary. Behaviors previously gated on `--rsc-pro` (Pro verification checklist, prerelease install note, exact Pro gem pin on prereleases) now fire on `--rsc` installs. See also [Issue 3104](https://github.com/shakacode/react_on_rails/issues/3104), which tracks unrelated silent-failure bugs in the Pro upgrade automation. [PR 3105](https://github.com/shakacode/react_on_rails/pull/3105) by [ihabadham](https://github.com/ihabadham).

#### Changed

- **[Pro]** **Pro generator now creates the Node Renderer at `renderer/node-renderer.js`**: The canonical location for the Node Renderer entry point is now a dedicated top-level `renderer/` directory instead of `client/`, making it straightforward to exclude from production Docker builds that strip JS sources after bundling. Docs and Pro `spec/dummy` now use the new path consistently. Existing apps are unaffected — the generator skips files that already exist (including a legacy `client/node-renderer.js`). Fixes [Issue 3073](https://github.com/shakacode/react_on_rails/issues/3073). [PR 3165](https://github.com/shakacode/react_on_rails/pull/3165) by [justin808](https://github.com/justin808).
- **[Pro] Documentation standardized on `REACT_RENDERER_URL` env var name**: The configuration example in `docs/oss/configuration/configuration-pro.md` now shows `ENV["REACT_RENDERER_URL"]` instead of the older `ENV["RENDERER_URL"]`, aligning with the rest of the docs and the generator template. Existing apps that read `ENV["RENDERER_URL"]` in their initializer continue to work — the Pro `renderer_url` config is whichever env var the user reads in their initializer; no gem code reads either name directly. Rename the env var in your infrastructure configs (and update the initializer to match) if you want to align with the new convention. `bin/dev` now also warns when `RENDERER_URL` is set without `REACT_RENDERER_URL` so the rename doesn't silently fall back to the default renderer URL. [PR 3142](https://github.com/shakacode/react_on_rails/pull/3142) by [justin808](https://github.com/justin808).

#### Fixed

- **Install generator validates the selected JavaScript package manager**: The install generator now checks the manager selected from `REACT_ON_RAILS_PACKAGE_MANAGER`, the `packageManager` field in `package.json`, or a lockfile on disk — instead of passing when any JavaScript package manager is installed. When the selected command is missing, the error names the selected manager, the source that selected it, and the available alternatives. The generator also warns when `REACT_ON_RAILS_PACKAGE_MANAGER` is set to a value outside the supported set (`npm`, `pnpm`, `yarn`, `bun`). Addresses package manager validation from [Issue 1958](https://github.com/shakacode/react_on_rails/issues/1958). [PR 3229](https://github.com/shakacode/react_on_rails/pull/3229) by [justin808](https://github.com/justin808).
- **Server-render error wrapping preserves original causes**: When server rendering catches a non-`Error` thrown value, React on Rails now wraps it with the original value attached as `cause`, making downstream debugging preserve more context. Fixes [Issue 1746](https://github.com/shakacode/react_on_rails/issues/1746). [PR 3230](https://github.com/shakacode/react_on_rails/pull/3230) by [justin808](https://github.com/justin808).
- **`bin/dev` now cleans copied runtime files before startup**: When you duplicate an app directory to run another local dev stack, `bin/dev` now removes copied stale Overmind sockets and stale `tmp/pids/server.pid` files that point to a Puma process running from another app directory. This prevents false startup failures in copied workspaces while still preserving active local sockets and pid files for the current app. [PR 3142](https://github.com/shakacode/react_on_rails/pull/3142) by [justin808](https://github.com/justin808).
- **[Pro]** **Node renderer now exposes `performance` when `supportModules: true`**: React 19's development build of `React.lazy` calls `performance.now()`, which previously threw `ReferenceError: performance is not defined` inside the node renderer's VM context unless users manually added `performance` via `additionalContext`. `performance` is now included in the default globals alongside `Buffer`, `process`, etc. Fixes [Issue 3154](https://github.com/shakacode/react_on_rails/issues/3154). [PR 3158](https://github.com/shakacode/react_on_rails/pull/3158) by [justin808](https://github.com/justin808).
- **Client startup now recovers if initialization begins during `interactive` after `DOMContentLoaded` already fired**: React on Rails now still initializes the page when the client bundle starts in the browser timing window after `DOMContentLoaded` but before the document reaches `complete`. Fixes [Issue 3150](https://github.com/shakacode/react_on_rails/issues/3150). [PR 3151](https://github.com/shakacode/react_on_rails/pull/3151) by [ihabadham](https://github.com/ihabadham).
- **Doctor accepts TypeScript server bundle entrypoints**: `react_on_rails:doctor` now resolves common source entrypoint suffixes (`.js`, `.jsx`, `.ts`, `.tsx`, `.mjs`, `.cjs`) before warning that the server bundle is missing, preventing false positives when apps use `server-bundle.ts`. [PR 3111](https://github.com/shakacode/react_on_rails/pull/3111) by [justin808](https://github.com/justin808).
Expand Down
123 changes: 123 additions & 0 deletions docs/oss/building-features/process-managers.md
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,129 @@ SHAKAPACKER_DEV_SERVER_PORT=3036

When `PORT` or `SHAKAPACKER_DEV_SERVER_PORT` are set, auto-detection is skipped entirely.

### Coding Agent / CI Integration

When using coding agent tools that run multiple workspaces concurrently
([Conductor.build](https://conductor.build), OpenAI Codex, Quad Code, etc.),
set `REACT_ON_RAILS_BASE_PORT` to derive all service ports from a single value.
This eliminates the need for per-worktree `.env` files.

> **Important:** Run each live `bin/dev` stack from a separate checkout, worktree,
> or copied app directory. Starting two stacks from the exact same app path is
> not supported because build tools such as ReScript and webpack watchers share
> lock files and runtime artifacts within that directory.

`bin/dev` assigns ports using fixed offsets from the base:

| Service | Offset | Example (base=4000) |
| ------------------- | ------ | ------------------- |
| Rails server | +0 | 4000 |
| Webpack dev server | +1 | 4001 |
| Node renderer (Pro) | +2 | 4002 |
| _(reserved)_ | +3–+9 | 4003–4009 |

When a base port is detected, `bin/dev` also sets `RENDERER_PORT` and
`REACT_RENDERER_URL` automatically so the Pro Node Renderer and Rails
initializer agree on the port without any additional configuration.

> **Heads up:** setting `RENDERER_PORT`, `RENDERER_URL`, or `REACT_RENDERER_URL`
> in your environment activates the Pro renderer code path even in OSS apps —
> in base-port mode this means `RENDERER_PORT` and `REACT_RENDERER_URL` will be
> derived from the base and propagated to spawned processes. If you use
> `RENDERER_PORT` in your environment for an unrelated purpose, rename your
> variable to avoid the side effect.

> **Note:** Base-port mode derives the node renderer URL as
> `http://localhost:<port>`. If you run the renderer in a Docker container or
> on a remote host (e.g. `REACT_RENDERER_URL=http://renderer:3800`), do not use
> base-port mode — set `REACT_RENDERER_URL` explicitly and use the
> [manual worktree setup](#manual-worktree-port-setup-pro) instead. `bin/dev`
> will warn at runtime if a pre-set `REACT_RENDERER_URL` is overridden.

**Recognized environment variables** (checked in order):

1. `REACT_ON_RAILS_BASE_PORT` — the canonical base port variable; any tool can set this.
2. `CONDUCTOR_PORT` — set automatically by [Conductor.build](https://conductor.build).

> **Note on `CONDUCTOR_PORT`:** React on Rails treats `CONDUCTOR_PORT` as the
> **base** of a consecutive port block (Rails = base + 0, webpack = base + 1,
> renderer = base + 2). This interpretation is not part of a public Conductor
> API; treat `CONDUCTOR_PORT` support as best-effort until Conductor documents
> the contract. If a future Conductor release redefines `CONDUCTOR_PORT` (for
> example, to mean the Rails port itself), override by setting
> `REACT_ON_RAILS_BASE_PORT` explicitly — it takes precedence and uses the
> same derivation rules.

**Priority chain:** base port > explicit per-service env vars (`PORT`, etc.) > auto-detect free ports.

**Example: setting the base port in a tool's configuration:**

```sh
# In your agent tool's workspace setup or .env
REACT_ON_RAILS_BASE_PORT=4000
```

#### Tool-specific setup

[Conductor.build](https://conductor.build) sets `CONDUCTOR_PORT` for you — no configuration
needed. Other tools (Claude Code CLI, [OpenAI Codex](https://github.com/openai/codex) CLI or app,
plain `git worktree`, etc.) don't inject a port variable, so pick a different base per checkout
using one of the options below.

- **Per-worktree `.env` file** (tool-agnostic; gitignored by default):

```sh
# .env at the root of each worktree
REACT_ON_RAILS_BASE_PORT=4000
```

- **Claude Code `.claude/settings.json`** (per-project, checked in or local):

```json
{
"env": {
"REACT_ON_RAILS_BASE_PORT": "4000"
}
}
```

- **Shell export** (ad hoc, one session):

```sh
REACT_ON_RAILS_BASE_PORT=4000 bin/dev
```

`bin/dev` reads the variable from the process environment regardless of how it was set, so mix
and match whichever is most convenient for each tool.

If you create a new checkout by copying an existing app directory while another
`bin/dev` is running, the copy can inherit `tmp/pids/server.pid` or Overmind
socket files from the original app. `bin/dev` now cleans those copied stale
runtime files automatically on startup. If you launch processes outside
`bin/dev`, clear those files yourself before booting the copied app.

### Manual Worktree Port Setup (Pro)

If you use the [Node Renderer](./node-renderer/basics.md) (React on Rails Pro) with manual
worktrees (no base port), you need to configure the renderer port in addition to the standard
Rails and webpack ports:

```sh
# .env in each worktree
PORT=3001
SHAKAPACKER_DEV_SERVER_PORT=3036
RENDERER_PORT=3801
REACT_RENDERER_URL=http://localhost:3801
```

The renderer port must match on both sides: `RENDERER_PORT` is read by the Node process and
`REACT_RENDERER_URL` is read by the Rails-side Pro initializer.
Comment thread
justin808 marked this conversation as resolved.

> **Note:** `bin/dev kill` only stops the renderer when `RENDERER_PORT` (or `REACT_RENDERER_URL`)
> is exported in the current shell. From a fresh terminal that hasn't sourced your worktree's
> `.env`, run e.g. `RENDERER_PORT=3801 bin/dev kill`, or source `.env` first. Otherwise the
> renderer process will silently keep running.

## See Also

- [HMR and Hot Reloading](./hmr-and-hot-reloading-with-the-webpack-dev-server.md)
Expand Down
2 changes: 1 addition & 1 deletion docs/oss/configuration/configuration-pro.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ ReactOnRailsPro.configure do |config|
# since it will be easy to intercept it.
# If you provide an ENV value (maybe only for production) and there is no value, then you get the default.
# Default for `renderer_url` is "http://localhost:3800".
config.renderer_url = ENV["RENDERER_URL"]
config.renderer_url = ENV["REACT_RENDERER_URL"]
Comment thread
justin808 marked this conversation as resolved.
Comment thread
justin808 marked this conversation as resolved.

# If you don't want to worry about special characters in your password within the url, use this config value
# Default for `renderer_password` is nil
Expand Down
1 change: 1 addition & 0 deletions react_on_rails/.rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ Metrics/ClassLength:
Exclude:
- 'lib/generators/react_on_rails/base_generator.rb' # Generator complexity justified
- 'lib/react_on_rails/dev/server_manager.rb' # Dev tool with comprehensive help system
- 'lib/react_on_rails/dev/port_selector.rb' # Base-port mode plus dual-stack probing keep this above the 150-line threshold

Metrics/MethodLength:
Exclude:
Expand Down
12 changes: 7 additions & 5 deletions react_on_rails/lib/generators/react_on_rails/pro_setup.rb
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,8 @@ def create_node_renderer
say "ℹ️ #{legacy_node_renderer_path} detected, keeping existing renderer; " \
"to migrate, move it to #{node_renderer_path} and update any references " \
"(e.g. Procfile.dev, Procfile.prod, Docker CMD / command):", :yellow
say " node-renderer: RENDERER_LOG_LEVEL=debug RENDERER_PORT=3800 node #{node_renderer_path}", :yellow
say " node-renderer: RENDERER_LOG_LEVEL=debug RENDERER_PORT=${RENDERER_PORT:-3800} " \
"node #{node_renderer_path}", :yellow
warn_on_stale_legacy_procfile_entry
return true
end
Expand Down Expand Up @@ -260,7 +261,7 @@ def warn_on_stale_legacy_procfile_entry
GeneratorMessages.add_warning(<<~MSG.strip)
⚠️ Procfile.dev still launches the legacy client/node-renderer.js.
After migrating the renderer file, update that line to:
node-renderer: RENDERER_LOG_LEVEL=debug RENDERER_PORT=3800 node renderer/node-renderer.js
node-renderer: RENDERER_LOG_LEVEL=debug RENDERER_PORT=${RENDERER_PORT:-3800} node renderer/node-renderer.js
MSG
end

Expand All @@ -272,7 +273,7 @@ def add_pro_to_procfile
⚠️ Procfile.dev not found. Skipping Node Renderer process addition.

You'll need to add the Node Renderer to your process manager manually:
node-renderer: RENDERER_LOG_LEVEL=debug RENDERER_PORT=3800 node renderer/node-renderer.js
node-renderer: RENDERER_LOG_LEVEL=debug RENDERER_PORT=${RENDERER_PORT:-3800} node renderer/node-renderer.js
MSG
return
end
Expand All @@ -287,7 +288,8 @@ def add_pro_to_procfile
if procfile_content.match?(/^[ \t]*node-renderer:/)
say "⚠️ Procfile.dev has a node-renderer: entry that doesn't reference " \
"renderer/node-renderer.js. Update it manually to:", :yellow
say " node-renderer: RENDERER_LOG_LEVEL=debug RENDERER_PORT=3800 node renderer/node-renderer.js", :yellow
say " node-renderer: RENDERER_LOG_LEVEL=debug RENDERER_PORT=${RENDERER_PORT:-3800} " \
"node renderer/node-renderer.js", :yellow
return
end

Expand All @@ -296,7 +298,7 @@ def add_pro_to_procfile
node_renderer_line = <<~PROCFILE

# React on Rails Pro - Node Renderer for SSR
node-renderer: RENDERER_LOG_LEVEL=debug RENDERER_PORT=3800 node renderer/node-renderer.js
node-renderer: RENDERER_LOG_LEVEL=debug RENDERER_PORT=${RENDERER_PORT:-3800} node renderer/node-renderer.js
Comment thread
justin808 marked this conversation as resolved.
PROCFILE

append_to_file("Procfile.dev", node_renderer_line)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,37 @@
# Shakapacker reads SHAKAPACKER_DEV_SERVER_PORT on both the Ruby (proxy) and
# JS (webpack-dev-server) sides, so no shakapacker.yml changes are needed.
#
# === Coding Agent / CI Integration ===
# Set REACT_ON_RAILS_BASE_PORT to derive all ports from a single value.
# This works with any tool (Conductor.build, Codex, Quad Code, etc.).
# Ports are assigned as: Rails = base+0, webpack = base+1, renderer = base+2.
# When set, no manual port configuration is needed.
#
# Conductor.build sets CONDUCTOR_PORT automatically, which is recognized
# as a fallback when REACT_ON_RAILS_BASE_PORT is not set.
#
# === Manual Worktree Setup ===
# Example for a second worktree:
# PORT=3001
# SHAKAPACKER_DEV_SERVER_PORT=3036
# RENDERER_PORT=3801 # React on Rails Pro only
# REACT_RENDERER_URL=http://localhost:3801 # Must match RENDERER_PORT

# Base port for coding agent tools (derives all other ports automatically)
# REACT_ON_RAILS_BASE_PORT=

# 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

# Node renderer port (React on Rails Pro only, default: 3800)
# RENDERER_PORT=3800

# Node renderer URL (React on Rails Pro only, must match RENDERER_PORT).
# If you set only RENDERER_PORT, bin/dev auto-derives
# REACT_RENDERER_URL=http://localhost:RENDERER_PORT. For a remote or
# non-localhost renderer (Docker service name, remote host), set
# REACT_RENDERER_URL explicitly so it is not replaced with the localhost default.
# REACT_RENDERER_URL=http://localhost:3800
Loading
Loading