Skip to content

Doctor: query runtime config instead of regex-parsing initializers#2823

Merged
justin808 merged 17 commits intomainfrom
jg-codex/2771-doctor-runtime-config-queries
Mar 28, 2026
Merged

Doctor: query runtime config instead of regex-parsing initializers#2823
justin808 merged 17 commits intomainfrom
jg-codex/2771-doctor-runtime-config-queries

Conversation

@justin808
Copy link
Copy Markdown
Member

@justin808 justin808 commented Mar 24, 2026

Summary

  • migrate doctor configuration analysis to prefer ReactOnRails.configuration runtime values after environment load
  • keep initializer parsing as fallback when Rails environment cannot be loaded
  • use Pro runtime renderer detection for server rendering engine checks, with initializer fallback
  • update server bundle/prerender consistency checks to use runtime config values
  • add specs covering runtime-over-file precedence and runtime-based NodeRenderer detection

Testing

  • bundle exec rspec react_on_rails/spec/lib/react_on_rails/doctor_spec.rb
  • bundle exec rspec react_on_rails/spec/lib/react_on_rails/doctor_rake_task_spec.rb
  • bundle exec rubocop react_on_rails/lib/react_on_rails/doctor.rb react_on_rails/spec/lib/react_on_rails/doctor_spec.rb

Fixes #2771


Note

Medium Risk
Moderate risk because it changes diagnostic/config-resolution behavior (runtime vs initializer parsing, SSR bundle checks, and Shakapacker config discovery), which may affect what setups are reported as healthy or broken across diverse project layouts.

Overview
Doctor now prefers loaded runtime configuration (ReactOnRails.configuration) for reporting and consistency checks, falling back to initializer regex parsing only when the Rails environment can’t be loaded; SSR bundle existence checks are skipped when server_bundle_js_file is blank/whitespace and default-valued settings are omitted to reduce noise.

Bundler config discovery was reworked to use Shakapacker’s assets_bundler_config_path (with memoized/normalized lookup) and to probe webpack/rspack config candidates deterministically (JS before TS, .cjs/.mjs only in Shakapacker-derived dirs), with clearer messaging in Doctor/SystemChecker and expanded specs covering these scenarios (including Pro NodeRenderer detection and ExecJS fallback messaging).

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

Summary by CodeRabbit

  • Improvements

    • Doctor now prioritizes runtime configuration over initializer parsing for SSR and performance reporting, skips SSR bundle checks when runtime bundle filename is blank/whitespace, and reduces noise by omitting default-valued settings.
    • Clarified diagnostics and messaging around runtime vs initializer-sourced settings.
  • Tests

    • Expanded specs covering runtime-vs-initializer precedence, SSR skip behavior, prerender consistency scenarios, Pro renderer resolution, and memoization.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 24, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

The doctor now prefers runtime configuration from ReactOnRails.configuration (when Rails can be loaded) over regex-parsing the initializer. Added memoized helpers for accessing runtime and Pro renderer values, and updated analysis/check helpers to accept and prefer runtime-derived values with initializer parsing as fallback.

Changes

Cohort / File(s) Summary
Doctor Implementation
react_on_rails/lib/react_on_rails/doctor.rb
Use react_on_rails_runtime_configuration instead of blind regex parsing; added memoized helpers react_on_rails_runtime_configuration and resolved_pro_server_renderer. Updated checks and analysis helpers (check_react_on_rails_initializer, check_javascript_bundles, check_server_rendering_engine, check_server_bundle_prerender_consistency, server_bundle_filename, analyze_* helpers) to accept an optional runtime_config and prefer runtime values; suppressed reporting of default-valued settings.
Test Coverage
react_on_rails/spec/lib/react_on_rails/doctor_spec.rb
Added and extended specs to validate runtime-config precedence, whitespace/nil handling for server_bundle_js_file, missing-initializer scenarios, prerender/bundle consistency with runtime values, Pro NodeRenderer detection via runtime, and memoization behavior for the new helpers.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I hopped through configs, sniffed the runtime air,
No more brittle regex traps hiding there.
Memoized helpers keep secrets neat and small,
The Doctor now listens to the app above all. 🥕✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.83% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main objective: migrating doctor configuration analysis from regex-parsing initializers to querying runtime configuration.
Linked Issues check ✅ Passed The PR implementation comprehensively addresses all requirements from issue #2771: runtime config queries replace regex parsing across ~10 checks, Pro NodeRenderer detection uses resolved runtime values, server bundle/prerender consistency checks use runtime config, and comprehensive test coverage was added.
Out of Scope Changes check ✅ Passed All changes directly support the migration objective: runtime config integration in doctor checks, supporting helper methods, test coverage, and documentation updates are all in-scope.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch jg-codex/2771-doctor-runtime-config-queries

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.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Mar 24, 2026

Greptile Summary

This PR upgrades the ReactOnRails::Doctor diagnostic tool to query live runtime configuration values (via ReactOnRails.configuration / ReactOnRailsPro.configuration) when the Rails environment can be loaded, falling back to regex-parsing the initializer file when it cannot. The change fixes the long-standing issue (#2771) where doctor output could diverge from what the app actually uses at runtime (e.g., config values set dynamically or by env variables).

Key changes:

  • Two new memoised helpers — react_on_rails_runtime_configuration and resolved_pro_server_renderer — wrap env-load and config-access with error handling.
  • All six analyze_* methods accept an optional runtime_config parameter and branch on it.
  • check_server_bundle_prerender_consistency and server_bundle_filename likewise prefer runtime values.
  • New specs cover runtime-over-file precedence, runtime NodeRenderer detection, and runtime vs. initializer conflicts.

Issues found:

  • react_on_rails_runtime_configuration uses a trailing if for assignment, so when ensure_rails_environment_loaded returns false, @react_on_rails_runtime_configuration is never defined and the defined? memoization guard never fires — every call re-enters the method body.
  • check_server_bundle_prerender_consistency still guards on File.exist?("config/initializers/react_on_rails.rb"), silently skipping the consistency check when the file is absent even though runtime config would provide all necessary values.
  • analyze_component_loading_config reports random_dom_id when the value is nil (not configured) as well as false, which could produce misleading output.

Confidence Score: 3/5

  • Safe to merge for most apps, but two logic gaps in the new runtime-config path mean the doctor can silently give incomplete output and has a caching bug that causes repeated work.
  • The core concept is sound and tests pass. However, the memoization bug in react_on_rails_runtime_configuration (repeated method body re-entry when env load fails) and the unclosed guard in check_server_bundle_prerender_consistency (consistency check silently skipped when initializer isn't at the default path despite runtime config being available) are genuine logic issues that touch the primary user path of this PR. Neither causes data loss, but they undermine the PR's stated goal of preferring runtime values.
  • react_on_rails/lib/react_on_rails/doctor.rb — specifically the new react_on_rails_runtime_configuration helper (memoization) and the guard in check_server_bundle_prerender_consistency.

Important Files Changed

Filename Overview
react_on_rails/lib/react_on_rails/doctor.rb Core diagnostic file refactored to prefer ReactOnRails runtime config over regex-parsed initializer values; two new memoised helpers added (react_on_rails_runtime_configuration, resolved_pro_server_renderer), but the memoisation in react_on_rails_runtime_configuration has a caching bug when env load fails, and check_server_bundle_prerender_consistency still guards on initializer file existence even when runtime config is available.
react_on_rails/spec/lib/react_on_rails/doctor_spec.rb New specs cover runtime-over-file precedence, runtime-based NodeRenderer detection, and check_server_bundle_prerender_consistency with a conflicting runtime config; existing tests updated with react_on_rails_runtime_configuration stubs where needed — good coverage.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Doctor check invoked] --> B{ensure_rails_environment_loaded?}
    B -- yes --> C[react_on_rails_runtime_configuration\n= ReactOnRails.configuration]
    B -- no --> D[react_on_rails_runtime_configuration = nil]

    C --> E{runtime_config present?}
    D --> E

    E -- yes --> F[Use live config values\nserver_bundle_js_file, prerender,\npool_size, timeout, etc.]
    E -- no --> G[Regex-parse initializer file\nconfig/initializers/react_on_rails.rb]

    F --> H[check_react_on_rails_initializer\ncheck_server_bundle_prerender_consistency\nserver_bundle_filename]
    G --> H

    B -- yes --> I{defined? ReactOnRailsPro?}
    I -- yes --> J[resolved_pro_server_renderer\n= ReactOnRailsPro.configuration.server_renderer]
    I -- no --> K{pro_initializer_has_node_renderer?}
    K -- yes --> L[resolved_pro_server_renderer = NodeRenderer]
    K -- no --> M[resolved_pro_server_renderer = nil]
    J --> N[check_server_rendering_engine]
    L --> N
    M --> N
Loading

Comments Outside Diff (2)

  1. react_on_rails/lib/react_on_rails/doctor.rb, line 1328-1330 (link)

    P1 Guard on initializer file existence defeats runtime-config goal

    The PR's stated purpose is to prefer runtime configuration values when the Rails environment is loaded. However, check_server_bundle_prerender_consistency still bails out at line 1330 if config/initializers/react_on_rails.rb does not exist — even when react_on_rails_runtime_configuration would have all the values needed for the check.

    If an app has its configuration at a non-standard path (or configures React on Rails programmatically without a file at the default path) but has a loaded Rails environment, the entire consistency check is silently skipped.

    The guard should be relaxed when runtime config is available:

    def check_server_bundle_prerender_consistency
      config_path = "config/initializers/react_on_rails.rb"
      runtime_config = react_on_rails_runtime_configuration
      return unless runtime_config || File.exist?(config_path)
      ...

    This keeps the old guard for the regex-fallback path while letting the runtime-config path proceed regardless of file existence.

  2. react_on_rails/lib/react_on_rails/doctor.rb, line 345 (link)

    P2 random_dom_id is reported when nil, not only when false

    The condition unless runtime_config.random_dom_id is truthy for both nil and false. When the config object returns nil for an unset attribute, this would silently add "random_dom_id: " (a blank value) to the output, which may mislead the user into thinking they explicitly disabled it.

    The old regex path only added the entry when it was explicitly set in the file. Consider matching that intent:

Reviews (1): Last reviewed commit: "Use runtime React on Rails config in doc..." | Re-trigger Greptile

Comment thread react_on_rails/lib/react_on_rails/doctor.rb Outdated
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Mar 24, 2026

test

Comment thread react_on_rails/lib/react_on_rails/doctor.rb Outdated
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.

Autofix Details

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

  • ✅ Fixed: Memoization fails when Rails environment cannot load
    • I changed the conditional assignment to an if/end expression so the instance variable is always assigned (including nil), allowing defined?-based memoization to work on subsequent calls.

Create PR

Or push these changes by commenting:

@cursor push 8bce0f4194
Preview (8bce0f4194)
diff --git a/react_on_rails/lib/react_on_rails/doctor.rb b/react_on_rails/lib/react_on_rails/doctor.rb
--- a/react_on_rails/lib/react_on_rails/doctor.rb
+++ b/react_on_rails/lib/react_on_rails/doctor.rb
@@ -2333,7 +2333,10 @@
     def react_on_rails_runtime_configuration
       return @react_on_rails_runtime_configuration if defined?(@react_on_rails_runtime_configuration)
 
-      @react_on_rails_runtime_configuration = ReactOnRails.configuration if ensure_rails_environment_loaded
+      @react_on_rails_runtime_configuration =
+        if ensure_rails_environment_loaded
+          ReactOnRails.configuration
+        end
     rescue StandardError => e
       checker.add_warning("⚠️  Could not query React on Rails runtime configuration: #{e.message}")
       @react_on_rails_runtime_configuration = nil

Comment thread react_on_rails/lib/react_on_rails/doctor.rb Outdated
@justin808
Copy link
Copy Markdown
Member Author

justin808 commented Mar 24, 2026

Superseded by the follow-up comment with corrected formatting: #2823 (comment)

@justin808
Copy link
Copy Markdown
Member Author

Addressed the Greptile findings in commit 2bd1c3a.\n\nChanges made:\n- Fixed runtime-config memoization so nil is memoized when Rails environment cannot be loaded, preventing repeated re-entry.\n- Updated the server bundle/prerender consistency check to run when runtime config is available even if config/initializers/react_on_rails.rb is missing.\n- Tightened random_dom_id runtime reporting so it is emitted only when explicitly false (not nil).\n- Added regression coverage for all three cases in doctor specs.\n\nRe-validated:\n- bundle exec rspec react_on_rails/spec/lib/react_on_rails/doctor_spec.rb\n- bundle exec rubocop react_on_rails/lib/react_on_rails/doctor.rb react_on_rails/spec/lib/react_on_rails/doctor_spec.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.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
react_on_rails/lib/react_on_rails/doctor.rb (1)

750-769: ⚠️ Potential issue | 🟠 Major

Don't let the initializer guard bypass the new runtime path.

Line 754 and Line 1330 still short-circuit on config/initializers/react_on_rails.rb, so apps that set ReactOnRails.configuration somewhere else will either be reported as missing config or skip the prerender/server-bundle consistency check entirely even when react_on_rails_runtime_configuration is already available. If you want to keep the missing-file warning, that's fine, but the runtime-derived diagnostics should still run when runtime_config exists.

💡 Suggested change
 def check_react_on_rails_initializer
   config_path = "config/initializers/react_on_rails.rb"
   runtime_config = react_on_rails_runtime_configuration
+  initializer_exists = File.exist?(config_path)

-  unless File.exist?(config_path)
+  unless runtime_config || initializer_exists
     checker.add_warning("⚠️  React on Rails configuration file not found: #{config_path}")
     checker.add_info("💡 Run 'rails generate react_on_rails:install' to create configuration file")
     return
   end

   begin
-    content = File.read(config_path)
+    content = initializer_exists ? File.read(config_path) : ""
     ...
   end
 end

 def check_server_bundle_prerender_consistency
   config_path = "config/initializers/react_on_rails.rb"
-  return unless File.exist?(config_path)
+  runtime_config = react_on_rails_runtime_configuration
+  return unless runtime_config || File.exist?(config_path)

   checker.add_info("\n🔍 Server Rendering Consistency:")

   begin
-    runtime_config = react_on_rails_runtime_configuration
     if runtime_config
       ...

Also applies to: 1328-1349

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@react_on_rails/lib/react_on_rails/doctor.rb` around lines 750 - 769, The
initializer check currently returns early when
config/initializers/react_on_rails.rb is missing, which prevents runtime-derived
diagnostics from running even if react_on_rails_runtime_configuration is
present; update check_react_on_rails_initializer to only return early when the
file is missing AND react_on_rails_runtime_configuration is nil — if
runtime_config exists, emit the missing-file warning via
checker.add_warning/checker.add_info but continue to read/use runtime_config and
run the subsequent diagnostics (references: check_react_on_rails_initializer,
react_on_rails_runtime_configuration, checker.add_warning); make the same change
in the other similar block (around the check that currently short-circuits at
lines ~1328-1349) so runtime-derived checks always run when runtime_config is
available.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@react_on_rails/lib/react_on_rails/doctor.rb`:
- Around line 1296-1300: The current logic uses configured_value.present? which
discards an intentionally empty runtime server_bundle_js_file; instead, when
react_on_rails_runtime_configuration is present return its server_bundle_js_file
value unchanged (including empty string) so callers can treat blank as
SSR-disabled. Update the block that reads react_on_rails_runtime_configuration
and server_bundle_js_file to return the runtime value directly when
runtime_config is non-nil (reference react_on_rails_runtime_configuration and
server_bundle_js_file) rather than only returning when present?.
- Around line 677-678: The code currently calls
ReactOnRails::Utils.react_on_rails_pro? again when setting uses_node_renderer,
which can re-run Pro license validation; instead use the result of
resolved_pro_server_renderer to determine availability and type. Change the
uses_node_renderer logic to rely only on pro_renderer (e.g., check that
pro_renderer is non-nil and equals "NodeRenderer") and remove the duplicate
ReactOnRails::Utils.react_on_rails_pro? invocation so
resolved_pro_server_renderer is the single source of truth for Pro availability.

---

Outside diff comments:
In `@react_on_rails/lib/react_on_rails/doctor.rb`:
- Around line 750-769: The initializer check currently returns early when
config/initializers/react_on_rails.rb is missing, which prevents runtime-derived
diagnostics from running even if react_on_rails_runtime_configuration is
present; update check_react_on_rails_initializer to only return early when the
file is missing AND react_on_rails_runtime_configuration is nil — if
runtime_config exists, emit the missing-file warning via
checker.add_warning/checker.add_info but continue to read/use runtime_config and
run the subsequent diagnostics (references: check_react_on_rails_initializer,
react_on_rails_runtime_configuration, checker.add_warning); make the same change
in the other similar block (around the check that currently short-circuits at
lines ~1328-1349) so runtime-derived checks always run when runtime_config is
available.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f3552bc6-b883-47f6-b5b2-18f8235eacdd

📥 Commits

Reviewing files that changed from the base of the PR and between c8267da and c3441ab.

📒 Files selected for processing (2)
  • react_on_rails/lib/react_on_rails/doctor.rb
  • react_on_rails/spec/lib/react_on_rails/doctor_spec.rb

Comment thread react_on_rails/lib/react_on_rails/doctor.rb Outdated
Comment thread react_on_rails/lib/react_on_rails/doctor.rb
Comment thread react_on_rails/lib/react_on_rails/doctor.rb Outdated
Comment thread react_on_rails/lib/react_on_rails/doctor.rb Outdated
Comment thread react_on_rails/lib/react_on_rails/doctor.rb
Comment thread react_on_rails/lib/react_on_rails/doctor.rb
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Mar 24, 2026

Review Summary

The direction is good — preferring runtime config over regex-parsing is more reliable and handles dynamic configuration (env-based values, computed expressions, multi-file initializers) that static parsing can never get right.

Bug

`server_bundle_filename` empty-string fallback (see inline at line 1299): when `server_bundle_js_file` is explicitly set to "" (SSR disabled), `configured_value.present?` is false so the method falls through to regex/default and returns "server-bundle.js". Downstream, `determine_server_bundle_path` will then construct a path for a file that does not exist and the doctor may report false-positive warnings about a missing bundle.

Design inconsistencies

  1. Runtime path reports default values (inline at lines 843-844): `server_renderer_pool_size`, `server_renderer_timeout`, `raise_on_prerender_error`, and `enforce_private_server_bundles` are now always emitted in the runtime path even when they hold factory defaults. The fallback path only prints them when explicitly set. Upgraders will see more output in the runtime path without having changed anything.

  2. `random_dom_id` runtime filter (inline at line 1018): only surfaced when == false; an explicit = true is silently skipped. A short comment stating the default value would make the intent unambiguous.

  3. `analyze_development_config` runtime path: always prints trace, logging_on_server, and replay_console regardless of whether they differ from defaults, while the fallback path omits them unless explicitly set.

Minor

The `react_on_rails_runtime_configuration` memoization is correct. One edge case worth a comment: if `ReactOnRails.configuration` raises (rescue fires), `ensure_rails_environment_loaded` remains true, so `resolved_pro_server_renderer` will still attempt `ReactOnRailsPro.configuration`. Not a safety issue, just worth documenting. (See inline at line 2334.)

Tests

The new specs are well-structured and cover the key scenarios (runtime-over-file precedence, nil memoization, NodeRenderer detection). The `around { Dir.mktmpdir }` pattern is clean. One suggestion: add a test for the `server_bundle_filename` empty-string case to lock in the intended behaviour once the bug above is resolved.

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.

♻️ Duplicate comments (2)
react_on_rails/lib/react_on_rails/doctor.rb (2)

677-678: ⚠️ Potential issue | 🟡 Minor

Avoid re-running Pro license checks in server rendering engine detection.

Line 678 calls ReactOnRails::Utils.react_on_rails_pro? again even though resolved_pro_server_renderer already resolved Pro availability. This can re-trigger license validation and mask the original diagnostic path.

💡 Suggested fix
-        uses_node_renderer = ReactOnRails::Utils.react_on_rails_pro? && pro_renderer == "NodeRenderer"
+        uses_node_renderer = pro_renderer == "NodeRenderer"

Based on learnings: ReactOnRails::Utils.react_on_rails_pro? validates licenses and should fail early, so redundant calls can introduce extra failure paths.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@react_on_rails/lib/react_on_rails/doctor.rb` around lines 677 - 678, The code
currently calls ReactOnRails::Utils.react_on_rails_pro? again when computing
uses_node_renderer which can re-run Pro license validation; instead, derive
uses_node_renderer solely from resolved_pro_server_renderer (e.g. set
uses_node_renderer = resolved_pro_server_renderer == "NodeRenderer") and remove
the duplicate ReactOnRails::Utils.react_on_rails_pro? invocation so license
checks remain controlled by resolved_pro_server_renderer resolution logic
(update the assignment that defines uses_node_renderer and any related
conditional logic around it).

1296-1300: ⚠️ Potential issue | 🟠 Major

Preserve empty runtime server_bundle_js_file values.

Line 1299 uses .present?, so an intentional runtime empty string falls back to initializer/default parsing and can incorrectly report server-bundle.js.

💡 Suggested fix
     def server_bundle_filename
       runtime_config = react_on_rails_runtime_configuration
-      if runtime_config
-        configured_value = runtime_config.server_bundle_js_file
-        return configured_value if configured_value.present?
-      end
+      return runtime_config.server_bundle_js_file if runtime_config
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@react_on_rails/lib/react_on_rails/doctor.rb` around lines 1296 - 1300, The
current check in react_on_rails_runtime_configuration lookup in doctor.rb uses
.present? and therefore ignores intentional empty-string values for
server_bundle_js_file; change the conditional to return
runtime_config.server_bundle_js_file when it is set (i.e., not nil) rather than
when it is non-blank—update the logic around
react_on_rails_runtime_configuration / server_bundle_js_file to use a nil check
(or `unless configured_value.nil?`) so an explicit empty string is preserved
instead of falling back to the initializer/default.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@react_on_rails/lib/react_on_rails/doctor.rb`:
- Around line 677-678: The code currently calls
ReactOnRails::Utils.react_on_rails_pro? again when computing uses_node_renderer
which can re-run Pro license validation; instead, derive uses_node_renderer
solely from resolved_pro_server_renderer (e.g. set uses_node_renderer =
resolved_pro_server_renderer == "NodeRenderer") and remove the duplicate
ReactOnRails::Utils.react_on_rails_pro? invocation so license checks remain
controlled by resolved_pro_server_renderer resolution logic (update the
assignment that defines uses_node_renderer and any related conditional logic
around it).
- Around line 1296-1300: The current check in
react_on_rails_runtime_configuration lookup in doctor.rb uses .present? and
therefore ignores intentional empty-string values for server_bundle_js_file;
change the conditional to return runtime_config.server_bundle_js_file when it is
set (i.e., not nil) rather than when it is non-blank—update the logic around
react_on_rails_runtime_configuration / server_bundle_js_file to use a nil check
(or `unless configured_value.nil?`) so an explicit empty string is preserved
instead of falling back to the initializer/default.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 84d4f404-f6a2-4ccf-8416-8ee8badf039a

📥 Commits

Reviewing files that changed from the base of the PR and between c3441ab and 2bd1c3a.

📒 Files selected for processing (2)
  • react_on_rails/lib/react_on_rails/doctor.rb
  • react_on_rails/spec/lib/react_on_rails/doctor_spec.rb

Comment thread react_on_rails/lib/react_on_rails/doctor.rb
Comment thread react_on_rails/lib/react_on_rails/doctor.rb Outdated
Comment thread react_on_rails/lib/react_on_rails/doctor.rb
Comment thread packages/create-react-on-rails-app/src/create-app.ts
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Mar 24, 2026

Review Summary

The core idea — prefer ReactOnRails.configuration runtime values over regex-parsing the initializer file — is sound and fixes a real class of diagnostic inaccuracies. The memoization pattern using defined?(@ivar) to distinguish un-set from nil is correct and well-tested.

A few things worth addressing before merge:

Bugs / correctness

resolved_pro_server_renderer memoization order (inline comment on line 2352–2353)
The return nil unless react_on_rails_pro? guard fires before the defined?(@ivar) check. For non-Pro apps the instance variable is never written, so react_on_rails_pro? is called on every invocation. Swap the two lines so the memoized value short-circuits first.

Diagnostic output consistency

server_renderer_pool_size / server_renderer_timeout always emitted with runtime config (inline comment on line 852)
The runtime path unconditionally prints these; the regex fallback only prints them when explicitly set. At runtime they're always present (default values), so users will see extra lines they never saw before. Either guard on non-default values or document the intentional verbosity increase.

same_bundle_for_client_and_server / random_dom_id asymmetry (inline comment on lines 1023–1026)
One is shown when truthy, the other when == false. The logic is correct (both suppress their respective defaults) but the asymmetry is confusing without a comment explaining the defaults.

Scope

Unrelated TypeScript refactors (inline on create-app.ts)
The ternary-to-if-else changes in create-react-on-rails-app are functional no-ops that have nothing to do with Doctor. They add review noise and should live in a separate commit/PR.


Overall the PR is a solid improvement. The three items above are all small fixes.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 24, 2026

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.7 KB (0%)
react-on-rails/client bundled (brotli) (time) 53.7 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.67 KB (0%)
react-on-rails-pro/client bundled (brotli) (time) 54.67 KB (0%)
registerServerComponent/client bundled (gzip) 127.46 KB (0%)
registerServerComponent/client bundled (gzip) (time) 127.46 KB (0%)
registerServerComponent/client bundled (brotli) 61.65 KB (0%)
registerServerComponent/client bundled (brotli) (time) 61.65 KB (0%)
wrapServerComponentRenderer/client bundled (gzip) 122.02 KB (-0.01% 🔽)
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%)

Comment thread react_on_rails/lib/react_on_rails/doctor.rb
Comment thread react_on_rails/lib/react_on_rails/doctor.rb Outdated
Comment thread react_on_rails/lib/react_on_rails/doctor.rb Outdated
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Mar 24, 2026

Review Summary

The core idea is solid — querying ReactOnRails.configuration at runtime is strictly more accurate than regex-parsing the initializer text, and the fallback chain (runtime → file parsing → defaults) is well-structured. The TypeScript refactors in create-react-on-rails-app are clean improvements.

Issues found

Bug — nil vs. "" conflation in analyze_server_rendering_config (see inline comment on line 800)

runtime_config.server_bundle_js_file returns nil when the user never set it (system default "server-bundle.js" applies) and "" when they explicitly disabled SSR. The current code collapses both to "disabled", which is misleading for the common case where the option is simply unconfigured. The initializer-parsing fallback handles this correctly by showing "server-bundle.js (default)" when nothing is found.

Minor — memoization bypass in resolved_pro_server_renderer (see inline comment on line 2354)

The return nil unless react_on_rails_pro? guard fires before the defined?(@resolved_pro_server_renderer) check, so every call hits react_on_rails_pro? again when Pro is inactive. Contrast with react_on_rails_runtime_configuration, which always writes through to the ivar. Low impact today since the method is called once per run, but worth aligning for consistency.

UX — spurious warning when runtime config is available but no initializer file (see inline comment on line 766)

When the Rails environment loads successfully and ReactOnRails.configuration is readable, emitting a warning because the initializer file is absent is noisy — the configuration is provably correct. Consider downgrading to add_info in that case, reserving add_warning for when runtime config is also unavailable.

Positive notes

  • Memoization via defined?(@ivar) is the right pattern for caching nil; the explanatory comment helps.
  • ensure_rails_environment_loaded is correctly guarded against repeated attempts via @rails_environment_attempted.
  • New specs cover the key precedence scenarios: runtime over initializer, blank server_bundle_js_file, and memoization of nil on load failure.
  • resolved_pro_server_renderer cleanly isolates Pro renderer detection from the check methods that consume it.

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.

Autofix Details

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

  • ✅ Fixed: Memoization bypassed on every call to renderer method
    • I swapped the guard order in resolved_pro_server_renderer so memoized values return before any Pro check, preventing unnecessary re-evaluation and preserving cached results.

Create PR

Or push these changes by commenting:

@cursor push 163c8620b8
Preview (163c8620b8)
diff --git a/react_on_rails/lib/react_on_rails/doctor.rb b/react_on_rails/lib/react_on_rails/doctor.rb
--- a/react_on_rails/lib/react_on_rails/doctor.rb
+++ b/react_on_rails/lib/react_on_rails/doctor.rb
@@ -2351,8 +2351,8 @@
     end
 
     def resolved_pro_server_renderer
+      return @resolved_pro_server_renderer if defined?(@resolved_pro_server_renderer)
       return nil unless ReactOnRails::Utils.react_on_rails_pro?
-      return @resolved_pro_server_renderer if defined?(@resolved_pro_server_renderer)
 
       @resolved_pro_server_renderer =
         if ensure_rails_environment_loaded && defined?(ReactOnRailsPro)

Comment thread react_on_rails/lib/react_on_rails/doctor.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.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
react_on_rails/lib/react_on_rails/doctor.rb (1)

685-692: ⚠️ Potential issue | 🟡 Minor

Gate the ExecJS fallback warning on the actual fallback setting.

This branch now resolves NodeRenderer from runtime config, but it still assumes renderer_use_fallback_exec_js is enabled whenever NodeRenderer is active. Apps that deliberately disable the fallback will still get the “ExecJS fallback is enabled” warning here when ExecJS is absent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@react_on_rails/lib/react_on_rails/doctor.rb` around lines 685 - 692, The code
assumes the ExecJS fallback is enabled whenever NodeRenderer is used and warns
even if the app has disabled the fallback; update the branch that handles
uses_node_renderer in react_on_rails/doctor.rb to check the actual
renderer_use_fallback_exec_js configuration before emitting the ExecJS warning:
only call checker.add_warning("  ⚠️  ExecJS fallback is enabled but ExecJS is
not available") and the subsequent checker.add_info hint when
renderer_use_fallback_exec_js is true (and ExecJS is missing); keep the existing
ExecJS.runtime availability check and messages otherwise, and reference the
uses_node_renderer boolean, NodeRenderer runtime resolution, and
renderer_use_fallback_exec_js setting when locating the code to change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@react_on_rails/lib/react_on_rails/doctor.rb`:
- Around line 201-203: Change the SSR fast-path check to use blank semantics
instead of empty? by replacing server_bundle_filename.to_s.empty? with
server_bundle_filename.to_s.blank? (or equivalent strip-empty check), and update
the add_info message in the same block (the string passed to checker.add_info)
to avoid the exact phrase "server bundle" (e.g., use "server-side bundle") so it
won't trigger the later literal-match rebuild hint; refer to
server_bundle_filename and checker.add_info in react_on_rails/doctor.rb.
- Around line 795-801: The doctor currently treats nil and empty-string runtime
values the same; update the runtime-config reporting in ReactOnRails::Doctor to
distinguish nil (meaning "use initializer/default") from "" (meaning
"disabled"). Specifically, in the blocks that read
runtime_config.server_bundle_js_file (and the similar block around the
server_bundle handling at the other occurrence), change the branching to: if
server_bundle_value.nil? then report it as falling back to initializer/default,
elsif server_bundle_value == "" then report as disabled, else report the actual
value via checker.add_info. Ensure you update both places that read
server_bundle_js_file so nil and "" have consistent meanings.

---

Outside diff comments:
In `@react_on_rails/lib/react_on_rails/doctor.rb`:
- Around line 685-692: The code assumes the ExecJS fallback is enabled whenever
NodeRenderer is used and warns even if the app has disabled the fallback; update
the branch that handles uses_node_renderer in react_on_rails/doctor.rb to check
the actual renderer_use_fallback_exec_js configuration before emitting the
ExecJS warning: only call checker.add_warning("  ⚠️  ExecJS fallback is enabled
but ExecJS is not available") and the subsequent checker.add_info hint when
renderer_use_fallback_exec_js is true (and ExecJS is missing); keep the existing
ExecJS.runtime availability check and messages otherwise, and reference the
uses_node_renderer boolean, NodeRenderer runtime resolution, and
renderer_use_fallback_exec_js setting when locating the code to change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 77835fcc-af6e-4472-8d05-5c152e6d5b7e

📥 Commits

Reviewing files that changed from the base of the PR and between 2bd1c3a and dc61e0e.

📒 Files selected for processing (4)
  • packages/create-react-on-rails-app/src/create-app.ts
  • packages/create-react-on-rails-app/src/index.ts
  • react_on_rails/lib/react_on_rails/doctor.rb
  • react_on_rails/spec/lib/react_on_rails/doctor_spec.rb
✅ Files skipped from review due to trivial changes (2)
  • packages/create-react-on-rails-app/src/index.ts
  • packages/create-react-on-rails-app/src/create-app.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • react_on_rails/spec/lib/react_on_rails/doctor_spec.rb

Comment thread react_on_rails/lib/react_on_rails/doctor.rb Outdated
Comment thread react_on_rails/lib/react_on_rails/doctor.rb
…hs (#2824)

## Summary
- prioritize exact `Shakapacker.config.assets_bundler_config_path` as
the first webpack config candidate in `ConfigPathResolver`
- keep directory-derived and default webpack/rspack fallback candidates
after the exact path
- make `SystemChecker#bundler_config_file_exists?` use resolver-based
path detection so custom config paths are recognized
- update doctor missing-config messaging to reference generic bundler
config discovery instead of only `config/webpack/webpack.config.js`
- add coverage for exact-path precedence and custom-path detection in
doctor/system checker specs

## Testing
- `bundle exec rspec
react_on_rails/spec/lib/react_on_rails/doctor_spec.rb
react_on_rails/spec/lib/react_on_rails/system_checker_spec.rb`
- `bundle exec rubocop
react_on_rails/lib/react_on_rails/config_path_resolver.rb
react_on_rails/lib/react_on_rails/doctor.rb
react_on_rails/lib/react_on_rails/system_checker.rb
react_on_rails/spec/lib/react_on_rails/doctor_spec.rb
react_on_rails/spec/lib/react_on_rails/system_checker_spec.rb`

Fixes #2633

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes bundler config discovery/selection logic (including candidate
ordering and shakapacker-derived paths), which can alter which
webpack/rspack config file is detected and what guidance/warnings are
emitted during doctor/install checks.
> 
> **Overview**
> **Improves bundler config path detection for Doctor/SystemChecker**,
especially when apps use a custom
`Shakapacker.config.assets_bundler_config_path`.
> 
> Config resolution now **prioritizes the exact Shakapacker-configured
path**, probes standard `webpack.config.*`/`rspack.config.*` siblings in
the same directory (including `.cjs`/`.mjs` there), and falls back to
deterministic default locations with **`.js` preferred over `.ts`**.
Doctor and SystemChecker messaging is updated to refer to generic
*bundler configuration* discovery (not just
`config/webpack/webpack.config.js`), and specs add coverage for these
precedence/ambiguity cases.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
c653830. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->


### Note
- Behavior change: config candidate ordering now prefers before for both
webpack and rspack candidates (including default fallbacks) to match
current generator output and keep selection deterministic when both
files exist.


### Note
- Behavior change: config candidate ordering now prefers `.js` before
`.ts` for both webpack and rspack candidates (including default
fallbacks) to match current generator output and keep selection
deterministic when both files exist.
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Mar 28, 2026

Review

Well-structured refactor that improves diagnostic accuracy by preferring runtime config over brittle regex parsing. The defined?(@ivar) memoization to cache nil is correct and well-commented. Test coverage of runtime-vs-initializer precedence is thorough.

Behavioral change worth documenting: the runtime path now omits settings at their default values (pool size, timeout, trace, logging_on_server, etc.), whereas the initializer-fallback path always showed defaults explicitly. Users auditing their full config may be surprised when entries silently disappear. Adding a note in the doctor output like 'Default-valued settings are omitted (see docs)' would make this self-documenting.

ensure_rails_environment_loaded warning deduplication confirmed: both react_on_rails_runtime_configuration and resolved_pro_server_renderer call ensure_rails_environment_loaded independently. @rails_environment_attempted is set before the require call, so if require throws, the rescue returns false but the flag stays set. The warning fires exactly once across all callers. Good design.

analyze_server_rendering_config falsy non-nil edge: the present? / nil? / else triple-branch silently maps false to the empty-string-disabled message. server_bundle_js_file should always be String or nil per the schema so this is harmless in practice, but a comment near is_a?(String) would clarify intent for future readers.

create-app.ts simplification looks good. The refactor preserves behavior correctly and the type narrowing to the literal union type is an improvement.

@shakapacker_assets_bundler_config_path = nil
rescue StandardError => e
message = "ReactOnRails could not read Shakapacker assets_bundler_config_path: #{e.class}: #{e.message}"
warn(message) unless Rails.logger
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 warn call here outputs to stderr when Rails.logger is nil. Since this is a debug-level diagnostic (Shakapacker config read failure), consider whether stderr noise is acceptable in environments where Rails logger hasn't been set up yet (e.g. during early boot). The logic is correct — warn fires only when logger is absent, logger.debug fires when present — but the severity mismatch (warn-to-stderr for a debug situation) may surprise operators.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good callout. I’m keeping this behavior as-is for now: when Rails.logger is unavailable, this fallback path uses warn intentionally so configuration-read failures are still visible during early boot/diagnostics. Once logger is available, it stays at debug level via Rails.logger.debug.

if runtime_config
raw_server_bundle_value = runtime_config.server_bundle_js_file
server_bundle_value =
if raw_server_bundle_value.is_a?(String)
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 else branch here fires for any falsy, non-nil value of server_bundle_js_file (e.g. false). Since the config schema only allows String or nil, this is unreachable in practice. A brief comment like # configured value is always String | nil near the is_a?(String) guard would save the next reader from having to trace through config defaults to reach the same conclusion.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Thanks. I’m leaving this unchanged for now. The branch is defensive to preserve clear handling of blank/disabled semantics, and in practice config values are constrained by the configuration layer. I agree a clarifying comment could help, but I’m keeping this PR focused on behavior fixes.

@resolved_pro_server_renderer =
if rails_environment_loaded && defined?(ReactOnRailsPro)
# server_renderer is stored as a plain string in Pro config (for example, "NodeRenderer").
ReactOnRailsPro.configuration.server_renderer.to_s
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.

Minor: if ReactOnRailsPro.configuration returns nil for some reason (e.g. Pro not fully initialized), this will raise NoMethodError which is correctly caught by the rescue below. The rescue coverage is sufficient but a &. safe-navigator here (ReactOnRailsPro.configuration&.server_renderer.to_s) would make the nil-tolerance explicit.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Agreed the rescue already covers this. I’m keeping the explicit call (without safe navigation) so unexpected nil/incomplete Pro initialization still takes the warning path rather than silently coercing to an empty renderer value.

@justin808
Copy link
Copy Markdown
Member Author

Addressed the outside-diff fallback warning concern from review #3996919903 in commit 7df8bb25.

Change made:

  • check_server_rendering_engine now checks whether renderer_use_fallback_exec_js is actually enabled before warning when ExecJS is absent under NodeRenderer.
  • If fallback is disabled, Doctor now reports that explicitly instead of showing a false warning.

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Mar 28, 2026

Code Review

Overall well-structured. Moving Doctor diagnostics from brittle regex parsing to runtime config queries is the right call, and the fallback chain is clearly documented. Test coverage is thorough.

Correctness

1. Silent fallback when Rails is loaded but ReactOnRailsPro is not defined

In resolved_pro_server_renderer, when rails_environment_loaded is true but defined?(ReactOnRailsPro) is false, the code silently falls through to pro_initializer_has_node_renderer? (regex parsing) without warning. Rails loaded but Pro constant missing likely indicates a gem load error. The regex result could be a false positive and the user gets no signal. A distinct warning is needed here.

2. server_bundle_output_path always reported - inconsistent with other fields

Every other field in the runtime-config path suppresses output when the value matches the default (pool size, timeout, raise_on_prerender_error, etc.). But server_bundle_output_path is always printed regardless of whether it equals DEFAULT_SERVER_BUNDLE_OUTPUT_PATH. This creates noise for apps that have not changed the default.

3. Misleading label for the server_bundle_value.nil? case

When runtime config returns nil for server_bundle_js_file, the code falls back to server_bundle_filename and displays server-bundle.js (initializer/default). This label appears even when there is no initializer -- the value is just the hardcoded Ruby default. Consider distinguishing the two sub-cases in the label (e.g. (default) vs (from initializer)).

Minor

4. Non-obvious nil-guard pattern in config_path_resolver.rb lines 79-82

The two-statement guard (warn unless Rails.logger then Rails.logger&.debug) is correct but easy to misread as both statements always executing. A single if/else would make the mutual exclusion explicit.

5. .uniq is load-bearing correctness in webpack_config_candidates

A Shakapacker assets_bundler_config_path equal to a WEBPACK_DEFAULT_CONFIG_CANDIDATES entry (e.g. config/webpack/webpack.config.js) would be inserted twice and corrected only by the trailing .uniq. The existing comment describes this as accidental duplication; it would be clearer to note that standard-path Shakapacker configs intentionally rely on .uniq.

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: Dedup logic over-skips sibling bundler config candidate
    • Updated candidate generation to reject only the exact shakapacker path so sibling standard bundler configs remain discoverable and added a regression spec covering that case.

Create PR

Or push these changes by commenting:

@cursor push 45bdd4451b
Preview (45bdd4451b)
diff --git a/react_on_rails/lib/react_on_rails/config_path_resolver.rb b/react_on_rails/lib/react_on_rails/config_path_resolver.rb
--- a/react_on_rails/lib/react_on_rails/config_path_resolver.rb
+++ b/react_on_rails/lib/react_on_rails/config_path_resolver.rb
@@ -34,20 +34,14 @@
 
       shakapacker_config_dir = bundler_config_directory(shakapacker_config_path)
       if shakapacker_config_dir
-        shakapacker_basename = File.basename(shakapacker_config_path.to_s)
-        shakapacker_config_ext = File.extname(shakapacker_config_path.to_s).delete_prefix(".")
         candidates.concat(%w[js ts cjs mjs].flat_map do |ext|
-          # Skip only exact standard-name duplicates. Non-standard configured
-          # paths (for example `custom.config.cjs`) still probe standard-name
-          # fallbacks in the same directory; any accidental duplicates are
-          # de-duplicated by `candidates.uniq` below.
-          config_basenames = ["webpack.config.#{ext}", "rspack.config.#{ext}"]
-          next [] if ext == shakapacker_config_ext && config_basenames.include?(shakapacker_basename)
-
+          # Skip only the exact shakapacker path; still probe sibling
+          # standard-name configs (for example rspack when shakapacker points to
+          # webpack in the same directory).
           [
             File.join(shakapacker_config_dir, "webpack.config.#{ext}"),
             File.join(shakapacker_config_dir, "rspack.config.#{ext}")
-          ]
+          ].reject { |path| path == shakapacker_config_path }
         end)
       end
 

diff --git a/react_on_rails/spec/lib/react_on_rails/doctor_spec.rb b/react_on_rails/spec/lib/react_on_rails/doctor_spec.rb
--- a/react_on_rails/spec/lib/react_on_rails/doctor_spec.rb
+++ b/react_on_rails/spec/lib/react_on_rails/doctor_spec.rb
@@ -1879,6 +1879,18 @@
         expect(doctor.send(:resolved_webpack_config_path)).to eq("config/custom/custom-bundler.config.js")
       end
 
+      it "keeps sibling bundler candidates when shakapacker path uses a standard filename" do
+        allow(File).to receive(:file?).and_return(false)
+        allow(File).to receive(:file?).with("config/webpack/rspack.config.js").and_return(true)
+        allow(doctor).to receive(:bundler_config_directory)
+          .with("config/webpack/webpack.config.js")
+          .and_return("config/webpack")
+        allow(doctor).to receive(:shakapacker_assets_bundler_config_path)
+          .and_return("config/webpack/webpack.config.js")
+
+        expect(doctor.send(:resolved_webpack_config_path)).to eq("config/webpack/rspack.config.js")
+      end
+
       it "falls back to shakapacker-derived webpack config candidates when exact shakapacker path is not a file" do
         allow(File).to receive(:file?).and_return(false)
         allow(File).to receive(:file?).with("config/custom/webpack.config.ts").and_return(true)

You can send follow-ups to this agent here.

Comment thread react_on_rails/lib/react_on_rails/config_path_resolver.rb Outdated
if rails_environment_loaded && defined?(ReactOnRailsPro)
# server_renderer is stored as a plain string in Pro config (for example, "NodeRenderer").
ReactOnRailsPro.configuration.server_renderer.to_s
elsif pro_initializer_has_node_renderer?
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.

When rails_environment_loaded is true but defined?(ReactOnRailsPro) is false, the code falls through here to regex parsing without any warning. This means Rails loaded successfully but the Pro constant is absent — likely a gem load failure. The regex parse could return "NodeRenderer" even if the Pro gem never actually loaded, producing a false positive diagnostic. Suggest adding an explicit warning branch:

Suggested change
elsif pro_initializer_has_node_renderer?
elsif rails_environment_loaded && !defined?(ReactOnRailsPro)
checker.add_warning(
"⚠️ Rails environment loaded but ReactOnRailsPro constant is not defined. " \
"The Pro gem may have failed to load; falling back to initializer parsing."
)
pro_initializer_has_node_renderer? ? "NodeRenderer" : nil
elsif pro_initializer_has_node_renderer?

rails_bundle_path = server_bundle_path_match ? server_bundle_path_match[1] : default_path
rails_bundle_path =
if runtime_config
runtime_config.server_bundle_output_path || default_path
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.

Unlike server_renderer_pool_size, server_renderer_timeout, and component_registry_timeout (which are suppressed when equal to their defaults), server_bundle_output_path is always emitted. For the vast majority of apps this will always print ssr-generated. Suggest applying the same default-suppression pattern:

Suggested change
runtime_config.server_bundle_output_path || default_path
rails_bundle_path =
if runtime_config
runtime_config.server_bundle_output_path || default_path
else
server_bundle_path_match = content.match(/config\.server_bundle_output_path\s*=\s*["']([^"']+)["']/)
server_bundle_path_match ? server_bundle_path_match[1] : default_path
end
checker.add_info(" server_bundle_output_path: #{rails_bundle_path}") if rails_bundle_path != default_path

if server_bundle_value.present?
checker.add_info(" server_bundle_js_file: #{server_bundle_value}")
elsif server_bundle_value.nil?
fallback_server_bundle = server_bundle_filename
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.

When server_bundle_js_file is nil in the runtime config (i.e. no one ever called config.server_bundle_js_file =), server_bundle_filename falls back through initializer parsing and ultimately returns "server-bundle.js" — the hardcoded default. The label "(initializer/default)" then gets printed even for a plain new app with no initializer at all, which is confusing. Consider distinguishing the two sub-cases:

Suggested change
fallback_server_bundle = server_bundle_filename
elsif server_bundle_value.nil?
fallback_server_bundle = server_bundle_filename
label = File.exist?("config/initializers/react_on_rails.rb") ? "(from initializer/default)" : "(default)"
checker.add_info(" server_bundle_js_file: #{fallback_server_bundle} #{label}")

@shakapacker_assets_bundler_config_path = nil
rescue StandardError => e
message = "ReactOnRails could not read Shakapacker assets_bundler_config_path: #{e.class}: #{e.message}"
warn(message) unless Rails.logger
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 two sequential statements are correct — warn fires when logger is nil, &.debug fires when logger is present — but easy to misread as both always executing. A single if/else makes the mutual exclusion explicit:

Suggested change
warn(message) unless Rails.logger
if Rails.logger
Rails.logger.debug { message }
else
warn(message)
end

Comment on lines +57 to 59
candidates.concat(WEBPACK_DEFAULT_CONFIG_CANDIDATES)
candidates.concat(RSPACK_DEFAULT_CONFIG_CANDIDATES)
candidates.uniq
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 comment says .uniq removes "accidental" duplicates, but when Shakapacker's assets_bundler_config_path happens to equal a default candidate (e.g. config/webpack/webpack.config.js), that path is intentionally inserted twice — first at line 33 as the explicit Shakapacker path, then again here via concat(WEBPACK_DEFAULT_CONFIG_CANDIDATES). The .uniq is therefore load-bearing correctness for this case, not just a safety net. Worth clarifying the comment so future readers don't treat .uniq as removable cleanup:

Suggested change
candidates.concat(WEBPACK_DEFAULT_CONFIG_CANDIDATES)
candidates.concat(RSPACK_DEFAULT_CONFIG_CANDIDATES)
candidates.uniq
# Default fallback candidates intentionally mirror generator defaults
# (`.js` / `.ts`), while `.cjs` / `.mjs` are probed only within resolved
# shakapacker config directories above.
# NOTE: When Shakapacker's assets_bundler_config_path is itself one of
# these defaults (e.g. config/webpack/webpack.config.js), it will appear
# in both the explicit-path slot above and here. The `.uniq` call below is
# load-bearing for that case, not just a cleanup step.
candidates.concat(WEBPACK_DEFAULT_CONFIG_CANDIDATES)
candidates.concat(RSPACK_DEFAULT_CONFIG_CANDIDATES)

checker.add_info(" ExecJS available as fallback: #{ExecJS.runtime.name}")
else
elsif pro_execjs_fallback_enabled?
checker.add_warning(" ⚠️ ExecJS fallback is enabled but ExecJS is not available")
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 warning text here is misleading. This branch fires when defined?(ExecJS) is truthy but ExecJS.runtime is nil — meaning the ExecJS gem is available (defined in the process), it just has no JavaScript runtime configured. Saying "ExecJS is not available" contradicts that.

Suggested change
checker.add_warning(" ⚠️ ExecJS fallback is enabled but ExecJS is not available")
checker.add_warning(" ⚠️ ExecJS fallback is enabled but no ExecJS runtime is configured")

A follow-on for the tip line:

💡 Install mini_racer or configure an ExecJS runtime, or set renderer_use_fallback_exec_js = false


def resolved_webpack_config_path
webpack_config_candidates.find { |path| File.exist?(path) }
webpack_config_candidates.find { |path| File.file?(path) }
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.

resolved_webpack_config_path is called from at least three places — bundler_config_file_exists?, detect_bundler_config_path, and (indirectly) explicit_shakapacker_bundler_config_path? — and each call rebuilds webpack_config_candidates and does N×File.file? syscalls. shakapacker_assets_bundler_config_path was carefully memoized to avoid re-requiring Shakapacker; resolved_webpack_config_path should get the same treatment for consistency and to avoid redundant filesystem probing in a single doctor run.

Suggested change
webpack_config_candidates.find { |path| File.file?(path) }
def resolved_webpack_config_path
return @resolved_webpack_config_path if instance_variable_defined?(:@resolved_webpack_config_path)
@resolved_webpack_config_path = webpack_config_candidates.find { |path| File.file?(path) }
end


# rubocop:disable Metrics/CyclomaticComplexity
def analyze_server_rendering_config(content)
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
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.

Five of the six analyze_* methods now carry Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity disables. The dual-path structure (if runtime_config … else … end) is repeated in each one and accounts for most of the complexity. A lightweight display-value helper that resolves each setting once could eliminate the duplication:

# Resolves a setting to a display value.
# When runtime_config is available, calls the given block to extract it.
# Falls back to regex-parsing content when not.
def resolve_config_value(runtime_config, content, regex)
  if runtime_config
    yield runtime_config
  else
    match = content.match(regex)
    match ? match[1] : nil
  end
end

Not a blocker, but the current structure will make future settings additions expensive to maintain since every new attribute requires touching both branches in each method.

end

def pro_execjs_fallback_enabled?
return ReactOnRailsPro.configuration.renderer_use_fallback_exec_js if defined?(ReactOnRailsPro)
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.

Minor: defined?(ReactOnRailsPro) returns a string ("constant") when the constant exists. The string is truthy, so calling .configuration.renderer_use_fallback_exec_js on the actual constant works correctly. However, defined? doesn't guard against ReactOnRailsPro being defined but not yet fully loaded (e.g. autoload stub). The existing code in resolved_pro_server_renderer uses the same pattern, so this is consistent — just worth a note that defined? alone won't catch a NameError from a broken autoload. Adding a rescue NameError to the rescue clause on line 2437 would make the two guard strategies uniform.

@@ -308,40 +309,29 @@ def check_webpack_configuration
🚫 Bundler configuration not found.
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.

detect_bundler_config_path calls resolved_webpack_config_path first, then resolve_default_bundler_config_path which calls existing_bundler_config_paths which in turn calls webpack_config_candidates again. If resolved_webpack_config_path gets memoized (see comment on config_path_resolver.rb:27), this is free. Without memoization there are two full candidate-list builds plus two sets of File.file? syscalls for every detect_bundler_config_path invocation.

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Mar 28, 2026

Review: Doctor runtime config migration

Overall this is a solid improvement — replacing fragile regex parsing of initializer files with ReactOnRails.configuration runtime values is the right direction, and the graceful fallback chain (runtime → initializer regex → hardcoded default) is well-structured. The memoization approach using instance_variable_defined?/defined? for nil-able results is idiomatic and correct throughout.

Issues worth fixing

1. Misleading warning text (doctor.rb:690) — see inline comment. The "ExecJS fallback is enabled but ExecJS is not available" message fires when the ExecJS gem is defined (so defined?(ExecJS) is truthy) but ExecJS.runtime is nil. ExecJS is available in that case; only the runtime is unconfigured. The message will confuse users who have ExecJS installed but no runtime gem (e.g. mini_racer).

2. resolved_webpack_config_path not memoized (config_path_resolver.rb:27) — see inline comment. shakapacker_assets_bundler_config_path was explicitly memoized to avoid re-requireing Shakapacker on each call; resolved_webpack_config_path deserves the same treatment since it's called from bundler_config_file_exists?, detect_bundler_config_path, and existing_bundler_config_paths (via detect_bundler_config_path), each of which rebuilds the candidate list and issues N×File.file? syscalls.

Worth considering

3. Dual-path boilerplate in analyze_* methods — each of the six analyze methods now has an if runtime_config … else … end mirroring every setting, plus three Rubocop cop disables. A small resolve_config_value(runtime_config, content, regex) { |cfg| ... } abstraction (suggested in the inline comment) would cut the per-method branch count roughly in half and make adding new settings much cheaper.

4. defined?(ReactOnRailsPro) doesn't guard NameError from a broken autoloadrescue already catches StandardError/LoadError; adding NameError to the rescue clause in pro_execjs_fallback_enabled? would close the last gap consistently with how resolved_pro_server_renderer is guarded.

Looks good

  • TypeScript refactor in create-app.ts is clean and equivalent.
  • File.exist?File.file? is the right fix (directories would previously pass).
  • nil-vs-"" semantics for server_bundle_js_file (disabled vs unset) are consistently handled in server_bundle_filename, check_javascript_bundles, analyze_server_rendering_config, and check_server_bundle_prerender_consistency.
  • Test coverage is thorough — the new specs covering runtime-over-file precedence, whitespace-blank bundle values, memoization, and Pro renderer resolution are well-targeted.
  • Path normalization in normalize_shakapacker_assets_bundler_config_path correctly guards the Rails.root="/" edge case.

@justin808
Copy link
Copy Markdown
Member Author

Thanks for the detailed review — this is helpful.

Status update on the points you listed:

  1. doctor.rb:690 warning wording: acknowledged. Current logic is intentionally keyed on runtime availability (defined?(ExecJS) && ExecJS.runtime) for the fallback path, but I agree the phrasing can be clearer when ExecJS is present but runtime is not configured.
  2. resolved_webpack_config_path memoization: acknowledged as a valid optimization; currently left as follow-up work.
  3. analyze_* dual-path boilerplate: agreed this is a refactor opportunity; deferred to keep this PR scoped to behavior fixes.
  4. NameError hardening in pro_execjs_fallback_enabled?: good suggestion; also deferred for scope control.

Separately, I did address the actionable resolver bug around candidate skipping (sibling bundler candidate loss) in commit 04a8ebc9, with regression coverage in doctor_spec.

@justin808 justin808 merged commit defad95 into main Mar 28, 2026
46 checks passed
@justin808 justin808 deleted the jg-codex/2771-doctor-runtime-config-queries branch March 28, 2026 23:17
justin808 added a commit that referenced this pull request Mar 29, 2026
…2823)

## Summary
- migrate doctor configuration analysis to prefer
`ReactOnRails.configuration` runtime values after environment load
- keep initializer parsing as fallback when Rails environment cannot be
loaded
- use Pro runtime renderer detection for server rendering engine checks,
with initializer fallback
- update server bundle/prerender consistency checks to use runtime
config values
- add specs covering runtime-over-file precedence and runtime-based
NodeRenderer detection

## Testing
- `bundle exec rspec
react_on_rails/spec/lib/react_on_rails/doctor_spec.rb`
- `bundle exec rspec
react_on_rails/spec/lib/react_on_rails/doctor_rake_task_spec.rb`
- `bundle exec rubocop react_on_rails/lib/react_on_rails/doctor.rb
react_on_rails/spec/lib/react_on_rails/doctor_spec.rb`

Fixes #2771

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Moderate risk because it changes diagnostic/config-resolution behavior
(runtime vs initializer parsing, SSR bundle checks, and Shakapacker
config discovery), which may affect what setups are reported as healthy
or broken across diverse project layouts.
> 
> **Overview**
> Doctor now **prefers loaded runtime configuration**
(`ReactOnRails.configuration`) for reporting and consistency checks,
falling back to initializer regex parsing only when the Rails
environment can’t be loaded; SSR bundle existence checks are skipped
when `server_bundle_js_file` is blank/whitespace and default-valued
settings are omitted to reduce noise.
> 
> Bundler config discovery was reworked to use Shakapacker’s
`assets_bundler_config_path` (with memoized/normalized lookup) and to
probe webpack/rspack config candidates deterministically (JS before TS,
`.cjs/.mjs` only in Shakapacker-derived dirs), with clearer messaging in
`Doctor`/`SystemChecker` and expanded specs covering these scenarios
(including Pro `NodeRenderer` detection and ExecJS fallback messaging).
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
04a8ebc. 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

* **Improvements**
* Doctor now prioritizes runtime configuration over initializer parsing
for SSR and performance reporting, skips SSR bundle checks when runtime
bundle filename is blank/whitespace, and reduces noise by omitting
default-valued settings.
* Clarified diagnostics and messaging around runtime vs
initializer-sourced settings.

* **Tests**
* Expanded specs covering runtime-vs-initializer precedence, SSR skip
behavior, prerender consistency scenarios, Pro renderer resolution, and
memoization.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
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.

Doctor: migrate from regex config parsing to runtime config queries

1 participant