Skip to content

Conversation

@justin808
Copy link
Member

@justin808 justin808 commented Nov 11, 2025

Summary

Adds a Shakapacker precompile hook to the Pro dummy app to support automatic ReScript builds and React on Rails pack generation before webpack compilation.

Changes

1. Added Precompile Hook Script (bin/shakapacker-precompile-hook)

  • Cross-platform Ruby script that runs before Shakapacker compilation
  • ReScript build support: Detects both bsconfig.json (legacy) and rescript.json config files
  • Automatic pack generation: Runs react_on_rails:generate_packs when auto_load_bundle or components_subdirectory are configured
  • Rails root resolution: Properly finds Rails root directory and runs commands from correct location
  • Error handling: Graceful warnings and proper exit codes
  • Package manager detection: Supports both yarn and npm

2. Configured Shakapacker (config/shakapacker.yml)

  • Added precompile_hook: 'bin/shakapacker-precompile-hook' setting
  • Includes security comment about trusted script paths

Technical Details

ReScript Build Logic

  • Resolves Rails root directory early
  • Uses File.join(rails_root, ...) for proper path resolution
  • Executes build commands from Rails root using Dir.chdir(rails_root)
  • Ensures yarn/npm can find the correct package.json
  • Aborts gracefully if Rails root cannot be found

Pack Generation Logic

  • Checks for React on Rails initializer existence
  • Parses config for auto_load_bundle or components_subdirectory settings (ignores comments)
  • Only runs if react_on_rails:generate_packs rake task exists
  • Uses array form for system calls to prevent shell injection

Why This Matters

The precompile hook ensures that:

  1. ReScript code is compiled before webpack attempts to bundle it
  2. Generated component packs are created before compilation in production
  3. Developer experience is seamless - no manual build steps required
  4. Works consistently across development and production environments

Test Plan

  • RuboCop passes with zero offenses
  • Pre-commit hooks pass
  • Verify hook runs during bin/shakapacker compilation
  • Confirm ReScript builds when config files present
  • Check pack generation when auto_load_bundle configured
  • Test production asset precompilation workflow

Related

🤖 Generated with Claude Code


This change is Reviewable

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 11, 2025

Warning

Rate limit exceeded

@justin808 has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 22 minutes and 4 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between ec22681 and 64d14e8.

📒 Files selected for processing (5)
  • lib/generators/react_on_rails/templates/base/base/bin/shakapacker-precompile-hook (1 hunks)
  • lib/tasks/precompile/shakapacker_precompile_hook_shared.rb (1 hunks)
  • 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)

Walkthrough

Replaced async: true with defer: true for the client bundle in the dummy layout; added a Shakapacker precompile hook script that optionally builds ReScript and conditionally runs React on Rails pack generation; added precompile_hook config to the dummy Shakapacker YAML and updated initializer comments to prefer :defer.

Changes

Cohort / File(s) Summary
Layout script loading
react_on_rails_pro/spec/dummy/app/views/layouts/application.html.erb
Switched javascript_pack_tag option from async: true to defer: true and updated the inline comment to describe defer-based registration/hydration ordering.
Shakapacker precompile hook (new)
react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook
Added Ruby precompile hook with find_rails_root, build_rescript_if_needed, and generate_packs_if_needed; detects Rails root, optionally runs yarn/npm to build ReScript if bsconfig.json/rescript.json exist, conditionally invokes react_on_rails:generate_packs, logs errors, and sets exit codes.
Shakapacker config
react_on_rails_pro/spec/dummy/config/shakapacker.yml
Added precompile_hook: 'bin/shakapacker-precompile-hook' under default config with comments about running trusted precompile hooks.
Initializer comment only
react_on_rails_pro/spec/dummy/config/initializers/react_on_rails.rb
Added comments advising not to override generated_component_packs_loading_strategy (rely on default :defer); no behavioral change.

Sequence Diagram(s)

sequenceDiagram
    participant Shakapacker as Shakapacker
    participant Hook as bin/shakapacker-precompile-hook
    participant FS as Filesystem
    participant Node as Yarn/NPM
    participant Rails as Rails env
    participant ROR as react_on_rails:generate_packs

    Shakapacker->>Hook: invoke precompile hook
    Hook->>FS: find_rails_root (traverse up to config/environment.rb)
    alt ReScript config present
        Hook->>Node: run yarn or npm build:rescript
        Node-->>Hook: success / failure
    end
    Hook->>Rails: load environment (if needed) and check initializer flags
    alt auto_load_bundle or components_subdirectory enabled
        Hook->>ROR: run react_on_rails:generate_packs
        ROR-->>Hook: success / failure
    end
    alt error
        Hook->>Shakapacker: exit 1 (log error & backtrace)
    else
        Hook->>Shakapacker: exit 0
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Areas needing extra attention:
    • correctness and edge cases of find_rails_root upward traversal
    • subprocess invocation and shell/security handling for yarn/npm
    • detection logic for initializer flags controlling pack generation
    • exit codes and error/logging behavior in the new hook script

Possibly related PRs

Suggested reviewers

  • AbanoubGhadban
  • alexeyr-ci

Poem

🐰 I hopped through code at break of dawn,

Swapped async for defer — no race upon,
I traced the Rails root with careful nose,
Built ReScript where config petals rose,
Packs bloom gentle — tidy garden grown 🌿

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Title check ⚠️ Warning The title mentions adding a Shakapacker precompile hook with ReScript support, which matches the new bin/shakapacker-precompile-hook file and shakapacker.yml changes. However, the primary objective is fixing Playwright test timeouts by changing the loading strategy from async to defer, which is not reflected in the title. Consider revising the title to highlight the main fix: 'Fix Pro dummy app Playwright timeouts by switching to defer loading strategy' or similar, which better captures the primary objective and rationale for the changes.
✅ Passed checks (2 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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 11, 2025

Code Review: Fix Pro dummy app Playwright test timeouts

✅ Overall Assessment

This is a well-structured fix that correctly addresses the Playwright test timeout issue by aligning the Pro dummy app with the open-source configuration from PR #1972. The changes are consistent, well-documented, and follow established patterns.


📋 Summary of Changes

  1. Changed javascript_pack_tag from async: true to defer: true in application.html.erb
  2. Added precompile_hook configuration to shakapacker.yml
  3. Added bin/shakapacker-precompile-hook script (executable)

✅ Strengths

1. Root Cause Analysis

2. Code Quality

  • Ruby best practices: Uses array form for system() calls for security (lines 76, 80)
  • Cross-platform support: Checks for both yarn and npm availability
  • Defensive programming: Multiple existence checks before proceeding
  • Error handling: Proper exception handling with backtrace output
  • RuboCop compliance: Includes rubocop directives for justified complexity

3. Documentation

  • Comprehensive inline comments explaining the "why" not just the "what"
  • Updated comments in application.html.erb to reflect the new strategy
  • Security warning in shakapacker.yml about precompile hook path

4. Consistency

  • Matches the open-source dummy app structure
  • Uses the same precompile hook script pattern
  • Aligns with established project conventions

🔍 Detailed Review

application.html.erb (lines 24-31)

Good: Updated comment accurately describes the defer strategy purpose
Good: Maintains the skip_js_packs test parameter
⚠️ Minor observation: The Pro version has more elaborate layout structure than open-source (includes CSS pack tags and more metadata), but this is expected for Pro features

shakapacker.yml (lines 22-25)

Excellent: Includes security warning about precompile hook
Good: Follows the same configuration pattern as open-source
Good: Uses relative path from project root

bin/shakapacker-precompile-hook (entire file)

Excellent: Robust implementation with multiple safety checks
Security: Uses array form for system calls to prevent shell injection
Cross-platform: Handles both yarn and npm
Fail-fast: Exits with error code 1 on failures
Good UX: Uses emoji indicators for status messages
Smart detection: Regex pattern properly excludes commented config lines (line 65-66)

Specific highlights:

  • Line 31-32: Proper use of File::NULL for silent checks
  • Line 65-66: Regex correctly matches only active config (not comments)
  • Line 76: Uses IO.popen with array form and proper error redirection
  • Line 92-101: Clean error handling in main execution block

🐛 Potential Issues

None identified - The code is production-ready.


🔒 Security Review

Command injection prevention: Uses array form for all system() and IO.popen() calls
Path traversal: Uses File.join() for path construction
Input validation: Checks file existence before reading
Secure defaults: Returns early if conditions not met
Documentation: Includes security warning in YAML config


⚡ Performance Considerations

Minimal overhead: Early returns prevent unnecessary work
Efficient checks: Uses simple file existence tests before complex operations
Parallel downloads: defer: true maintains parallel script downloads while ensuring execution order


🧪 Test Coverage

Test Plan Status: ⚠️ Checklist items are unchecked in PR description

Recommendations for testing:

  1. ✅ Verify Playwright E2E tests pass in CI (should be automatic)
  2. ✅ Confirm ToggleContainer hydrates and logs console message
  3. ✅ Check all 11 streaming tests complete successfully
  4. Consider adding a test that specifically verifies component registration order
  5. Test both development and production builds with the new precompile hook

💡 Suggestions

1. Test Plan Completion
Update the PR description to check off test plan items once CI passes.

2. Verification Script (optional)
Consider adding a simple verification that the hook executes correctly:

# In CI or locally
RAILS_ENV=test bin/shakapacker-precompile-hook
echo "Hook exit code: $?"

3. Documentation (optional, future work)
Consider documenting this pattern in the main React on Rails docs, as the defer vs async strategy is critical for auto_load_bundle users.


📝 Checklist

  • ✅ Code follows project conventions (CLAUDE.md)
  • ✅ RuboCop compliant (has proper directives)
  • ✅ Security best practices followed
  • ✅ Cross-platform compatibility
  • ✅ Error handling in place
  • ✅ Comments explain "why" not just "what"
  • ✅ Consistent with related changes (PR Fix component registration race condition with defer loading strategy #1972)
  • ⚠️ Test plan needs verification (wait for CI)

🎯 Recommendation

LGTM after CI passes

This is a solid fix that:

  • Correctly diagnoses and resolves the root cause
  • Follows established patterns from the open-source codebase
  • Includes proper security considerations
  • Has excellent documentation

Once the Playwright tests pass in CI, this is ready to merge.


📚 References


Review generated with assistance from Claude Code

@coderabbitai coderabbitai bot added the bug label Nov 11, 2025
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)
react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook (1)

72-78: Surface skip reason when Bundler is unavailable. Right now we return silently if bundle_available is false (or later when the task is missing), so folks won’t know packs were skipped. Consider emitting a warn before those early returns to make the hook’s behavior explicit during CI troubleshooting.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ad7c916 and 5cd3cc6.

📒 Files selected for processing (3)
  • react_on_rails_pro/spec/dummy/app/views/layouts/application.html.erb (1 hunks)
  • react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook (1 hunks)
  • react_on_rails_pro/spec/dummy/config/shakapacker.yml (1 hunks)
🧰 Additional context used
🧠 Learnings (10)
📓 Common learnings
Learnt from: AbanoubGhadban
Repo: shakacode/react_on_rails PR: 1781
File: node_package/src/ClientSideRenderer.ts:82-95
Timestamp: 2025-09-15T21:24:48.207Z
Learning: In React on Rails, the force_load feature includes both explicit `data-force-load="true"` usage and the ability to hydrate components during the page loading state (`document.readyState === 'loading'`). Both capabilities require a Pro license, so the condition `!railsContext.rorPro && (isComponentForceLoaded || document.readyState === 'loading')` correctly gates both scenarios.
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.
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.
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.
Learnt from: theforestvn88
Repo: shakacode/react_on_rails PR: 1620
File: spec/dummy/client/app/startup/HelloTurboStream.jsx:3-3
Timestamp: 2024-07-27T10:08:35.868Z
Learning: The `RailsContext` import in `spec/dummy/client/app/startup/HelloTurboStream.jsx` is used later in the project, as clarified by the user theforestvn88.
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.
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.
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.
Learnt from: AbanoubGhadban
Repo: shakacode/react_on_rails PR: 1644
File: node_package/src/clientStartup.ts:18-21
Timestamp: 2025-02-13T16:50:47.848Z
Learning: In the react_on_rails module, the `reactOnRailsPageUnloaded` function in clientStartup.ts is intentionally kept private as it's only used internally as a callback for `onPageUnloaded`.
Learnt from: AbanoubGhadban
Repo: shakacode/react_on_rails PR: 1696
File: node_package/src/RSCPayloadContainer.ts:0-0
Timestamp: 2025-04-09T12:56:10.756Z
Learning: In the react_on_rails codebase, RSC payloads are already stringified using `JSON.stringify()` before being processed by the `escapeScript` function, which handles escaping of special characters. The function only needs to handle specific HTML markers like comments and closing script tags.
📚 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/app/views/layouts/application.html.erb
  • react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook
  • react_on_rails_pro/spec/dummy/config/shakapacker.yml
📚 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/app/views/layouts/application.html.erb
  • react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook
📚 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/app/views/layouts/application.html.erb
  • react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook
📚 Learning: 2024-07-27T10:08:35.868Z
Learnt from: theforestvn88
Repo: shakacode/react_on_rails PR: 1620
File: spec/dummy/client/app/startup/HelloTurboStream.jsx:3-3
Timestamp: 2024-07-27T10:08:35.868Z
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/app/views/layouts/application.html.erb
📚 Learning: 2025-09-15T21:24:48.207Z
Learnt from: AbanoubGhadban
Repo: shakacode/react_on_rails PR: 1781
File: node_package/src/ClientSideRenderer.ts:82-95
Timestamp: 2025-09-15T21:24:48.207Z
Learning: In React on Rails, the force_load feature includes both explicit `data-force-load="true"` usage and the ability to hydrate components during the page loading state (`document.readyState === 'loading'`). Both capabilities require a Pro license, so the condition `!railsContext.rorPro && (isComponentForceLoaded || document.readyState === 'loading')` correctly gates both scenarios.

Applied to files:

  • react_on_rails_pro/spec/dummy/app/views/layouts/application.html.erb
📚 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:

  • react_on_rails_pro/spec/dummy/app/views/layouts/application.html.erb
  • react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook
📚 Learning: 2025-02-13T16:50:26.861Z
Learnt from: AbanoubGhadban
Repo: shakacode/react_on_rails PR: 1644
File: node_package/src/turbolinksUtils.ts:34-36
Timestamp: 2025-02-13T16:50:26.861Z
Learning: In React on Rails, when checking for Turbolinks version 5 using `turbolinksVersion5()`, always ensure `Turbolinks` exists first by checking `turbolinksInstalled()` to prevent TypeError when accessing properties.

Applied to files:

  • react_on_rails_pro/spec/dummy/app/views/layouts/application.html.erb
📚 Learning: 2025-02-13T16:50:47.848Z
Learnt from: AbanoubGhadban
Repo: shakacode/react_on_rails PR: 1644
File: node_package/src/clientStartup.ts:18-21
Timestamp: 2025-02-13T16:50:47.848Z
Learning: In the react_on_rails module, the `reactOnRailsPageUnloaded` function in clientStartup.ts is intentionally kept private as it's only used internally as a callback for `onPageUnloaded`.

Applied to files:

  • react_on_rails_pro/spec/dummy/app/views/layouts/application.html.erb
📚 Learning: 2025-04-09T12:56:10.756Z
Learnt from: AbanoubGhadban
Repo: shakacode/react_on_rails PR: 1696
File: node_package/src/RSCPayloadContainer.ts:0-0
Timestamp: 2025-04-09T12:56:10.756Z
Learning: In the react_on_rails codebase, RSC payloads are already stringified using `JSON.stringify()` before being processed by the `escapeScript` function, which handles escaping of special characters. The function only needs to handle specific HTML markers like comments and closing script tags.

Applied to files:

  • react_on_rails_pro/spec/dummy/app/views/layouts/application.html.erb
🔇 Additional comments (2)
react_on_rails_pro/spec/dummy/app/views/layouts/application.html.erb (1)

27-33: Defer attribute restores hydration order. Switching to defer mirrors the OSS dummy fix and guarantees component packs register before hydration—looks solid.

react_on_rails_pro/spec/dummy/config/shakapacker.yml (1)

22-25: Hook config lines up with the automation. Wiring the precompile hook here cleanly connects the new script into pack generation flow.

@claude
Copy link

claude bot commented Nov 11, 2025

Code Review: PR #1977

Overview

This PR successfully addresses Playwright E2E test timeouts in the Pro dummy app by aligning with the defer loading strategy fix from PR #1972. The changes are well-documented and the root cause analysis is thorough.


✅ Strengths

1. Excellent Problem Analysis

2. Consistent Implementation

The bin/shakapacker-precompile-hook script is identical between open-source and Pro dummy apps, ensuring consistency across the monorepo. This is excellent for maintainability.

3. Robust Hook Script

The precompile hook demonstrates several best practices:

  • Cross-platform compatibility (yarn/npm detection)
  • Security-conscious (uses array form for system calls)
  • Graceful degradation (silent returns when features aren't needed)
  • Proper error handling with exit codes
  • Good logging with emoji indicators

4. Security Awareness

The shakapacker.yml includes a security comment about the precompile hook, which is important for preventing malicious hook injection.


🔍 Code Quality Observations

Precompile Hook Script (bin/shakapacker-precompile-hook)

Positives:

  • Clean separation of concerns with dedicated functions
  • Appropriate RuboCop disables with justification
  • Good comments explaining the rationale

Minor Suggestions:

  1. Regex Complexity (lines 65-66): The regex /^\s*(?!#).*config\.auto_load_bundle\s*=/ uses negative lookahead which is correct, but could be more precise:

    # Current approach might match commented lines with leading whitespace
    # Consider: /^\s*[^#]*config\.auto_load_bundle\s*=/
    # Or even simpler: check if line.strip starts with '#'

    However, the current implementation should work fine in practice.

  2. File.read Performance: Reading the entire initializer file (line 63) is fine for this use case, but be aware this loads the full file into memory. For very large files, consider line-by-line processing (though unlikely to be an issue here).

  3. Error Messages: Consider adding the Rails root path to error messages for easier debugging:

    warn "❌ Pack generation failed in #{rails_root}"

View Template Changes (application.html.erb)

Positives:

  • Clear comments explaining the rationale for defer: true
  • Maintains the skip_js_packs testing parameter

Observation:

  • The comment accurately explains that defer ensures proper script execution order for generated component packs

🎯 Test Coverage Considerations

Strong Points:

  • The PR targets the exact failing test scenario (line 52 in streaming.spec.ts)
  • Test waits for console message 'ToggleContainer with title' which directly validates component hydration

Recommendations:

  1. Add Regression Test: Consider adding a specific test case that validates script execution order, perhaps by:

    • Checking that component registrations complete before hydration
    • Verifying no "component not registered" errors in console
    • Testing with multiple generated component packs
  2. Test Hook Execution: Add tests for the precompile hook script itself:

    # Test that hook runs successfully
    # Test with/without ReScript config
    # Test with/without React on Rails config

⚡ Performance Considerations

Defer vs Async:

  • Correct choice: defer provides deterministic execution order while maintaining parallel downloads
  • No performance regression: Scripts still download in parallel, only execution order changes
  • Better TTI: Prevents hydration failures which would cause worse performance

Precompile Hook:

  • The hook runs on every Shakapacker compilation
  • Early returns prevent unnecessary work when features aren't configured
  • ✅ Minimal performance impact in normal operation

🔒 Security Considerations

Strengths:

  1. ✅ Uses array form for system calls (lines 76, 80) - prevents shell injection
  2. ✅ Security comment in shakapacker.yml about trusted scripts
  3. ✅ Proper validation before executing commands

Best Practices Followed:

  • No use of string interpolation in system calls
  • Explicit path checking before file operations
  • Fail-fast behavior on errors

📦 Dependency Updates

The Gemfile.lock changes show:

  • Shakapacker: 8.0.0 → 9.3.0
  • semantic_range: 3.0.0 → 3.1.0
  • Open source adds: rbs gem

These dependency updates align with the PR's goals and are consistent with the project's CI configuration (as mentioned in CLAUDE.md).


📝 Documentation & Style

Strengths:

  • PR description is comprehensive with clear problem/solution sections
  • Code comments are helpful and explain the "why"
  • Follows repository conventions from CLAUDE.md

Minor Note:

  • Ensure CHANGELOG.md is updated (per CLAUDE.md guidelines) since this is a bug fix that affects users
  • Format: [PR 1977](https://github.com/shakacode/react_on_rails/pull/1977) by [justin808](https://github.com/justin808)

✨ Additional Recommendations

  1. DRY Opportunity: The precompile hook script is duplicated between open-source and Pro. Consider:

    • Symlinking to a shared script
    • Or documenting why duplication is intentional (if Pro needs to diverge in the future)
  2. Monitoring: Consider adding telemetry or logging to track:

    • How often the race condition would have occurred (if async were still used)
    • Hook execution time for performance monitoring
  3. Documentation Update: Update any documentation that mentions async: true as the recommended approach for Shakapacker >= 8.2.0


🎬 Conclusion

Overall Assessment: ✅ LGTM with minor suggestions

This is a solid fix that:

  • ✅ Addresses the root cause correctly
  • ✅ Follows project conventions
  • ✅ Maintains consistency across the monorepo
  • ✅ Includes proper error handling and security considerations
  • ✅ Has comprehensive documentation

The code quality is high and the approach is sound. The minor suggestions above are optional improvements that don't block merging.

Recommendation: Approve and merge after:

  1. CI passes (especially the Playwright E2E tests this targets)
  2. CHANGELOG.md is updated
  3. Consider the DRY opportunity for the precompile hook script

Great work on the thorough analysis and consistent implementation! 🚀

@claude
Copy link

claude bot commented Nov 11, 2025

Code Review - PR #1977

Summary

This PR successfully addresses Playwright E2E test timeouts in the Pro dummy app by aligning it with the defer loading strategy from PR #1972. The changes are well-motivated, properly documented, and follow established patterns from the open-source codebase.


Positive Aspects

1. Excellent Problem Analysis

The PR description clearly explains:

  • The specific symptom (30-second timeout waiting for console message)
  • The root cause (race condition between async script loading)
  • Why the fix works (defer ensures proper execution order)

2. Code Consistency

3. Security Awareness

The shakapacker.yml includes appropriate security documentation:

# SECURITY: Only reference trusted scripts within your project. Ensure the hook path
# points to a file within the project root that you control.

4. Robust Hook Implementation

The precompile hook demonstrates good practices:

  • Cross-platform compatibility (works with both yarn/npm)
  • Graceful degradation (returns early if dependencies not found)
  • Proper error handling with exit codes
  • Security-conscious (uses array form for system commands)
  • RuboCop compliant (disables only necessary complexity check with explanation)

Observations & Minor Suggestions

1. Comment Update in Layout ⚠️

The old comment in application.html.erb (lines 27-31 in current file) still references async: true and discusses Selective Hydration. While the new comment (lines 27-29 in the diff) is more accurate for the defer strategy, consider:

Before (lines 27-31):

<%# async: true is the recommended approach for Shakapacker >= 8.2.0 (currently using 9.3.0).
    It enables React 18's Selective Hydration and provides optimal Time to Interactive (TTI).
    Use immediate_hydration feature to control hydration timing for Selective/Immediate Hydration.
    See docs/building-features/streaming-server-rendering.md
    skip_js_packs param is used for testing purposes to simulate hydration failure %>

After (lines 27-29 in diff):

<%# Use defer: true to ensure proper script execution order.
    When using generated component packs (auto_load_bundle), defer ensures
    component registrations complete before React hydration begins.

Suggestion: The skip_js_packs usage context is lost in the new comment. Consider adding:

<%# Use defer: true to ensure proper script execution order.
    When using generated component packs (auto_load_bundle), defer ensures
    component registrations complete before React hydration begins.
    skip_js_packs param is used for testing purposes to simulate hydration failure %>

2. Gemfile.lock Changes ℹ️

The PR includes Shakapacker version bump from 8.0.0 → 9.3.0 and semantic_range updates. These appear intentional for aligning with the "Latest" CI configuration, but worth confirming:

  • Is this a deliberate upgrade as part of CI alignment?
  • Have all Shakapacker 9.3.0 compatibility concerns been addressed?

3. Test Verification 📋

The test plan checklist is still unchecked. Ensure:

  • Playwright E2E tests pass in CI
  • ToggleContainer console message appears (line 52 in streaming.spec.ts)
  • All 11 streaming tests complete without timeouts

4. rbs Gem Addition ℹ️

The open-source Gemfile.lock shows rbs was added as a dependency. This seems unrelated to the defer loading fix - was this intentional or a side effect of bundle operations?


Code Quality Assessment

Security ✅

  • Hook script uses secure command execution patterns (system with array form)
  • Proper path validation for Rails root detection
  • Security warnings documented in config

Performance ✅

  • Defer loading maintains parallel download benefits while fixing execution order
  • Hook runs only when needed (checks for config presence)
  • Early returns prevent unnecessary work

Test Coverage ✅

  • Existing Playwright tests (streaming.spec.ts:52) will verify the fix
  • Tests specifically check for the console message that was timing out

Best Practices ✅

  • Follows repository conventions per CLAUDE.md
  • Ruby script has proper shebang and frozen_string_literal
  • Cross-platform compatibility considered
  • Error handling with meaningful messages

Potential Issues

None Identified 🎉

No bugs, security vulnerabilities, or significant issues found. The implementation is solid.


Recommendations

  1. Verify CI Passes: Ensure all Playwright tests complete successfully
  2. Update Comment: Consider preserving the skip_js_packs documentation in the ERB comment
  3. Confirm Shakapacker Upgrade: Verify the 8.0.0 → 9.3.0 upgrade is intentional and complete
  4. Check Test Plan: Mark off the checklist items in the PR description once verified

Conclusion

Recommendation: Approve with minor suggestions

This is a well-executed fix that:

The minor suggestions above are non-blocking improvements. Great work tracking down the race condition and applying the consistent fix across both codebases!


Review conducted using repository guidelines from CLAUDE.md

@justin808 justin808 force-pushed the justin808/playwright-streaming-timeout branch from c8be1f5 to 36d768b Compare November 12, 2025 21:31
@justin808 justin808 force-pushed the justin808/playwright-streaming-timeout branch from 36d768b to cbb7df2 Compare November 13, 2025 03:23
@claude
Copy link

claude bot commented Nov 13, 2025

Code Review: PR #1977

Overview

This PR fixes Playwright E2E test timeouts in the Pro dummy app by applying the same defer loading strategy from #1972. The changes are well-motivated and align the Pro dummy app with the open-source configuration.


✅ Strengths

  1. Root Cause Analysis: Excellent problem diagnosis. The PR clearly identifies the race condition where async: true caused unpredictable script execution order, leading to React hydration failures.

  2. Consistent Solution: Mirrors the fix from Fix component registration race condition with defer loading strategy #1972 (commit d1a8a1a), maintaining consistency across the monorepo.

  3. Comprehensive Fix: Addresses both the immediate issue (defer loading) and the underlying build automation (precompile hook).

  4. Good Documentation: Clear comments in the ERB template explaining why defer: true is needed for auto-generated component packs.

  5. Executable Permissions: The precompile hook has correct permissions (755).

  6. Cross-platform Compatibility: The precompile hook script handles multiple scenarios (yarn/npm, bundle availability) gracefully.


🔍 Issues & Recommendations

1. Security: Command Injection Risk (Medium Priority)

Location: bin/shakapacker-precompile-hook:35-37

success = if yarn_available
            system("yarn", "build:rescript")
          elsif npm_available
            system("npm", "run", "build:rescript")

Issue: The script uses the array form of system() which is secure, but this should be explicitly documented as a security best practice since this hook runs during build/deployment.

Recommendation: Add a comment explaining the security consideration:

# Use array form to prevent shell injection
success = if yarn_available
            system("yarn", "build:rescript")

2. Error Handling: Silent Failures (Low Priority)

Location: bin/shakapacker-precompile-hook:39-40

else
  warn "⚠️  Warning: Neither yarn nor npm found. Skipping ReScript build."
  return

Issue: When neither package manager is found, the script warns but continues. If ReScript is required, this could lead to broken builds that fail later.

Recommendation: Consider making this configurable or failing fast if ReScript config exists:

else
  warn "❌ Error: ReScript config found but no package manager available"
  exit 1
end

3. Code Quality: Complex Regex (Low Priority)

Location: bin/shakapacker-precompile-hook:65-66

has_auto_load = config_file =~ /^\s*(?\!#).*config\.auto_load_bundle\s*=/
has_components_subdir = config_file =~ /^\s*(?\!#).*config\.components_subdirectory\s*=/

Issue: The negative lookahead (?\!#) doesn't correctly skip comments. A line like # config.auto_load_bundle = true would match because the lookahead only checks the first character after whitespace.

Recommendation: Use a more robust regex:

# Match lines that don't start with # (after optional whitespace)
has_auto_load = config_file =~ /^\s*[^#]*config\.auto_load_bundle\s*=/
has_components_subdir = config_file =~ /^\s*[^#]*config\.components_subdirectory\s*=/

Or use Ruby parsing:

has_auto_load = config_file.lines.any? { |line| line.strip[0] \!= '#' && line.include?('config.auto_load_bundle =') }

4. Missing Test Coverage (Medium Priority)

Issue: No tests for the precompile hook script. Given its critical role in the build process, it should have unit tests.

Recommendation: Add tests in react_on_rails_pro/spec/dummy/spec/bin/shakapacker_precompile_hook_spec.rb:

  • Test ReScript build detection
  • Test Rails root finding
  • Test config parsing (especially edge cases with comments)
  • Test error handling

5. Documentation: Shakapacker Version (Low Priority)

Location: config/shakapacker.yml:25

The comment references Shakapacker docs, but doesn't specify the minimum version required for precompile_hook support.

Recommendation: Add version requirement to the comment:

# Hook to run before webpack compilation (requires Shakapacker >= X.X.X)

🧪 Testing Considerations

The PR description mentions a test plan but doesn't show completed checkboxes. Before merging:

  • ✅ Verify CI passes (especially the Playwright streaming tests)
  • ✅ Confirm ToggleContainer console message appears (check CI logs)
  • ✅ Test the precompile hook runs correctly in both development and production builds
  • ✅ Verify the hook doesn't break builds when ReScript isn't present

🎯 Performance Considerations

Positive: Using defer: true maintains parallel download performance while ensuring deterministic execution order. This is the optimal strategy for this use case.

Neutral: The precompile hook adds build time overhead, but this is necessary for auto-generated packs.


📋 Summary

Approve with minor suggestions. The core fix is sound and well-implemented. The issues identified are mostly improvements rather than blockers:

  • Must fix: None (code is mergeable)
  • Should fix: Add test coverage for the precompile hook
  • Nice to have: Improve regex robustness, add security comments

The PR successfully addresses the test timeout issue and aligns the Pro dummy app with the open-source configuration. Great work documenting the root cause and solution!


📚 References

@claude
Copy link

claude bot commented Nov 13, 2025

Code Review for PR #1977

Thank you for this PR! The changes effectively address the Playwright test timeout issue by aligning the Pro dummy app with the open-source configuration. Here's my detailed review:


Strengths

1. Root Cause Analysis is Solid

The PR correctly identifies the race condition caused by async: true loading strategy where component registration might not complete before React hydration begins. The switch to defer: true is the right solution.

2. Consistency Across Codebase

Aligning the Pro dummy app with the open-source dummy app (from #1972) ensures consistent behavior and reduces maintenance burden.

3. Comprehensive Solution

The PR includes all necessary pieces:

  • Changed loading strategy in application.html.erb
  • Added precompile hook for automatic pack generation
  • Updated configuration files
  • Added helpful comments explaining the rationale

4. Good Documentation

The inline comments explain why defer: true is used, which is crucial for future maintainers.


🔍 Observations & Suggestions

1. Code Duplication: Precompile Hook

The shakapacker-precompile-hook file in the Pro dummy app is nearly identical to the one in the open-source dummy app, with one difference:

Pro version (line 75):

task_list = IO.popen(["bundle", "exec", "rails", "-T"], err: %i[child out], &:read)

Open-source version (line 76):

task_list = IO.popen(["bundle", "exec", "rails", "-T"], err: [:child, :out], &:read)

The Pro version uses %i[child out] (symbol array literal) while the open-source version uses [:child, :out] (explicit array). Both are functionally equivalent.

Suggestion: Consider one of these approaches to reduce duplication:

  • Extract the hook to a shared location (e.g., lib/generators/react_on_rails/templates/)
  • Add a note in both files indicating they should be kept in sync
  • Use a symlink if the codebase structure supports it

2. RuboCop Violation in Pro Version

The open-source version has a RuboCop disable comment for Metrics/CyclomaticComplexity (lines 52, 89), but the Pro version doesn't. This might cause CI failures if RuboCop runs on the Pro package.

Action Required:

# Add these lines to react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook
# rubocop:disable Metrics/CyclomaticComplexity
def generate_packs_if_needed
  # ... existing code ...
end
# rubocop:enable Metrics/CyclomaticComplexity

3. Security Note is Well-Placed

The security comment in shakapacker.yml about trusting the precompile hook script is excellent. This is important for preventing arbitrary code execution.

4. Test Coverage

The PR description mentions test timeouts were the issue, and the fix ensures proper hydration. However:

  • Question: Have you verified that all 11 streaming tests now pass consistently?
  • Suggestion: Consider adding a test that explicitly verifies the script loading order (though this might be overkill for this issue)

5. Comment Accuracy in react_on_rails.rb

Lines 42-43 in react_on_rails_pro/spec/dummy/config/initializers/react_on_rails.rb:

# Don't explicitly set generated_component_packs_loading_strategy - let it default to :defer
# which ensures generated component packs load and register components before main bundle executes

This is good documentation. However, it might be worth explicitly stating that this default behavior changed or was introduced in a specific version for future reference.


🐛 Potential Issues

1. Missing RuboCop Pragmas (Critical)

As mentioned above, the Pro version is missing the RuboCop disable comment that exists in the open-source version. This will cause CI failures based on the CLAUDE.md requirements:

BEFORE EVERY COMMIT/PUSH:

  1. ALWAYS run bundle exec rubocop and fix ALL violations

Priority: High - Should be fixed before merging.

2. File Permissions

The script needs to be executable. Based on the diff, the file is new. Ensure it has execute permissions:

chmod +x react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook

Git should preserve this, but worth verifying.


🎯 Performance Considerations

Script Loading Performance

Before (async): Scripts load in parallel, potentially faster initial load but risk of race conditions.
After (defer): Scripts load in parallel but execute in order, slightly slower but reliable.

Trade-off is acceptable because:

  • The performance difference is negligible (milliseconds)
  • Correctness > speed in this case
  • User experience improves due to proper hydration

Precompile Hook Performance

The hook runs during precompilation, not runtime, so performance impact is minimal. The conditional checks (file existence, config parsing) are efficient.


🔒 Security Considerations

Precompile Hook Execution

Good: The hook uses array form for system calls (system("bundle", "exec", "rails", ...)), which prevents shell injection.
Good: Security warning in shakapacker.yml alerts users to only use trusted scripts.
Good: Hook validates command availability before execution.

No security concerns identified.


📋 Required Actions Before Merge

  1. Add RuboCop pragmas to react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook (lines 52 and 87)
  2. Run bundle exec rubocop on the Pro package to verify no violations
  3. Verify all Playwright tests pass in CI
  4. Confirm file permissions are correct for the hook script

📝 Optional Improvements

  1. Consider documenting the code duplication between Pro and open-source hook scripts
  2. Add a test that verifies the precompile hook runs successfully during asset compilation
  3. Consider adding a comment in the open-source hook pointing to the Pro version and vice versa

Overall Assessment

Rating: Approve with minor changes

This is a well-researched fix that addresses a real issue with a proven solution. The root cause analysis is thorough, and the implementation is sound. The only blocking issue is the missing RuboCop pragma, which should be a quick fix.

Recommended next steps:

  1. Add the RuboCop disable/enable comments
  2. Run bundle exec rubocop to verify
  3. Confirm CI passes
  4. Merge

Great work on identifying and fixing this race condition! 🚀

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

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 36d768b and 9c9abd1.

📒 Files selected for processing (4)
  • react_on_rails_pro/spec/dummy/app/views/layouts/application.html.erb (1 hunks)
  • react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook (1 hunks)
  • react_on_rails_pro/spec/dummy/config/initializers/react_on_rails.rb (1 hunks)
  • react_on_rails_pro/spec/dummy/config/shakapacker.yml (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • react_on_rails_pro/spec/dummy/config/initializers/react_on_rails.rb
🚧 Files skipped from review as they are similar to previous changes (2)
  • react_on_rails_pro/spec/dummy/app/views/layouts/application.html.erb
  • react_on_rails_pro/spec/dummy/config/shakapacker.yml
🧰 Additional context used
🧠 Learnings (6)
📓 Common learnings
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.
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.
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.
Learnt from: theforestvn88
Repo: shakacode/react_on_rails PR: 1620
File: spec/dummy/client/app/startup/HelloTurboStream.jsx:3-3
Timestamp: 2024-07-27T10:08:35.868Z
Learning: The `RailsContext` import in `spec/dummy/client/app/startup/HelloTurboStream.jsx` is used later in the project, as clarified by the user theforestvn88.
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.
Learnt from: AbanoubGhadban
Repo: shakacode/react_on_rails PR: 1781
File: node_package/src/ClientSideRenderer.ts:82-95
Timestamp: 2025-09-15T21:24:48.207Z
Learning: In React on Rails, the force_load feature includes both explicit `data-force-load="true"` usage and the ability to hydrate components during the page loading state (`document.readyState === 'loading'`). Both capabilities require a Pro license, so the condition `!railsContext.rorPro && (isComponentForceLoaded || document.readyState === 'loading')` correctly gates both scenarios.
📚 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/bin/shakapacker-precompile-hook
📚 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/bin/shakapacker-precompile-hook
📚 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:

  • react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook
📚 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/bin/shakapacker-precompile-hook
📚 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
⏰ 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). (4)
  • GitHub Check: build-dummy-app-webpack-test-bundles
  • GitHub Check: build-dummy-app-webpack-test-bundles
  • GitHub Check: lint-js-and-ruby
  • GitHub Check: claude-review

@justin808 justin808 force-pushed the justin808/playwright-streaming-timeout branch from 9c9abd1 to ec22681 Compare November 16, 2025 06:32
@justin808 justin808 changed the title Fix Pro dummy app Playwright test timeouts by aligning with defer loading strategy Add Shakapacker precompile hook with ReScript support to Pro dummy app Nov 17, 2025
justin808 and others added 4 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>
@justin808 justin808 force-pushed the justin808/playwright-streaming-timeout branch from 8ef957c to 0f91a20 Compare November 17, 2025 05:26
@claude
Copy link

claude bot commented Nov 17, 2025

Code Review

Summary

This PR converts the precompile hook from Ruby to POSIX shell script, which is a solid improvement for cross-platform compatibility and reduces dependencies. The implementation is clean and well-structured. However, there are several important issues that need to be addressed.


Critical Issues

1. Shell Script Execution Issue with set -e

Location: Lines 13, 44, 97, 53, 61

Problem: The script uses set -e which causes the script to exit immediately on any error. However, within functions, there are conditional blocks that check command success:

if yarn build:rescript; then
  echo "✅ ReScript build completed successfully"
  return 0
else
  echo "❌ ReScript build failed" >&2
  exit 1
fi

With set -e, if yarn build:rescript fails, the script will exit before reaching the else block. This makes the error handling logic unreachable.

Impact: The custom error messages ("❌ ReScript build failed") will never be displayed. The script will just exit silently with a non-zero status.

Fix: Either:

  • Remove set -e and rely on explicit error checking in if/else blocks
  • Use set -e with || { echo "error"; exit 1; } patterns
  • Use trap for error handling

2. Directory Change Side Effect

Location: Lines 44, 97

Problem: Both functions use cd "$rails_root" which permanently changes the working directory for the rest of the script execution. If build_rescript_if_needed() changes to Rails root, then generate_packs_if_needed() will be running from a different directory than expected.

Impact: Could cause issues if the script is called from a subdirectory and subsequent operations depend on the original working directory.

Fix: Either:

  • Use subshells: (cd "$rails_root" && yarn build:rescript)
  • Save and restore the original directory
  • Document the behavior clearly

Medium Priority Issues

3. Missing ReScript Build Script Validation

Location: Lines 47-62

Problem: The script assumes build:rescript exists in package.json but doesn't verify this before attempting to run it.

Impact: If a project has rescript.json but no build:rescript script, you'll get a confusing error message from yarn/npm instead of a helpful message from the hook.

Suggestion: Add validation:

if \! grep -q '"build:rescript"' "$rails_root/package.json"; then
  echo "⚠️  Warning: ReScript config found but no build:rescript script in package.json"
  return 0
fi

4. Grep Pattern May Match Comments

Location: Lines 84-85

Problem: While the regex ^[[:space:]]*config\. attempts to exclude comments, it could match code within multi-line comments or commented-out code that looks like:

# config.auto_load_bundle = true  # This is commented but will match\!

Suggestion: Consider a more robust check or add documentation about this limitation.

5. bundle exec rails -T Performance

Location: Line 100

Problem: Running rails -T loads the entire Rails environment just to check if a rake task exists. This adds significant overhead (1-3 seconds) to every webpack compilation.

Impact: Slows down development workflow, especially with hot reload.

Suggestion:

  • Cache the result in a temp file with timestamp
  • Or just try running the task and handle the error gracefully
  • Or check for the existence of the rake task file directly

Minor Issues / Suggestions

6. Inconsistent Emoji Usage

The script uses emojis in output, which may not render correctly in all terminals or CI environments. Consider documenting this or making it optional.

7. Error Messages to stderr

Good practice using >&2 for error messages on lines 52, 60, 113. However, warnings (lines 32, 64) should also go to stderr for consistency.

8. Redundant Return Statements

Lines 50, 58, 111 have return 0 before exit 1 in else blocks - the return is unreachable. Not a bug, just unnecessary.


Security Considerations

Good: The script uses proper quoting around variables ("$rails_root")
Good: The shakapacker.yml includes security comments about trusting the hook path
Good: No user input is directly executed
Good: Commands use direct invocation (not eval or string-based execution)


Testing Recommendations

The PR checklist shows several test items are unchecked:

  • Verify hook runs during bin/shakapacker compilation
  • Confirm ReScript builds when config files present
  • Check pack generation when auto_load_bundle configured
  • Test production asset precompilation workflow

These tests are critical - please complete them before merging. The set -e issue (#1) will only be caught by actual execution testing.

Suggested test scenarios:

  1. Test when ReScript config exists but build:rescript script is missing
  2. Test when called from a subdirectory
  3. Test when Rails root cannot be found
  4. Test when bundle is not available
  5. Test error propagation to webpack/shakapacker

Positive Aspects

✅ Excellent migration from Ruby to POSIX shell - much lighter weight
✅ Good graceful degradation (warnings instead of errors when optional tools missing)
✅ Clear, readable code with good comments
✅ Cross-platform package manager detection
✅ Security-conscious implementation
✅ Proper use of REACT_ON_RAILS_SKIP_VALIDATION environment variable


Recommendation

Request Changes - The set -e issue (#1) is critical and will cause silent failures. The directory change issue (#2) could cause subtle bugs. Please address these before merging.

Once fixed, this is a solid improvement to the codebase. The shell script approach is definitely the right direction for a precompile hook.

cc @justin808

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>
@claude
Copy link

claude bot commented Nov 17, 2025

Code Review - PR #1977

Summary

This PR refactors the Shakapacker precompile hook to add ReScript build support and consolidate duplicate code across dummy apps. The changes follow good DRY principles and improve the developer experience.


✅ Strengths

1. Excellent Code Reuse

The creation of shakapacker_precompile_hook_shared.rb eliminates duplication between the two test dummy apps. The wrapper scripts are clean and minimal.

2. Robust Error Handling

  • Graceful degradation with warnings when dependencies are missing
  • Proper exception handling with informative error messages
  • Exits with proper error codes (exit 1) on failures

3. Security Improvements

  • Uses array form for system() calls to prevent shell injection
  • Validates script existence before loading
  • Good security comment in shakapacker.yml about trusted script paths

4. Cross-Platform Compatibility

  • Package manager detection works for both yarn and npm
  • Portable shell command detection using which

5. Good Documentation

  • Clear comments explaining the purpose and usage
  • Links to Shakapacker documentation
  • Helpful emoji indicators for user feedback

🐛 Issues Found

1. Critical: Regex Pattern Missing Comment Exclusion

Location: lib/tasks/precompile/shakapacker_precompile_hook_shared.rb:88-89

The regex patterns for detecting config need to exclude comments:

# Current (INCORRECT - matches commented lines):
return unless initializer_content.match?(/^\s*config\.auto_load_bundle\s*=/) ||
              initializer_content.match?(/^\s*config\.components_subdirectory\s*=/)

# Should be (matches only uncommented lines):
return unless initializer_content.match?(/^\s*(?\!#).*config\.auto_load_bundle\s*=/) ||
              initializer_content.match?(/^\s*(?\!#).*config\.components_subdirectory\s*=/)

This is inconsistent with the generator template version (lines 37-38), which correctly excludes comments. This could cause the hook to run even when auto-pack generation is commented out.

2. Platform Compatibility Issue: which Command

Location: lib/tasks/precompile/shakapacker_precompile_hook_shared.rb:61,63

Using which with shell redirection in system() is problematic:

# Current (shell-dependent):
if system("which yarn > /dev/null 2>&1")

# Better (cross-platform):
if system("yarn", "--version", out: File::NULL, err: File::NULL)

Why this matters:

  • The current approach passes the entire string to the shell, which is less portable
  • Windows doesn't have which by default
  • The improved version uses Ruby's built-in null device and array form

3. Potential Race Condition with load vs require

Location: spec/dummy/bin/shakapacker-precompile-hook:19 and Pro version

Using load instead of require means:

  • The shared script is executed every time, even if already loaded
  • Functions are redefined on each load
  • For this use case, this is probably intentional and fine
  • However, consider documenting why load is used instead of require

This is likely correct behavior for a hook script (you want fresh execution), but worth documenting.

4. Missing JSON Parse Error Handling

Location: lib/tasks/precompile/shakapacker_precompile_hook_shared.rb:52

package_json = JSON.parse(File.read(package_json_path))

Should handle malformed JSON:

begin
  package_json = JSON.parse(File.read(package_json_path))
rescue JSON::ParserError => e
  warn "⚠️  Warning: Invalid package.json format: #{e.message}"
  return
end

5. Inconsistent Error Handling Between Functions

Location: lib/tasks/precompile/shakapacker_precompile_hook_shared.rb

  • build_rescript_if_needed catches StandardError and exits with error code (lines 72-74)
  • generate_packs_if_needed catches both Errno::ENOENT (warns only) and StandardError (exits)

This asymmetry could be confusing. Consider:

  • Why does Errno::ENOENT only warn in pack generation but not in ReScript build?
  • Should both functions have consistent error handling?

🔍 Performance Considerations

1. File I/O Optimization

Location: lib/tasks/precompile/shakapacker_precompile_hook_shared.rb

Multiple file reads could be optimized:

  • find_rails_root walks the directory tree twice (once for ReScript, once for packs)
  • Consider caching the Rails root in a module variable if performance becomes an issue

Current impact: Negligible for typical use, but worth noting.


🧪 Test Coverage Concerns

Missing Test Coverage

The PR description shows test plan checkboxes, but several are unchecked:

  • Verify hook runs during bin/shakapacker compilation
  • Confirm ReScript builds when config files present
  • Check pack generation when auto_load_bundle configured
  • Test production asset precompilation workflow

Recommendation: Add integration tests or document manual testing results before merging.

Suggested Test Scenarios

  1. ReScript detection:

    • With bsconfig.json only
    • With rescript.json only
    • With both files
    • With config but no build:rescript script
    • With invalid package.json
  2. Pack generation:

    • With commented config (should NOT run)
    • With uncommented config (should run)
    • Without Rails initializer
  3. Error cases:

    • Missing Rails root
    • Missing package manager
    • Failed ReScript build
    • Failed pack generation

🛡️ Security Considerations

✅ Good Security Practices

  1. Uses array form for system() calls - prevents shell injection
  2. Validates file existence before loading
  3. Clear security comment in config file
  4. Proper path resolution without user input

⚠️ Minor Concern: Command Execution

The hook executes arbitrary rake tasks and npm scripts. This is acceptable because:

  • Scripts are defined in the project's own files
  • No user input is interpolated
  • The security comment in shakapacker.yml warns about this

📝 Code Style & Conventions

✅ Follows React on Rails Conventions

  • Proper RuboCop disables with justification
  • Frozen string literal comments
  • Good emoji usage for user feedback
  • Proper newlines at end of files

Minor Style Notes

  1. Inconsistent loop style:

    • Generator template uses while loop (lines 18-23)
    • Shared implementation uses while loop (lines 21-27)
    • Both are fine, good consistency
  2. Comment quality: Excellent throughout


🎯 Recommendations

High Priority (Fix Before Merge)

  1. ✅ Fix regex to exclude commented config lines (consistency bug)
  2. ✅ Replace which with cross-platform package manager detection
  3. ✅ Add JSON parse error handling

Medium Priority

  1. Document why load is used instead of require
  2. Make error handling consistent between functions
  3. Complete manual testing checklist

Low Priority

  1. Add integration tests for the hook
  2. Consider caching Rails root if performance becomes an issue

📋 Checklist Verification

According to CLAUDE.md requirements:

  • ✅ RuboCop passes (mentioned in PR description)
  • ✅ Pre-commit hooks pass (mentioned in PR description)
  • ✅ Files end with newlines (verified in diff)
  • ❓ Need to verify: All tests passing
  • ❓ Need to verify: Changelog entry (if user-visible feature)

🎓 Overall Assessment

Quality: Good ⭐⭐⭐⭐ (4/5)

Verdict: Approve with minor fixes required

This is a well-structured refactoring that improves code reuse and adds valuable ReScript support. The main issues are:

  1. A regex inconsistency that could cause incorrect behavior
  2. Minor cross-platform compatibility improvements needed
  3. Test coverage needs verification

The code demonstrates good practices overall - security-conscious, well-documented, and following the project's conventions.


💬 Questions for Author

  1. Was the regex difference between the generator template and shared implementation intentional?
  2. Have you tested this with an actual ReScript project?
  3. Should we add integration tests for the precompile hook?
  4. Is there a specific reason for catching Errno::ENOENT separately in generate_packs_if_needed?

Generated by Claude Code Review

…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>
@justin808
Copy link
Member Author

/run-skipped-ci

@github-actions
Copy link
Contributor

🚀 Full CI Mode Enabled

All checks are already running - nothing to do!

All CI checks are already running on this PR. Added the full-ci label - future commits will run the full CI suite.

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