Skip to content

Conversation

@justin808
Copy link
Member

@justin808 justin808 commented Nov 19, 2025

Summary

This PR improves the organization and documentation of precompile hooks across the React on Rails codebase.

Changes

1. Shared Test Helper for Dummy Apps

  • Created spec/support/shakapacker_precompile_hook_shared.rb (112 lines)
  • Both OSS and Pro dummy apps now load this shared implementation
  • Eliminates code duplication between test apps
  • Includes ReScript build support and proper error handling
  • Keeps test-only code in test directories (not in lib/)

2. Test Dummy App Hooks (Simple Wrappers)

Both spec/dummy/bin/shakapacker-precompile-hook and react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook are now just ~19 lines that load the shared implementation.

3. Generator Template (Standalone, Production-Ready)

The lib/generators/react_on_rails/templates/base/base/bin/shakapacker-precompile-hook is a 30-line Ruby script that:

  • Uses ReactOnRails::PacksGenerator.instance.generate_packs_if_stale directly
  • Requires Rails environment to properly use the gem's API
  • Has clear error handling with Rainbow-colored output
  • Well-documented with emoji scheme and comments
  • Sets REACT_ON_RAILS_SKIP_VALIDATION=true with explanation
  • Completely standalone - no dependencies on gem internals, works in any generated app

4. Restored Pro Dummy App to async: true

  • Changed from defer: true back to async: true in application.html.erb
  • async: true is the recommended approach for Shakapacker >= 8.2.0
  • Enables React 18's Selective Hydration for optimal Time to Interactive (TTI)
  • Aligns with best practices documented in streaming-server-rendering.md

5. Better Pro Dummy App Configuration Documentation

  • Added clear header: "⚠️ TEST CONFIGURATION - Do not copy directly for production apps"
  • Organized config into sections: Essential, Pro Features, Test-specific
  • Added comments explaining each configuration choice
  • References production docs for users

Architecture

Generator Template (for user apps)
└── Standalone Ruby script using PacksGenerator API

Test Dummy Apps
├── spec/dummy/bin/shakapacker-precompile-hook (loads shared)
├── react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook (loads shared)
└── spec/support/shakapacker_precompile_hook_shared.rb (shared implementation)

Known Issues

The "React Router Sixth Page" RSpec test failure is a pre-existing flaky test that also fails intermittently on master. This is not a regression from these changes.

🤖 Generated with Claude Code

Co-Authored-By: Claude noreply@anthropic.com

justin808 and others added 12 commits November 16, 2025 19:26
…ding strategy

Apply the same defer loading strategy fix from commit d1a8a1a to the Pro dummy app
to resolve race condition causing Playwright test timeouts.

## Problem
Playwright E2E tests for streaming were timing out waiting for console message
"ToggleContainer with title", indicating React components weren't hydrating.

## Root Cause
The Pro dummy app was still using async: true for javascript_pack_tag while the
open-source dummy app was updated to defer: true in commit d1a8a1a. This created
a race condition where:
- Generated component packs load asynchronously
- Main client-bundle also loads asynchronously
- If client-bundle executes before component registrations complete, React tries
  to hydrate unregistered components
- ToggleContainer never hydrates, useEffect never runs, console.log never fires

## Solution
1. Changed javascript_pack_tag from async: true to defer: true in application.html.erb
2. Added precompile_hook to shakapacker.yml for pack generation
3. Added bin/shakapacker-precompile-hook script

Using defer: true ensures script execution order - generated component packs load
and register components before main bundle executes, preventing the race condition.

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

Co-Authored-By: Claude <noreply@anthropic.com>
- Remove unnecessary rubocop disable/enable directives
- Fix Style/SymbolArray violation in shakapacker-precompile-hook
- Add explanatory comment about generated_component_packs_loading_strategy
  defaulting to :defer to match OSS dummy app configuration

Note: The failing "React Router Sixth Page" RSpec test is a known flaky
test that also fails intermittently on master. This is not a regression
from the defer loading strategy changes.

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

Co-Authored-By: Claude <noreply@anthropic.com>
- Revert Pro dummy app back to async: true (Pro supports Selective Hydration)
- Fix ReScript build to run from Rails root instead of current directory
- Use File.join for proper path resolution of config files
- Wrap build commands in Dir.chdir(rails_root) for correct execution
- Add early Rails root resolution with proper error handling
- Remove unnecessary defer strategy comment from initializer
- Add blank line before main execution section for style

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

Co-Authored-By: Claude <noreply@anthropic.com>
Convert precompile hooks from Ruby to bash and DRY them up:

**Why Bash Over Ruby:**
- Simpler: No Ruby stdlib dependencies
- Faster: No Ruby interpreter startup overhead
- Standard: Most build hooks are shell scripts
- Cross-platform: Works everywhere (sh/bash universal)
- Fewer dependencies: Doesn't require Ruby loaded

**Changes:**
- Create shared bash script in generator templates
- Fix ReScript build to run from Rails root (cd into rails_root)
- Fix pack generation to run from Rails root
- Use proper path resolution with Rails root for all file checks
- Replace Ruby scripts in both dummy apps with bash version
- All three copies identical (generator template + 2 dummy apps)

**Script Features:**
- Finds Rails root by walking up directory tree
- Detects ReScript config (bsconfig.json or rescript.json)
- Runs ReScript builds from correct directory
- Detects auto_load_bundle/components_subdirectory config
- Generates packs when configured
- Cross-platform package manager detection (yarn/npm)
- Proper error handling and exit codes
- Sets REACT_ON_RAILS_SKIP_VALIDATION for build context

**Testing:**
- Shellcheck passes with no warnings
- Script executes successfully in non-pro dummy app
- ReScript builds complete successfully
- Pack generation runs successfully
- All files have trailing newlines

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

Co-Authored-By: Claude <noreply@anthropic.com>
Consolidate duplicate precompile hook logic across OSS dummy app, Pro dummy app,
and generator template into a single shared Ruby implementation.

Changes:
- Create lib/tasks/precompile/shakapacker_precompile_hook_shared.rb with common logic
- Update spec/dummy/bin/shakapacker-precompile-hook to load shared implementation
- Update react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook to load shared implementation
- Simplify generator template to use Ruby instead of shell script (removes ReScript build logic)

Benefits:
- Eliminates 342 lines of duplicate code across 3 files
- Easier to maintain and update precompile hook logic in one place
- Consistent behavior across all environments
- Better error handling with Ruby exceptions

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

Co-Authored-By: Claude <noreply@anthropic.com>
…n available

Update the generator template to intelligently use the shared precompile hook
implementation when running in the gem's development/test context, while
maintaining a self-contained fallback for generated user applications.

This provides the best of both worlds:
- Test dummy apps and generator template all use the same shared code
- Generated user apps have a standalone copy that works independently
- Easier to maintain and update the precompile hook logic

The hook now tries to load the shared implementation from the gem first,
and falls back to its inline implementation if the gem isn't available
or accessible.

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

Co-Authored-By: Claude <noreply@anthropic.com>
…mplate

Address feedback to properly organize the precompile hook code:

1. Move shared implementation to spec/support/ (test-only location)
   - Renamed: lib/tasks/precompile/shakapacker_precompile_hook_shared.rb
   - To: spec/support/shakapacker_precompile_hook_shared.rb
   - This makes it clear the shared code is only for test dummy apps

2. Simplify generator template to be standalone
   - Remove logic to load shared implementation from gem
   - Generator template is for newly created production apps
   - Should be simple, self-contained, and production-ready
   - No references to development/testing concerns

3. Update test dummy apps to reference new location
   - spec/dummy/bin/shakapacker-precompile-hook
   - react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook
   - Both now load from spec/support/

The generator template is now clean and focused on production use cases,
while test dummy apps share common implementation from the test support directory.

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

Co-Authored-By: Claude <noreply@anthropic.com>
Replace overly complicated 58-line Ruby implementation with a simple
22-line bash script that just checks for auto_load_bundle configuration
and runs the rake task if needed.

The bash script is:
- Easier to read and understand
- Fewer dependencies (no Ruby parsing, no fileutils)
- Standard shell script that works everywhere
- Does exactly what's needed, nothing more

Perfect for newly generated production apps.

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

Co-Authored-By: Claude <noreply@anthropic.com>
Key improvements to shakapacker precompile hook:

1. **Better error messages for ReScript builds**
   - Fail fast when ReScript config exists but package.json is missing
   - Fail fast when build:rescript script is not defined
   - Provide actionable error messages with exact fixes needed
   - Add JSON parse error handling

2. **More robust config detection**
   - Improved regex to properly ignore commented configuration lines
   - Allow flexible spacing in config assignments
   - Prevent false positives from commented-out config

3. **Consistent error handling**
   - Changed warnings to errors when builds are required but missing
   - Exit with status 1 on all failure scenarios
   - Better distinction between optional vs required failures

Addresses code review feedback on error handling and regex robustness.

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

Co-Authored-By: Claude <noreply@anthropic.com>
…late

The SKIP_VALIDATION environment variable is not needed in the generator
template because:

1. By the time apps use this hook, packages are already installed
2. The engine's skip_version_validation? already handles edge cases:
   - package_json_missing? check
   - running_generator? check
3. This was cargo-culted from the Ruby version which needed it for
   different reasons (loading Rails environment directly)

The shared implementation in spec/support still sets this variable
because test apps have different requirements, but user-generated
apps should rely on the engine's built-in validation logic.

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

Co-Authored-By: Claude <noreply@anthropic.com>
Reverted from the hacky bash grep approach back to a clean Ruby script.

The bash version had several problems:
1. Hacky grep parsing to detect config - fragile and error-prone
2. Duplicates logic that already exists in PacksGenerator
3. More complex (20+ lines vs 21 lines)
4. Harder to maintain

The Ruby version is superior because:
1. Leverages PacksGenerator.generate_packs_if_stale which already
   knows when to generate vs skip
2. Simple and clean - just load Rails and call the method
3. Proper error handling with backtrace
4. Let the engine's built-in validation logic handle skipping

The engine already skips validation appropriately via:
- package_json_missing? check
- running_generator? check

So we don't need REACT_ON_RAILS_SKIP_VALIDATION here either.

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

Co-Authored-By: Claude <noreply@anthropic.com>
Restored the original implementation which had:
1. Rainbow colored output for better UX during builds
2. ENV["REACT_ON_RAILS_SKIP_VALIDATION"] = "true" because this hook
   loads Rails environment and validation would fail during builds

The SKIP_VALIDATION is needed here because:
- The hook loads Rails via require_relative "../config/environment"
- This triggers the engine's validation initializer
- During precompile, packages may not be fully available yet
- Setting this env var prevents spurious validation failures

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

Co-Authored-By: Claude <noreply@anthropic.com>
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 19, 2025

Walkthrough

This PR refactors the Shakapacker precompile hook mechanism by extracting complex build and pack-generation logic into a shared, reusable module. The main hook script is simplified to locate and load the shared hook, while configuration ties the hook into Shakapacker's precompile workflow. The hook automatically builds ReScript assets and generates React on Rails packs before webpack compilation.

Changes

Cohort / File(s) Change Summary
Precompile Hook Implementation
react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook, spec/dummy/bin/shakapacker-precompile-hook
Adds new precompile hook at react_on_rails_pro/.../bin/shakapacker-precompile-hook that resolves gem root and loads shared hook. Simplifies existing spec/dummy/bin/shakapacker-precompile-hook to a minimal guard-and-load pattern that checks for shared hook presence and exits with status 1 if missing.
Shared Precompile Hook Library
spec/support/shakapacker_precompile_hook_shared.rb
Adds new shared hook module with three public methods: find_rails_root() traverses filesystem to locate Rails root; build_rescript_if_needed() conditionally builds ReScript with package manager detection and error handling; generate_packs_if_needed() conditionally generates React on Rails packs with validation bypass. Includes robust error handling for missing configs and package managers.
Shakapacker Configuration
react_on_rails_pro/spec/dummy/config/shakapacker.yml
Adds precompile_hook configuration under default settings to specify the pre-compile script path, enabling dynamic asset preparation before webpack compilation.

Sequence Diagram

sequenceDiagram
    participant Webpack as Webpack Build
    participant Hook as Precompile Hook
    participant SharedHook as Shared Hook Module
    participant ReScript as ReScript Builder
    participant Rails as Rails Generator
    
    Webpack->>Hook: Invoke precompile hook
    Hook->>Hook: Resolve gem root
    Hook->>SharedHook: Load and execute
    
    SharedHook->>SharedHook: Find Rails root
    
    alt ReScript config exists
        SharedHook->>ReScript: build_rescript_if_needed()
        ReScript->>ReScript: Detect yarn/npm
        ReScript->>ReScript: Run build:rescript
    end
    
    alt Auto-load or components configured
        SharedHook->>Rails: generate_packs_if_needed()
        Rails->>Rails: Set REACT_ON_RAILS_SKIP_VALIDATION=1
        Rails->>Rails: Execute bundle exec rails react_on_rails:generate_packs
    end
    
    SharedHook-->>Webpack: Return (continue build)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Shared hook module contains multiple error-handling paths for ReScript detection, package manager selection, and JSON parsing that warrant careful review
  • Logic extraction from the original spec/dummy/bin/shakapacker-precompile-hook to the shared module requires verification that behavior is preserved
  • Configuration wiring via shakapacker.yml should be validated against Shakapacker's precompile hook contract

Possibly related PRs

Suggested labels

review-needed, full-ci

Poem

🐰 The hooks are bundled, so neat and so clean,
No duplication in this shared routine!
ReScript builds swiftly, packs generate fast,
Shakapacker's precompile holds them steadfast.
One hook to rule them, eight levels down...
A refactored gem of our webpacked town! 🎉

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Title check ✅ Passed The title directly summarizes the main changes: simplifying the precompile hook implementation and restoring the Pro dummy app to async loading, which aligns with the primary objectives of the PR.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch justin808/improve-precompile-hook

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

❤️ Share

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

@claude
Copy link

claude bot commented Nov 19, 2025

Code Review - PR #2053

I've reviewed this PR and have the following feedback:

Strengths

  1. Excellent Refactoring - The new approach using ReactOnRails::PacksGenerator.instance.generate_packs_if_stale is much cleaner and more maintainable than the previous ~100 line implementation.

  2. DRY Principle - Creating spec/support/shakapacker_precompile_hook_shared.rb to avoid duplication between test dummy apps is a great design decision.

  3. Better Error Handling - The new generator template has improved error handling with colored output using Rainbow.

  4. Clear Documentation - Good emoji scheme and inline comments explaining the purpose and context.

  5. Security Enhancement - Adding the security comment about precompile_hook in shakapacker.yml is valuable.

🔍 Code Quality Observations

1. Path Traversal Logic (Minor)

In both dummy app hooks, there's path traversal logic that is correct but fragile - if directory structure changes, it breaks. Consider adding a comment explaining why different traversal depths are needed.

2. Error Handling Inconsistency

In spec/support/shakapacker_precompile_hook_shared.rb:

  • build_rescript_if_needed exits with code 1 on errors ✅
  • generate_packs_if_needed exits with code 1 on StandardError but returns quietly on other failures ⚠️

The return unless rails_root and similar early returns mean failures won't be reported. Consider documenting why silent failures are acceptable.

🐛 Potential Issues

1. Cross-Platform Compatibility (line 62)

if system("which yarn > /dev/null 2>&1")

The which command doesn't exist on Windows. Consider using:

if system("yarn", "--version", out: File::NULL, err: File::NULL)

2. Missing Rainbow Dependency

The generator template (line 24) uses:

puts Rainbow("🔄 Running React on Rails precompile hook...").cyan

But there's no require "rainbow" in the template. This will fail unless Rails has already loaded it.

Recommendation: Either add require "rainbow" or remove Rainbow from the template to keep it dependency-free.

Performance Considerations

No significant performance concerns. The refactoring actually improves performance by:

  • Reusing existing PacksGenerator singleton
  • Leveraging its built-in staleness detection
  • Avoiding redundant file system checks

🔒 Security Review

  1. Command Injection - ✅ All system() calls use array form, preventing injection
  2. Path Traversal - ✅ No user input in file paths
  3. Environment Variables - ✅ ENV["REACT_ON_RAILS_SKIP_VALIDATION"] usage is documented and appropriate

The security comment added to shakapacker.yml is excellent.

🧪 Test Coverage

The PacksGenerator class has excellent test coverage (875 lines!). However:

Missing: Tests specifically for the precompile hook scripts themselves

Recommendation: Consider adding integration tests that execute the hook script directly and verify error handling.

🎯 Recommendations

High Priority:

  1. Fix Rainbow require in generator template or remove Rainbow usage
  2. Make which command cross-platform compatible

Medium Priority:
3. Add integration tests for hook scripts
4. Document why silent failures are acceptable for missing Rails root/config
5. Add comments explaining path traversal depth differences

Overall Assessment

This is a strong PR that significantly improves code quality and maintainability. The refactoring from a complex 100-line script to a clean 30-line implementation using the existing PacksGenerator API is exactly the right approach.

The issues identified are mostly minor and don't block the PR. The Rainbow dependency issue should be addressed before merge, but everything else is optional improvement.

Recommendation: Approve with minor fixes requested (Rainbow dependency).

Great work! 🚀

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: 0

🧹 Nitpick comments (1)
spec/support/shakapacker_precompile_hook_shared.rb (1)

62-71: Misleading cross-platform claim.

The comment claims "Cross-platform package manager detection" but uses the Unix-specific which command (lines 63, 65), which is not available on native Windows systems. While this works on Unix-like systems and Windows with Git Bash, it's not truly cross-platform.

Consider either:

  1. Updating the comment to clarify "Unix-like systems" instead of "cross-platform"
  2. Using Ruby's Gem.win_platform? and platform-specific detection
  3. Using system("yarn", "--version", out: File::NULL, err: File::NULL) which works cross-platform

Apply this diff for option 1 (simplest):

-  # Cross-platform package manager detection
+  # Package manager detection (Unix-like systems)

Or this diff for option 3 (more robust):

-    # Cross-platform package manager detection
-    if system("which yarn > /dev/null 2>&1")
+    # Cross-platform package manager detection
+    if system("yarn", "--version", out: File::NULL, err: File::NULL)
       system("yarn", "build:rescript", exception: true)
-    elsif system("which npm > /dev/null 2>&1")
+    elsif system("npm", "--version", out: File::NULL, err: File::NULL)
       system("npm", "run", "build:rescript", exception: true)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0f6bdcc and 6da1351.

📒 Files selected for processing (4)
  • react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook (1 hunks)
  • react_on_rails_pro/spec/dummy/config/shakapacker.yml (1 hunks)
  • spec/dummy/bin/shakapacker-precompile-hook (1 hunks)
  • spec/support/shakapacker_precompile_hook_shared.rb (1 hunks)
🧰 Additional context used
🧠 Learnings (7)
📚 Learning: 2025-09-16T08:01:11.146Z
Learnt from: justin808
Repo: shakacode/react_on_rails PR: 1770
File: lib/generators/react_on_rails/templates/base/base/app/javascript/src/HelloWorld/ror_components/HelloWorld.client.jsx:2-2
Timestamp: 2025-09-16T08:01:11.146Z
Learning: React on Rails uses webpack CSS Modules configuration with namedExports: true, which requires the import syntax `import * as style from './file.module.css'` rather than the default export pattern. This configuration enables better tree shaking and bundle size optimization for CSS modules.

Applied to files:

  • react_on_rails_pro/spec/dummy/config/shakapacker.yml
  • spec/dummy/bin/shakapacker-precompile-hook
  • react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook
  • spec/support/shakapacker_precompile_hook_shared.rb
📚 Learning: 2025-02-12T16:38:06.537Z
Learnt from: Romex91
Repo: shakacode/react_on_rails PR: 1697
File: package-scripts.yml:28-28
Timestamp: 2025-02-12T16:38:06.537Z
Learning: The file `node_package/lib/ReactOnRails.full.js` is autogenerated during the build process and should not be present in the repository.

Applied to files:

  • react_on_rails_pro/spec/dummy/config/shakapacker.yml
  • spec/dummy/bin/shakapacker-precompile-hook
  • react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook
  • spec/support/shakapacker_precompile_hook_shared.rb
📚 Learning: 2025-04-26T21:55:55.874Z
Learnt from: alexeyr-ci2
Repo: shakacode/react_on_rails PR: 1732
File: spec/dummy/client/app-react16/startup/ReduxSharedStoreApp.client.jsx:40-44
Timestamp: 2025-04-26T21:55:55.874Z
Learning: In the react_on_rails project, files under `app-react16` directories are copied/moved to corresponding `/app` directories during the conversion process (removing the `-react16` suffix), which affects their relative import paths at runtime.

Applied to files:

  • react_on_rails_pro/spec/dummy/config/shakapacker.yml
  • react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook
  • spec/support/shakapacker_precompile_hook_shared.rb
📚 Learning: 2025-07-08T05:57:29.630Z
Learnt from: AbanoubGhadban
Repo: shakacode/react_on_rails PR: 1745
File: node_package/src/RSCRequestTracker.ts:8-14
Timestamp: 2025-07-08T05:57:29.630Z
Learning: The global `generateRSCPayload` function in React on Rails Pro (RORP) is provided by the framework during rendering requests, not implemented in application code. The `declare global` statements are used to document the expected interface that RORP will inject at runtime.

Applied to files:

  • spec/dummy/bin/shakapacker-precompile-hook
  • react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook
  • spec/support/shakapacker_precompile_hook_shared.rb
📚 Learning: 2025-10-23T17:22:01.074Z
Learnt from: AbanoubGhadban
Repo: shakacode/react_on_rails PR: 1875
File: lib/react_on_rails/utils.rb:112-124
Timestamp: 2025-10-23T17:22:01.074Z
Learning: In React on Rails, when Pro is installed but not licensed, the intended behavior is to raise an error on boot. The `react_on_rails_pro?` method validates licenses and should raise errors early (including during path resolution in methods like `server_bundle?`) to enforce licensing requirements rather than failing later with obscure errors.

Applied to files:

  • react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook
  • spec/support/shakapacker_precompile_hook_shared.rb
📚 Learning: 2024-10-08T20:53:47.076Z
Learnt from: theforestvn88
Repo: shakacode/react_on_rails PR: 1620
File: spec/dummy/client/app/startup/HelloTurboStream.jsx:3-3
Timestamp: 2024-10-08T20:53:47.076Z
Learning: The `RailsContext` import in `spec/dummy/client/app/startup/HelloTurboStream.jsx` is used later in the project, as clarified by the user theforestvn88.

Applied to files:

  • react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook
📚 Learning: 2025-02-18T13:08:01.477Z
Learnt from: AbanoubGhadban
Repo: shakacode/react_on_rails PR: 1644
File: lib/react_on_rails/helper.rb:190-197
Timestamp: 2025-02-18T13:08:01.477Z
Learning: RSC support validation in React on Rails Pro is handled through a chain of validations:
1. Pro version check in `run_stream_inside_fiber`
2. RSC support check during pack generation via `ReactOnRailsPro.configuration.enable_rsc_support`
3. RSC support validation during component registration
This makes additional validation in the helper methods unnecessary.

Applied to files:

  • spec/support/shakapacker_precompile_hook_shared.rb
🧬 Code graph analysis (1)
spec/support/shakapacker_precompile_hook_shared.rb (2)
lib/react_on_rails/version_checker.rb (1)
  • package_json_path (223-225)
lib/generators/react_on_rails/generator_helper.rb (1)
  • package_json (7-20)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: claude-review
🔇 Additional comments (6)
react_on_rails_pro/spec/dummy/config/shakapacker.yml (1)

22-25: LGTM! Clear documentation with security guidance.

The precompile hook configuration is well-documented with inline comments that appropriately emphasize security considerations for hook scripts.

spec/dummy/bin/shakapacker-precompile-hook (1)

4-19: LGTM! Clean delegation to shared implementation.

The refactored hook properly locates the gem root, validates the shared hook exists, and delegates to the shared implementation. Error handling with emoji and clear messaging is a nice touch.

react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook (1)

4-19: LGTM! Correct path calculation for Pro dummy app.

The hook correctly calculates the gem root as four levels up from react_on_rails_pro/spec/dummy/bin, properly accounting for the additional Pro directory level. The implementation mirrors the non-Pro version appropriately.

spec/support/shakapacker_precompile_hook_shared.rb (3)

20-28: LGTM! Robust Rails root detection.

The upward directory traversal approach is a standard and reliable method for locating the Rails root by finding config/environment.rb.


85-113: LGTM! Proper pack generation with validation skip.

The function correctly:

  • Detects auto-pack configuration using regex that avoids commented lines
  • Sets REACT_ON_RAILS_SKIP_VALIDATION=true as mentioned in the PR objectives
  • Provides appropriate error handling with informative messages

116-119: LGTM! Proper main guard pattern.

The __FILE__ == $PROGRAM_NAME guard correctly allows the file to be loaded by other scripts (via load) without executing the main logic, while also supporting direct execution. The ordering (ReScript build → pack generation) is logical.

@claude
Copy link

claude bot commented Nov 19, 2025

Code Review - PR #2053

I've reviewed the changes in this PR that improve the Shakapacker precompile hook implementation. Overall, this is a well-executed refactoring with several significant improvements. Here's my detailed feedback:

✅ Strengths

1. Excellent Code Simplification

The new generator template (lib/generators/react_on_rails/templates/base/base/bin/shakapacker-precompile-hook) is much cleaner:

  • Reduced from ~100 lines to ~30 lines (70% reduction)
  • Leverages existing ReactOnRails::PacksGenerator.instance.generate_packs_if_stale instead of duplicating logic
  • Clear separation of concerns: generator template is minimal, shared logic is centralized

2. Smart Code Reuse with Shared Implementation

Creating spec/support/shakapacker_precompile_hook_shared.rb is an excellent architectural decision:

  • Eliminates duplication between the two test dummy apps
  • Makes the shared logic testable and maintainable in one place
  • Clear comments explaining this is for test apps only

3. Improved Error Handling

The shared implementation has robust error handling:

  • Validates ReScript configuration exists before attempting build
  • Checks for package.json and build:rescript script
  • Provides helpful error messages with actionable guidance
  • Uses exception: true with system calls for proper error propagation

4. Better Documentation

  • Clear emoji scheme in generator template (🔄/✅/❌)
  • Excellent inline comments explaining SKIP_VALIDATION rationale
  • Security warning in shakapacker.yml about precompile_hook path

🔍 Potential Issues & Recommendations

1. Security: Path Traversal Risk (Medium)

# spec/support/shakapacker_precompile_hook_shared.rb:20-27
def find_rails_root
  dir = Dir.pwd
  while dir != "/"
    return dir if File.exist?(File.join(dir, "config/environment.rb"))
    dir = File.dirname(dir)
  end
  nil
end

Issue: The loop continues until filesystem root. On Windows, this could behave unexpectedly (no single root).

Recommendation: Add a maximum depth limit:

def find_rails_root
  dir = Dir.pwd
  max_depth = 10
  depth = 0
  
  while depth < max_depth
    return dir if File.exist?(File.join(dir, "config/environment.rb"))
    parent = File.dirname(dir)
    break if parent == dir  # Reached root
    dir = parent
    depth += 1
  end
  nil
end

2. Cross-Platform Compatibility Issue (Low)

# Line 63
if system("which yarn > /dev/null 2>&1")

Issue: which is not available on all Windows systems. The earlier dummy app version used a better approach.

Recommendation: Use Ruby's built-in methods:

if system("yarn", "--version", out: File::NULL, err: File::NULL)
  system("yarn", "build:rescript", exception: true)
elsif system("npm", "--version", out: File::NULL, err: File::NULL)
  system("npm", "run", "build:rescript", exception: true)
else
  # error handling
end

3. Inconsistent Error Handling for Errno::ENOENT (Low)

# Lines 108-109
rescue Errno::ENOENT => e
  warn "⚠️  Warning: #{e.message}"

Issue: This catches ENOENT (file not found) but only warns instead of exiting. This could lead to silent failures if bundle executable is missing.

Recommendation: Either exit with error code or document why this is a warning:

rescue Errno::ENOENT => e
  warn "⚠️  Warning: bundle command not found - #{e.message}"
  warn "    Skipping pack generation (bundle not available)"
  # Or: exit 1 if this should be an error

4. Missing Test Coverage (Medium)

The shared implementation file doesn't have dedicated tests. While the integration tests cover this indirectly, unit tests would help catch edge cases.

Recommendation: Add specs for:

  • find_rails_root edge cases (no Rails root found, deep nesting)
  • ReScript configuration validation
  • Package manager detection logic

5. Regex Pattern Complexity (Low)

# Lines 95-96
return unless initializer_content.match?(/^\s*(?!#).*config\.auto_load_bundle\s*=/) ||
              initializer_content.match?(/^\s*(?!#).*config\.components_subdirectory\s*=/)

Issue: The negative lookahead (?!#) could match lines like # config.auto_load_bundle = true if there's whitespace before the #.

Recommendation: Improve the regex:

return unless initializer_content.match?(/^\s*[^#]*config\.auto_load_bundle\s*=/) ||
              initializer_content.match?(/^\s*[^#]*config\.components_subdirectory\s*=/)

🎯 Best Practices Adherence

RuboCop Compliance: Properly uses # rubocop:disable comments where needed
CLAUDE.md Adherence: Follows error message format with emoji scheme
DRY Principle: Excellent elimination of duplication
Error Messages: Clear, actionable, and helpful
Documentation: Good inline comments explaining rationale

🧪 Test Coverage Considerations

The PR mentions a flaky test failure ("React Router Sixth Page"). Based on the code:

  • The refactoring maintains the same behavior (calls same methods)
  • The flaky test is pre-existing and unrelated to these changes
  • Integration tests should cover the precompile hook execution path

Recommendation: Consider adding a specific test in spec/react_on_rails/dev/pack_generator_spec.rb to verify that calling generate_packs_if_stale behaves correctly when called from the precompile hook context.

📊 Performance Considerations

✅ No performance concerns:

  • The refactoring doesn't add overhead
  • File staleness checking remains efficient
  • Early returns prevent unnecessary work

🔒 Security Assessment

Generally secure with minor notes:

  • Good: Sets REACT_ON_RAILS_SKIP_VALIDATION=true with clear explanation
  • Good: Security comment in shakapacker.yml about trusted scripts
  • Minor: Path traversal concern mentioned above (low risk in practice)

🎯 Overall Assessment

LGTM with minor suggestions

This is a high-quality refactoring that:

  • ✅ Significantly simplifies the codebase
  • ✅ Eliminates duplication effectively
  • ✅ Improves maintainability
  • ✅ Adds helpful documentation
  • ⚠️ Has a few minor cross-platform and error handling edge cases to consider

The issues I've raised are relatively minor and shouldn't block the PR. They can be addressed in follow-up improvements if desired.

📝 Suggested Follow-ups (Optional)

  1. Add unit tests for shakapacker_precompile_hook_shared.rb
  2. Improve cross-platform compatibility for package manager detection
  3. Add maximum depth limit to find_rails_root
  4. Document the ENOENT warning behavior

Great work on this refactoring! 🎉

@justin808 justin808 changed the title Improve precompile hook implementation and fix Pro dummy app configuration Simplify precompile hook and restore Pro dummy app to async loading Nov 19, 2025
@claude
Copy link

claude bot commented Nov 19, 2025

Code Review - PR #2053

I've reviewed this PR and it's a well-structured refactoring that successfully eliminates code duplication. Here's my detailed feedback:


✅ Strengths

  1. Excellent Code Consolidation - Eliminated ~342 lines of duplicate code across 3 files
  2. Improved Error Handling - Fail-fast approach with actionable error messages
  3. Good Security Practices - Uses array form for system commands, no shell injection vulnerabilities
  4. Clear Documentation - Pro dummy app has excellent warning header, async: true choice is well-explained

🔍 Critical Issue: Cross-Platform Bug in find_rails_root

Location: spec/support/shakapacker_precompile_hook_shared.rb:20-28

Problem: The while loop uses 'while dir != "/"' which won't work on Windows (uses C:\ not /).

Fix needed: Change to use 'loop do' with parent comparison (parent == dir) to detect filesystem root cross-platform.


🔍 Other Recommendations

  1. Medium: Regex pattern (lines 95-96) uses complex negative lookahead that may be unnecessary
  2. Low: Missing validation that build:rescript script is non-empty string
  3. Low: Consider using Pathname class for cleaner code

🧪 Test Coverage

Missing tests for:

  • spec/support/shakapacker_precompile_hook_shared.rb
  • ReScript build path
  • Config detection regex

Recommend adding unit tests for find_rails_root, build_rescript_if_needed, and generate_packs_if_needed.


🔒 Security Notes

  • Shakapacker.yml security comment is good
  • Minor risk: No validation that precompile_hook path is within project (low priority)
  • Generator template doesn't validate Rails.root before requiring files

🎯 Summary

Verdict: ✅ Approve with minor fixes

Before merging:

  1. ⚠️ Fix find_rails_root cross-platform issue (CRITICAL)
  2. Consider adding unit tests
  3. Validate Rainbow gem availability in generator template

Great work on this refactoring! The iterative evolution through commits shows thoughtful problem-solving. 🚀

@justin808 justin808 merged commit 8eca8d2 into master Nov 19, 2025
56 checks passed
@justin808 justin808 deleted the justin808/improve-precompile-hook branch November 19, 2025 06:07
@claude
Copy link

claude bot commented Nov 19, 2025

Code Review for PR #2053

Thank you for this refactoring! This is a well-structured PR that successfully simplifies the precompile hook implementation. Here's my detailed feedback:


Strengths

1. Excellent Code Simplification

The new generator template (lib/generators/react_on_rails/templates/base/base/bin/shakapacker-precompile-hook) is significantly clearer than the previous bash-heavy approach:

  • 30 lines vs 101 lines - much more maintainable
  • Direct use of the gem's API (ReactOnRails::PacksGenerator.instance.generate_packs_if_stale) instead of shelling out to rake tasks
  • Proper error handling with colored output using Rainbow (which is already a gem dependency per react_on_rails.gemspec:33)

2. Good DRY Principle Application

Creating spec/support/shakapacker_precompile_hook_shared.rb eliminates duplication between OSS and Pro dummy apps. This is the right approach for test infrastructure.

3. Clear Documentation Improvements

The Pro dummy app config (react_on_rails_pro/spec/dummy/config/initializers/react_on_rails.rb) now has excellent warnings that this is test configuration, not production templates.

4. Correct Technical Choice

Restoring async: true for Shakapacker >= 8.2.0 is the right call for optimal React 18 Selective Hydration and TTI performance.


🔍 Issues Found

CRITICAL: Potential Security/Robustness Issue in Generator Template

Location: lib/generators/react_on_rails/templates/base/base/bin/shakapacker-precompile-hook:24

Issue: The template uses Rainbow() for colored output, but Rainbow is a gem dependency, not available during precompile hooks unless the Rails environment is loaded. The current template correctly loads Rails environment on line 21 (require_relative "../config/environment"), so this works. However, there's a concern:

Problem: If this script fails to load the Rails environment (e.g., during asset precompilation in deployment where some gems might not be available in all groups), the script will crash with NameError: uninitialized constant Rainbow.

Recommendation: Add a fallback for Rainbow or make it conditional:

begin
  require 'rainbow'
  USE_RAINBOW = true
rescue LoadError
  USE_RAINBOW = false
end

# Then in the script:
def colored_output(text, color)
  USE_RAINBOW ? Rainbow(text).send(color) : text
end

puts colored_output("🔄 Running React on Rails precompile hook...", :cyan)

Alternative: Since the template already requires Rails environment, you could add a comment explaining this dependency:

# Requires Rails environment which loads rainbow gem (react_on_rails dependency)
require_relative "../config/environment"

MODERATE: Missing Error Context in Shared Hook

Location: spec/support/shakapacker_precompile_hook_shared.rb:104-110

Issue: The error handling in generate_packs_if_needed catches StandardError and Errno::ENOENT, but the error messages don't provide actionable debugging info:

rescue StandardError => e
  warn "❌ Pack generation failed: #{e.message}"
  exit 1
end

Recommendation: Add more context to help developers debug failures:

rescue StandardError => e
  warn "❌ Pack generation failed: #{e.message}"
  warn "   Rails root: #{rails_root}"
  warn "   Initializer: #{initializer_path}"
  warn e.backtrace.first(5).join("\n   ") if ENV['VERBOSE']
  exit 1
end

MINOR: Inconsistent Main Execution Guard

Location: spec/support/shakapacker_precompile_hook_shared.rb:114-117

Issue: The guard if __FILE__ == $PROGRAM_NAME is good practice, but the shared file is always loaded via load (not require) from the dummy app hooks, which means $PROGRAM_NAME will be the calling script, not the shared file.

Impact: This guard is ineffective - the code will always execute when loaded.

Recommendation: Either:

  1. Remove the guard since this file is meant to be executed when loaded
  2. Document that this is intentional for the load pattern

Preferred approach:

# Main execution (executed when loaded via 'load' from dummy app hooks)
build_rescript_if_needed
generate_packs_if_needed

MINOR: RuboCop Disable Comments Could Be More Specific

Location: spec/support/shakapacker_precompile_hook_shared.rb:30,81

Issue: Disabling multiple cops on one line:

# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity

Recommendation: Consider refactoring build_rescript_if_needed to reduce complexity instead of disabling cops. For example, extract the package.json validation into a separate method:

def validate_rescript_package_json(rails_root)
  package_json_path = File.join(rails_root, "package.json")
  # validation logic...
end

def build_rescript_if_needed
  rails_root = find_rails_root
  return unless rails_root && rescript_config_exists?(rails_root)
  
  validate_rescript_package_json(rails_root)
  run_rescript_build(rails_root)
end

🧪 Testing Concerns

Missing Test Coverage for New Shared Hook

Observation: The PR creates a new shared implementation but doesn't add explicit tests for:

  • The shared hook's behavior when loaded
  • Error scenarios (missing package.json, invalid JSON, missing bundle command)
  • Cross-platform compatibility (yarn vs npm detection)

Recommendation: Add spec tests in spec/support/shakapacker_precompile_hook_shared_spec.rb to cover:

  • Successful ReScript builds
  • Missing dependencies handling
  • Pack generation edge cases

📝 Minor Suggestions

1. REACT_ON_RAILS_SKIP_VALIDATION Documentation

Location: lib/generators/react_on_rails/templates/base/base/bin/shakapacker-precompile-hook:16-18

Suggestion: The comment is good, but could be clearer:

# Skip package version validation during precompile hooks
# Rationale: This hook runs during the build process before full Rails initialization.
# Package validation is unnecessary since we're building the assets themselves.
# This prevents potential race conditions and reduces build time.
ENV["REACT_ON_RAILS_SKIP_VALIDATION"] = "true"

2. File Existence Check Messaging

Location: Both dummy app hooks (spec/dummy/bin/shakapacker-precompile-hook:13-15)

Suggestion: The error message could be more helpful:

unless File.exist?(shared_hook)
  warn "❌ Error: Shared precompile hook not found at #{shared_hook}"
  warn "   This indicates a React on Rails gem installation issue."
  warn "   Expected location: #{shared_hook}"
  exit 1
end

🚀 Performance & Security

Security: ✅ Good

  • Proper use of system() with array arguments for safe command execution
  • No shell injection vulnerabilities
  • Appropriate file path validation

Performance: ✅ Good

  • Efficient staleness checking prevents unnecessary regeneration
  • Direct gem API calls avoid subprocess overhead compared to rake tasks

📋 Summary

Verdict:Approve with minor suggestions

This PR achieves its goals well:

  • ✅ Simplifies generator template significantly
  • ✅ Eliminates code duplication via shared implementation
  • ✅ Improves documentation clarity
  • ✅ Makes correct technical choice with async: true

Priority Fixes:

  1. HIGH: Add fallback/documentation for Rainbow dependency in generator template
  2. MEDIUM: Improve error context in shared hook error handling
  3. LOW: Remove or document the ineffective main execution guard

Nice-to-have:

  • Add test coverage for shared hook
  • Refactor to reduce RuboCop disables
  • Enhance error messages for better debugging

Great work on this refactoring! The code is much cleaner and more maintainable. 🎉


Tested: Code review based on static analysis and project conventions from CLAUDE.md
CI Status: Pending checks (recommended to verify E2E tests pass with precompile hook changes)

cc: @justin808

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants