Skip to content

Commit 29ecb3b

Browse files
committed
Merge remote-tracking branch 'origin/main' into jg/3122-rolling-deploy-adapter
Brings in #3142 (REACT_ON_RAILS_BASE_PORT for concurrent worktree ports) and #3124 (renderer cache staging alignment with bundle-hash layout). Conflict resolution preserved this branch's rolling-deploy adapter additions: - CHANGELOG: kept rolling-deploy-adapter entry alongside main's updated "Unified renderer cache staging" entry. - docs/pro/node-renderer.md: kept the rolling-deploy-adapter-aware "Rolling deploys" section over main's interim copy. - doctor.rb / doctor_spec.rb: kept check_rolling_deploy_adapter while adopting main's multi-line .kamal/ suggestion, config/deploy/staging.rb scan path, and staging-config spec. - react_on_rails_pro.rb / assets_precompile.rb: kept rolling_deploy_cache_stager require, publish_current_bundle_if_configured call, and supporting private methods. - pre_seed_renderer_cache.rb / renderer_cache_helpers.rb: kept the branch's refactor that moves stage_file / make_relative_symlink into helpers and threads current_hashes into RollingDeployCacheStager.call. - assets.rake: adopted main's NodeRenderer guard and BlockLength disable. - specs: kept rolling_deploy_adapter stub on configuration doubles.
2 parents 879993f + 505920e commit 29ecb3b

37 files changed

Lines changed: 3061 additions & 120 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,3 +101,4 @@ packages/**/node_modules/
101101
junit.xml
102102
.lycheecache
103103
.playwright-cli/
104+
.gstack/

.lychee.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ exclude = [
3232
'^https://invite\.reactrails\.com/?$', # Slack invite endpoint returns 403 for automated requests
3333
'^https://docs\.google\.com', # Private docs require auth
3434
'^https://claude\.ai', # Returns 403 for automated requests
35+
'^https://bencher\.dev/(console/projects|perf)/react-on-rails-t8a9ncxo(/.*)?$', # Dashboard and perf URLs require auth; return 403
3536
'^https://(www\.)?110grill\.com/?$', # Returns 403 for automated requests
3637

3738
# ============================================================================

CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ After a release, run `/update-changelog` in Claude Code to analyze commits, writ
2424

2525
### [Unreleased]
2626

27+
#### Added
28+
29+
- **`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).
30+
- **[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).
31+
2732
#### Removed
2833

2934
- **[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).
@@ -37,15 +42,17 @@ After a release, run `/update-changelog` in Claude Code to analyze commits, writ
3742
#### Changed
3843

3944
- **[Pro]** **`PreSeedRendererCache` and `PrepareNodeRenderBundles` now auto-stage `loadable-stats.json`**: `ReactOnRailsPro::RendererCacheHelpers.collect_assets` now appends `loadable-stats.json` whenever the file exists on disk, so every caller (rolling-deploy seeding, `pre_seed_renderer_cache`, `pre_stage_bundle_for_node_renderer`) stages it automatically. **Action required for upgraders:** if your `assets_to_copy` config explicitly listed `loadable-stats.json`, remove that entry — otherwise you'll see a "Duplicate asset basenames in assets_to_copy" warning on every stage. The duplicate is harmless (`stage_assets` keeps the last entry per basename), but the warning is noise.
40-
- **[Pro]** **Unified renderer cache staging**: `ReactOnRailsPro::PreSeedRendererCache.call(mode: :copy | :symlink)` is now the single entry point for staging the Node Renderer cache. Both modes produce the same `<cache>/<bundleHash>/<bundleHash>.js` layout. The `react_on_rails_pro:pre_seed_renderer_cache` rake task accepts `MODE=copy` (default; Docker/image builds) or `MODE=symlink` (same-filesystem). The auto-invocation at the end of `assets:precompile` defaults to `:symlink` (preserving prior behavior) and now honors `ASSETS_PRECOMPILE_RENDERER_CACHE_MODE=copy|symlink` so Docker builds that run `rake assets:precompile` as the final asset step can opt into copy mode without invoking the rake task separately. `MODE=copy` raises a clear error when neither `RENDERER_SERVER_BUNDLE_CACHE_PATH` nor `RENDERER_BUNDLE_PATH` is set in non-dev/test environments, because the Node renderer's default lookup can differ from the Ruby side and would silently drop pre-seeded bundles in the wrong directory. The legacy `react_on_rails_pro:pre_stage_bundle_for_node_renderer` task and `ReactOnRailsPro::PrepareNodeRenderBundles` class remain as deprecated shims that emit a once-per-process warning and delegate to `mode: :symlink`. `react_on_rails:doctor` flags deploy scripts that still reference the deprecated task. [PR 3124](https://github.com/shakacode/react_on_rails/pull/3124) by [justin808](https://github.com/justin808).
45+
- **[Pro]** **Unified renderer cache staging**: `ReactOnRailsPro::PreSeedRendererCache.call(mode: :copy | :symlink)` is now the single entry point for staging the Node Renderer cache. Both modes produce the same `<cache>/<bundleHash>/<bundleHash>.js` layout. The `react_on_rails_pro:pre_seed_renderer_cache` rake task accepts `MODE=copy` (default; Docker/image builds) or `MODE=symlink` (same-filesystem). The auto-invocation at the end of `assets:precompile` defaults to `:symlink` (preserving prior behavior) and now honors `ASSETS_PRECOMPILE_RENDERER_CACHE_MODE=copy|symlink` so Docker builds that run `rake assets:precompile` as the final asset step can opt into copy mode without invoking the rake task separately. `MODE=copy` raises a clear error when neither `RENDERER_SERVER_BUNDLE_CACHE_PATH` nor `RENDERER_BUNDLE_PATH` is set in non-dev/test environments, because the Node renderer's default lookup can differ from the Ruby side and would silently drop pre-seeded bundles in the wrong directory. The legacy `react_on_rails_pro:pre_stage_bundle_for_node_renderer` task and `ReactOnRailsPro::PrepareNodeRenderBundles` class remain as deprecated shims that emit a once-per-process warning and delegate to `mode: :symlink`. `react_on_rails:doctor` flags deploy scripts that still reference the deprecated task. **Heads-up for custom scripts:** the previous flat layout wrote `$RENDERER_BUNDLE_PATH/<renderer_bundle_file_name>`; any external scripts (health checks, renderer launchers) that read that path directly must now read `$RENDERER_SERVER_BUNDLE_CACHE_PATH/<bundleHash>/<bundleHash>.js` instead. [PR 3124](https://github.com/shakacode/react_on_rails/pull/3124) by [justin808](https://github.com/justin808).
4146
- **[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).
47+
- **[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).
4248

4349
#### Fixed
4450

4551
- **Doctor now honors nested JavaScript package roots**: `react_on_rails:doctor` now checks package-manager lockfiles, `package.json`, and installed React from the configured `node_modules_location`, reducing false diagnostics for legacy apps that keep dependencies under `client/`. The Vite migration guide now documents the supported thin-wrapper pattern for those layouts. Note: a missing `package.json` at the configured `node_modules_location` now emits a warning instead of being silently skipped, so apps misconfigured against a nonexistent path will see new diagnostics on upgrade. Fixes [Issue 3205](https://github.com/shakacode/react_on_rails/issues/3205). [PR 3220](https://github.com/shakacode/react_on_rails/pull/3220) by [justin808](https://github.com/justin808).
4652
- **Generated pack regeneration is now serialized**: `generate_packs_if_stale` now uses a Rails `tmp/` lock file, re-checks staleness after waiting, and avoids concurrent cleanup/regeneration races when multiple processes trigger auto-bundling at the same time. Fixes [Issue 1627](https://github.com/shakacode/react_on_rails/issues/1627). [PR 3231](https://github.com/shakacode/react_on_rails/pull/3231) by [justin808](https://github.com/justin808).
4753
- **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).
4854
- **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).
55+
- **`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).
4956
- **[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).
5057
- **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).
5158
- **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).

docs/oss/building-features/process-managers.md

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,129 @@ SHAKAPACKER_DEV_SERVER_PORT=3036
371371

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

374+
### Coding Agent / CI Integration
375+
376+
When using coding agent tools that run multiple workspaces concurrently
377+
([Conductor.build](https://conductor.build), OpenAI Codex, Quad Code, etc.),
378+
set `REACT_ON_RAILS_BASE_PORT` to derive all service ports from a single value.
379+
This eliminates the need for per-worktree `.env` files.
380+
381+
> **Important:** Run each live `bin/dev` stack from a separate checkout, worktree,
382+
> or copied app directory. Starting two stacks from the exact same app path is
383+
> not supported because build tools such as ReScript and webpack watchers share
384+
> lock files and runtime artifacts within that directory.
385+
386+
`bin/dev` assigns ports using fixed offsets from the base:
387+
388+
| Service | Offset | Example (base=4000) |
389+
| ------------------- | ------ | ------------------- |
390+
| Rails server | +0 | 4000 |
391+
| Webpack dev server | +1 | 4001 |
392+
| Node renderer (Pro) | +2 | 4002 |
393+
| _(reserved)_ | +3–+9 | 4003–4009 |
394+
395+
When a base port is detected, `bin/dev` also sets `RENDERER_PORT` and
396+
`REACT_RENDERER_URL` automatically so the Pro Node Renderer and Rails
397+
initializer agree on the port without any additional configuration.
398+
399+
> **Heads up:** setting `RENDERER_PORT`, `RENDERER_URL`, or `REACT_RENDERER_URL`
400+
> in your environment activates the Pro renderer code path even in OSS apps —
401+
> in base-port mode this means `RENDERER_PORT` and `REACT_RENDERER_URL` will be
402+
> derived from the base and propagated to spawned processes. If you use
403+
> `RENDERER_PORT` in your environment for an unrelated purpose, rename your
404+
> variable to avoid the side effect.
405+
406+
> **Note:** Base-port mode derives the node renderer URL as
407+
> `http://localhost:<port>`. If you run the renderer in a Docker container or
408+
> on a remote host (e.g. `REACT_RENDERER_URL=http://renderer:3800`), do not use
409+
> base-port mode — set `REACT_RENDERER_URL` explicitly and use the
410+
> [manual worktree setup](#manual-worktree-port-setup-pro) instead. `bin/dev`
411+
> will warn at runtime if a pre-set `REACT_RENDERER_URL` is overridden.
412+
413+
**Recognized environment variables** (checked in order):
414+
415+
1. `REACT_ON_RAILS_BASE_PORT` — the canonical base port variable; any tool can set this.
416+
2. `CONDUCTOR_PORT` — set automatically by [Conductor.build](https://conductor.build).
417+
418+
> **Note on `CONDUCTOR_PORT`:** React on Rails treats `CONDUCTOR_PORT` as the
419+
> **base** of a consecutive port block (Rails = base + 0, webpack = base + 1,
420+
> renderer = base + 2). This interpretation is not part of a public Conductor
421+
> API; treat `CONDUCTOR_PORT` support as best-effort until Conductor documents
422+
> the contract. If a future Conductor release redefines `CONDUCTOR_PORT` (for
423+
> example, to mean the Rails port itself), override by setting
424+
> `REACT_ON_RAILS_BASE_PORT` explicitly — it takes precedence and uses the
425+
> same derivation rules.
426+
427+
**Priority chain:** base port > explicit per-service env vars (`PORT`, etc.) > auto-detect free ports.
428+
429+
**Example: setting the base port in a tool's configuration:**
430+
431+
```sh
432+
# In your agent tool's workspace setup or .env
433+
REACT_ON_RAILS_BASE_PORT=4000
434+
```
435+
436+
#### Tool-specific setup
437+
438+
[Conductor.build](https://conductor.build) sets `CONDUCTOR_PORT` for you — no configuration
439+
needed. Other tools (Claude Code CLI, [OpenAI Codex](https://github.com/openai/codex) CLI or app,
440+
plain `git worktree`, etc.) don't inject a port variable, so pick a different base per checkout
441+
using one of the options below.
442+
443+
- **Per-worktree `.env` file** (tool-agnostic; gitignored by default):
444+
445+
```sh
446+
# .env at the root of each worktree
447+
REACT_ON_RAILS_BASE_PORT=4000
448+
```
449+
450+
- **Claude Code `.claude/settings.json`** (per-project, checked in or local):
451+
452+
```json
453+
{
454+
"env": {
455+
"REACT_ON_RAILS_BASE_PORT": "4000"
456+
}
457+
}
458+
```
459+
460+
- **Shell export** (ad hoc, one session):
461+
462+
```sh
463+
REACT_ON_RAILS_BASE_PORT=4000 bin/dev
464+
```
465+
466+
`bin/dev` reads the variable from the process environment regardless of how it was set, so mix
467+
and match whichever is most convenient for each tool.
468+
469+
If you create a new checkout by copying an existing app directory while another
470+
`bin/dev` is running, the copy can inherit `tmp/pids/server.pid` or Overmind
471+
socket files from the original app. `bin/dev` now cleans those copied stale
472+
runtime files automatically on startup. If you launch processes outside
473+
`bin/dev`, clear those files yourself before booting the copied app.
474+
475+
### Manual Worktree Port Setup (Pro)
476+
477+
If you use the [Node Renderer](./node-renderer/basics.md) (React on Rails Pro) with manual
478+
worktrees (no base port), you need to configure the renderer port in addition to the standard
479+
Rails and webpack ports:
480+
481+
```sh
482+
# .env in each worktree
483+
PORT=3001
484+
SHAKAPACKER_DEV_SERVER_PORT=3036
485+
RENDERER_PORT=3801
486+
REACT_RENDERER_URL=http://localhost:3801
487+
```
488+
489+
The renderer port must match on both sides: `RENDERER_PORT` is read by the Node process and
490+
`REACT_RENDERER_URL` is read by the Rails-side Pro initializer.
491+
492+
> **Note:** `bin/dev kill` only stops the renderer when `RENDERER_PORT` (or `REACT_RENDERER_URL`)
493+
> is exported in the current shell. From a fresh terminal that hasn't sourced your worktree's
494+
> `.env`, run e.g. `RENDERER_PORT=3801 bin/dev kill`, or source `.env` first. Otherwise the
495+
> renderer process will silently keep running.
496+
374497
## See Also
375498

376499
- [HMR and Hot Reloading](./hmr-and-hot-reloading-with-the-webpack-dev-server.md)

docs/oss/configuration/configuration-pro.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ ReactOnRailsPro.configure do |config|
7575
# since it will be easy to intercept it.
7676
# If you provide an ENV value (maybe only for production) and there is no value, then you get the default.
7777
# Default for `renderer_url` is "http://localhost:3800".
78-
config.renderer_url = ENV["RENDERER_URL"]
78+
config.renderer_url = ENV["REACT_RENDERER_URL"]
7979

8080
# If you don't want to worry about special characters in your password within the url, use this config value
8181
# Default for `renderer_password` is nil

0 commit comments

Comments
 (0)