Skip to content

Quiet bin/dev shutdown on Ctrl-C#2652

Merged
justin808 merged 4 commits intomainfrom
jg-codex/bin-dev-interrupt-fix
Mar 30, 2026
Merged

Quiet bin/dev shutdown on Ctrl-C#2652
justin808 merged 4 commits intomainfrom
jg-codex/bin-dev-interrupt-fix

Conversation

@justin808
Copy link
Copy Markdown
Member

@justin808 justin808 commented Mar 16, 2026

Summary

  • rescue top-level Interrupt around the overmind/foreman system call so bin/dev exits quietly on Ctrl-C
  • add a generated bin/shakapacker-watch wrapper and switch Procfiles to use it for watcher processes
  • update generator/spec fixtures so the new watcher command is covered in the base and RSC flows

Why

During the fresh-app validation runs, bin/dev shut down successfully but printed Ruby interrupt backtraces from both the main process manager and the server-bundle watcher. That is a bad first-run experience immediately after following the docs.

What changed

  • ProcessManager.run_process_if_available now treats Ctrl-C during process-manager shutdown as a clean exit path
  • new generated bin/shakapacker-watch traps INT/TERM, forwards TERM to bin/shakapacker, and waits quietly
  • Procfile.dev, Procfile.dev-static-assets, and the RSC watcher insertion now use bin/shakapacker-watch --watch
  • updated generator specs and spec/dummy Procfiles to reflect the new watcher path
  • cleaned up process_manager_spec to stub described_class.system directly instead of using flaky expect_any_instance_of(Kernel) expectations

Validation

  • bundle exec rspec react_on_rails/spec/react_on_rails/dev/process_manager_spec.rb
  • bundle exec rspec react_on_rails/spec/react_on_rails/generators/install_generator_spec.rb:39
  • sh -n react_on_rails/lib/generators/react_on_rails/templates/base/base/bin/shakapacker-watch
  • manually reproduced the old backtraces in a preserved fresh app, then reran bin/dev with the patched gem code + watcher wrapper and confirmed Ctrl-C exits cleanly with no Ruby backtrace

Notes


Note

Low Risk
Low risk dev-workflow change: adjusts signal/interrupt handling and Procfile watcher commands, with minimal impact outside local development but potential for minor behavior differences in process shutdown.

Overview
Improves local bin/dev shutdown by treating Interrupt during overmind/foreman execution as a clean exit in ReactOnRails::Dev::ProcessManager (avoids noisy backtraces on Ctrl-C).

Updates generated Procfiles (including RSC watcher insertion) to run watchers via a new bin/shakapacker-watch wrapper that traps INT/TERM and forwards termination to the underlying bin/shakapacker --watch process. Generator and dummy/spec fixtures are updated accordingly, and process_manager_spec stubs described_class.system directly for more reliable expectations.

Written by Cursor Bugbot for commit 951e126. This will update automatically on new commits. Configure here.

Summary by CodeRabbit

  • New Features

    • Enhanced asset watcher process management with improved signal handling and graceful shutdown capabilities for better stability, responsiveness, and control during development workflows
  • Bug Fixes

    • Improved Ctrl-C interrupt signal handling during development process startup and initialization for faster and more reliable process termination with better developer experience

Comment on lines +17 to +18
bin/shakapacker "$@" &
child_pid=$!
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Narrow race condition: There is a window between the & (line 17) and child_pid=$! (line 18) where a signal can arrive. If INT or TERM lands in that window, cleanup runs with child_pid="", the guard [ -n "${child_pid}" ] is false, and the function calls exit 0 — leaving the already-launched bin/shakapacker child as an orphan.

The window is tiny, but in a busy CI environment it can be hit. A common mitigation is to delay trapping the signal until after child_pid is captured:

Suggested change
bin/shakapacker "$@" &
child_pid=$!
bin/shakapacker "$@" &
child_pid=$!
trap cleanup INT TERM

This is simpler than the current placement and eliminates the race entirely (signals before the background launch are still handled by the default shell behaviour, which is the same exit outcome).

If keeping the early trap, an alternative is to check-and-kill in a loop or use a wait with trap ... && kill $child_pid pattern.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Mar 16, 2026

Greptile Summary

This PR eliminates noisy Ruby interrupt backtraces when pressing Ctrl-C to stop bin/dev by addressing the problem at two levels: a new bin/shakapacker-watch shell wrapper that traps INT/TERM and exits cleanly, and a rescue Interrupt in ProcessManager.run_process_if_available that treats Ctrl-C during process-manager shutdown as a successful exit.

  • Added bin/shakapacker-watch shell script that backgrounds bin/shakapacker, traps INT/TERM signals, forwards TERM to the child, and exits with status 0 on interrupt
  • Added rescue Interrupt in ProcessManager.run_process_if_available so the Ruby side also exits cleanly on Ctrl-C
  • Updated all Procfile templates and spec/dummy Procfiles to use bin/shakapacker-watch --watch instead of bin/shakapacker --watch
  • Updated RSC setup to reference the new watcher wrapper in both the Procfile insertion and the manual-setup warning message
  • Replaced expect_any_instance_of(Kernel) with expect(described_class) stubs in process_manager_spec.rb — a welcome test quality improvement
  • Added new test cases for the Interrupt rescue path and for asserting the watcher wrapper is generated correctly

Confidence Score: 5/5

  • This PR is safe to merge — it only adds signal handling for clean shutdown with no behavioral changes to the build pipeline.
  • The changes are focused, well-tested, and low-risk. The shell script follows standard POSIX signal-handling patterns. The Ruby rescue is narrowly scoped. The generator already handles chmod on all bin/ scripts via Dir.glob. Test improvements (replacing expect_any_instance_of) are strictly better. No functional behavior of the build process is altered.
  • No files require special attention.

Important Files Changed

Filename Overview
react_on_rails/lib/generators/react_on_rails/templates/base/base/bin/shakapacker-watch New shell wrapper that traps INT/TERM, forwards TERM to child shakapacker process, and exits cleanly. Well-structured signal handling.
react_on_rails/lib/react_on_rails/dev/process_manager.rb Adds rescue Interrupt in run_process_if_available to catch Ctrl-C during overmind/foreman shutdown, returning true for a clean exit path.
react_on_rails/lib/generators/react_on_rails/rsc_setup.rb Updates RSC watcher Procfile references from bin/shakapacker to bin/shakapacker-watch, consistent with the new wrapper script.
react_on_rails/spec/react_on_rails/dev/process_manager_spec.rb Replaces flaky expect_any_instance_of(Kernel) with expect(described_class) stubs, adds test for Interrupt handling. Cleaner and more reliable test patterns.
react_on_rails/spec/react_on_rails/generators/install_generator_spec.rb Adds test coverage for the new bin/shakapacker-watch wrapper existence and Procfile references.

Sequence Diagram

sequenceDiagram
    participant User
    participant bin/dev
    participant ProcessManager
    participant Overmind/Foreman
    participant shakapacker-watch
    participant shakapacker

    User->>bin/dev: Start
    bin/dev->>ProcessManager: run_with_process_manager("Procfile.dev")
    ProcessManager->>Overmind/Foreman: system("overmind", "start", ...)
    Overmind/Foreman->>shakapacker-watch: Launch watcher process
    shakapacker-watch->>shakapacker: bin/shakapacker "$@" & (backgrounded)
    shakapacker-watch->>shakapacker-watch: wait $child_pid

    User->>Overmind/Foreman: Ctrl-C (SIGINT)
    Overmind/Foreman->>shakapacker-watch: SIGINT/SIGTERM
    shakapacker-watch->>shakapacker-watch: trap fires cleanup()
    shakapacker-watch->>shakapacker: kill -TERM
    shakapacker-->>shakapacker-watch: exits
    shakapacker-watch->>shakapacker-watch: exit 0 (clean)
    Overmind/Foreman-->>ProcessManager: Interrupt raised
    ProcessManager->>ProcessManager: rescue Interrupt → true
    ProcessManager-->>bin/dev: Clean exit (no backtrace)
Loading

Last reviewed commit: ddf61ce

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ddf61ce716

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


# React on Rails Pro - RSC bundle watcher
rsc-bundle: RSC_BUNDLE_ONLY=yes bin/shakapacker --watch
rsc-bundle: RSC_BUNDLE_ONLY=yes bin/shakapacker-watch --watch
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Ensure RSC generator installs shakapacker-watch

This line switches the RSC watcher to bin/shakapacker-watch --watch, but standalone rails g react_on_rails:rsc only appends to Procfile.dev and never creates bin/shakapacker-watch. That standalone path does not run InstallGenerator#add_bin_scripts, so apps that were installed before this commit will get a Procfile entry pointing to a missing executable, and bin/dev/foreman fails with No such file or directory when starting rsc-bundle.

Useful? React with 👍 / 👎.

Comment thread react_on_rails/spec/dummy/Procfile.dev
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 16, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 394be56b-c689-4624-9a1f-31c055746473

📥 Commits

Reviewing files that changed from the base of the PR and between b41c506 and 951e126.

📒 Files selected for processing (2)
  • react_on_rails/lib/generators/react_on_rails/templates/base/base/bin/shakapacker-watch
  • react_on_rails/spec/dummy/bin/shakapacker-watch
🚧 Files skipped from review as they are similar to previous changes (1)
  • react_on_rails/lib/generators/react_on_rails/templates/base/base/bin/shakapacker-watch

Walkthrough

The PR introduces a shakapacker-watch wrapper script to manage the shakapacker process with proper signal handling for graceful shutdown. All Procfile references across templates and specs are updated to invoke this wrapper instead of calling shakapacker directly. The process manager is also updated to handle Interrupt exceptions gracefully.

Changes

Cohort / File(s) Summary
Shakapacker-watch wrapper scripts
lib/generators/.../base/bin/shakapacker-watch, spec/dummy/bin/shakapacker-watch
New shell script that launches shakapacker in background, traps INT/TERM signals, and propagates the child process exit code after cleanup.
Procfile template updates
lib/generators/.../Procfile.dev, lib/generators/.../Procfile.dev-static-assets, spec/dummy/Procfile.dev*
Updated five Procfile files to invoke bin/shakapacker-watch --watch instead of bin/shakapacker --watch for server-bundle and js/webpack processes.
Generator and process manager
lib/generators/react_on_rails/rsc_setup.rb, lib/react_on_rails/dev/process_manager.rb
RSC generator updated to reference new wrapper script in warning text and Procfile additions; process manager now rescues Interrupt exceptions and returns true.
Test updates
spec/.../process_manager_spec.rb, spec/.../install_generator_spec.rb, spec/.../rsc_generator_spec.rb
Refactored process manager specs to mock ProcessManager.system and ProcessManager.exit directly; added assertions verifying wrapper script generation and Procfile references; added test case for Interrupt exception handling.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A wrapper hops in with signals so keen,
To catch INT and TERM in between,
Child processes managed with grace and care,
Procfiles updated everywhere!
Shutdown cleanup now polished and bright,
Dev processes exit just right! 🌟

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title directly and concisely summarizes the main change: suppressing interrupt backtraces during bin/dev shutdown triggered by Ctrl-C.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch jg-codex/bin-dev-interrupt-fix

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Mar 16, 2026

CLAUDE.md

Tool-specific guidance for Claude Code in this repository.

Source of Truth

AGENTS.md is the canonical policy for:

  • Commands, tests, and lint workflow
  • Formatting and style requirements
  • Git/PR safety boundaries
  • Project directory boundaries

If this file conflicts with AGENTS.md, follow AGENTS.md.

Behavioral Defaults

  • When confident in your changes, commit and push without asking for permission. Always monitor CI after pushing.

Git Safety

  • NEVER force-push (--force, --force-with-lease) unless the user explicitly requests it. Force-pushing destroys commit history that may represent significant prior work.
  • NEVER git reset --hard on a branch that has existing commits (yours or others'). This destroys work.
  • When you need to start fresh or test something without affecting an existing branch, use a git worktree (git worktree add) or create a new branch instead of resetting the current one.
  • If a rebase has conflicts, abort and ask the user how to proceed rather than force-pushing a rewritten history.

Claude-Specific Workflow

Use these docs for Claude-oriented operational guidance:

  • .claude/docs/avoiding-ci-failure-cycles.md
  • .claude/docs/replicating-ci-failures.md
  • .claude/docs/playwright-e2e-testing.md
  • .claude/docs/merge-conflict-workflow.md
  • .claude/docs/pr-splitting-strategy.md
  • .claude/docs/changelog-guidelines.md
  • .claude/docs/project-architecture.md
  • .claude/docs/rails-engine-nuances.md
  • .claude/docs/debugging-webpack.md
  • .claude/docs/rbs-type-checking.md
  • .claude/docs/conductor-compatibility.md
  • .claude/docs/testing-build-scripts.md
  • .claude/docs/master-health-monitoring.md
  • .claude/docs/managing-file-paths.md

For Pro-package specifics, also read react_on_rails_pro/CLAUDE.md.

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Missing bin/shakapacker-watch in spec/dummy directory
    • Added the missing executable spec/dummy/bin/shakapacker-watch script so all updated dummy Procfiles now reference an existing watcher command.

Create PR

Or push these changes by commenting:

@cursor push aa0c1272de
Preview (aa0c1272de)
diff --git a/react_on_rails/spec/dummy/bin/shakapacker-watch b/react_on_rails/spec/dummy/bin/shakapacker-watch
new file mode 100644
--- /dev/null
+++ b/react_on_rails/spec/dummy/bin/shakapacker-watch
@@ -1,0 +1,27 @@
+#!/usr/bin/env sh
+set -eu
+
+child_pid=""
+
+cleanup() {
+  if [ -n "${child_pid}" ] && kill -0 "$child_pid" 2>/dev/null; then
+    kill -TERM "$child_pid" 2>/dev/null || true
+    wait "$child_pid" 2>/dev/null || true
+  fi
+
+  exit 0
+}
+
+trap cleanup INT TERM
+
+bin/shakapacker "$@" &
+child_pid=$!
+
+if wait "$child_pid"; then
+  status=0
+else
+  status=$?
+fi
+
+child_pid=""
+exit "$status"

Comment thread react_on_rails/spec/dummy/Procfile.dev
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Mar 16, 2026

AGENTS.md

Instructions for AI coding agents working on the React on Rails codebase.

React on Rails is a Ruby gem + npm package that integrates React with Ruby on Rails, providing server-side rendering (SSR) via Node.js or ExecJS. This is a monorepo: the open-source gem lives at react_on_rails/, the npm package at packages/react-on-rails/, and the Pro package at react_on_rails_pro/.

Canonical Agent Policy

AGENTS.md is the canonical source for repository-wide agent rules:

  • Commands and test/lint workflow
  • Code style and formatting expectations
  • Git/PR boundaries and safety rules
  • Directory and documentation boundaries

Other agent-facing docs (for example CLAUDE.md) should contain only tool-specific workflow notes and link back here.
If there is a conflict, AGENTS.md wins.

Commands

# Install dependencies
bundle && pnpm install

# Build TypeScript → JavaScript
pnpm run build

# Lint (MANDATORY before every commit)
bundle exec rubocop                  # Ruby — must pass with zero offenses
pnpm run lint                        # JS/TS via ESLint
pnpm start format.listDifferent      # Check Prettier formatting
rake lint                            # All linting (Ruby + JS + formatting)

# Auto-fix formatting
rake autofix                         # Preferred for all formatting

# Run tests
rake run_rspec:gem                   # Ruby unit tests (gem code)
rake run_rspec:dummy                 # Ruby integration tests (dummy Rails app)
pnpm run test                        # JavaScript/TypeScript tests
rake                                 # Full suite (lint + all tests except examples)

# Type checking
pnpm run type-check                  # TypeScript
bundle exec rake rbs:validate        # RBS signatures

# Additional test subsets
rake run_rspec                       # All Ruby tests
rake all_but_examples                # All tests except generated examples
rake run_rspec:shakapacker_examples_basic  # Single example test

# Full initial setup
bundle && pnpm install && rake shakapacker_examples:gen_all && rake node_package && rake

# CI/workflow linting
actionlint                           # GitHub Actions lint
yamllint .github/                    # YAML lint (do NOT run RuboCop on .yml files)

# Dependency version updates
rake shakapacker:update_version[9.6.1]  # Update shakapacker across the monorepo

Updating Shakapacker

Use rake shakapacker:update_version[VERSION] to update shakapacker across the entire monorepo. This single command updates all Gemfiles, package.json files, Gemfile.lock files, and pnpm-lock.yaml. Do not manually edit individual version references — always use the rake task to keep everything in sync.

The task handles Ruby version switching for apps that require a different Ruby version (set RUBY_VERSION_MANAGER to rvm, rbenv, asdf, or mise if needed; defaults to rvm). It continues gracefully if a single lock file update fails (e.g., due to a missing Ruby version).

Testing

  • Prefer local testing over CI iteration — don't push "hopeful" fixes. Apply the 15-minute rule: if 15 more minutes of local testing would catch the issue before CI does, spend the 15 minutes.
  • Never claim a test is "fixed" without running it locally first. Use "This SHOULD fix..." or "Proposed fix (UNTESTED)" for unverified changes.
  • Ruby: RSpec. Unit tests in react_on_rails/spec/react_on_rails/, integration tests via a dummy Rails app in react_on_rails/spec/dummy/.
  • JavaScript/TypeScript: Jest. Tests in packages/react-on-rails/tests/.
  • E2E: Playwright. Tests in react_on_rails/spec/dummy/e2e/playwright/e2e/. Run with cd react_on_rails/spec/dummy && pnpm test:e2e.
  • The dummy app (react_on_rails/spec/dummy/) is a full Rails application used for integration testing. Many tests require it.

Run specific test files:

bundle exec rspec react_on_rails/spec/react_on_rails/path/to/spec.rb
cd react_on_rails/spec/dummy && bundle exec rspec spec/path/to/spec.rb

Project Structure

Directory Purpose
react_on_rails/lib/react_on_rails/ Ruby gem source — helpers, configuration, SSR pool, engine
react_on_rails/lib/generators/ Rails generators for react_on_rails:install
react_on_rails/spec/ RSpec tests (unit + integration via dummy app)
react_on_rails/spec/dummy/ Full Rails app for integration testing and E2E
packages/react-on-rails/src/ TypeScript source — client-side React integration
packages/react-on-rails/tests/ Jest tests for the npm package
react_on_rails_pro/ Pro package (separate gem + npm, own lint config)
rakelib/ Rake task definitions
docs/oss/ OSS documentation — published to the ShakaCode website
docs/pro/ Pro documentation — installation, configuration, RSC, node renderer, caching
internal/contributor-info/ Internal contributor docs (not published to the website)
internal/planning/ Internal planning docs, drafts, and historical analysis
internal/react_on_rails_pro/contributors-info/ Internal Pro contributor docs (not published to the website)
analysis/ Investigation and analysis documents (kebab-case .md files)

Code Style

Ruby (RuboCop)

Line length max 120 characters. Run bundle exec rubocop [file] to check.

Line length — break long chains:

# Bad
content = pack_content.gsub(/import.*from.*['"];/, "").gsub(/ReactOnRails\.register.*/, "")

# Good
content = pack_content.gsub(/import.*from.*['"];/, "")
                      .gsub(/ReactOnRails\.register.*/, "")

Named subjects in RSpec:

# Bad
subject { instance.method_name(arg) }

# Good
subject(:method_result) { instance.method_name(arg) }

Security violations — scope disable comments tightly:

# rubocop:disable Security/Eval
expect { evaluate(sanitized_content) }.not_to raise_error
# rubocop:enable Security/Eval

JavaScript/TypeScript

Prettier handles all formatting. Never manually format — run rake autofix instead.

Git Workflow

Branch naming: type/descriptive-name (e.g., fix/ssr-hydration-mismatch)

Commit messages: Explain why, not what. One logical change per commit.

PR creation: Use gh pr create with a clear title, summary, and test plan.

Review Workflow

For small, focused PRs (roughly 5 files changed or fewer and one clear purpose):

  • Use at most one AI reviewer that leaves inline comments. Additional AI tools should be summary-only or used manually.
  • Wait for the first full review pass to finish before pushing follow-up commits.
  • Batch review fixes into one follow-up push when practical. Do not create a new commit for each minor comment.
  • Treat as blocking only: correctness bugs, failing tests, regressions, and clear inconsistencies with adjacent code. Nits and style suggestions are optional unless a maintainer asks for them.
  • Verify language, runtime, and library claims locally before changing code in response to AI review comments.
  • Deduplicate repeated bot comments before acting on them. Fix the underlying issue once, then resolve the duplicates.
  • Rebase or merge master once, near the end of the review cycle. For CHANGELOG.md conflicts, prefer resolving them as the final step before merge.
  • When asking an agent to address review comments, instruct it to classify comments into blocking, optional, and noise, then apply only the blocking items plus any explicitly selected optional items.

Boundaries

Always

  • Run bundle exec rubocop before committing — CI will reject violations
  • Use pnpm for all JS operations — never npm or yarn
  • Use bundle exec for Ruby commands
  • Ensure all files end with a newline
  • Let Prettier and RuboCop handle formatting — never format manually

Ask First

  • Destructive git operations (force push, reset --hard, branch deletion)
  • Changes to CI workflows (.github/workflows/)
  • Changes to build configuration (package.json scripts, webpack config)
  • Modifying the Pro package (react_on_rails_pro/)

Never

  • Skip pre-commit hooks (--no-verify)
  • Commit secrets, credentials, or .env files
  • Commit package-lock.json, yarn.lock, or other non-pnpm lock files
  • Add files to the docs/ root — OSS docs go in docs/oss/ subdirectories (getting-started/, core-concepts/, building-features/, configuration/, api-reference/, deployment/, migrating/, upgrading/, misc/); Pro docs go in docs/pro/
  • Force push to main or master

Key Concept: File Suffixes vs. RSC Directive

React on Rails has two independent systems that both use "client" and "server" terminology. Do not confuse them.

1. Bundle Placement (.client. / .server. file suffixes)

A React on Rails auto-bundling feature that controls which webpack bundle imports a file. This exists independently of React Server Components and is used with or without RSC:

  • Component.client.jsx → imported only in the client bundle (browser)
  • Component.server.jsx → imported only in the server bundle (and RSC bundle when RSC enabled)
  • Component.jsx (no suffix) → imported in both bundles

This controls where the source file is loaded, nothing more. A .server.jsx file is NOT a React Server Component — it is simply a file that webpack includes in the server bundle (and the RSC bundle when RSC is enabled). These suffixes only make sense for client components, as server components exist only in the RSC bundle.

2. RSC Classification ('use client' directive)

The 'use client' directive is part of the React Server Components architecture. It marks a component as a React Client Component. Components without it are treated as React Server Components.

When auto-bundling is enabled with RSC support (Pro feature), React on Rails uses this directive to control:

  • Registration: 'use client'ReactOnRails.register(), no 'use client'registerServerComponent()
  • RSC bundling: The RSC webpack loader uses this directive to decide whether a component is included in the RSC bundle or replaced with a client reference in that bundle

The client_entrypoint? method in packs_generator.rb checks for this directive.

They Are Orthogonal

A .client.jsx file can be a React Server Component (if it lacks 'use client'), and a .server.jsx file can be a React Client Component (if it has 'use client'). In practice, paired .client./.server. files should have consistent 'use client' status because the client and server must agree on the component's RSC role for hydration to work.

Changelog

Update /CHANGELOG.md for user-visible changes only (features, bug fixes, breaking changes, deprecations, performance improvements). Do not add entries for linting, formatting, refactoring, tests, or doc fixes.

  • Format: [PR 1818](https://github.com/shakacode/react_on_rails/pull/1818) by [username](https://github.com/username) (no hash before PR number)
  • Pro-only changes use an inline **[Pro]** tag prefix within the standard category sections (e.g., - **[Pro]** **Feature name**: Description...); do NOT create separate #### Pro subsections

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Mar 16, 2026

test single line

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Mar 16, 2026

React on Rails

React on Rails integrates React with Ruby on Rails, providing server-side rendering (SSR), component registration, and seamless Rails view integration. React on Rails Pro adds advanced features: a high-performance Node.js rendering server, fragment caching, prerender caching, React Server Components (RSC), and streaming SSR.

Package Relationships

React on Rails consists of Ruby gems and npm packages that must be paired correctly:

Open-Source (base):

  • Ruby gem: react_on_rails
  • npm package: react-on-rails

Pro (commercial, extends base):

  • Ruby gem: react_on_rails_pro (depends on react_on_rails)
  • npm package: react-on-rails-pro (REPLACES react-on-rails, do NOT use both)
  • npm package: react-on-rails-pro-node-renderer (standalone Fastify SSR server, optional)

IMPORTANT: When using the react_on_rails_pro gem, you MUST use the react-on-rails-pro npm package. The base react-on-rails npm package will be rejected at runtime.

Quick Setup

Base (open-source):

rails generate react_on_rails:install

Pro with Node Renderer:

bundle add react_on_rails_pro
rails generate react_on_rails:install --pro

Pro with React Server Components:

bundle add react_on_rails_pro
rails generate react_on_rails:install --rsc

Node Renderer API

The Node Renderer is a Fastify-based Node.js server for high-performance SSR.

Correct usage:

const { reactOnRailsProNodeRenderer } = require('react-on-rails-pro-node-renderer');

reactOnRailsProNodeRenderer({
  serverBundleCachePath: path.resolve(__dirname, '.node-renderer-bundles'),
  port: 3800,
  supportModules: true,
  password: process.env.RENDERER_PASSWORD,
  workersCount: 3,
  logLevel: 'info',
});

The function name is reactOnRailsProNodeRenderer (NOT createServer or startRenderer).
The config key for the bundle cache directory is serverBundleCachePath (NOT bundlePath, which is deprecated).

Rails-side configuration:

# config/initializers/react_on_rails_pro.rb
ReactOnRailsPro.configure do |config|
  config.server_renderer = "NodeRenderer"
  config.renderer_url = ENV.fetch("REACT_RENDERER_URL", "http://localhost:3800")
  config.renderer_password = ENV.fetch("RENDERER_PASSWORD", "devPassword")
  config.prerender_caching = true
end

Key attributes: server_renderer, renderer_url, renderer_password, prerender_caching, ssr_timeout, renderer_use_fallback_exec_js, throw_js_errors, tracing, dependency_globs.

Webpack server config requirements (for Pro Node Renderer):

// serverWebpackConfig.js output section
libraryTarget: 'commonjs2',

// after output section
serverWebpackConfig.target = 'node';

Client-Side Component Registration

// With base package
import ReactOnRails from 'react-on-rails';
ReactOnRails.register({ MyComponent });

// With Pro package (use this when react_on_rails_pro gem is installed)
import ReactOnRails from 'react-on-rails-pro';
ReactOnRails.register({ MyComponent });

Pro-Exclusive Imports

import { RSCRoute } from 'react-on-rails-pro/RSCRoute';
import registerServerComponent from 'react-on-rails-pro/registerServerComponent/client';
import { wrapServerComponentRenderer } from 'react-on-rails-pro/wrapServerComponentRenderer/client';

Documentation Links

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Mar 16, 2026

test 2

@justin808 justin808 force-pushed the jg-codex/docs-validation-phase1 branch from 7a4915e to 30668d4 Compare March 17, 2026 22:01
@justin808 justin808 force-pushed the jg-codex/bin-dev-interrupt-fix branch from ddf61ce to c07a2c1 Compare March 17, 2026 22:01
Comment thread react_on_rails/lib/react_on_rails/dev/process_manager.rb
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c07a2c1e88

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

rails: bundle exec rails s -p ${PORT:-3000}
dev-server: bin/shakapacker-dev-server
server-bundle: SERVER_BUNDLE_ONLY=yes bin/shakapacker --watch
server-bundle: SERVER_BUNDLE_ONLY=yes bin/shakapacker-watch --watch
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Update watcher auto-detection for wrapped command

Switching the generated server-bundle process to bin/shakapacker-watch --watch breaks ServerManager#shakapacker_watch_process_running?, which still only searches for SERVER_BUNDLE_ONLY=yes bin/shakapacker --watch / bin/shakapacker --watch in react_on_rails/lib/react_on_rails/dev/server_manager.rb (lines 333-345). In apps generated from this template, bin/dev test-watch in --test-watch-mode=auto will no longer detect an existing dev watcher and will incorrectly choose full mode, causing duplicate watch processes in the shared-output workflow this detection is meant to avoid.

Useful? React with 👍 / 👎.

@@ -1,2 +1,2 @@
web: bin/rails server -p ${PORT:-3000}
js: bin/shakapacker --watch
js: bin/shakapacker-watch --watch
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep doctor static-Procfile detection in sync

This template now uses bin/shakapacker-watch --watch, but Doctor#static_procfile_available? still checks for bin/shakapacker --watch in react_on_rails/lib/react_on_rails/doctor.rb:1442. For newly generated apps, that check returns false even when Procfile.dev-static-assets is present, so the shared-output-path diagnostics can emit the wrong “static Procfile missing” guidance.

Useful? React with 👍 / 👎.

@justin808 justin808 force-pushed the jg-codex/docs-validation-phase1 branch from 30668d4 to 1da23d5 Compare March 17, 2026 22:17
@justin808 justin808 force-pushed the jg-codex/bin-dev-interrupt-fix branch from c07a2c1 to 054740b Compare March 17, 2026 22:27
@justin808 justin808 force-pushed the jg-codex/docs-validation-phase1 branch from 1da23d5 to 03111e6 Compare March 17, 2026 22:28
Comment thread react_on_rails/lib/react_on_rails/dev/process_manager.rb
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Mar 17, 2026

Review: Quiet bin/dev shutdown on Ctrl-C

Overall this is a clean, well-scoped fix for a real first-run UX problem. The two-pronged approach (shell-level signal forwarding in bin/shakapacker-watch + Ruby-level rescue Interrupt in ProcessManager) is the right architecture. The test cleanup replacing expect_any_instance_of(Kernel) with expect(described_class) is a nice improvement in its own right.

Issues

1. cleanup() always exits 0 — no comment explaining why (inline)
The shell script exits 0 on both INT and TERM. This is intentional but unconventional (Unix norm is 128+signal). Any tool that inspects watcher exit codes for health monitoring will see a clean exit even on TERM. A comment explaining the deliberate choice is needed; see the inline note on line 12 of bin/shakapacker-watch.

2. rescue Interrupt scope is wider than the comment implies (inline)
The rescue covers the entire run_process_if_available method, including the installed? version-check phase before the process manager starts. This is almost certainly fine, but the current comment only mentions "overmind/foreman shutdown" and should also cover the startup case. See inline on process_manager.rb:102.

3. Relative bin/shakapacker path is implicit (inline)
The wrapper assumes it is run from the Rails root, which is standard for Procfile processes but nowhere documented. See inline on bin/shakapacker-watch:17.

Observations (no action needed)

  • Executable bit: Handled correctly. install_generator.rb already does Dir.glob over the template bin dir and File.chmod(0o755) on all results, so bin/shakapacker-watch will be made executable automatically on generation.
  • Procfile.dev.no.turbolinks update: The compound command is semantically unchanged; the || true ensures the && clause always runs. Correct.
  • Test coverage: The new install_generator_spec block and the Interrupt spec are good additions.
  • Stacking on Improve create/install validation flows and migration docs #2650: Worth calling out in the merge checklist that this should not be merged before Improve create/install validation flows and migration docs #2650 lands, or the base Procfile references will be orphaned.

@github-actions
Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size
react-on-rails/client bundled (gzip) 62.63 KB (0%)
react-on-rails/client bundled (gzip) (time) 62.63 KB (0%)
react-on-rails/client bundled (brotli) 53.74 KB (0%)
react-on-rails/client bundled (brotli) (time) 53.74 KB (0%)
react-on-rails-pro/client bundled (gzip) 63.65 KB (0%)
react-on-rails-pro/client bundled (gzip) (time) 63.65 KB (0%)
react-on-rails-pro/client bundled (brotli) 54.68 KB (0%)
react-on-rails-pro/client bundled (brotli) (time) 54.68 KB (0%)
registerServerComponent/client bundled (gzip) 127.46 KB (0%)
registerServerComponent/client bundled (gzip) (time) 127.46 KB (0%)
registerServerComponent/client bundled (brotli) 61.64 KB (0%)
registerServerComponent/client bundled (brotli) (time) 61.64 KB (0%)
wrapServerComponentRenderer/client bundled (gzip) 122.02 KB (0%)
wrapServerComponentRenderer/client bundled (gzip) (time) 122.02 KB (0%)
wrapServerComponentRenderer/client bundled (brotli) 56.77 KB (0%)
wrapServerComponentRenderer/client bundled (brotli) (time) 56.77 KB (0%)

@justin808 justin808 force-pushed the jg-codex/docs-validation-phase1 branch 2 times, most recently from 83c65e5 to 725bcfd Compare March 19, 2026 01:18
Base automatically changed from jg-codex/docs-validation-phase1 to main March 19, 2026 01:31
@justin808 justin808 force-pushed the jg-codex/bin-dev-interrupt-fix branch from 054740b to 378e1fd Compare March 23, 2026 09:12
Comment thread docs/oss/getting-started/create-react-on-rails-app.md Outdated
@justin808 justin808 force-pushed the jg-codex/bin-dev-interrupt-fix branch from 378e1fd to b41c506 Compare March 26, 2026 01:46
@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Mar 26, 2026

Review: Quiet bin/dev shutdown on Ctrl-C

Overall this is a clean, well-scoped fix. The two-pronged approach (Ruby-level rescue Interrupt + a POSIX wrapper script for the shakapacker watcher) is solid. Tests are improved by replacing expect_any_instance_of(Kernel) with expect(described_class), which is strictly better.

Three things to address:

Bug — duplicate PostgreSQL entry in docs

docs/oss/getting-started/create-react-on-rails-app.md now lists PostgreSQL twice in the prerequisites block. The new bare bullet lands above npm or pnpm, while the existing bullet with the parenthetical clarification about bin/rails db:prepare is still there. The bare new line should be removed — the existing more-informative line should be kept.

Line 81 currently reads: "If any of the first four are missing…" — this text was accurate before (Node.js, Ruby, Rails, npm/pnpm). With the duplicate PostgreSQL entry the list appears to have five CLI-checked items, which contradicts the prose. Removing the duplicate restores consistency.

Minor — template bin/shakapacker-watch committed without executable bit

Every other file in react_on_rails/lib/generators/react_on_rails/templates/base/base/bin/ is committed -rwxr-xr-x. The new shakapacker-watch is -rw-r--r--. The generator's add_bin_scripts already calls File.chmod(0o755, ...) on all files in that directory, so the generated file will be executable at install time — no runtime bug. But the inconsistency is misleading. Worth running chmod +x on the template and amending the commit.

Minor — rescue Interrupt scope is wider than the comment implies

The rescue at the bottom of run_process_if_available catches Interrupt raised anywhere in the method, including during the installed?(process) version-check calls, not just during the long-running system(process, *args) call. The comment says "Ctrl-C during overmind/foreman shutdown" which implies the signal arrives while the process manager is running. Silently exiting on Ctrl-C is always correct here, so there is no real bug — but the comment is misleading. A small wording tweak such as "Ctrl-C at any point — treat as a clean exit" would make the intent clearer.

Comment thread docs/oss/getting-started/create-react-on-rails-app.md Outdated
Comment thread react_on_rails/lib/react_on_rails/dev/process_manager.rb
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
react_on_rails/spec/react_on_rails/dev/process_manager_spec.rb (1)

126-135: Make this exit-path spec fail-fast for consistency with sibling tests.

This case expects exit(1) but does not raise SystemExit, unlike similar exit-path examples above. Raising here too makes control-flow intent explicit.

Suggested consistency tweak
     it "exits with error when no process manager available" do
       expect(described_class).to receive(:run_process_if_available)
         .with("overmind", ["start", "-f", "Procfile.dev"]).and_return(nil)
       expect(described_class).to receive(:run_process_if_available)
         .with("foreman", ["start", "-f", "Procfile.dev"]).and_return(nil)
       expect(described_class).to receive(:show_process_manager_installation_help)
-      expect(described_class).to receive(:exit).with(1)
+      expect(described_class).to receive(:exit).with(1).and_raise(SystemExit)
 
-      described_class.run_with_process_manager("Procfile.dev")
+      expect { described_class.run_with_process_manager("Procfile.dev") }.to raise_error(SystemExit)
     end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@react_on_rails/spec/react_on_rails/dev/process_manager_spec.rb` around lines
126 - 135, The spec "exits with error when no process manager available" should
assert that exit(1) raises SystemExit like sibling tests: change the final
expectation so that calling
described_class.run_with_process_manager("Procfile.dev") is wrapped in an
expectation that it raises SystemExit (with status 1) instead of merely
expecting described_class to receive(:exit); keep the existing stubs/mocks for
run_process_if_available and show_process_manager_installation_help unchanged
and ensure the test now asserts the SystemExit is raised.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@react_on_rails/spec/react_on_rails/dev/process_manager_spec.rb`:
- Around line 126-135: The spec "exits with error when no process manager
available" should assert that exit(1) raises SystemExit like sibling tests:
change the final expectation so that calling
described_class.run_with_process_manager("Procfile.dev") is wrapped in an
expectation that it raises SystemExit (with status 1) instead of merely
expecting described_class to receive(:exit); keep the existing stubs/mocks for
run_process_if_available and show_process_manager_installation_help unchanged
and ensure the test now asserts the SystemExit is raised.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d1f048a2-5681-4804-a23a-9c3c493ad3de

📥 Commits

Reviewing files that changed from the base of the PR and between 378e1fd and b41c506.

📒 Files selected for processing (12)
  • docs/oss/getting-started/create-react-on-rails-app.md
  • react_on_rails/lib/generators/react_on_rails/rsc_setup.rb
  • react_on_rails/lib/generators/react_on_rails/templates/base/base/Procfile.dev
  • react_on_rails/lib/generators/react_on_rails/templates/base/base/Procfile.dev-static-assets
  • react_on_rails/lib/generators/react_on_rails/templates/base/base/bin/shakapacker-watch
  • react_on_rails/lib/react_on_rails/dev/process_manager.rb
  • react_on_rails/spec/dummy/Procfile.dev
  • react_on_rails/spec/dummy/Procfile.dev-static-assets
  • react_on_rails/spec/dummy/Procfile.dev.no.turbolinks
  • react_on_rails/spec/react_on_rails/dev/process_manager_spec.rb
  • react_on_rails/spec/react_on_rails/generators/install_generator_spec.rb
  • react_on_rails/spec/react_on_rails/generators/rsc_generator_spec.rb
✅ Files skipped from review due to trivial changes (8)
  • react_on_rails/lib/generators/react_on_rails/templates/base/base/Procfile.dev-static-assets
  • docs/oss/getting-started/create-react-on-rails-app.md
  • react_on_rails/lib/generators/react_on_rails/templates/base/base/Procfile.dev
  • react_on_rails/spec/react_on_rails/generators/rsc_generator_spec.rb
  • react_on_rails/spec/dummy/Procfile.dev
  • react_on_rails/spec/dummy/Procfile.dev-static-assets
  • react_on_rails/lib/generators/react_on_rails/rsc_setup.rb
  • react_on_rails/spec/react_on_rails/generators/install_generator_spec.rb
🚧 Files skipped from review as they are similar to previous changes (3)
  • react_on_rails/spec/dummy/Procfile.dev.no.turbolinks
  • react_on_rails/lib/react_on_rails/dev/process_manager.rb
  • react_on_rails/lib/generators/react_on_rails/templates/base/base/bin/shakapacker-watch

…mplate

- Add bin/shakapacker-watch to spec/dummy/bin/ (was referenced by
  Procfiles but never created since dummy doesn't use the generator)
- Remove duplicate PostgreSQL bullet in create-react-on-rails-app.md
- Set executable bit on bin/shakapacker-watch template for consistency
  with other files in templates/base/base/bin/

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.


# Process not available in either context
nil
rescue Interrupt
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rescue Interrupt covers the entire method body, including the installed? and process_available_in_system? version-check calls, not just the system(process, *args) call that actually runs the process manager. That means if Ctrl-C arrives during the version probe (before overmind/foreman has even started), this still returns true, which causes run_with_process_manager to treat it as "process ran and exited cleanly" and skip trying the next process manager.

This is functionally correct (Ctrl-C is always a clean exit intent), but the return value of true is semantically misleading — it implies the process ran and succeeded. Returning a dedicated sentinel (e.g. nil with an early return in the caller, or a new :interrupted value) would make the intent clearer. At minimum, the comment could say:

Ctrl-C at any point during process-manager setup or execution should exit quietly; returning true skips further fallback attempts in the caller.

wait "$child_pid" 2>/dev/null || true
fi

exit 0
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cleanup always calls exit 0, discarding the child's actual exit code. This is intentional (a signal-triggered shutdown is considered clean), but it means that if overmind/foreman kills the watcher via TERM for reasons other than a normal shutdown (e.g. it detected the child was misbehaving), the process always reports success to the process manager.

For a dev-only watcher this is acceptable, but a short comment here would help the next reader understand the design choice:

Suggested change
exit 0
exit 0 # treat signal-triggered shutdown as clean regardless of child exit code


trap cleanup INT TERM

bin/shakapacker "$@" &
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The script uses #!/usr/bin/env sh (POSIX sh), but POSIX does not guarantee that a signal delivered during wait will cause wait to return with the trap executing before the next command. In bash and dash (the common /bin/sh on Ubuntu/macOS) this works as expected — the signal interrupts wait, the trap runs and calls exit 0, and the if wait block is never reached. However, on stricter POSIX shells the timing can vary.

Given that Rails developers predominantly use macOS (bash/zsh → /bin/sh is usually bash 3.x or a BSD sh) and Ubuntu (dash), this is fine in practice. Just worth noting in case the script is ever ported to more exotic environments.

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Mar 26, 2026

Review

Overall this is a clean, well-scoped fix. The approach is sound and the added test coverage is solid.

rescue Interrupt in process_manager.rb

Returning true on Interrupt works correctly (Ctrl-C should produce a clean exit), but the rescue guard covers the entire method body including the version-check probes, not just the actual system call that runs the process manager. See the inline note for a suggestion on making this explicit in the comment.

bin/shakapacker-watch shell script

The signal-trap-then-wait pattern is idiomatic and correct for the target platforms (macOS/Ubuntu). Two minor observations via inline comments:

  • cleanup hardcodes exit 0 — intentional but worth a comment so the next reader understands why the child's real exit status is discarded on signal.
  • The POSIX wait-plus-trap interaction is platform-dependent; works correctly on the likely target shells (bash, dash), but worth noting for portability.

Test refactoring

The shift from expect_any_instance_of(Kernel).to receive(:system) to expect(described_class).to receive(:system) is a clear improvement. The any_instance_of(Kernel) approach intercepts all Kernel method dispatch globally, which is fragile in a shared test run. The new form targets only ProcessManager's own method resolution — more precise and more reliable.

No issues found

  • No security concerns: the valid_procfile_path? guard is untouched; the new wrapper script never interpolates user input into shell commands.
  • No performance concerns: the wrapper adds a single fork with negligible overhead.
  • Generator template and spec fixture updates are consistent with each other.

Move trap installation after child_pid=$! so a signal arriving
between fork and assignment cannot leave cleanup() with an empty
pid.  Before the trap is registered, default signal behavior
(immediate exit) applies, which is the desired outcome anyway.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

bin/shakapacker "$@" &
child_pid=$!
trap cleanup INT TERM
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Race window between child start and trap setup.

Lines 15–17 read:

bin/shakapacker "$@" &
child_pid=$!
trap cleanup INT TERM

If SIGINT or SIGTERM is delivered in the narrow window after the & fork but before the trap line executes, the default signal disposition applies and the child process is left running (orphaned) while the wrapper exits without forwarding TERM or waiting.

Consider setting the trap before forking:

Suggested change
trap cleanup INT TERM
trap cleanup INT TERM
bin/shakapacker "$@" &
child_pid=$!

With this ordering, cleanup can still handle the case gracefully because child_pid starts as "" and the guard [ -n "${child_pid}" ] prevents a kill attempt before the PID is recorded. The window effectively vanishes for TERM, and the SIGINT window shrinks to a single $! assignment.

wait "$child_pid" 2>/dev/null || true
fi

exit 0
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cleanup always exits 0, masking non-Ctrl-C failures.

The function is shared between the INT and TERM trap. When overmind/foreman sends SIGTERM because a different Procfile process crashed (not because the user pressed Ctrl-C), this wrapper silently exits 0, hiding the fact that the watcher was force-killed. A developer running bin/dev after a webpack crash would see no indication that the watcher was terminated abnormally.

One option is to forward the original signal so the process exits with the conventional signal status:

cleanup() {
  if [ -n "${child_pid}" ] && kill -0 "$child_pid" 2>/dev/null; then
    kill -TERM "$child_pid" 2>/dev/null || true
    wait "$child_pid" 2>/dev/null || true
  fi

  exit 0  # keep quiet-exit for Ctrl-C (INT); consider re-raising TERM for error cases
}

This is a minor UX trade-off rather than a bug — just worth a conscious decision. If quiet exit on both INT and TERM is the desired behaviour, a brief comment here would make the intent clear to future maintainers.

nil
rescue Interrupt
# Ctrl-C during overmind/foreman shutdown should exit quietly.
true
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rescue Interrupt returning true correctly triggers clean exit — one edge to be aware of.

Returning true here causes run_with_process_manager to return (line 51/54) rather than calling exit 1, so bin/dev exits with code 0. This is the right behaviour for interactive Ctrl-C.

The one scenario worth noting: if installed? (which calls system internally) raises Interrupt — i.e., the user hits Ctrl-C before the process manager even launches — this rescue also fires and returns true. The caller then sees "process ran successfully" even though it never ran at all. In practice this is harmless for bin/dev (quiet exit is still the right outcome), but if any future caller of run_process_if_available distinguishes true from nil for logic beyond exit-code handling, the distinction would be silently lost.

A tiny comment on the rescue clause would make the intent explicit:

Suggested change
true
rescue Interrupt
# Ctrl-C during process-manager startup or execution: treat as a clean exit.
# Returning true (rather than re-raising) suppresses both the Ruby backtrace
# and the "process not found" error path in run_with_process_manager.
true

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Mar 26, 2026

Overall this is clean, well-motivated work. The two-layer fix is the right approach, and the test improvements replacing expect_any_instance_of(Kernel) with expect(described_class) are a genuine quality uplift.

bin/shakapacker-watch is emitted by the generator via Thor's directory action in install_generator.rb line 347, which bulk-copies everything under templates/base/base/bin/. bin_scripts_to_chmod uses Dir.children on that same path (line 400) so the file is also chmod 755 automatically. No changes to the explicit base_files list in base_generator.rb are needed.

Three inline issues flagged:

  1. Race window before trap: the trap is set after the child is backgrounded; moving it above the ampersand eliminates the window (comment on bin/shakapacker-watch line 17).
  2. cleanup() always exits 0: harmless for Ctrl-C but silently swallows TERM-triggered failures from other Procfile processes (comment on bin/shakapacker-watch line 12).
  3. rescue Interrupt coverage: the rescue also fires if Interrupt arrives during the installed? check, before the process manager launches; the true return value could mislead future callers (comment on process_manager.rb line 110).

None of these block merging.

@justin808
Copy link
Copy Markdown
Member Author

The bin/shakapacker-watch wrapper script belongs in shakapacker since shakapacker already generates bin/shakapacker, bin/shakapacker-dev-server, and bin/shakapacker-config. I've opened shakacode/shakapacker#1026 to add it there.

With that merged, this PR can drop the bin/shakapacker-watch template from react_on_rails and just rely on the shakapacker-generated binstub. The Procfile changes to reference bin/shakapacker-watch --watch would still be needed here, but the script itself would come from shakapacker's install.

justin808 added a commit to shakacode/shakapacker that referenced this pull request Mar 29, 2026
## Summary
- Adds a new `bin/shakapacker-watch` shell script binstub that traps
INT/TERM signals and forwards TERM to the underlying `bin/shakapacker`
process for clean shutdown
- Updates dummy app Procfiles to use `bin/shakapacker-watch --watch`
instead of `bin/shakapacker --watch`
- The script is automatically installed alongside `bin/shakapacker` and
`bin/shakapacker-dev-server` via `rake shakapacker:binstubs`

## Why
When using `bin/dev` with overmind or foreman, pressing Ctrl-C causes
Ruby interrupt backtraces from watcher processes. This is a bad
first-run experience. The wrapper script absorbs the INT signal and
sends a clean TERM to the child process.

Related: shakacode/react_on_rails#2652 — this PR moves the
`bin/shakapacker-watch` wrapper from react_on_rails into shakapacker
where it belongs, since shakapacker already generates the other bin
scripts.

## Test plan
- [ ] `sh -n lib/install/bin/shakapacker-watch` passes syntax check
- [ ] `bundle exec rubocop` passes
- [ ] `yarn lint` passes
- [ ] Existing `bundle exec rspec spec/shakapacker/` tests pass
(979/981, 2 pre-existing failures unrelated to this change)
- [ ] `rake shakapacker:binstubs` copies `bin/shakapacker-watch` to the
app
- [ ] `bin/dev` with Ctrl-C exits cleanly without Ruby backtraces

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Low risk: adds a new optional binstub and updates docs/examples; core
compilation/runtime logic is unchanged aside from doctor reporting an
extra expected binstub.
> 
> **Overview**
> Adds a new `bin/shakapacker-watch` shell wrapper binstub that runs
`bin/shakapacker` and traps `INT`/`TERM` to terminate the child process
cleanly (avoiding interrupt backtraces in Procfile runners like
`bin/dev`).
> 
> Updates docs and diagnostics to recognize/recommend the new binstub
(`README`, `CHANGELOG`, and `Shakapacker::Doctor` binstub reporting),
and switches the dummy app Procfiles (plus dummy binstub) to use
`shakapacker-watch` for `--watch` workflows.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
210eac5. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added a shakapacker-watch binstub to run watch mode and forward
termination signals, exiting with the underlying process status.

* **Documentation**
* Updated README and CHANGELOG with usage guidance and Procfile
recommendation to use shakapacker-watch --watch.

* **Chores**
* Updated example Procfiles and dev fixtures to invoke the new binstub;
health checks now consider the watch binstub.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
@justin808 justin808 merged commit ab386a2 into main Mar 30, 2026
32 checks passed
@justin808 justin808 deleted the jg-codex/bin-dev-interrupt-fix branch March 30, 2026 03:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant