Skip to content

fix: replace marker file with Thor-idiomatic force option (#2289)#2411

Open
ihabadham wants to merge 10 commits intomasterfrom
ihabadham/fix/replace-marker-with-force-option
Open

fix: replace marker file with Thor-idiomatic force option (#2289)#2411
ihabadham wants to merge 10 commits intomasterfrom
ihabadham/fix/replace-marker-with-force-option

Conversation

@ihabadham
Copy link
Collaborator

@ihabadham ihabadham commented Feb 14, 2026

Summary

Fixes #2289 — when rails generate react_on_rails:install runs on a fresh Rails app (Shakapacker not pre-installed), private_output_path: ssr-generated stays commented out in config/shakapacker.yml.

Root cause: The .shakapacker_just_installed marker file approach caused copy_packer_config to skip RoR's shakapacker.yml.tt template entirely, so the <% if shakapacker_version_9_or_higher? %> conditional that uncomments private_output_path never executed.

Fix: Replace the marker file with Thor-idiomatic force: true option:

  • Record config_existed = File.exist?("config/shakapacker.yml") before running install_shakapacker
  • Pass shakapacker_just_installed: !config_existed to base_generator via invoke options
  • copy_packer_config uses template(..., force: true) for fresh installs (safe to overwrite Shakapacker defaults), and plain template(...) for pre-existing configs (prompts user)

This also fixes a secondary edge case: in partial-install scenarios (customized config exists but shakapacker_configured? returns false), the old approach would silently force-overwrite user customizations.

Changes

  • install_generator.rb
    • config_existed check before install, passes boolean via invoke options, removed marker file creation
    • Declared hidden class_option :shakapacker_just_installed so tests can pass the flag via CLI
    • Forward force/skip flags to invoked sub-generators (Thor's explicit options hash was dropping them)
  • base_generator.rb — Hidden class_option :shakapacker_just_installed, conditional force: true in copy_packer_config
  • generator_messages.rb — Removed dead marker file branch from build_shakapacker_status_section
  • install_generator_spec.rb — Regression test for Generator skips private_output_path configuration when Shakapacker is freshly installed #2289, unit test for force/non-force behavior, --skip flag for pre-existing config tests

Related

Test plan

  • Fresh install via test app: private_output_path: ssr-generated uncommented in generated config
  • CI passes (RSpec generator tests)
  • Pre-installed Shakapacker: user prompted before overwriting customized config

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Refactor

    • Improved Shakapacker configuration handling during installation to better detect and manage configuration state.
  • Tests

    • Enhanced test coverage for fresh Shakapacker installations and configuration force-overwriting scenarios.
    • Expanded testing for rspack integration with and without TypeScript.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 14, 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

Replaces the on-disk marker file .shakapacker_just_installed with a hidden boolean option shakapacker_just_installed propagated from InstallGenerator to BaseGenerator. copy_packer_config now applies the shakapacker template with force: true for fresh installs and without force for pre-installed configurations; marker-file creation/removal was removed.

Changes

Cohort / File(s) Summary
BaseGenerator configuration
react_on_rails/lib/generators/react_on_rails/base_generator.rb
Added class_option :shakapacker_just_installed (hidden boolean). Reworked copy_packer_config to branch on the option instead of an on-disk marker; apply template with force: true for fresh installs, otherwise apply non-forced template and update log messages.
InstallGenerator logic
react_on_rails/lib/generators/react_on_rails/install_generator.rb
Added class_option :shakapacker_just_installed and compute config_existed = File.exist?("config/shakapacker.yml"); set @shakapacker_just_installed = !config_existed (in-memory) and pass that flag to downstream generators. Removed creation/removal of .shakapacker_just_installed; updated finalize_shakapacker_setup signature to accept config_existed.
Generator messages
react_on_rails/lib/generators/react_on_rails/generator_messages.rb
Removed verbose marker-file-specific messaging branch. Simplified Shakapacker status output to two-path logic based on presence of bin/shakapacker and bin/shakapacker-dev-server.
InstallGenerator tests
react_on_rails/spec/react_on_rails/generators/install_generator_spec.rb
Reworked tests to simulate pre-installed vs freshly installed Shakapacker via filesystem setup rather than marker file; removed marker-file assertions; added regression tests (including #2289) and assertions verifying copy_packer_config uses force: true for fresh installs and non-forced otherwise; expanded rspack/TypeScript scenarios and helpers.

Sequence Diagram

sequenceDiagram
    participant User as User
    participant InstallGen as InstallGenerator
    participant BaseGen as BaseGenerator
    participant Template as shakapacker.yml.tt
    participant Config as config/shakapacker.yml

    User->>InstallGen: run react_on_rails:install
    InstallGen->>InstallGen: config_existed = File.exist?("config/shakapacker.yml")
    alt fresh install
        InstallGen->>InstallGen: set `@shakapacker_just_installed` = true
    else pre-installed
        InstallGen->>InstallGen: set `@shakapacker_just_installed` = false
    end
    InstallGen->>BaseGen: invoke generators (pass shakapacker_just_installed, force, skip, typescript)
    BaseGen->>BaseGen: evaluate shakapacker_just_installed?
    alt true
        BaseGen->>Template: apply template (force: true)
        Template->>Config: overwrite/update (ensure version-aware settings)
    else false
        BaseGen->>Template: apply template (no force)
        Template->>Config: preserve existing file
    end
    BaseGen->>User: complete
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

  • #2289: Generator skips private_output_path configuration when Shakapacker is freshly installed — addressed by applying the RoR template with force: true on fresh installs (regression test added).

Poem

🐰 A hop, a flag no longer on disk,
Memory keeps whether Shaka was brisk,
Templates run where markers once stood,
private paths unhidden — configuration good,
Hooray for clean hops and fewer risks! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately describes the main fix: replacing the marker file approach with a Thor-idiomatic force option to resolve issue #2289.
Linked Issues check ✅ Passed The changes comprehensively address issue #2289 by implementing the suggested Thor-idiomatic force option approach with proper version-conditional template execution and interactive behavior preservation.
Out of Scope Changes check ✅ Passed All changes are scoped to fixing the marker file mechanism and ensuring proper Shakapacker config handling as specified in the linked issue.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch ihabadham/fix/replace-marker-with-force-option

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

greptile-apps bot commented Feb 14, 2026

Greptile Summary

Replaces the .shakapacker_just_installed marker file with Thor-idiomatic force: true option to fix issue #2289 where private_output_path: ssr-generated stayed commented in fresh Shakapacker installs.

Key changes:

  • install_generator.rb now checks config_existed = File.exist?("config/shakapacker.yml") before running install_shakapacker, then sets @shakapacker_just_installed = !config_existed to pass to base generator
  • base_generator.rb uses template(..., force: true) when shakapacker_just_installed? is true (safe to overwrite fresh defaults), otherwise uses plain template(...) which respects --force/--skip flags or prompts user
  • Added hidden class_option :shakapacker_just_installed to both generators for test support
  • Forwarded force: and skip: flags to invoked sub-generators (Thor's explicit options hash was dropping them)
  • generator_messages.rb removed dead marker file detection branch
  • Tests updated to use --skip flag and added regression test for Generator skips private_output_path configuration when Shakapacker is freshly installed #2289

This ensures RoR's version-aware template (with private_output_path uncommented for Shakapacker 9+) is always applied to fresh installs, while preserving user customizations in pre-existing configs.

Confidence Score: 5/5

  • Safe to merge - well-tested fix with comprehensive test coverage and no breaking changes
  • The PR fixes a real bug with a clean, Thor-idiomatic solution. The logic is sound: checking file existence before install, passing the flag through options, and using force only when safe. Tests cover the regression case, force/non-force behavior, and existing workflows. No breaking changes or risky patterns.
  • No files require special attention

Important Files Changed

Filename Overview
react_on_rails/lib/generators/react_on_rails/install_generator.rb Replaced marker file with runtime detection of pre-existing config, added hidden class option for tests, forwarded force/skip flags to sub-generators
react_on_rails/lib/generators/react_on_rails/base_generator.rb Added hidden shakapacker_just_installed option, uses force: true when Shakapacker was freshly installed to ensure RoR template is applied
react_on_rails/lib/generators/react_on_rails/generator_messages.rb Removed dead code branch for marker file detection
react_on_rails/spec/react_on_rails/generators/install_generator_spec.rb Updated tests to use --skip flag instead of marker file simulation, added regression test for #2289, added unit tests for force/non-force behavior

Flowchart

flowchart TD
    A[rails generate react_on_rails:install] --> B{config/shakapacker.yml exists?}
    B -->|Yes| C[config_existed = true]
    B -->|No| D[config_existed = false]
    C --> E[install_shakapacker runs]
    D --> E
    E --> F[Set @shakapacker_just_installed = !config_existed]
    F --> G[Pass shakapacker_just_installed flag to base_generator]
    G --> H{shakapacker_just_installed?}
    H -->|true| I[template with force: true]
    H -->|false| J[template without force - Thor prompts user]
    I --> K[RoR template applies: private_output_path uncommented, precompile_hook configured]
    J --> L[User decides: overwrite, skip, or diff]
    K --> M[gsub_file for additional config: rspack, precompile_hook]
    L --> M
    M --> N[Generation complete]
Loading

Last reviewed commit: f93a161

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

4 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

@greptile-apps
Copy link

greptile-apps bot commented Feb 14, 2026

Additional Comments (1)

react_on_rails/lib/generators/react_on_rails/install_generator.rb
Edge case with --ignore-warnings and failed install

If install_shakapacker fails but --ignore-warnings is set, handle_shakapacker_install_error doesn't raise, install_shakapacker returns early, and then finalize_shakapacker_setup(config_existed) still runs — potentially setting @shakapacker_just_installed = true even though the install didn't succeed.

This is a pre-existing pattern (the old marker file would also have been created in this scenario), so it's not a regression. But you could guard against it:

      def ensure_shakapacker_installed
        return if shakapacker_configured?

        print_shakapacker_setup_banner
        ensure_shakapacker_in_gemfile

        config_existed = File.exist?("config/shakapacker.yml")

        return unless install_shakapacker

        finalize_shakapacker_setup(config_existed)

This would require install_shakapacker to return a truthy value on success (it currently returns nil on both success and failure paths).

Copy link
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: 1

🤖 Fix all issues with AI agents
In `@react_on_rails/spec/react_on_rails/generators/install_generator_spec.rb`:
- Around line 233-263: The test never triggers
InstallGenerator.finalize_shakapacker_setup because it simulates existing
shakapacker files (so shakapacker_configured? returns true) and the CLI flag
--shakapacker-just-installed isn't effective; to fix, change the before(:all)
setup for this context so ensure_shakapacker_installed actually runs — either
remove the simulated files ("config/shakapacker.yml", "bin/shakapacker",
"config/webpack/webpack.config.js") so InstallGenerator will proceed to call
finalize_shakapacker_setup and set `@shakapacker_just_installed`, or explicitly
set `@shakapacker_just_installed` = true before invoke so BaseGenerator receives
shakapacker_just_installed: true and copy_packer_config will use force: true;
also confirm the test does not rely on run_generator_test_with_args always
adding --force which masks the bug.

Copy link
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.

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/generators/react_on_rails/install_generator.rb (1)

177-187: ⚠️ Potential issue | 🟡 Minor

finalize_shakapacker_setup runs even when install_shakapacker fails under --ignore-warnings.

If install_shakapacker fails and --ignore-warnings is set, the error handler returns without raising, and execution falls through to finalize_shakapacker_setup. This prints "✅ Shakapacker installed successfully!" and sets @shakapacker_just_installed, which could lead to a force: true overwrite of a config that wasn't actually freshly generated.

Consider guarding finalize on install success:

Proposed fix
-        install_shakapacker
-        finalize_shakapacker_setup(config_existed)
+        install_shakapacker
+        return unless shakapacker_configured?
+
+        finalize_shakapacker_setup(config_existed)

@ihabadham ihabadham added the bug label Feb 15, 2026
@ihabadham ihabadham requested a review from justin808 February 15, 2026 00:24
@justin808
Copy link
Member

@greptile, review this PR.
@claude, review this PR.

@justin808 justin808 added the P0 Merge this week label Feb 15, 2026
@claude
Copy link

claude bot commented Feb 15, 2026

Claude finished @justin808's task in 2m 24s —— View job


PR Review Complete ✅

Summary

This PR successfully fixes issue #2289 by replacing the fragile marker file approach with Thor-idiomatic option passing. The implementation is well-designed and the previous review concerns from CodeRabbit have been properly addressed in commit f93a161.

Verdict: Ready to merge ✅ All CI checks pass, good test coverage, clean implementation.


What This PR Does

Problem: When rails generate react_on_rails:install runs on a fresh Rails app (Shakapacker not pre-installed), the private_output_path: ssr-generated setting stayed commented out in config/shakapacker.yml.

Root Cause: The .shakapacker_just_installed marker file caused copy_packer_config to skip React on Rails' version-aware template entirely, so the ERB conditional <% if shakapacker_version_9_or_higher? %> at react_on_rails/lib/generators/react_on_rails/templates/base/base/config/shakapacker.yml.tt:35 never executed.

Fix: Replace marker file with in-memory boolean flag:

  1. install_generator.rb:192 records config_existed = File.exist?("config/shakapacker.yml") before running Shakapacker installer
  2. install_generator.rb:318 sets @shakapacker_just_installed = !config_existed after install completes
  3. install_generator.rb:116 passes shakapacker_just_installed: just_installed to base generator via invoke options
  4. base_generator.rb:104-112 uses template(..., force: true) for fresh installs vs. plain template(...) for pre-existing configs

Code Quality Assessment

✅ Strengths

  1. Previous Review Issue Fixed: Commit f93a161 properly addresses CodeRabbit's concern about the regression test not exercising the intended code path by declaring class_option :shakapacker_just_installed on InstallGenerator (lines 46-52).

  2. Clean Thor Integration: The implementation is Thor-idiomatic:

    • Uses hidden class_option instead of filesystem side effects
    • Properly forwards force/skip flags to invoked generators (install_generator.rb:119-120)
    • Respects Thor's conflict resolution when shakapacker_just_installed is false
  3. Good Test Coverage:

  4. Clear Comments: The code includes helpful inline documentation explaining the approach (base_generator.rb:30-36, install_generator.rb:46-49).

  5. No Breaking Changes: The fix is backward compatible and doesn't affect existing workflows.


Issues & Observations

⚠️ Minor: Edge Case with --ignore-warnings (Pre-existing, not a regression)

Location: install_generator.rb:194-195

If install_shakapacker fails but --ignore-warnings is set, execution continues to finalize_shakapacker_setup(config_existed), which:

  • Prints "✅ Shakapacker installed successfully!" (misleading)
  • Sets @shakapacker_just_installed = true even though install failed
  • Could lead to force-overwriting a config that wasn't actually freshly generated

This was also flagged by Greptile's review and is a pre-existing pattern, not introduced by this PR. The old marker file approach had the same issue. Consider guarding with:

def ensure_shakapacker_installed
  return if shakapacker_configured?

  print_shakapacker_setup_banner
  ensure_shakapacker_in_gemfile

  config_existed = File.exist?("config/shakapacker.yml")

  install_shakapacker
  return unless shakapacker_configured?  # Guard against failed install

  finalize_shakapacker_setup(config_existed)
end

This would require verifying that Shakapacker actually got configured before finalizing. However, since this is a pre-existing issue and not a regression, it can be addressed in a follow-up PR if desired.

ℹ️ Code Organization

The logic at install_generator.rb:116 has a bit of defensive redundancy:

just_installed = options.shakapacker_just_installed? || @shakapacker_just_installed || false

This handles three scenarios:

  1. CLI flag passed (tests)
  2. Runtime detection (normal flow)
  3. Fallback to false (when neither is set)

This is correct and defensive, but the || false is technically unnecessary since nil || nil would already be falsy. However, being explicit is fine for clarity.


Test Coverage Analysis

✅ Well-Tested Scenarios

  1. Fresh install with Shakapacker 9+ (lines 240-291):

    • Verifies private_output_path: ssr-generated is uncommented
    • Confirms full RoR template is applied (not Shakapacker's default)
  2. Pre-installed Shakapacker (lines 162-235):

    • Uses --skip to preserve existing config
    • Verifies configure_precompile_hook_in_shakapacker still runs via gsub_file
    • Confirms YAML structure, comments, and anchors are preserved
  3. Force behavior unit tests (lines 293-327):

    • Mocks verify force: true is passed when shakapacker_just_installed: true
    • Confirms no force when shakapacker_just_installed: false

📊 Coverage Improvements

The test suite properly simulates the scenario from issue #2289 by:

  • Creating a Shakapacker-default config with # private_output_path: ssr-generated commented out
  • Passing --shakapacker-just-installed to trigger force-overwrite
  • Asserting that the final config has the setting uncommented

Architecture & Design

✅ Solid Design Choices

  1. Separation of Concerns:

    • InstallGenerator detects whether config pre-existed
    • BaseGenerator decides whether to force-overwrite based on flag
    • Clean responsibility separation
  2. Follows Thor Conventions:

    • Hidden options for internal coordination between generators
    • Proper use of invoke with options hash
    • Respects user's --force and --skip flags
  3. Removes Filesystem Side Effects:

    • No temporary marker files to clean up
    • No race condition risks
    • Simpler control flow

CI Status

All checks passing:

  • Generator tests: ✅ Success
  • Rspec test for gem: ✅ Success
  • Integration Tests: ✅ Success
  • Lint JS and Ruby: ✅ Success
  • Assets Precompile Check: ✅ Success

Files Changed Review

react_on_rails/lib/generators/react_on_rails/base_generator.rb (+17/-9)

Rating: ⭐⭐⭐⭐⭐ Excellent

  • Clean replacement of marker file check with option check
  • Proper use of force: true vs. letting Thor handle conflicts
  • Clear comments explaining the approach

react_on_rails/lib/generators/react_on_rails/install_generator.rb (+24/-7)

Rating: ⭐⭐⭐⭐ Very Good

  • Declares hidden option (fixes CodeRabbit's concern)
  • Records config existence before install
  • Properly forwards options to invoked generators
  • One minor edge case with --ignore-warnings (pre-existing)

react_on_rails/lib/generators/react_on_rails/generator_messages.rb (+1/-11)

Rating: ⭐⭐⭐⭐⭐ Excellent

  • Clean removal of dead marker file code
  • Simplified logic maintains same functionality

react_on_rails/spec/react_on_rails/generators/install_generator_spec.rb (+196/-89)

Rating: ⭐⭐⭐⭐⭐ Excellent

  • Comprehensive test coverage for regression
  • Unit tests for force/non-force behavior
  • Good use of test helpers and context setup
  • Clear test descriptions

Recommendations

For This PR (Optional)

  • The --ignore-warnings edge case mentioned above could be addressed, but it's not a blocker since it's pre-existing behavior

For Follow-up (Optional)

  • Consider adding integration test that actually runs rails shakapacker:install in a test Rails app to verify end-to-end behavior
  • Document the --shakapacker-just-installed flag in generator help text (currently hidden, which is fine for internal use)

Final Verdict

✅ Approved - Ready to Merge

This is a well-designed fix that:

The one minor edge case with --ignore-warnings is pre-existing and doesn't block this PR.

Great work on the fix and the follow-up commit addressing the test issue! 🎉


| Branch: ihabadham/fix/replace-marker-with-force-option

@claude
Copy link

claude bot commented Feb 24, 2026

Review: fix: replace marker file with Thor-idiomatic force option (#2289)

The approach here is well-reasoned — replacing a filesystem side-channel (marker file) with in-process state is a clear improvement, and forwarding force/skip flags to sub-generators fixes a real gap. A few issues worth addressing before merge:

Issues

1. finalize_shakapacker_setup is called unconditionally after a potentially-failed install

In ensure_shakapacker_installed, finalize_shakapacker_setup(config_existed) is called whether or not install_shakapacker succeeds. When --ignore-warnings is set, handle_shakapacker_install_error records the error and returns (rather than raising), so finalize_shakapacker_setup still runs and sets @shakapacker_just_installed = true. On the next step, copy_packer_config then silently force-overwrites whatever partial or missing config/shakapacker.yml exists. In a broken install this is probably harmless, but it masks the failure path — consider early-returning or checking a failure flag before calling finalize_shakapacker_setup.

2. Hidden --shakapacker-just-installed flag enables silent force-overwrite of user configs

While hide: true keeps it out of --help output, the flag is still accessible from the command line. If a user with a customized config/shakapacker.yml passes --shakapacker-just-installed (e.g. after reading docs or a StackOverflow answer), their customizations are silently overwritten without any Thor conflict prompt. For tests, this could alternatively be driven by stubbing shakapacker_configured? and install_shakapacker rather than exposing a destructive flag on the public interface.

3. Redundant || false in just_installed assignment

just_installed = options.shakapacker_just_installed? || @shakapacker_just_installed || false

options.shakapacker_just_installed? always returns a boolean (Thor type: :boolean), and @shakapacker_just_installed can only be nil or true. The trailing || false is unreachable.

4. Test isolation: describe "copy_packer_config force behavior" creates real files without cleanup

The before block writes config/shakapacker.yml into the shared dummy-for-generators destination, and since template is mocked, nothing cleans it up. There is no after block. This file persists and could affect subsequent test contexts that rely on the absence of config/shakapacker.yml. An after { FileUtils.rm_f(...) } would prevent leakage.

Minor

The regression test for #2289 exercises the options.shakapacker_just_installed? CLI flag path but does not exercise the @shakapacker_just_installed instance-variable path (the one that fires in real production use). Covering that path — e.g. by stubbing install_shakapacker and letting ensure_shakapacker_installed set the ivar — would give better confidence the fix works end-to-end.


Overall direction is correct; the marker-file approach was fragile. Items 1 and 4 are the most actionable before merge.

Copy link
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/lib/generators/react_on_rails/generator_messages.rb (1)

145-152: Consider also checking for config/shakapacker.yml before reporting “already configured.”

If the bin scripts exist but the config file was deleted or never created, the status message will be misleading. Including the config file in the check reduces false positives.

Suggested tweak
-      if File.exist?("bin/shakapacker") && File.exist?("bin/shakapacker-dev-server")
+      if File.exist?("bin/shakapacker") &&
+         File.exist?("bin/shakapacker-dev-server") &&
+         File.exist?("config/shakapacker.yml")
         "\n📦 #{Rainbow('Shakapacker already configured ✓').green}#{version_warning}"
       else
         "\n📦 #{Rainbow('Shakapacker setup may be incomplete').yellow}#{version_warning}"
       end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@react_on_rails/lib/generators/react_on_rails/generator_messages.rb` around
lines 145 - 152, The status message in build_shakapacker_status_section can be
misleading because it only checks for bin/shakapacker and
bin/shakapacker-dev-server; update the conditional to also verify the presence
of the Shakapacker config (e.g. File.exist?("config/shakapacker.yml")) so the
"already configured" branch only runs when both bin scripts and the config file
exist; keep the appended version_warning (from
check_shakapacker_version_warning) unchanged and adjust the if condition in
build_shakapacker_status_section accordingly.
🤖 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/lib/generators/react_on_rails/generator_messages.rb`:
- Around line 145-152: The status message in build_shakapacker_status_section
can be misleading because it only checks for bin/shakapacker and
bin/shakapacker-dev-server; update the conditional to also verify the presence
of the Shakapacker config (e.g. File.exist?("config/shakapacker.yml")) so the
"already configured" branch only runs when both bin scripts and the config file
exist; keep the appended version_warning (from
check_shakapacker_version_warning) unchanged and adjust the if condition in
build_shakapacker_status_section accordingly.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f93a161 and 5a98064.

📒 Files selected for processing (4)
  • react_on_rails/lib/generators/react_on_rails/base_generator.rb
  • react_on_rails/lib/generators/react_on_rails/generator_messages.rb
  • react_on_rails/lib/generators/react_on_rails/install_generator.rb
  • react_on_rails/spec/react_on_rails/generators/install_generator_spec.rb
🚧 Files skipped from review as they are similar to previous changes (2)
  • react_on_rails/lib/generators/react_on_rails/base_generator.rb
  • react_on_rails/lib/generators/react_on_rails/install_generator.rb

@claude
Copy link

claude bot commented Feb 24, 2026

Review: Replace marker file with Thor-idiomatic force option

Overall: The approach is sound - moving from a filesystem marker file to an in-memory boolean eliminates the surface-area risks (leftover files, concurrent runs, partial cleanup) and aligns with Thor idioms. The bug fix for #2289 is correct. A few issues worth addressing.

1. Runtime detection path is untested (main gap)

The regression test for #2289 uses the hidden CLI flag --shakapacker-just-installed to drive copy_packer_config with force: true. This correctly tests BaseGenerator's branch, but the actual end-to-end detection path in InstallGenerator - the config_existed = File.exist?(...) then @shakapacker_just_installed = \!config_existed chain - is never exercised by any test. In all new test contexts, shakapacker_configured? returns true (because bin stubs + yml + webpack config all pre-exist), so ensure_shakapacker_installed exits early and finalize_shakapacker_setup is never called.

A unit test for ensure_shakapacker_installed that stubs install_shakapacker returning true and asserts @shakapacker_just_installed is set correctly would close this gap.

2. Edge case: Shakapacker installer may re-create a pre-existing config

If config/shakapacker.yml exists but the binaries do not (partial install), shakapacker_configured? returns false, so we enter the setup path with config_existed = true. Then bundle exec rails shakapacker:install runs and may overwrite the yml with a fresh Shakapacker default. Because config_existed = true, we set @shakapacker_just_installed = false and skip the force-copy, leaving private_output_path commented out in the now-fresh config - reproducing the original bug.

This tradeoff is reasonable (protecting potential customizations), but it is worth documenting with a comment that this known edge case exists.

3. UX regression: fresh-install summary loses detail

The removed branch in generator_messages.rb used to show an informative just-installed summary (Added to Gemfile / Installer ran successfully / Webpack integration configured). After this change, a fresh install will show the generic 'Shakapacker already configured' message, since the binaries now exist post-install. Still accurate, just less informative. Low priority.

4. Minor: trailing || false is redundant

just_installed = options.shakapacker_just_installed? || @shakapacker_just_installed || false

@shakapacker_just_installed is true, false, or nil (unset). The trailing || false adds no value since all three cases are already handled by the || chain. Nit-level, no behavioral impact.

@claude
Copy link

claude bot commented Feb 24, 2026

PR Review: Replace Marker File with Thor-idiomatic Force Option

Overall assessment: The approach is sound and the root-cause fix is correct. Replacing the marker-file mechanism with content comparison in finalize_shakapacker_setup cleanly handles all three scenarios (fresh install, user accepted overwrite, user declined overwrite). The fix to forward force/skip flags through invoke is also a real correctness win. A few issues worth addressing before merge:


Issues

1. force/skip forwarding is incomplete

The PR correctly forwards force: options[:force], skip: options[:skip] to :base, :react_with_redux, and :react_no_redux, but NOT to :pro and :rsc (lines 156-159 of install_generator.rb). If a user runs rails g react_on_rails:install --rsc --force, the --force flag will apply to base-generator file operations but silently drop when :rsc is invoked. This should be consistent.

2. Duplicated just_installed resolution logic

options.shakapacker_just_installed? || @shakapacker_just_installed appears in two methods (invoke_generators and add_post_install_message). This should be extracted to a private helper method (e.g. shakapacker_just_installed?) to eliminate duplication and prevent the two sites from diverging.

3. yml_path string duplicated across two methods

"config/shakapacker.yml" is defined independently in ensure_shakapacker_installed and finalize_shakapacker_setup. A private constant or passing the path as a parameter would guard against future typos.

4. File.exist?/File.read use Dir.pwd rather than destination_root

The content-capture code in ensure_shakapacker_installed and finalize_shakapacker_setup reads relative to Dir.pwd, not destination_root. This works in practice (generators run from the Rails app root), but diverges from Thor's model and required the unit tests to use Dir.chdir(dir) rather than just stubbing destination_root. A comment noting this reliance would help future maintainers.

5. --shakapacker-just-installed hidden option on InstallGenerator is primarily a test seam

The comment acknowledges this ("In production, this is normally detected at runtime"), but exposing a hidden CLI option specifically to make integration tests easier is a design smell. The option is partly redundant: the unit tests for ensure_shakapacker_installed detection path already cover the runtime path without it. Consider whether the integration test for #2289 could instead stub @shakapacker_just_installed directly, or whether the option's value to advanced users justifies keeping it.


What's done well

  • Content comparison (yml_content_before \!= yml_content_after) correctly handles all three cases including the previously-missed "user accepted overwrite" scenario.
  • install_shakapacker now returns an explicit true/false, making the guard clause in ensure_shakapacker_installed clear.
  • Removing the marker-file side effect eliminates the risk of stale .shakapacker_just_installed files polluting the app working tree.
  • Unit tests for the detection path are well-structured and cover all cases including install failure.

@claude
Copy link

claude bot commented Feb 24, 2026

Review

Previous rounds addressed most of the feedback well — the content-comparison approach in finalize_shakapacker_setup, the extracted shakapacker_just_installed? helper, the SHAKAPACKER_YML_PATH constant, the Dir.pwd comment, the test isolation fix, and guarding finalize_shakapacker_setup behind if install_shakapacker are all solid improvements.

Two remaining issues:

1. force/skip forwarding is incomplete

Commit 30be511 correctly forwarded force/skip to :base, :react_with_redux, and :react_no_redux, but the :pro and :rsc invocations were not updated:

# install_generator.rb:155-158
invoke "react_on_rails:pro", [], { invoked_by_install: true } if use_pro?
return unless use_rsc?
invoke "react_on_rails:rsc", [], { typescript: options.typescript?, invoked_by_install: true }

If a user runs rails g react_on_rails:install --rsc --force, the --force flag applies to the base generator's file operations but is silently dropped when :rsc is invoked, causing inconsistent conflict resolution behavior. Both should receive force: options[:force], skip: options[:skip] like the other invocations.

2. Dead destination_root stubs in detection path tests

See inline comment on line 1202 of the spec. The stubs are a minor cleanup but worth removing since they imply the code uses destination_root for path resolution when it actually relies on Dir.pwd.

@claude
Copy link

claude bot commented Feb 24, 2026

Review: fix: replace marker file with Thor-idiomatic force option (#2289)

This is a well-designed fix. The content-comparison approach in finalize_shakapacker_setup is significantly more robust than the marker file and handles three important cases correctly: fresh install, user consenting to overwrite, and user keeping their custom config. Forwarding force:/skip: to all sub-generator invocations is a good secondary fix that was clearly missing before.

A few issues to flag:

Test isolation: leftover directories

The describe "copy_packer_config force behavior" block creates spec/react_on_rails/dummy-for-generators/config/ in before but the after only removes config/shakapacker.yml, leaving behind the dummy-for-generators/config/ directory tree. Since template is mocked the file write is not strictly needed anyway. Consider FileUtils.rm_rf(destination) in the after block, or replacing the hardcoded path with Dir.mktmpdir.

Edge case in content comparison

In finalize_shakapacker_setup:

@shakapacker_just_installed = yml_content_before != yml_content_after

If install_shakapacker returns true but Shakapacker somehow does not write config/shakapacker.yml (highly unlikely but theoretically possible), both sides are nil, giving nil != nil which evaluates to false. The generator would skip the force-overwrite even though this is a fresh install. Worth a brief comment explaining this is treated as "no change needed" or adding a post-install check that the file was created.

puts vs. say in copy_packer_config

The new branch uses puts "Replacing Shakapacker default config with React on Rails version". This is consistent with the pre-existing pattern in this file, but Thor's say respects the shell interface (colour, quiet mode). Not a blocker given the existing codebase pattern.

Minor: hidden option is CLI-accessible

The --shakapacker-just-installed option is hidden from --help but still reachable on the command line. An advanced user could pass it to force-overwrite a pre-existing customised shakapacker.yml. The comment explains it is for tests and runtime detection supersedes it in normal use, which is reasonable. Just worth a note in the option description that it overrides the runtime detection.


Overall the approach is correct and the test coverage is solid. The regression test is particularly well-constructed: using --shakapacker-just-installed without --force ensures the fix works via force: true in copy_packer_config, not via a global overwrite.

@claude
Copy link

claude bot commented Feb 24, 2026

Review: Replace marker file with Thor-idiomatic force option

The overall approach is solid — replacing a file-based sentinel with an in-memory flag and explicit option forwarding is more idiomatic Thor and eliminates the cleanup burden. The regression test for #2289 and the unit tests for copy_packer_config force behavior are good additions.

Issues found

Stale marker file in --pro --rspack test (spec line 675)

This context still creates .shakapacker_just_installed and uses the old run_generator_test_with_args helper, which was not migrated to the new pattern. The marker file is now completely ignored by the generator, so this context is silently testing the pre-installed scenario while its comment says "just installed". The test likely still passes because configure_rspack_in_shakapacker uses gsub_file regardless of the force flag, but the scenario is not what is documented. The other rspack tests (plain --rspack and --rspack --typescript) were correctly migrated in this PR — this one was missed.

Content-comparison detection has one unacknowledged edge case

@shakapacker_just_installed = yml_content_before != yml_content_after works correctly for the four cases documented in the comments. The only unacknowledged case is if Shakapacker's installer normalises YAML (e.g., trailing whitespace, quoting style) without the user agreeing to overwrite — the content would differ, triggering a force-overwrite of the user's customised config. This is unlikely today but could silently break if Shakapacker's conflict-prompt behaviour changes.

puts in generator action methods

copy_packer_config uses raw puts. Thor generators should use say or say_status so output respects the --quiet/verbosity level. Not introduced by this PR, but the new message on line 126 has the same issue.

SHAKAPACKER_YML_PATH constant declared after private

Ruby's private keyword does not affect constants — SHAKAPACKER_YML_PATH is still public as InstallGenerator::SHAKAPACKER_YML_PATH. Declaring it before private (with the other class-level declarations) would make the intent clearer.

force:/skip: forwarding passes nil when flags are absent

options[:force] and options[:skip] return nil (not false) when neither flag was passed. Passing force: nil explicitly to invoke is harmless in practice (Thor treats it as falsy), but it differs from not passing the key at all.

Minor nit

The comment on shakapacker_just_installed? (lines 65-68) says "In production, this is normally detected at runtime via @shakapacker_just_installed" but the method evaluates options first. Worth rewording to clarify that the CLI option overrides runtime detection.

@justin808 justin808 added the release:16.4.0-must-have Must-have for 16.4.0: critical bug/perf/usability label Feb 25, 2026
ihabadham and others added 10 commits February 26, 2026 17:57
The .shakapacker_just_installed marker file caused copy_packer_config to
skip RoR's template entirely, leaving private_output_path commented out.

Replace the marker with a hidden --shakapacker-just-installed class option
passed through invoke, and use template(..., force: true) to overwrite
Shakapacker's defaults without prompting. Pre-installed configs still get
the interactive prompt.

Guard against the partial-install edge case: only set the flag when
config/shakapacker.yml did not exist before running the Shakapacker
installer, so a pre-existing customized config is never force-overwritten.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Test contexts that simulate pre-existing shakapacker.yml (pre-installed,
rspack, rspack+typescript) now use Thor's --skip flag instead of --force.
This makes template() skip existing files while gsub_file patchers still
run, correctly testing the "user declines overwrite" scenario.

Also reverts the elsif File.exist? branch in copy_packer_config — the
standard Thor behavior (prompt on conflict, respect --force/--skip) is
correct for the pre-installed case.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Thor's invoke with an explicit options hash replaces parent options,
dropping force/skip flags. This meant `rails g react_on_rails:install
--skip` wouldn't propagate to BaseGenerator's file operations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The hidden class_option was only declared on BaseGenerator, so passing
--shakapacker-just-installed to InstallGenerator (e.g. in tests) was
silently ignored. The regression test for #2289 was passing by accident
due to global --force, not because the intended code path was exercised.

Now InstallGenerator declares the option too, and the invoke line checks
both the CLI flag and the runtime detection (@shakapacker_just_installed).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ession test

- install_shakapacker now returns true/false instead of nil, so the
  guard in ensure_shakapacker_installed (finalize_shakapacker_setup only
  runs when install actually succeeded) works correctly
- Regression test for #2289 no longer uses run_generator_test_with_args
  which unconditionally appends --force; now calls run_generator directly
  so the test only passes because --shakapacker-just-installed enables
  force: true in copy_packer_config, not because --force overwrites all
  conflicting files globally
- Add after cleanup to copy_packer_config force behavior describe block
  to remove shakapacker.yml left by the before hook
- Add CHANGELOG entry for the #2289 fix

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…regression, redundant guard

- Use content comparison (before/after File.read) in ensure_shakapacker_installed so
  @shakapacker_just_installed is set correctly in all cases: fresh install (nil→content),
  user said y to overwrite (old→new), and user said n to keep custom config (same→same).
  Previously config_existed only handled the fresh-install case and missed the y-overwrite
  scenario, leaving private_output_path commented after the user consented to a fresh config.

- Add 4 unit tests for the ensure_shakapacker_installed detection path, covering all
  three scenarios plus the install failure case. This path was previously untested because
  install_shakapacker always returned false in the test environment.

- Restore the detailed Shakapacker setup summary in the post-install message. Pass
  shakapacker_just_installed through helpful_message_after_installation so fresh installs
  show the three-checkmark summary instead of "already configured".

- Remove redundant || false from just_installed assignment.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Extract `options.shakapacker_just_installed? || @shakapacker_just_installed`
  into a private `shakapacker_just_installed?` helper to eliminate duplication
  between invoke_generators and add_post_install_message
- Extract "config/shakapacker.yml" into SHAKAPACKER_YML_PATH constant to
  prevent the two read sites in ensure_shakapacker_installed and
  finalize_shakapacker_setup from diverging
- Add comment explaining Dir.pwd reliance for File.exist?/File.read calls

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Forward force:/skip: to react_on_rails:pro and react_on_rails:rsc
  invocations, consistent with :base, :react_with_redux, and
  :react_no_redux which were updated in the same PR commit (30be511)
- Remove dead destination_root stubs from detection path unit tests:
  ensure_shakapacker_installed uses Dir.pwd (via relative SHAKAPACKER_YML_PATH),
  not destination_root, so the stubs had no effect on test outcomes —
  confirmed by running the 4 tests with and without them (4/4 pass either way)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…etup

- Add comment in finalize_shakapacker_setup documenting the nil→nil case
  (install_shakapacker returns true but writes nothing): treated as "no
  change needed" and force-overwrite is skipped — theoretically possible
  but extremely unlikely given how Shakapacker's installer works
- Clarify --shakapacker-just-installed option comment: passing it manually
  overrides runtime detection and force-overwrites the yml unconditionally
- Remove dead before/after blocks from copy_packer_config force behavior
  tests: template is fully mocked so neither the file creation nor cleanup
  has any effect on test outcomes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ment

Ruby's private keyword does not affect constants — SHAKAPACKER_YML_PATH was
accessible as InstallGenerator::SHAKAPACKER_YML_PATH regardless of placement.
Moving it alongside the class_option declarations makes the intent clear.

Also reword the shakapacker_just_installed class_option comment to explicitly
state that the CLI flag takes precedence over the @shakapacker_just_installed
ivar (via Ruby short-circuit evaluation of ||), not the other way around.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@ihabadham ihabadham force-pushed the ihabadham/fix/replace-marker-with-force-option branch from d616da8 to 5190008 Compare February 26, 2026 16:38
@claude
Copy link

claude bot commented Feb 26, 2026

Code Review: PR #2411

Overall assessment: This is a solid refactor that correctly removes a fragile on-disk marker file approach in favour of in-memory state. The core logic is sound and the regression test for issue #2289 is appropriate. A few items need attention before merge.


Bug: Missed marker-file cleanup in the --pro --rspack test

install_generator_spec.rb line 675 still creates .shakapacker_just_installed inside the with --pro --rspack context, but the new code never reads this file. It is dead test code left over from the old approach.

This context was also not refactored to the new pattern used by --rspack and --rspack --typescript (manual prepare_destination + simulate_* + Dir.chdir { run_generator([..., --skip]) }). The inconsistency leaves a false impression that the marker file still matters.


Minor: force: true silently overrides --skip

In copy_packer_config, passing force: true explicitly to Thor's template bypasses any global --skip the user may have provided. For a genuine fresh Shakapacker install this is intentional (the file is a Shakapacker default, not user-customised). However, the hidden CLI flag --shakapacker-just-installed lets an advanced user trigger this path against an existing config. A brief inline comment acknowledging that this intentionally overrides --skip would help future readers.


Nit: CLI-flag short-circuit in shakapacker_just_installed?

The method is options.shakapacker_just_installed? || @shakapacker_just_installed.

@shakapacker_just_installed is only assigned by finalize_shakapacker_setup, which is only reached when install_shakapacker returns true. If it is never set, Ruby returns nil (falsy) — correct. But options.shakapacker_just_installed? short-circuits first, so a user who passes the hidden flag can force-overwrite even when shakapacker_configured? returned true and ensure_shakapacker_installed returned early. The option-level comment documents this, but calling it out explicitly inside the method would make the intent clearer.


Positive notes

  • The content-diff approach (yml_content_before \!= yml_content_after) is robust and the four-case comment in finalize_shakapacker_setup maps each transition clearly — excellent documentation.
  • Forwarding force: and skip: explicitly to each invoke call is the correct fix for Thor silently dropping them when an options hash is provided.
  • The describe "copy_packer_config force behavior" unit tests are clean and verify the exact branching logic without needing filesystem state.
  • install_shakapacker now returns a meaningful boolean and finalize_shakapacker_setup is only called on success — a clear improvement over the previous implicit return.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug P0 Merge this week release:16.4.0-must-have Must-have for 16.4.0: critical bug/perf/usability

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Generator skips private_output_path configuration when Shakapacker is freshly installed

2 participants