Skip to content

Conversation

@justin808
Copy link
Member

@justin808 justin808 commented Nov 22, 2025

Summary

Adds automatic precompile hook coordination to bin/dev, eliminating the need for manual coordination, sleep hacks, and duplicate task calls in Procfile.dev.

This PR is focused solely on the bin/dev coordination feature. The complementary idempotent locale generation feature is in PR #2093.

How It Works

When you configure a precompile_hook in config/shakapacker.yml:

default: &default
  precompile_hook: 'bundle exec rake react_on_rails:locale'

bin/dev will now:

  1. ✅ Run the hook once before starting development processes
  2. ✅ Set SHAKAPACKER_SKIP_PRECOMPILE_HOOK=true environment variable
  3. ✅ Pass the env var to all spawned processes (Rails, webpack, etc.)
  4. ✅ Prevent webpack processes from re-running the hook independently

Before & After

Before (manual coordination with sleep hacks):

# Procfile.dev
wp-server: sleep 15 && bundle exec rake react_on_rails:locale && bin/shakapacker --watch

After (automatic coordination via bin/dev):

# Procfile.dev
wp-server: bin/shakapacker --watch
# config/shakapacker.yml
default: &default
  precompile_hook: 'bundle exec rake react_on_rails:locale'

Clean, simple, reliable - no sleep hacks required!

Key Features

Shakapacker Version Detection

  • Checks if you're using Shakapacker < 9.4.0
  • Displays friendly warning about SHAKAPACKER_SKIP_PRECOMPILE_HOOK support
  • Recommends upgrading to 9.4.0+ to avoid duplicate hook execution

Error Handling

  • Exits immediately if precompile hook fails
  • Shows clear error message with the failed command
  • Suggests fixing or removing the hook from config

Smart Skipping

  • Skips hook execution for bin/dev kill and bin/dev help commands
  • Only runs hook for actual development modes (hmr, static, prod)

Help Flag Handling

  • Detects -h/--help flags early
  • Prevents hook execution when user just wants help
  • Maintains clean separation between help and runtime logic

Use Cases

Locale generation (with PR #2093):

precompile_hook: 'bundle exec rake react_on_rails:locale'

ReScript compilation:

precompile_hook: 'yarn rescript'

Multiple tasks:

precompile_hook: 'bundle exec rake react_on_rails:locale && yarn rescript'

Any expensive build task that needs to run before webpack starts!

Implementation Details

Modified Files:

  • lib/react_on_rails/dev/server_manager.rb - Core coordination logic
  • spec/react_on_rails/dev/server_manager_spec.rb - Comprehensive test coverage (161 lines)
  • docs/building-features/process-managers.md - Feature documentation
  • docs/building-features/i18n.md - Usage example with locale generation

Test Coverage:

  • ✅ Hook execution for all modes (development, static, prod)
  • ✅ Environment variable setting across all modes
  • ✅ Skipping for kill/help commands
  • ✅ Help flag handling (-h/--help)
  • ✅ Shakapacker version warning (< 9.4.0)
  • ✅ Error handling when hook fails
  • ✅ No-hook configuration scenario

Shakapacker Version Requirements

  • 9.3.0+: precompile_hook configuration support
  • 9.4.0+: SHAKAPACKER_SKIP_PRECOMPILE_HOOK env var support (prevents duplicate execution)

If you're on Shakapacker < 9.4.0, bin/dev will warn you that the hook may run multiple times.

Related PRs

Breaking Changes

None - this is purely additive functionality.

Testing

Run the test suite:

bundle exec rspec spec/react_on_rails/dev/server_manager_spec.rb

Manual testing:

# Configure a precompile_hook in config/shakapacker.yml
echo 'default: &default\n  precompile_hook: "echo Running hook..."' >> config/shakapacker.yml

# Run bin/dev and verify:
bin/dev
# Should see: "🔧 Running Shakapacker precompile hook..."
# Should see: "Running hook..."
# Should see: "✅ Precompile hook completed successfully"

🤖 Generated with Claude Code

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 22, 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 16 minutes and 28 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 b7ee06b and 797f3a5.

📒 Files selected for processing (5)
  • CHANGELOG.md (1 hunks)
  • docs/building-features/i18n.md (1 hunks)
  • docs/building-features/process-managers.md (1 hunks)
  • lib/react_on_rails/dev/server_manager.rb (5 hunks)
  • spec/react_on_rails/dev/server_manager_spec.rb (1 hunks)

Walkthrough

This PR implements automatic precompile hook coordination in React on Rails' development server manager and adds idempotent locale generation. The changes enable bin/dev to run Shakapacker's precompile hook once before spawning processes and set an environment variable to prevent duplicate execution, while making locale generation skip regeneration if files are already up-to-date.

Changes

Cohort / File(s) Summary
Changelog and Documentation
CHANGELOG.md, docs/building-features/i18n.md, docs/building-features/process-managers.md
Updates changelog with new features; rewrites i18n locale-generation guidance to recommend Shakapacker precompile_hook pattern with idempotent skip logic; documents precompile hook integration in bin/dev flow with environment variable propagation and version requirements
Precompile Hook Runtime Support
lib/react_on_rails/dev/server_manager.rb
Adds precompile hook invocation logic: run_precompile_hook_if_present method, version-checking via warn_if_shakapacker_version_too_old, SHAKAPACKER_SKIP_PRECOMPILE_HOOK environment variable management, early help-flag detection, and hook bypass for kill/help commands
Idempotent Locale Generation
lib/react_on_rails/locales/base.rb, lib/tasks/locale.rake
Adds optional force parameter to locale compilation workflow; implements timestamp-based skip logic to skip generation if locale files are current; updates rake task to parse force environment variable
Type Signatures
sig/react_on_rails/locales.rbs
Establishes Ruby type signatures for ReactOnRails::Locales module, including compile method, Base/ToJs/ToJson classes, and internal helper methods
Test Coverage
spec/react_on_rails/dev/server_manager_spec.rb, spec/react_on_rails/locales_spec.rb, spec/react_on_rails/locales_to_js_spec.rb
Adds comprehensive test suite for precompile hook execution scenarios, environment variable handling, version warnings, and force flag propagation; tests idempotent behavior with force override

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant bin_dev as bin/dev
    participant ServerManager as ServerManager<br/>(precompile_hook)
    participant Hook as Precompile Hook<br/>(rake locale)
    participant Spawner as Process Spawner<br/>(Overmind/Foreman)
    participant Webpack as Webpack<br/>(skips hook)
    
    User->>bin_dev: Start development server
    bin_dev->>ServerManager: run_from_command_line(ARGV)
    
    alt Precompile hook configured
        ServerManager->>Hook: Execute precompile_hook once
        activate Hook
        Hook->>Hook: Check if locales outdated
        alt Locales up-to-date
            Hook->>Hook: Skip (idempotent)
        else Locales outdated or force=true
            Hook->>Hook: Generate locale files
        end
        deactivate Hook
        Hook-->>ServerManager: ✓ Complete
    end
    
    ServerManager->>ServerManager: Set SHAKAPACKER_SKIP_PRECOMPILE_HOOK=true
    ServerManager->>Spawner: Spawn processes with env var set
    activate Spawner
    Spawner->>Webpack: Start webpack --watch
    activate Webpack
    Webpack->>Webpack: Detect SHAKAPACKER_SKIP_PRECOMPILE_HOOK
    Webpack->>Webpack: Skip running precompile_hook
    deactivate Webpack
    deactivate Spawner
    
    Spawner-->>User: ✓ Development environment ready
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Server manager logic: Review run_precompile_hook_if_present and warn_if_shakapacker_version_too_old for correct environment variable handling and version detection
  • Idempotent locale generation: Verify timestamp checking logic in files_are_outdated correctly handles the "files absent" case and force flag behavior
  • Force parameter propagation: Ensure the force parameter flows correctly from rake task through compile methods to locale writers (ToJs/ToJson)
  • Test coverage: Review server_manager_spec comprehensively for precompile hook scenarios, version checks, and environment variable assertions

Possibly related issues

Possibly related PRs

Suggested labels

enhancement, documentation

Suggested reviewers

  • Judahmeek
  • alexeyr-ci
  • AbanoubGhadban

Poem

🐰 Hooks now hop in perfect time,
No more duplicate locale chimes,
Webpack skips with ENV so bright,
Idempotent—fast and right!
bin/dev coordination—
Dev server's cause for celebration!

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 44.44% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ⚠️ Warning The PR title focuses on precompile hook coordination in bin/dev, but the primary objectives address making locale generation idempotent with force flag support and preventing duplicate execution during assets:precompile. Update the title to reflect the main objective: 'Make locale generation idempotent with force flag support' or similar, as this is the core change across the codebase.
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR successfully implements all primary coding requirements from issue #2090: idempotent locale generation with timestamp checking, force flag support, precompile hook coordination with SHAKAPACKER_SKIP_PRECOMPILE_HOOK, updated rake task, test coverage, and documentation updates.
Out of Scope Changes check ✅ Passed All changes are directly aligned with issue #2090 objectives: locale generation idempotency, force flag, precompile hook coordination, bin/dev updates, documentation updates, and comprehensive test coverage. No unrelated changes detected.
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 22, 2025

Code Review

This PR successfully implements idempotent locale generation with a force flag. Overall the implementation is solid and well-tested. Here's my detailed feedback:


✅ Strengths

1. Clean Implementation

  • The force parameter is threaded cleanly through the call chain: rake task → Locales.compileBase.new
  • The logic properly inverts the condition: \!force && \!obsolete? correctly skips when files are up-to-date unless forced
  • User-facing messages are clear and helpful

2. Excellent Test Coverage

  • Tests verify the force parameter propagates to both ToJson and ToJs
  • The timestamp-based test in locales_to_js_spec.rb:50-70 properly validates the force flag bypasses up-to-date checks
  • Good use of sleep 0.01 to ensure timestamp differences in tests

3. Good Documentation

  • The i18n.md update provides a clear recommended pattern using Shakapacker's precompile_hook
  • Changelog entry is well-formatted and informative

🔍 Potential Issues & Suggestions

1. Missing RBS Type Signatures (Minor)

Per CLAUDE.md, this project uses RBS for type checking. The new force: parameter should have type signatures added:

# sig/react_on_rails/locales.rbs
module ReactOnRails
  module Locales
    def self.compile: (?force: bool) -> (ToJs | ToJson)
    
    class Base
      def initialize: (?force: bool) -> void
    end
  end
end

Action: Check if sig/react_on_rails/locales.rbs exists and update it, or create if needed. Run bundle exec rake rbs:validate after adding.

2. Race Condition in File Timestamp Checking (Low Severity)

The files_are_outdated method in base.rb:61-65 compares file timestamps:

def files_are_outdated
  latest_yml = locale_files.map { |file| File.mtime(file) }.max
  earliest = exist_files.map { |file| File.mtime(file) }.min
  latest_yml > earliest
end

Issue: If two processes run simultaneously, both might see obsolete? == true and regenerate files concurrently, potentially corrupting the output.

Suggested improvement: Consider using a lockfile pattern for production environments:

def with_generation_lock
  lockfile = "\#{i18n_dir}/.generation.lock"
  File.open(lockfile, File::CREAT | File::WRONLY) do |f|
    f.flock(File::LOCK_EX)
    yield
  end
ensure
  File.delete(lockfile) if File.exist?(lockfile)
end

Note: This might be overkill for typical usage. If Shakapacker's precompile_hook runs serially, it's fine. But worth considering for CI parallelism or dev servers with multiple workers.

3. Test Reliability: Time-Based Testing (Low Severity)

The test at locales_to_js_spec.rb:50-70 uses:

  • sleep 0.01 to ensure timestamp differences
  • Time.current + 1.minute to set future timestamps

Potential issue: On very slow/busy CI systems, sleep 0.01 might not be enough to guarantee different mtimes (filesystem timestamp resolution varies).

Suggested improvement:

# More robust approach
sleep 0.1  # Increase to 100ms for filesystem consistency
# Or use explicit mtime setting without relying on current time

4. Return Value Consistency (Minor)

The Base#initialize method has inconsistent return values:

  • Returns nil when i18n_dir.nil?
  • Returns nil when files are up-to-date (after the change)
  • Implicitly returns the result of puts when files are generated

Suggestion: This is fine for a constructor, but consider documenting the behavior or returning self consistently if the return value is ever used.

5. ENV Variable Parsing (Very Minor)

In locale.rake:

force = ENV["force"] == "true"

This only accepts force=true. Consider also accepting force=1, force=yes, etc., for flexibility:

force = %w[true 1 yes].include?(ENV["force"]&.downcase)

🔒 Security Considerations

No security concerns identified. The changes:

  • Don't introduce any new file I/O vulnerabilities
  • Don't parse untrusted input (ENV variable is simple boolean)
  • Don't change the YAML parsing logic (which already has safe load options)

⚡ Performance Considerations

Performance impact is positive:

  • Skipping regeneration when files are up-to-date saves significant time in development
  • The overhead of timestamp checking is negligible (just stat calls)
  • No N+1 queries or expensive operations added

📋 Recommendations

High Priority

  • Add RBS type signatures for the new force parameter (per CLAUDE.md requirements)
  • Run bundle exec rake rbs:validate to ensure no type errors

Medium Priority

  • Consider adding file locking if concurrent regeneration is a concern
  • Increase sleep duration in test to 0.1s for better reliability

Low Priority

  • Support more flexible ENV variable values (1, yes, etc.)
  • Document return value behavior of Base#initialize

🎯 Final Verdict

APPROVED ✅ - This is a well-implemented feature that solves a real problem (duplicate work in precompile hooks). The code is clean, well-tested, and the documentation is excellent.

The suggestions above are minor improvements, not blockers. The implementation is production-ready as-is.

Great work! 🚀

@claude
Copy link

claude bot commented Nov 22, 2025

Code Review - PR #2092

Overview

This PR successfully implements idempotent locale generation with a force flag, addressing issue #2090. The implementation is clean, well-tested, and follows the project's conventions. Overall, this is a high-quality contribution that improves developer experience.


✅ Strengths

1. Clean Architecture

  • The force parameter is properly threaded through the call chain: rake task → compile()ToJs/ToJson.new()
  • Early return pattern in Base#initialize keeps the logic simple and readable
  • Minimal changes to existing code, reducing risk of regression

2. Excellent Test Coverage

  • Unit tests verify force parameter propagation (locales_spec.rb:39-54)
  • Integration test validates the idempotent behavior with force override (locales_to_js_spec.rb:54-70)
  • Tests use proper timestamp manipulation to verify behavior
  • The new test at line 54-70 is particularly well-designed: it ensures files are up-to-date, forces regeneration, and validates the mtime changed

3. User Experience

  • Clear console messages inform users when skipping/generating
  • Helpful documentation in the rake task description
  • Example usage in both README and CHANGELOG

4. Documentation

  • Excellent addition of Shakapacker precompile_hook recommendation in i18n.md
  • Clear explanation of when to use force flag
  • CHANGELOG entry is properly formatted per project guidelines

🔍 Minor Observations & Suggestions

1. Edge Case: Empty locale_files Array (Low Priority)

In base.rb:84-92, if i18n_yml_dir is nil and Rails.application.config.i18n.load_path returns an empty array or nil, locale_files could be empty.

Current behavior:

def files_are_outdated
  latest_yml = locale_files.map { |file| File.mtime(file) }.max
  earliest = exist_files.map { |file| File.mtime(file) }.min
  latest_yml > earliest
end

If locale_files is empty, latest_yml would be nil, causing a NoMethodError when comparing nil > earliest.

Likelihood: Very low - the check_config_directory_exists validation should catch most misconfigurations.

Suggestion: Consider adding a guard:

def files_are_outdated
  return true if locale_files.empty?  # Force regeneration if no source files found
  
  latest_yml = locale_files.map { |file| File.mtime(file) }.max
  earliest = exist_files.map { |file| File.mtime(file) }.min
  latest_yml > earliest
end

This is optional and could be addressed in a follow-up if deemed necessary.

2. Test Improvement: sleep 0.01 (Nitpick)

spec/react_on_rails/locales_to_js_spec.rb:59:

sleep 0.01 # Ensure timestamp difference

The comment suggests this ensures timestamp difference, but the next line immediately sets future_time = Time.current + 1.minute which already guarantees a difference. The sleep appears unnecessary.

Impact: None - the test works correctly. This is purely for code clarity.

3. Console Output Uses puts (Style Question)

base.rb:43, 49 use puts for user feedback. This is fine for a Rake task, but worth noting:

  • puts output doesn't go through Rails logger
  • Won't be captured by test frameworks or structured logging
  • Direct stdout is appropriate for CLI tools, so this is likely intentional

Verdict: Current approach is correct for a Rake task. No change needed.

4. CHANGELOG PR Number Mismatch

The CHANGELOG entry references:

[PR 2091](https://github.com/shakacode/react_on_rails/pull/2091)

But this is PR #2092. Should be:

[PR 2092](https://github.com/shakacode/react_on_rails/pull/2092)

This needs to be fixed before merging.


🔒 Security Considerations

No security concerns identified:

  • No user input is processed beyond the boolean force flag
  • File operations are limited to configured directories
  • Proper error handling for YAML parsing (existing code)

⚡ Performance Considerations

Excellent performance improvements:

  • Idempotent behavior prevents unnecessary regeneration
  • Timestamp checking is O(n) where n = number of locale files (acceptable)
  • Safe for use in hot-reloading development environments

Recommendation: This is perfect for the Shakapacker precompile_hook use case.


🧪 Testing Recommendations

The test coverage is strong, but consider these additional manual tests before merging:

  1. Test with Shakapacker integration:

    # Add to spec/dummy/config/shakapacker.yml
    precompile_hook: 'bundle exec rake react_on_rails:locale'
    
    # Then run webpack compilation multiple times
    cd spec/dummy && yarn build
    # Should see "Locale files are up to date" message
  2. Test force flag:

    cd spec/dummy
    bundle exec rake react_on_rails:locale force=true
    # Should regenerate regardless of timestamps
  3. Verify CI passes - especially the linting checks mentioned in CLAUDE.md


📋 Action Items

Required Before Merge:

  • Fix CHANGELOG PR number: 2091 → 2092

Optional Improvements (can be follow-up PRs):

  • Consider adding guard for empty locale_files array (edge case protection)
  • Remove unnecessary sleep 0.01 from test for clarity

Final Verdict

LGTM with one required fix (CHANGELOG PR number).

This is a well-implemented feature that:

  • ✅ Solves a real developer pain point
  • ✅ Has excellent test coverage
  • ✅ Includes clear documentation
  • ✅ Follows project conventions
  • ✅ Maintains backward compatibility
  • ✅ Improves performance through smart caching

Great work! The idempotent behavior combined with the Shakapacker integration guidance will significantly improve the developer experience.


References:

@justin808
Copy link
Member Author

/run-skipped-ci

@github-actions
Copy link
Contributor

🚀 Full CI Mode Enabled

Triggered all workflows for full CI coverage

Note: Added the full-ci label to this PR. All future commits will run the full CI suite (including minimum dependency tests) until the label is removed.

To disable full CI mode, use the /stop-run-skipped-ci command.

View progress in the Actions tab.

@claude
Copy link

claude bot commented Nov 22, 2025

Code Review

Thanks for this PR! This is a solid implementation that adds idempotent behavior to locale generation. Here's my detailed feedback:


✅ Strengths

1. Clean Architecture

  • The force parameter is properly threaded through the call chain (rake task → compile() → ToJs/ToJson → Base#initialize)
  • The implementation correctly uses optional parameters with sensible defaults
  • Good separation of concerns between timestamp checking and forced regeneration

2. User Experience

  • Helpful console messages inform users when generation is skipped vs. when it occurs
  • Clear documentation in rake task description about the force option
  • Flexible ENV parsing accepts true, 1, or yes for better UX

3. Test Coverage

  • Tests verify force parameter propagation through the entire chain
  • Tests confirm idempotent behavior (skipping when up-to-date)
  • Tests verify force flag bypasses the timestamp check
  • Good use of RSpec shared examples for DRY testing

4. Documentation

  • Excellent addition to i18n.md recommending the Shakapacker precompile_hook pattern
  • Clear usage examples in both docs and rake task description
  • CHANGELOG entry properly formatted per project guidelines

5. Type Safety

  • New RBS signatures provide static type checking for the Locales module
  • Comprehensive method signatures cover all public and private methods

🔍 Issues & Suggestions

1. Critical: Empty Array Safety ⚠️

File: lib/react_on_rails/locales/base.rb:62

The check for empty locale_files is good, but there's a potential edge case:

def files_are_outdated
  return true if locale_files.empty?

  latest_yml = locale_files.map { |file| File.mtime(file) }.max
  earliest = exist_files.map { |file| File.mtime(file) }.min
  latest_yml > earliest
end

Issue: If exist_files is empty (no generated files exist yet), .min returns nil, causing nil > Time comparison error.

Recommendation:

def files_are_outdated
  return true if locale_files.empty?
  return true if exist_files.empty?  # No generated files yet

  latest_yml = locale_files.map { |file| File.mtime(file) }.max
  earliest = exist_files.map { |file| File.mtime(file) }.min
  latest_yml > earliest
end

However, looking at the code flow, obsolete? already checks exist_files.empty?, so this might be redundant. Consider whether this method should even be called when exist_files.empty?.

2. Minor: Inconsistent Return Values

File: lib/react_on_rails/locales/base.rb:39-45

def initialize(force: false)
  return if i18n_dir.nil?

  if \!force && \!obsolete?
    puts "Locale files are up to date, skipping generation. Use force=true to regenerate."
    return
  end

  @translations, @defaults = generate_translations
  convert
  puts "Generated locale files in #{i18n_dir}"
end

Issue: The method returns nil in early exit cases but doesn't have a consistent return value when generation succeeds. This is fine for Ruby, but the RBS signature shows -> void, which is correct.

Suggestion: Consider returning self for potential method chaining, though not critical for this use case.

3. RBS Type Accuracy

File: sig/react_on_rails/locales.rbs

Some observations:

def locale_files: () -> Array[String]

This should account for the memoization returning either an Array or the result from Rails.application.config.i18n.load_path:

def locale_files: () -> Array[String]

Actually, looking at the implementation, load_path could return various types. Consider:

def locale_files: () -> (Array[String] | nil)

But the current implementation wraps it with truthy_presence(...).presence, which would return nil if falsy. The type might need refinement based on actual behavior.

4. Test Improvement: Time-based Race Condition

File: spec/react_on_rails/locales_to_js_spec.rb:50-68

The test is good, but there's a minor improvement possible:

it "updates files when force is true" do
  initial_mtime = File.mtime(translations_path)

  future_time = Time.current + 1.minute
  FileUtils.touch(translations_path, mtime: future_time)
  FileUtils.touch(default_path, mtime: future_time)

  described_class.new(force: true)

  expect(File.mtime(translations_path)).not_to eq(future_time)
  expect(File.mtime(translations_path)).to be > initial_mtime
end

Suggestion: The second expectation (be > initial_mtime) could fail if the test runs extremely fast. Consider using be_within(1.second).of(Time.current) or just verifying it's different from future_time:

expect(File.mtime(translations_path)).not_to eq(future_time)
expect(File.mtime(default_path)).not_to eq(future_time)

5. Documentation: Edge Case Clarification

File: docs/building-features/i18n.md

The documentation is excellent, but could mention what happens if locale files don't exist yet:

The idempotent locale generation task can be safely called multiple times and will skip generation if files are already up-to-date. **On first run or when locale YAML files change, generation will occur automatically.**

🛡️ Security Considerations

No security concerns identified

  • The force parameter is read from ENV and properly validated
  • No SQL injection, XSS, or command injection vectors
  • File operations use safe paths from configuration

⚡ Performance Considerations

Performance improvements achieved

  • Idempotent behavior prevents unnecessary file regeneration
  • Timestamp checking is O(n) where n = number of locale files (acceptable)
  • No blocking operations or expensive computations

Minor optimization opportunity: The files_are_outdated method could short-circuit earlier if it finds any outdated file instead of computing all mtimes, but the current implementation is clear and performance is likely negligible for typical use cases.


📋 Checklist Items

Before merging, please verify:

  • Run bundle exec rubocop (per CLAUDE.md requirements) ✅ (assuming this passes)
  • Run bundle exec rake rbs:validate to validate new RBS signatures
  • Run bundle exec rake rbs:steep to type-check (may show existing errors per CLAUDE.md)
  • Consider adding a test for the empty exist_files edge case mentioned above
  • All CI checks pass (currently queued)

🎯 Overall Assessment

This is a well-implemented feature that solves a real problem. The code quality is high, tests are comprehensive, and documentation is excellent. The few suggestions above are minor improvements rather than blocking issues.

Recommendation: APPROVE with minor suggestions for consideration

Great work! 🚀

@claude
Copy link

claude bot commented Nov 22, 2025

Code Review - PR #2092

✅ Overall Assessment

This PR implements a well-designed solution for making the locale generation task idempotent. The implementation is clean, well-tested, and follows the project's conventions. Great work!


🎯 Strengths

  1. Clean API Design: The force parameter is intuitive and follows common CLI conventions (accepting 'true', '1', 'yes')

  2. Comprehensive Testing: Excellent test coverage including:

    • Force parameter propagation through the call chain
    • Idempotent behavior verification
    • Force flag override testing
  3. Good User Experience: Clear console messages inform users when files are skipped or generated

  4. Type Safety: RBS signatures properly updated to reflect the new optional parameter

  5. Documentation: The i18n docs now include the recommended Shakapacker precompile_hook pattern, which is exactly what users need


🔍 Code Quality Observations

lib/tasks/locale.rake

  • ✅ Clean implementation parsing the force flag from ENV
  • ✅ Good task description with usage examples

lib/react_on_rails/locales/base.rb

  • ✅ Proper early return pattern when i18n_dir is nil
  • ✅ Clear separation between idempotent check and force override
  • ✅ Helpful console output for both skip and success cases

Test Coverage

  • ✅ Tests verify parameter passing at all layers (compile → ToJs/ToJson → Base)
  • ✅ Shared examples ensure consistency across both JS and JSON output formats
  • ✅ Edge case testing with timestamp manipulation

💡 Minor Suggestions (Optional)

  1. Console Output Consistency: Consider using Rails logger instead of puts for better integration with Rails logging:

    # Instead of:
    puts "Locale files are up to date, skipping generation. Use force=true to regenerate."
    
    # Consider:
    Rails.logger.info "Locale files are up to date, skipping generation. Use force=true to regenerate."

    This would respect Rails log levels and integrate better with production logging.

  2. Test Robustness: In spec/react_on_rails/locales_to_js_spec.rb:54-69, the test uses Time.current + 1.minute which could theoretically cause flakiness if the test runs slowly. Consider using a more explicit time value:

    # Instead of relative time
    future_time = Time.current + 1.minute
    
    # Consider fixed future time
    future_time = Time.new(2030, 1, 1)
  3. Documentation Enhancement: In the docs, you might want to mention that the idempotent check is based on file modification times:

    The task compares the modification times of generated locale files against 
    the source YAML files. If all generated files are newer than all source files,
    generation is skipped.

🔒 Security Considerations

✅ No security concerns identified. The force flag is read from ENV and properly sanitized with a whitelist approach.


⚡ Performance Considerations

Excellent: The idempotent behavior prevents unnecessary file I/O and YAML parsing when files are up-to-date. This will significantly improve performance in development workflows and CI pipelines.

Timestamp checking overhead: The obsolete? check adds minimal overhead:

  • File existence checks: O(n) where n = number of generated files (typically 2)
  • mtime comparisons: Very fast system calls
  • Early return prevents expensive YAML parsing and file writing

📝 CHANGELOG Entry

✅ The CHANGELOG entry is well-written and follows the project conventions:

  • Includes PR number and author
  • Clearly explains the feature and its benefits
  • Placed in the "Improved" section (appropriate for enhancements)

✨ RBS Type Signatures

The RBS signatures in sig/react_on_rails/locales.rbs look correct:

  • compile method signature: (?force: bool) -> (ToJs | ToJson)
  • Base#initialize signature: (?force: bool) -> void

🧪 Testing Recommendations

Before merging, please verify:

  1. ✅ Run bundle exec rubocop (should pass based on project requirements)
  2. ✅ Run bundle exec rake rbs:validate to verify type signatures
  3. ✅ Test the actual workflow manually:
    # First run - should generate files
    bundle exec rake react_on_rails:locale
    
    # Second run - should skip
    bundle exec rake react_on_rails:locale
    
    # Force regeneration - should generate
    bundle exec rake react_on_rails:locale force=true

📚 Documentation Quality

The documentation changes in docs/building-features/i18n.md are excellent:

  • ✅ Clear recommendation for Shakapacker integration
  • ✅ Example configuration snippet
  • ✅ Explanation of when to use force flag
  • ✅ Maintains backward compatibility guidance

🎉 Conclusion

This is a solid, well-implemented PR that solves a real problem (issue #2090). The code is clean, well-tested, and properly documented. The idempotent behavior will make life easier for users who want to integrate locale generation into their build pipelines.

Recommendation: ✅ Approve and merge (pending CI passing)

Great work, @justin808! This is exactly the kind of improvement that makes React on Rails more pleasant to use.


Review generated with Claude Code - focusing on code quality, testing, security, and performance considerations.

@claude
Copy link

claude bot commented Nov 22, 2025

Code Review - PR #2092

Summary

This PR successfully makes the react_on_rails:locale rake task idempotent with force flag support. The implementation is clean and well-tested. Overall, this is high-quality work with just a few minor suggestions.


✅ Strengths

  1. Well-designed implementation

    • Clean separation of concerns: force parameter flows from rake task → compile → Base class
    • Backward compatible: no breaking changes, defaults to existing behavior
    • User-friendly messaging when skipping generation
  2. Excellent test coverage

    • Tests verify force parameter propagation through the call chain
    • Tests verify idempotent behavior (skips when up-to-date)
    • Tests verify force flag bypasses up-to-date check
    • Edge case handled: empty locale_files array
  3. Great documentation

    • Clear usage examples in rake task description
    • Recommended Shakapacker integration pattern documented
    • CHANGELOG entry follows project conventions
  4. Type safety

    • RBS signatures added for new force parameter
    • Comprehensive type definitions for all methods in Locales module

🔍 Minor Issues & Suggestions

1. Potential nil error in files_are_outdated (Low priority)

def files_are_outdated
  return true if locale_files.empty?

  latest_yml = locale_files.map { |file| File.mtime(file) }.max
  earliest = exist_files.map { |file| File.mtime(file) }.min
  latest_yml > earliest
end

Issue: If exist_files is empty, earliest will be nil, causing latest_yml > earliest to raise an error.

Suggestion: Add a guard:

def files_are_outdated
  return true if locale_files.empty?
  return true if exist_files.empty?  # Add this line

  latest_yml = locale_files.map { |file| File.mtime(file) }.max
  earliest = exist_files.map { |file| File.mtime(file) }.min
  latest_yml > earliest
end

Note: This should already be caught by obsolete? returning true when exist_files.empty?, but defensive programming is good practice.

2. ENV variable parsing could be more Ruby-idiomatic

force = %w[true 1 yes].include?(ENV["force"]&.downcase)

This is fine, but you could consider extracting to a helper if this pattern is used elsewhere:

def self.env_truthy?(key)
  %w[true 1 yes].include?(ENV[key]&.downcase)
end

Low priority - current implementation is clear and works well.

3. Test improvement opportunity

In spec/react_on_rails/locales_to_js_spec.rb, the force test uses:

future_time = Time.current + 1.minute
FileUtils.touch(translations_path, mtime: future_time)

Suggestion: Consider using Time.now instead of Time.current for consistency with File.mtime, or document why Time.current is preferred (ActiveSupport time zone awareness).

4. RBS signatures completeness

The RBS signatures look good, but verify they match the actual return types:

def self.compile: (?force: bool) -> (ToJs | ToJson)

This looks correct - compile returns an instance of either ToJs or ToJson.

Suggestion: Run bundle exec rake rbs:validate to ensure signatures are correct (per CLAUDE.md requirements).


🔒 Security

✅ No security concerns identified:

  • No user input directly used in file paths
  • ENV variable parsing is safe
  • Force flag only affects timestamp checking logic

⚡ Performance

✅ No performance concerns:

  • Idempotent behavior improves performance by skipping unnecessary work
  • File timestamp checking is O(n) on number of locale files (acceptable)
  • No changes to file parsing logic

🧪 Testing Recommendations

  1. Manual testing checklist (per CLAUDE.md requirements):

    # Test normal use (should skip if up-to-date)
    bundle exec rake react_on_rails:locale
    bundle exec rake react_on_rails:locale  # Should skip
    
    # Test force flag
    bundle exec rake react_on_rails:locale force=true
    
    # Test with Shakapacker precompile_hook
    # Add to config/shakapacker.yml:
    # default:
    #   precompile_hook: 'bundle exec rake react_on_rails:locale'
    bundle exec rake assets:precompile
  2. Verify RBS signatures:

    bundle exec rake rbs:validate
    bundle exec rake rbs:steep
  3. Run full test suite:

    bundle exec rake run_rspec

📋 Final Checklist

Before merging, ensure:

  • All tests pass locally
  • RuboCop passes: bundle exec rubocop
  • RBS validation passes: bundle exec rake rbs:validate
  • Manual testing completed (see above)
  • Files end with newline (per CLAUDE.md)

🎯 Verdict

APPROVE with minor suggestions. The implementation is solid, well-tested, and follows project conventions. The suggestions above are optional improvements that don't block merging.

Great work on solving the duplicate work issue in precompile hooks! This will be very helpful for users integrating with Shakapacker.


References:

@claude
Copy link

claude bot commented Nov 22, 2025

Code Review: PR #2092 - Make locale generation idempotent with force flag support

👋 Great work on this PR! This is a well-thought-out solution that addresses the precompile hook coordination problem. Here's my detailed review:


✅ Strengths

1. Excellent Design - Idempotent by Default

The idempotent behavior is properly implemented in lib/react_on_rails/locales/base.rb:

  • Checks file timestamps to skip regeneration when up-to-date (lines 42-44, 66-72)
  • Provides clear user feedback with helpful messages
  • The force flag provides an escape hatch when needed

2. Comprehensive Test Coverage

  • Tests verify force parameter propagation through the call chain (locales_spec.rb)
  • Tests confirm idempotent behavior and force override (locales_to_js_spec.rb:54-69)
  • Extensive tests for bin/dev precompile hook integration with edge cases (server_manager_spec.rb)
  • Tests cover help flag detection, version warnings, and environment variable handling

3. Proper Error Handling

  • run_precompile_hook_if_present exits with clear error message on hook failure (lines 226-233)
  • Good user guidance in error messages
  • Graceful handling of missing hooks

4. Type Safety with RBS

  • Added comprehensive type signatures in sig/react_on_rails/locales.rbs
  • Follows project's type checking standards

5. Documentation Excellence

  • Updated i18n.md with recommended Shakapacker configuration pattern
  • Added detailed process-managers.md section explaining precompile hook integration
  • CHANGELOG entries are well-formatted and informative
  • Clear before/after examples for users

🔍 Observations & Minor Suggestions

1. Edge Case: Empty locale_files Array ✅ Already Handled

In lib/react_on_rails/locales/base.rb:67, you properly handle the case where locale_files.empty?:

def files_are_outdated
  return true if locale_files.empty?  # ✅ Good!
  # ...
end

This is correct - if there are no source files, we should still generate the locale files.

2. Force Flag Parsing 💡 Consider Enhancement

In lib/tasks/locale.rake:18:

force = %w[true 1 yes].include?(ENV["force"]&.downcase)

This is good, but consider if you want to handle the case where someone passes force=false explicitly (currently it would be falsy anyway, so this is fine). The current implementation is solid.

3. Help Flag Detection ✅ Well Done

The early help flag detection in server_manager.rb:154-159 is clever:

command = args.find { |arg| !arg.start_with?("--") && !arg.start_with?("-") }
help_requested = args.any? { |arg| HELP_FLAGS.include?(arg) }

This prevents running the precompile hook when the user just wants help. Smart optimization!

4. Version Warning Logic 💡 Potential Issue

In lib/react_on_rails/dev/server_manager.rb:241-256:

def warn_if_shakapacker_version_too_old
  return unless PackerUtils.shakapacker_version_requirement_met?("9.0.0")
  return if PackerUtils.shakapacker_version_requirement_met?("9.4.0")
  # ... warning
end

Question: What happens if Shakapacker version is < 9.0.0? The warning won't show. Is this intentional?

  • If Shakapacker < 9.0.0, the first return triggers and no warning is shown
  • If Shakapacker is 9.0.0-9.3.x, the warning shows ✅
  • If Shakapacker >= 9.4.0, no warning ✅

Recommendation: Consider adding a comment explaining why versions < 9.0.0 don't trigger a warning (perhaps they don't support precompile hooks at all?).

5. Environment Variable Always Set ⚠️ Design Question

In server_manager.rb:184-189:

unless %w[kill help].include?(command) || help_requested
  run_precompile_hook_if_present
  ENV["SHAKAPACKER_SKIP_PRECOMPILE_HOOK"] = "true"
end

The environment variable is set even when hook_value is nil (no hook configured). This is probably fine since Shakapacker would ignore it anyway, but you could alternatively only set it when a hook exists:

unless %w[kill help].include?(command) || help_requested
  if run_precompile_hook_if_present
    ENV["SHAKAPACKER_SKIP_PRECOMPILE_HOOK"] = "true"
  end
end

However, your current approach is safer as a defense-in-depth measure. ✅

6. Console Output for Up-to-Date Files 💭 UX Consideration

In lib/react_on_rails/locales/base.rb:43:

puts "Locale files are up to date, skipping generation. Use force=true to regenerate."

This is helpful, but consider:

  • Should this use Rails logger instead of puts for consistency?
  • In CI/production, this message will appear every time the rake task runs

Recommendation: Consider using a logger with appropriate log level, or keep as-is if you want visibility. This is a minor style preference.


🔒 Security

No security concerns identified

  • No shell injection risks (force flag is properly parsed as boolean)
  • No path traversal issues
  • Proper escaping in error messages

🎯 Performance

Excellent performance characteristics

  • Timestamp checking is fast (no file parsing unless needed)
  • Idempotent behavior prevents wasted work
  • No regression in build times

📋 Code Quality & Best Practices

Following CLAUDE.md Guidelines ✅

  • ✅ RuboCop compliance (no issues in diff)
  • ✅ Files end with newlines
  • ✅ Comprehensive tests added
  • ✅ CHANGELOG updated properly
  • ✅ RBS signatures added for type safety
  • ✅ User-facing documentation updated

Rails Engine Best Practices ✅

  • ✅ No duplicate rake task loading issues
  • ✅ Proper use of engine initializers
  • ✅ Configuration properly handled

🧪 Test Coverage Assessment

Excellent Coverage ✅

  • Unit tests for force parameter propagation
  • Integration tests for idempotent behavior
  • Edge case tests (empty files, force override)
  • Server manager tests for all command modes
  • Tests for version detection and warnings

Potential Additional Tests (Optional):

  1. Test locale generation with no YAML files - Verify behavior when locale_files is empty
  2. Test rake task force flag parsing - Verify force=TRUE (uppercase) works
  3. Test concurrent execution - Verify no race conditions if hook runs twice simultaneously (though this shouldn't happen with your design)

📚 Documentation Quality

Outstanding documentation

  • Clear before/after examples in docs/building-features/i18n.md
  • Comprehensive explanation in docs/building-features/process-managers.md
  • Good inline code comments
  • Helpful user-facing messages

🎉 Overall Assessment

Rating: Excellent ⭐⭐⭐⭐⭐

This PR demonstrates:

  • Strong software engineering principles (idempotency, separation of concerns)
  • Excellent test coverage and edge case handling
  • Clear documentation and user guidance
  • Proper type safety with RBS
  • No breaking changes

Recommendation: Approve with minor comments addressed

The only item worth considering is clarifying the version warning logic for Shakapacker < 9.0.0 (point #4 above). Everything else is either already excellent or minor style preferences.

Great work! 🚀

@claude
Copy link

claude bot commented Nov 22, 2025

Code Review - PR #2092

Thank you for this comprehensive PR! This is a well-thought-out solution to the precompile hook coordination problem. Here's my detailed review:


🟢 Strengths

1. Excellent Design - Idempotent Behavior

The idempotent locale generation is exactly the right approach. By checking timestamps and skipping when files are up-to-date, this prevents wasteful regeneration while still allowing force override when needed.

2. Great Test Coverage

  • Force parameter propagation tests ✅
  • Idempotent behavior tests ✅
  • Comprehensive bin/dev integration tests ✅
  • Tests for all command paths (dev, static, prod, kill, help) ✅
  • Version-specific warning tests ✅

3. Clear Documentation

The i18n.md and process-managers.md updates clearly explain the new pattern with before/after examples.

4. Good Error Handling

  • Exits cleanly when precompile hook fails
  • Provides helpful error message pointing to shakapacker.yml
  • Warns users on older Shakapacker versions

🟡 Potential Issues & Suggestions

1. Critical: Missing Edge Case in files_are_outdated Logic

Current code (lib/react_on_rails/locales/base.rb:64):

def files_are_outdated
  return true if locale_files.empty?  # <-- Added in PR
  
  latest_yml = locale_files.map { |file| File.mtime(file) }.max
  earliest = exist_files.map { |file| File.mtime(file) }.min
  latest_yml > earliest
end

Issue: What happens if exist_files is empty but locale_files is not?

The obsolete? method has:

def obsolete?
  return true if exist_files.empty?  # <-- Forces generation
  files_are_outdated
end

This is correct! But files_are_outdated will crash with NoMethodError if exist_files is empty (calling .min on empty array returns nil, then comparing with >).

This can never happen because obsolete? short-circuits, but it's fragile. If someone refactors later, this could break.

Recommendation: Add defensive check in files_are_outdated:

def files_are_outdated
  return true if locale_files.empty?
  return true if exist_files.empty?  # <-- Add this for safety
  
  latest_yml = locale_files.map { |file| File.mtime(file) }.max
  earliest = exist_files.map { |file| File.mtime(file) }.min
  latest_yml > earliest
end

2. Minor: Early Command Extraction Logic is Fragile

In lib/react_on_rails/dev/server_manager.rb:156:

# Get the command early to check for help/kill before running hooks
command = args.find { |arg| !arg.start_with?("--") && !arg.start_with?("-") }

Issue: This assumes commands never start with -, but what if someone runs:

bin/dev --verbose hmr

The .find will correctly get "hmr", which is good. But if someone runs:

bin/dev -v

Then command will be nil, which triggers the default HMR mode. This works, but the logic is subtle.

Current behavior is correct, but consider adding a comment explaining this:

# Get the command early (before OptionParser processes args)
# This finds the first non-flag argument, or nil for default mode
command = args.find { |arg| !arg.start_with?("--") && !arg.start_with?("-") }

3. Minor: Inconsistent Help Flag Handling

Two different approaches:

  1. OptionParser's -h flag calls show_help and exit (line 169-172)
  2. Case statement checks for "help", "--help", "-h" (line 187)

Issue: If someone passes -h, OptionParser will handle it and exit. The case statement's "-h" branch is unreachable.

Current code works correctly, but the case statement should only check:

when "help"  # Remove --help and -h since OptionParser handles them
  show_help

4. Security: Command Injection Risk in system() Call

In lib/react_on_rails/dev/server_manager.rb:218:

unless system(hook_value.to_s)

Issue: hook_value comes from config/shakapacker.yml, which is under user control. However, since this is:

  1. A development-only tool (bin/dev)
  2. Reading from local config file (not user input)
  3. The user already controls their codebase

This is acceptable. But consider documenting that precompile_hook should not contain untrusted input (though in this context, that's obvious).

5. Minor: Environment Variable Side Effect

ENV["SHAKAPACKER_SKIP_PRECOMPILE_HOOK"] = "true"

Current behavior: This modifies the global environment permanently.

Consideration: This is intentional and correct for bin/dev, but it means:

  • If bin/dev is required as a library (unusual), this persists
  • Tests need to clean up (✅ you already do this in spec)

This is fine, just noting it's a global side effect.

6. Missing Test Case: Empty locale_files Array

Test coverage gap: The new line in files_are_outdated:

return true if locale_files.empty?

This is not tested. Consider adding a test:

it "considers files outdated when no locale YAML files exist" do
  allow_any_instance_of(described_class).to receive(:locale_files).and_return([])
  
  described_class.new(force: true)
  
  # Should not crash, should generate files
  expect(File.exist?(translations_path)).to be true
end

🔒 Security Considerations

  1. Command injection via system(): Acceptable for dev-only tool reading from local config ✅
  2. Path traversal: Not applicable - paths are config-controlled ✅
  3. Environment variable pollution: Intentional and documented ✅

⚡ Performance Considerations

  1. Timestamp checking: File.mtime calls are fast, negligible overhead ✅
  2. Precompile hook runs once: This is the key performance win ✅
  3. No N+1 issues: File operations are batched appropriately ✅

📊 Code Quality

  1. RuboCop compliance: # rubocop:disable comments are justified ✅
  2. Method complexity: run_from_command_line is complex but well-tested ✅
  3. Naming: Clear and descriptive ✅
  4. Documentation: Excellent inline comments and docs ✅

🎯 Recommendations

Must Fix (Before Merge)

  • Add defensive check in files_are_outdated for empty exist_files

Nice to Have (Optional)

  • Add test for locale_files.empty? path
  • Clean up unreachable case branches in help flag handling
  • Add comment explaining early command extraction logic

Follow-up (Future PR)

  • Consider extracting precompile hook logic to separate class (current implementation is fine, but could be cleaner)

✅ Overall Assessment

Strong approval with minor suggestions. This PR:

  • ✅ Solves the stated problem effectively
  • ✅ Has excellent test coverage
  • ✅ Follows project conventions
  • ✅ Includes clear documentation
  • ✅ Handles edge cases thoughtfully

The issues I've identified are minor and mostly defensive programming suggestions. The core implementation is solid.

Recommendation: Merge after addressing the files_are_outdated defensive check.


Great work! This will significantly improve the developer experience for users with expensive precompile hooks.

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

Caution

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

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

154-189: Consider more robust command detection to avoid edge cases.

The current command detection logic (line 156) finds the first non-flag argument before OptionParser.parse! executes. This can incorrectly identify option values as commands.

Example edge case:

bin/dev --route dashboard
# args = ["--route", "dashboard"]
# command = "dashboard" (incorrect - this is an option value, not a command)
# Precompile hook runs, then exits with "Unknown argument: dashboard"

While this doesn't cause incorrect behavior (the hook runs, then the command exits), it wastes time running the precompile hook before failing.

Suggested improvement:

-  # Get the command early to check for help/kill before running hooks
-  # We need to do this before OptionParser processes flags like -h/--help
-  command = args.find { |arg| !arg.start_with?("--") && !arg.start_with?("-") }
-
-  # Check if help flags are present in args (before OptionParser processes them)
-  help_requested = args.any? { |arg| HELP_FLAGS.include?(arg) }
+  # Check for help flags or help/kill commands before running hooks
+  help_requested = args.any? { |arg| HELP_FLAGS.include?(arg) }
+  skip_hook_commands = %w[help kill]
+  has_skip_command = args.any? { |arg| skip_hook_commands.include?(arg) }

Then update line 186:

-  unless %w[kill help].include?(command) || help_requested
+  unless has_skip_command || help_requested

And retrieve the command after parsing (line 182):

   end.parse!(args)

+  command = args.first

This avoids misidentifying option values as commands and is more explicit about intent.

🧹 Nitpick comments (4)
spec/react_on_rails/locales_to_js_spec.rb (1)

54-69: Spec: good coverage for forced regeneration; could also assert default.js mtime

This example accurately simulates “up-to-date” JS files and verifies that force: true triggers regeneration by checking translations.js mtime. For extra confidence, you could also assert a changed mtime for default.js, but that’s optional since the underlying code writes both files together.

spec/react_on_rails/dev/server_manager_spec.rb (1)

272-431: Specs: comprehensive coverage of precompile hook + skip env behavior

The new examples thoroughly exercise run_from_command_line across modes (dev/static/prod/kill/help/-h) with and without a precompile hook, and correctly validate SHAKAPACKER version gating and SHAKAPACKER_SKIP_PRECOMPILE_HOOK semantics.

One minor hygiene tweak: you already ENV.delete("SHAKAPACKER_SKIP_PRECOMPILE_HOOK") in a before; adding a matching after in this describe to restore or delete the var would further guard against leakage into any future examples that might run after this block.

Based on learnings

lib/react_on_rails/locales/base.rb (1)

7-21: Locales: idempotent compile + force flag design looks correct

The compile(force:) API cleanly forwards the flag to ToJs/ToJson, and Base#initialize(force:) now implements the expected behavior: skip when not forced and files aren’t obsolete, regenerate otherwise, with helpful console messages. The files_are_outdated tweak to treat an empty locale_files set as “outdated” also makes sense so that missing YAMLs trigger regeneration to an empty payload rather than silently skipping.

One small hardening you might consider: files_are_outdated currently calls locale_files.empty?. If locale_files were ever nil (for example, from an unexpected i18n.load_path state), this would raise; using locale_files.blank? (or return true if !locale_files || locale_files.empty?) would make this defensive without changing the intended behavior.

Also applies to: 39-50, 67-68

lib/react_on_rails/dev/server_manager.rb (1)

214-238: Consider using safer command execution to prevent potential shell injection.

The hook command is executed using system(hook_value.to_s) (line 226), which passes the command as a string to the shell. While shakapacker.yml is typically developer-controlled, using the array form of system() is a safer practice that avoids shell interpretation of metacharacters.

Apply this improvement:

+  require "shellwords"
+  
   def run_precompile_hook_if_present
     hook_value = PackerUtils.shakapacker_precompile_hook_value
     return unless hook_value

     # Warn if Shakapacker version doesn't support SHAKAPACKER_SKIP_PRECOMPILE_HOOK
     warn_if_shakapacker_version_too_old

     puts Rainbow("🔧 Running Shakapacker precompile hook...").cyan
     puts Rainbow("   Command: #{hook_value}").cyan
     puts ""

-    unless system(hook_value.to_s)
+    unless system(*Shellwords.split(hook_value.to_s))
       puts ""
       puts Rainbow("❌ Precompile hook failed!").red.bold
       puts Rainbow("   Command: #{hook_value}").red

This ensures that the command is parsed into separate arguments, preventing shell metacharacter interpretation and making the execution more predictable.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2306825 and b7ee06b.

📒 Files selected for processing (10)
  • CHANGELOG.md (1 hunks)
  • docs/building-features/i18n.md (1 hunks)
  • docs/building-features/process-managers.md (1 hunks)
  • lib/react_on_rails/dev/server_manager.rb (5 hunks)
  • lib/react_on_rails/locales/base.rb (4 hunks)
  • lib/tasks/locale.rake (1 hunks)
  • sig/react_on_rails/locales.rbs (1 hunks)
  • spec/react_on_rails/dev/server_manager_spec.rb (1 hunks)
  • spec/react_on_rails/locales_spec.rb (1 hunks)
  • spec/react_on_rails/locales_to_js_spec.rb (1 hunks)
🧰 Additional context used
🧠 Learnings (12)
📓 Common learnings
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.
📚 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:

  • spec/react_on_rails/locales_to_js_spec.rb
  • sig/react_on_rails/locales.rbs
  • docs/building-features/i18n.md
  • lib/react_on_rails/locales/base.rb
📚 Learning: 2024-12-12T13:07:09.929Z
Learnt from: alexeyr-ci
Repo: shakacode/react_on_rails PR: 1644
File: node_package/src/ReactOnRailsRSC.ts:87-87
Timestamp: 2024-12-12T13:07:09.929Z
Learning: When handling errors in 'node_package/src/ReactOnRailsRSC.ts', include the error stack in error messages in development and test environments to aid debugging.

Applied to files:

  • spec/react_on_rails/dev/server_manager_spec.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:

  • spec/react_on_rails/dev/server_manager_spec.rb
  • lib/react_on_rails/dev/server_manager.rb
📚 Learning: 2024-10-08T20:53:47.076Z
Learnt from: justin808
Repo: shakacode/react_on_rails PR: 1622
File: spec/dummy/spec/rake/assets_precompile_rake_spec.rb:12-12
Timestamp: 2024-10-08T20:53:47.076Z
Learning: When stubbing environment variables in RSpec tests, use `before` and `after` hooks to ensure that the original values are restored after the tests, preventing any side effects on other tests. Example provided by justin808:
```ruby
describe "My test" do
  before do
    original_value = ENV["VARIABLE_NAME"]
    allow(ENV).to receive(:[]).with("VARIABLE_NAME").and_return("stubbed_value")
  end

  after do
    allow(ENV).to receive(:[]).with("VARIABLE_NAME").and_call_original
    ENV["VARIABLE_NAME"] = original_value
  end

  it "tests something" do
    # Your test code here
  end
end
```
This practice ensures test isolation and reliability.

Applied to files:

  • spec/react_on_rails/dev/server_manager_spec.rb
📚 Learning: 2025-09-29T14:45:42.687Z
Learnt from: AbanoubGhadban
Repo: shakacode/react_on_rails PR: 1833
File: lib/react_on_rails/dev/process_manager.rb:72-83
Timestamp: 2025-09-29T14:45:42.687Z
Learning: In Ruby bundler contexts, when bundler intercepts system commands for executables not in the Gemfile, both version checks (like `system("foreman", "--version")`) and execution commands (like `system("foreman", "start", ...)`) fail equally, both returning false. This means availability checks using version flags accurately reflect whether execution commands will work in the current bundler context.

Applied to files:

  • spec/react_on_rails/dev/server_manager_spec.rb
📚 Learning: 2025-02-13T14:29:49.267Z
Learnt from: AbanoubGhadban
Repo: shakacode/react_on_rails PR: 1644
File: spec/react_on_rails/utils_spec.rb:218-218
Timestamp: 2025-02-13T14:29:49.267Z
Learning: In RSpec tests, prefer using local variables over constants within test blocks to avoid constant redefinition warnings and maintain better test isolation.

Applied to files:

  • spec/react_on_rails/dev/server_manager_spec.rb
📚 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:

  • spec/react_on_rails/locales_spec.rb
  • sig/react_on_rails/locales.rbs
  • lib/react_on_rails/locales/base.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:

  • CHANGELOG.md
  • sig/react_on_rails/locales.rbs
  • docs/building-features/i18n.md
  • lib/react_on_rails/locales/base.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:

  • sig/react_on_rails/locales.rbs
  • lib/react_on_rails/locales/base.rb
📚 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:

  • sig/react_on_rails/locales.rbs
  • docs/building-features/i18n.md
  • lib/react_on_rails/locales/base.rb
📚 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:

  • sig/react_on_rails/locales.rbs
  • lib/react_on_rails/locales/base.rb
🧬 Code graph analysis (4)
spec/react_on_rails/dev/server_manager_spec.rb (1)
lib/react_on_rails/dev/server_manager.rb (1)
  • run_from_command_line (151-209)
spec/react_on_rails/locales_spec.rb (2)
lib/react_on_rails/configuration.rb (1)
  • configuration (33-71)
lib/react_on_rails/locales/base.rb (1)
  • compile (7-22)
lib/react_on_rails/dev/server_manager.rb (1)
lib/react_on_rails/packer_utils.rb (3)
  • shakapacker_precompile_hook_value (235-241)
  • shakapacker_version_requirement_met? (30-33)
  • shakapacker_version (15-19)
lib/react_on_rails/locales/base.rb (1)
lib/react_on_rails/configuration.rb (1)
  • initialize (134-190)
⏰ 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). (5)
  • GitHub Check: examples (3.2, minimum)
  • GitHub Check: build-dummy-app-webpack-test-bundles
  • GitHub Check: rspec-package-tests (3.4, latest)
  • GitHub Check: pro-lint-js-and-ruby
  • GitHub Check: claude-review
🔇 Additional comments (9)
docs/building-features/i18n.md (1)

24-46: Docs: precompile_hook + force flag guidance is clear and consistent

The new recommended Shakapacker precompile_hook section and the force=true example line up with the rake task behavior and make the idempotent-flow story easy to follow. No changes needed here.

docs/building-features/process-managers.md (1)

19-58: Docs: bin/dev + precompile_hook integration matches implementation

The updated bin/dev flow, precompile hook integration note, SHAKAPACKER_SKIP_PRECOMPILE_HOOK version caveat, and Procfile/shakapacker.yml examples are all consistent with the tested server_manager behavior. Looks good.

lib/tasks/locale.rake (1)

13-20: Rake task: force flag wiring and docs look correct

The Options block clearly documents force=true, and the %w[true 1 yes].include?(ENV["force"]&.downcase) parsing plus Locales.compile(force: force) call cleanly hook into the new API. No changes needed.

CHANGELOG.md (1)

26-31: Changelog: entries accurately describe new behavior

The Unreleased “Improved” items succinctly capture the new bin/dev precompile_hook coordination and idempotent react_on_rails:locale behavior (including force=true). They match the implementation and docs.

spec/react_on_rails/locales_spec.rb (1)

40-54: Specs: correctly verify force propagation to writer classes

The added examples neatly confirm that Locales.compile(force: true) forwards force: true to the appropriate writer (ToJson vs ToJs) based on i18n_output_format, keeping configuration state restored via the existing before/after hooks. Looks solid.

lib/react_on_rails/dev/server_manager.rb (3)

11-12: LGTM!

The HELP_FLAGS constant is well-defined and appropriately frozen.


240-257: LGTM!

The version checking logic correctly identifies Shakapacker versions in the 9.0.0–9.3.x range that lack SHAKAPACKER_SKIP_PRECOMPILE_HOOK support, providing clear guidance to upgrade.


184-189: Effective coordination mechanism for preventing duplicate hook execution.

Setting SHAKAPACKER_SKIP_PRECOMPILE_HOOK after running the hook ensures spawned processes (via Procfile) will skip their hooks, preventing race conditions and duplicate work.

sig/react_on_rails/locales.rbs (1)

1-46: Well-structured type signatures for the locale compilation API.

The RBS signatures accurately reflect the idempotent locale generation feature:

  • compile(?force: bool) correctly accepts an optional force parameter
  • Base#initialize(?force: bool) properly propagates the force flag
  • Subclasses ToJs and ToJson appropriately override specific methods
  • Type signatures for complex hash operations (lines 26-27) accurately represent the nested translation structures

The nullable return type for Base#file_format (line 11) versus non-nullable in subclasses (lines 35, 41) is a valid type refinement pattern, indicating that Base may be abstract or have a default implementation.

@claude
Copy link

claude bot commented Nov 22, 2025

Code Review: Idempotent Locale Generation with Precompile Hook Integration

This is an excellent enhancement that addresses a real pain point in development workflows. The implementation is well-structured with good test coverage. Here's my detailed feedback:


Strengths

1. Solid Core Implementation

  • The idempotent check using file modification times (files_are_outdated) is a clean approach
  • The force parameter is properly threaded through the call chain (compile → ToJson/ToJs → Base)
  • Good user messaging when skipping regeneration vs. generating files

2. Comprehensive Test Coverage

  • Tests verify the force parameter propagation
  • Tests confirm idempotent behavior (skips when up-to-date)
  • Tests verify force flag bypasses the timestamp check
  • Extensive tests for bin/dev precompile hook integration

3. Excellent Documentation

  • Clear usage examples in the PR description
  • Updated i18n docs with recommended precompile_hook configuration
  • Process manager docs updated with detailed explanation of the integration

4. Type Safety

  • Added RBS signatures for the Locales module (commit 2)
  • This improves static type checking coverage

5. Smart Integration with bin/dev

  • Automatic precompile hook execution eliminates manual coordination
  • SHAKAPACKER_SKIP_PRECOMPILE_HOOK environment variable prevents duplicate execution
  • Helpful version warning for Shakapacker < 9.4.0

🔍 Issues & Concerns

Critical: Potential Nil Comparison Bug in files_are_outdated

Location: lib/react_on_rails/locales/base.rb:62-64

def files_are_outdated
  latest_yml = locale_files.map { |file| File.mtime(file) }.max
  earliest = exist_files.map { |file| File.mtime(file) }.min
  latest_yml > earliest
end

Problem: If locale_files is empty, latest_yml will be nil, causing a NoMethodError when comparing nil > earliest.

Fix Applied: Commit 2 adds:

return true if locale_files.empty?

But wait... This guard is placed in files_are_outdated, but there's a second issue:

Looking at the original code in the current master branch (before this PR), files_are_outdated is only called from obsolete?:

def obsolete?
  return true if exist_files.empty?
  files_are_outdated
end

Additional Issue: The new code in the PR changes the behavior in Base#initialize:

BEFORE (current master):

def initialize
  return if i18n_dir.nil?
  return unless obsolete?
  # ...
end

AFTER (PR commit 1):

def initialize(force: false)
  return if i18n_dir.nil?

  if !force && !obsolete?
    puts "Locale files are up to date, skipping generation. Use force=true to regenerate."
    return
  end

  @translations, @defaults = generate_translations
  convert
  puts "Generated locale files in #{i18n_dir}"
end

This is fine, BUT there's a subtle issue: The message "Locale files are up to date..." will print even when files don't exist yet. Let me trace through the logic:

  1. First run (no files exist):

    • exist_files.empty? → true
    • obsolete? returns true (early return in obsolete?)
    • !obsolete? → false
    • Condition !force && !obsolete? → false
    • Correctly generates files
  2. Second run (files exist and up-to-date):

    • exist_files.empty? → false
    • files_are_outdated → false (assuming files are newer)
    • obsolete? returns false
    • !obsolete? → true
    • Condition !force && !obsolete? → true
    • Correctly skips generation

Actually, the logic looks correct on closer inspection. Good!

Minor: ENV Variable Parsing Enhancement

Location: lib/tasks/locale.rake:18

BEFORE (commit 1):

force = ENV["force"] == "true"

AFTER (commit 2):

force = %w[true 1 yes].include?(ENV["force"]&.downcase)

Good improvement! This makes the interface more user-friendly by accepting common boolean representations. Consider documenting this in the task description.

Minor: Unnecessary sleep removal

Location: spec/react_on_rails/locales_to_js_spec.rb:56

Commit 2 removes sleep 0.01 from the test. The comment justifies this saying "timestamp difference already guaranteed", but looking at the test:

initial_mtime = File.mtime(translations_path)
# sleep 0.01 was here
future_time = Time.current + 1.minute
FileUtils.touch(translations_path, mtime: future_time)

The sleep was unnecessary since future_time = Time.current + 1.minute already creates a future timestamp. Good catch!


🚨 Potential Issues

1. Race Condition in bin/dev Hook Execution?

Location: lib/react_on_rails/dev/server_manager.rb:149-156

The code extracts the command early to check for help/kill before running hooks:

command = args.find { |arg| !arg.start_with?("--") && !arg.start_with?("-") }
help_requested = args.any? { |arg| HELP_FLAGS.include?(arg) }

Then later:

unless %w[kill help].include?(command) || help_requested
  run_precompile_hook_if_present
  ENV["SHAKAPACKER_SKIP_PRECOMPILE_HOOK"] = "true"
end

Question: What happens if a user runs bin/dev -h?

  1. command = args.find { |arg| !arg.start_with?("--") && !arg.start_with?("-") } → nil
  2. help_requested = args.any? { |arg| HELP_FLAGS.include?(arg) } → true
  3. Hook is skipped ✅
  4. OptionParser processes -h and calls show_help; exit

Looks good!

Question: What happens if a user runs bin/dev --help?

Same flow - help_requested will be true, hook is skipped. ✅

Question: What happens if a user runs just bin/dev help?

  1. command → "help"
  2. Hook is skipped via %w[kill help].include?(command)

Great defensive programming!

2. RBS Type Signatures - Minor Nitpick

Location: sig/react_on_rails/locales.rbs

The RBS signatures look comprehensive, but I notice:

def files_are_outdated: () -> bool

Given the guard added in commit 2 (return true if locale_files.empty?), this is correct. However, the signature for locale_files is:

def locale_files: () -> Array[String]

Question: Can locale_files actually return nil? Looking at the implementation:

def locale_files
  @locale_files ||= if i18n_yml_dir.present?
                      Dir["#{i18n_yml_dir}/**/*.yml"]
                    else
                      ReactOnRails::Utils.truthy_presence(
                        Rails.application && Rails.application.config.i18n.load_path
                      ).presence
                    end
end

The .presence call at the end could return nil. So the signature should probably be:

def locale_files: () -> Array[String]?

But this is a minor issue - the actual code handles it with the locale_files.empty? check in commit 2.


💡 Suggestions for Improvement

1. Consider Logging Instead of puts

Location: lib/react_on_rails/locales/base.rb:43-48

if !force && !obsolete?
  puts "Locale files are up to date, skipping generation. Use force=true to regenerate."
  return
end

@translations, @defaults = generate_translations
convert
puts "Generated locale files in #{i18n_dir}"

Suggestion: Consider using Rails logger instead of puts:

  • puts writes to stdout unconditionally
  • In some contexts (background jobs, test suites), this might create unwanted noise
  • Rails logger allows controlling verbosity via log levels

However, for a Rake task, puts is reasonable. Just something to consider.

2. Document ENV Variable Options in Rake Task Description

Location: lib/tasks/locale.rake:11-15

Options:
  force=true - Force regeneration even if files are up to date
               Example: rake react_on_rails:locale force=true

Suggestion: Document all accepted values:

Options:
  force=true|1|yes - Force regeneration even if files are up to date
                     Example: rake react_on_rails:locale force=true

3. RBS Validation Command in CI

The PR adds RBS signatures but I don't see them being validated in CI. The CLAUDE.md mentions:

Validate RBS signatures: bundle exec rake rbs:validate

Recommendation: Ensure this runs in CI to catch type signature drift.


🧪 Testing

Excellent test coverage overall! The tests verify:

✅ Force parameter propagation through the call chain
✅ Idempotent behavior (skipping when up-to-date)
✅ Force flag bypassing timestamp checks
✅ bin/dev hook execution
✅ Environment variable setting
✅ Version warnings for old Shakapacker
✅ Help/kill command handling

One suggestion: Consider adding a test for the edge case where locale_files returns an empty array or nil. The guard in commit 2 (return true if locale_files.empty?) should be tested explicitly.


📝 Documentation

Excellent documentation! The PR includes:

✅ Comprehensive PR description with usage examples
✅ Updated i18n.md with recommended precompile_hook pattern
✅ Updated process-managers.md with detailed integration explanation
✅ Proper CHANGELOG entries
✅ Clear commit messages

Minor suggestion: The CHANGELOG entry is quite long (3 lines). Consider condensing slightly while keeping the key information.


🏁 Overall Assessment

This is a high-quality PR that solves a real problem with a thoughtful implementation. The three-commit structure shows good iterative refinement:

  1. Commit 1: Core idempotent locale generation
  2. Commit 2: Refinements (nil guards, ENV parsing, RBS signatures)
  3. Commit 3: bin/dev integration for seamless developer experience

Recommendation: APPROVE with minor suggestions

The code is well-tested, well-documented, and follows the project's conventions. The minor issues I raised are mostly nitpicks and don't block merging.

Great work! 🎉

@claude
Copy link

claude bot commented Nov 22, 2025

Code Review - PR #2092: Make locale generation idempotent with force flag support

I've reviewed this PR and it looks excellent overall! The implementation is clean, well-tested, and addresses a real pain point with development workflows. Here's my detailed feedback:


Strengths

1. Clean Architecture

  • The force parameter flows cleanly through the call chain: rake task → Locales.compileBase#initializeToJs/ToJson
  • Proper separation of concerns between timestamp checking and forced regeneration
  • User-facing messages are clear and helpful

2. Excellent Test Coverage

  • Tests verify the force parameter is properly passed through to both ToJson and ToJs
  • Tests verify that force flag bypasses the up-to-date check
  • Tests verify the idempotent behavior (skips when up-to-date)
  • Integration with ServerManager has comprehensive tests covering all modes and edge cases

3. Documentation

  • Great documentation updates in i18n.md with recommended Shakapacker configuration
  • Clear examples in the rake task description
  • Well-documented changelog entries

4. Type Safety

  • RBS type signatures added for the new module (good practice!)
  • Proper type definitions for all methods including the new force parameter

🔍 Issues & Recommendations

Critical: Missing Feature in Base Implementation

Issue: The PR diff shows changes to lib/react_on_rails/locales/base.rb, but when I read the current master version, it doesn't have the force parameter implementation. Looking at the diff, the PR adds:

def initialize(force: false)
  return if i18n_dir.nil?

  if !force && !obsolete?
    puts "Locale files are up to date, skipping generation. Use force=true to regenerate."
    return
  end

  @translations, @defaults = generate_translations
  convert
  puts "Generated locale files in #{i18n_dir}"
end

However, I notice a potential issue:

Problem: The logic if !force && !obsolete? means:

  • When force=false AND files are up-to-date → Skip (✅ correct)
  • When force=false AND files are outdated → Generate (✅ correct)
  • When force=true AND files are up-to-date → Generate (✅ correct)
  • When force=true AND files are outdated → Generate (✅ correct)

Actually, the logic looks correct! The condition properly handles all cases.


Minor: User Message Could Be More Informative

Current:

puts "Locale files are up to date, skipping generation. Use force=true to regenerate."

Suggestion:

puts "✓ Locale files are up to date (skipping generation). Use force=true to regenerate."

Rationale: The checkmark emoji makes it clearer this is a success state, not an error. The parentheses clarify what's being skipped.


Minor: Consider Adding File Path Context to Messages

Current:

puts "Generated locale files in #{i18n_dir}"

Suggestion:

puts "✓ Generated locale files: #{files.join(', ')}"

Rationale: Users might want to know which specific files were generated, especially when troubleshooting.


Code Quality: Potential Edge Case with Empty Locale Files

In files_are_outdated:

def files_are_outdated
  return true if locale_files.empty?

  latest_yml = locale_files.map { |file| File.mtime(file) }.max
  earliest = exist_files.map { |file| File.mtime(file) }.min
  latest_yml > earliest
end

Observation: The PR adds return true if locale_files.empty? to the files_are_outdated method. This is good defensive coding!

Question: What happens if exist_files.empty?? The method will call .min on an empty array, which returns nil, then compare latest_yml > nil, which will raise a NoMethodError.

However, looking at the caller in obsolete?:

def obsolete?
  return true if exist_files.empty?
  files_are_outdated
end

Good! The code already handles this by returning early if exist_files.empty?. So this edge case is already covered. ✅


ServerManager Integration: Excellent Work!

The integration with bin/dev is really well done:

  1. Precompile hook runs before starting processes - prevents race conditions
  2. Sets SHAKAPACKER_SKIP_PRECOMPILE_HOOK=true - prevents duplicate execution
  3. Version checking - warns users on Shakapacker < 9.4.0
  4. Help flags handled correctly - doesn't run hooks for help/kill commands
  5. Proper error handling - exits with clear messages if hook fails

One minor suggestion: The logic for detecting help flags is slightly complex:

# Get the command early to check for help/kill before running hooks
command = args.find { |arg| !arg.start_with?("--") && !arg.start_with?("-") }

# Check if help flags are present in args
help_requested = args.any? { |arg| HELP_FLAGS.include?(arg) }

unless %w[kill help].include?(command) || help_requested
  run_precompile_hook_if_present
  ENV["SHAKAPACKER_SKIP_PRECOMPILE_HOOK"] = "true"
end

This works, but there's a subtle issue: if someone runs bin/dev -h static, the code will:

  1. Set command = "static" (first non-flag arg)
  2. Set help_requested = true (found -h)
  3. Skip precompile hook ✅

Actually, this is correct! The logic properly handles all edge cases. Great job! ✅


🔒 Security Review

Rake Task Parameter Parsing

force = %w[true 1 yes].include?(ENV["force"]&.downcase)

✅ Good: Safe parsing of the force parameter - no shell injection risk
✅ Good: Uses allowlist approach (only specific values are truthy)


🎯 Performance Considerations

  1. Timestamp checking is efficient - uses File.mtime which is fast
  2. Skips file generation entirely when up-to-date - excellent optimization
  3. No unnecessary file reads - only reads YAML when regeneration is needed

No performance concerns.


📋 Test Coverage Assessment

What's Tested:

  • ✅ Force parameter propagation to ToJson
  • ✅ Force parameter propagation to ToJs
  • ✅ Force flag bypasses up-to-date check
  • ✅ Precompile hook integration in all modes (dev, static, prod)
  • ✅ Hook skipped for kill/help commands
  • ✅ Version warning for Shakapacker < 9.4.0
  • ✅ Hook failure causes exit

Potential Gaps:

  • Missing: Test that verifies the skip message is shown when files are up-to-date
  • Missing: Test that verifies the generation message is shown after generation

Suggestion: Add a simple test in locales_to_js_spec.rb:

it "shows skip message when files are up-to-date" do
  # First generation
  described_class.new

  # Second call should skip and show message
  expect { described_class.new }.to output(/up to date.*skipping/i).to_stdout
end

it "shows generation message after generating" do
  expect { described_class.new }.to output(/Generated locale files/i).to_stdout
end

🎨 Style & Conventions

Follows Project Guidelines:

  • ✅ RuboCop-compliant (based on the code structure)
  • ✅ Proper use of keyword arguments (force: false)
  • ✅ Clear method names
  • ✅ Good comments where needed
  • ✅ Changelog updated appropriately

RBS Type Signatures:

The new sig/react_on_rails/locales.rbs file looks comprehensive and correct:

  • ✅ All public methods have signatures
  • ✅ Private methods included
  • ✅ Proper use of optional parameters (?force: bool)
  • ✅ Return types specified

📖 Documentation Review

i18n.md Updates:

  • ✅ Clear explanation of idempotent behavior
  • ✅ Recommended Shakapacker configuration shown
  • ✅ Force flag usage documented
  • ✅ Alternatives provided for manual coordination

process-managers.md Updates:

  • ✅ Excellent explanation of precompile hook integration
  • ✅ Before/after examples are very helpful
  • ✅ Version compatibility warnings included

🎯 Summary & Recommendation

Overall Assessment: This is a high-quality PR that solves a real problem with a clean, well-tested solution.

Required Changes:

None - the code is production-ready as-is.

Nice-to-Have Improvements:

  1. Add tests for stdout messages (skip message, generation message)
  2. Consider adding emoji to success messages for consistency with other parts of the codebase
  3. Consider showing which specific files were generated in the success message

Verdict:APPROVE

This PR is ready to merge. The implementation is solid, well-tested, and follows all project conventions. The nice-to-have improvements can be addressed in a follow-up PR if desired.


🙏 Great Work!

Special kudos for:

  • Comprehensive test coverage across multiple test files
  • Excellent documentation updates with real-world examples
  • Proper RBS type signatures
  • Clean integration with the existing bin/dev workflow
  • Version compatibility handling

This will make the development workflow much smoother for React on Rails users! 🚀

@claude
Copy link

claude bot commented Nov 22, 2025

Code Review: Idempotent Locale Generation & Precompile Hook Coordination

Great work on this PR! The implementation is solid and addresses a real pain point with duplicate task execution. Here's my detailed feedback:


Strengths

  1. Well-tested implementation: Comprehensive test coverage for both locale generation and server manager changes
  2. Clear documentation: Excellent updates to i18n.md and process-managers.md with practical examples
  3. Backward compatibility: Force parameter defaults to false, maintaining existing behavior
  4. Security: Good shell injection protection for rails_env parameter (line 314-318 in server_manager.rb)
  5. Type safety: Added RBS signatures for the Locales module
  6. User-friendly: Clear console output and helpful warning messages

🔍 Issues Found

1. Critical Bug: Potential nil comparison error in files_are_outdated

Location: lib/react_on_rails/locales/base.rb:64

Issue: The original code has a guard for empty locale_files array, but the PR adds it to the wrong method:

# Current PR code (lines 61-65):
def files_are_outdated
  return true if locale_files.empty?  # ← Added in PR, good\!
  
  latest_yml = locale_files.map { |file| File.mtime(file) }.max
  earliest = exist_files.map { |file| File.mtime(file) }.min
  latest_yml > earliest  # ← Can crash if exist_files is empty\!
end

Problem: If exist_files is empty, .min returns nil, and latest_yml > nil raises ArgumentError: comparison of Time with nil failed.

Fix: Add guard for exist_files as well:

def files_are_outdated
  return true if locale_files.empty?
  return true if exist_files.empty?  # ← Add this line
  
  latest_yml = locale_files.map { |file| File.mtime(file) }.max
  earliest = exist_files.map { |file| File.mtime(file) }.min
  latest_yml > earliest
end

2. Missing RBS file in repository

Location: Referenced in PR diff as sig/react_on_rails/locales.rbs

Issue: The RBS file is shown in the diff but doesn't exist in the repository:

$ cat sig/react_on_rails/locales.rbs
cat: sig/react_on_rails/locales.rbs: No such file or directory

Impact:

  • bundle exec rake rbs:validate will fail
  • Type checking is incomplete
  • CI will likely fail on RBS validation

Fix: Ensure the file is committed and contains the signatures from the PR diff.


3. Environment variable parsing could be more robust

Location: lib/tasks/locale.rake:18

Current code:

force = %w[true 1 yes].include?(ENV["force"]&.downcase)

Issue:

  • ENV["force"] = "TRUE" works ✅
  • ENV["force"] = "Yes" works ✅
  • ENV["force"] = " true " fails ❌ (has whitespace)
  • ENV["force"] = "t" fails ❌ (common shorthand)

Suggestion: Add .strip for robustness:

force = %w[true 1 yes t].include?(ENV["force"]&.downcase&.strip)

💡 Suggestions for Improvement

4. Missing test coverage for edge cases

Location: spec/react_on_rails/locales_to_js_spec.rb

Missing tests:

  1. Empty locale directory: What happens if no YAML files exist?
  2. Corrupted locale files: Force flag should still work even with invalid YAML
  3. Permission errors: What if files can't be written?
  4. Concurrent execution: Two processes running compile(force: true) simultaneously

Suggested test:

it "handles empty locale_files array gracefully" do
  allow_any_instance_of(described_class).to receive(:locale_files).and_return([])
  
  # Should not crash, should regenerate
  expect { described_class.new(force: false) }.not_to raise_error
end

5. Inconsistent messaging in Base#initialize

Location: lib/react_on_rails/locales/base.rb:42-48

Issue: The skip message says "Use force=true" but doesn't explain how to use it:

puts "Locale files are up to date, skipping generation. Use force=true to regenerate."

Suggestion: Make it actionable:

puts "Locale files are up to date, skipping generation."
puts "To force regeneration: bundle exec rake react_on_rails:locale force=true"

6. Precompile hook version warning could be clearer

Location: lib/react_on_rails/dev/server_manager.rb:241-250

Current: Warning appears but doesn't explain the impact clearly.

Suggestion: Add severity indicator:

puts Rainbow("   ⚠️  IMPACT: The locale generation task will run multiple times:").yellow.bold
puts Rainbow("      - Once by bin/dev before starting").yellow
puts Rainbow("      - Again by each webpack process during startup").yellow
puts Rainbow("      This wastes time but shouldn't break anything.").yellow

🧪 Testing Recommendations

Before merging, verify:

  1. RBS validation: bundle exec rake rbs:validate
  2. Type checking: bundle exec rake rbs:steep
  3. Edge cases:
    # Empty locale directory
    rm -rf config/locales/*.yml
    bundle exec rake react_on_rails:locale
    
    # Force with existing files
    bundle exec rake react_on_rails:locale force=true
    
    # Force with whitespace
    force=" true " bundle exec rake react_on_rails:locale
  4. Integration: Start bin/dev with precompile_hook configured and verify it only runs once

📋 Security Considerations

Good: Shell injection protection for rails_env
Good: No user input in system() calls for precompile hook
Good: Force parameter properly typed as boolean


🎯 Performance Impact

  • Positive: Prevents duplicate locale generation (saves time in dev/CI)
  • Neutral: Timestamp checking adds negligible overhead (~ms)
  • No regressions expected: All changes are opt-in or idempotent

📝 Documentation Quality

✅ Excellent - Clear examples, recommended patterns, version requirements
✅ Changelog entries follow project conventions
✅ Code comments explain the "why" not just the "what"


🚀 Recommendation

Status: Approve with minor fixes required

Required changes:

  1. ✋ Fix the exist_files.empty? guard in files_are_outdated
  2. ✋ Ensure RBS file is committed
  3. 💡 Consider the whitespace/shorthand fix for ENV parsing

Nice to have:

  • Add edge case tests
  • Improve user messaging
  • Clarify version warning impact

Great work overall! This is a solid improvement that will save developers time and reduce confusion. The idempotent design is exactly right for this use case.

🤖 Generated by Claude Code Review

justin808 and others added 2 commits November 22, 2025 11:00
The bin/dev command now automatically runs Shakapacker's precompile_hook
once before starting development processes and sets
SHAKAPACKER_SKIP_PRECOMPILE_HOOK=true to prevent duplicate execution in
spawned webpack processes.

Key improvements:
- Executes precompile_hook once before launching Procfile processes
- Sets SHAKAPACKER_SKIP_PRECOMPILE_HOOK environment variable
- All spawned processes inherit the variable to prevent re-execution
- Displays warning for Shakapacker versions below 9.4.0
- Skips hook execution for help and kill commands

This eliminates the need for:
- Manual coordination in Procfile.dev
- Sleep hacks to sequence tasks
- Duplicate task calls across processes
- Race conditions when multiple processes generate files

Users can now configure expensive build tasks (locale generation, ReScript
compilation, etc.) once in config/shakapacker.yml and bin/dev handles the
coordination automatically.

Changes:
- lib/react_on_rails/dev/server_manager.rb: Added hook execution logic
  with version warning for Shakapacker < 9.4.0
- spec/react_on_rails/dev/server_manager_spec.rb: Added comprehensive
  tests for hook execution, environment variable setting, and version
  warnings
- docs/building-features/process-managers.md: Updated documentation with
  precompile hook integration details and version requirements
- CHANGELOG.md: Added entry for automatic precompile hook coordination

Addresses #2091

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

Co-Authored-By: Claude <noreply@anthropic.com>
References the idempotent locale generation feature (PR #2093) and
explains how bin/dev automatically coordinates the precompile hook.
@justin808 justin808 force-pushed the jg-/coordinate-precompile-hooks branch from b7ee06b to 02b6100 Compare November 22, 2025 21:01
@justin808 justin808 changed the title Make locale generation idempotent with force flag support (#2090) Add automatic precompile hook coordination in bin/dev Nov 22, 2025
@justin808
Copy link
Member Author

@claude review this PR

@claude
Copy link

claude bot commented Nov 22, 2025

Claude finished @justin808's task —— View job


Comprehensive PR Review

I've completed a detailed review of this PR. Here are my findings:

✅ Overall Assessment

This is a well-implemented PR that successfully adds automatic precompile hook coordination to bin/dev. The implementation is clean, well-tested, and includes excellent documentation. The feature effectively eliminates the need for manual coordination and sleep hacks in development workflows.


🎯 Key Strengths

  1. Smart Design

    • Runs the precompile hook exactly once before spawning processes
    • Sets SHAKAPACKER_SKIP_PRECOMPILE_HOOK=true to prevent duplicate execution
    • Properly skips hook execution for help and kill commands
    • Early help flag detection prevents unnecessary hook execution
  2. Excellent Test Coverage (161 new lines)

    • Tests all execution modes (development, static, prod)
    • Tests skip logic for help/kill commands
    • Tests version-specific warnings
    • Tests error handling when hook fails
    • Tests environment variable setting/skipping
    • Properly cleans up environment variables in tests
  3. User-Friendly Features

    • Clear, colorized console output
    • Version-specific warnings for Shakapacker < 9.4.0
    • Helpful error messages when hook fails
    • Comprehensive documentation updates
  4. Solid Error Handling

    • Exits cleanly with helpful message when hook fails
    • Points users to the configuration file
    • Suggests fix or removal of problematic hook

🔍 Implementation Review

Command Detection Logic (lines 154-159)

command = args.find { |arg| !arg.start_with?("--") && !arg.start_with?("-") }
help_requested = args.any? { |arg| HELP_FLAGS.include?(arg) }

Good: This early detection correctly identifies commands before OptionParser processes them.

Observation: The logic assumes the first non-flag argument is the command. This works correctly for all current use cases:

  • bin/dev → command = nil (defaults to hmr) ✅
  • bin/dev static → command = "static" ✅
  • bin/dev -h static → help_requested = true, skips hook ✅
  • bin/dev static -h → help_requested = true, skips hook ✅

Environment Variable Management (lines 184-189)

unless %w[kill help].include?(command) || help_requested
  run_precompile_hook_if_present
  ENV["SHAKAPACKER_SKIP_PRECOMPILE_HOOK"] = "true"
end

Good:

  • Sets the environment variable even when no hook is configured (defense-in-depth)
  • Correctly skips for help/kill commands
  • The environment variable persists for spawned processes

Note: Tests verify this behavior in lines 417-423 (no hook configured case).

System Command Execution (line 226)

unless system(hook_value.to_s)

Security: Using system() with a string passes the command to the shell, which could be a shell injection risk. However:

  • ✅ The hook_value comes from config/shakapacker.yml, which is developer-controlled
  • ✅ This is a development-only tool (bin/dev)
  • ✅ Users already have full control over their codebase

Verdict: Acceptable for this context. The configuration file is under version control and reviewed.

Version Warning Logic (lines 241-256)

def warn_if_shakapacker_version_too_old
  return unless PackerUtils.shakapacker_version_requirement_met?("9.0.0")
  return if PackerUtils.shakapacker_version_requirement_met?("9.4.0")
  # ... warning
end

Analysis:

  • Shakapacker < 9.0.0: No warning (silently returns)
  • Shakapacker 9.0.0 - 9.3.x: Shows warning ✅
  • Shakapacker >= 9.4.0: No warning ✅

Question: Why is there no warning for Shakapacker < 9.0.0?

Likely reason: Versions below 9.0.0 don't support precompile_hook at all, so the hook wouldn't be configured. This makes sense.

Suggestion: Add a comment explaining this logic for future maintainers:

# Only warn if version is between 9.0.0 and 9.3.x
# Versions below 9.0.0 don't support precompile_hook at all
return unless PackerUtils.shakapacker_version_requirement_met?("9.0.0")
return if PackerUtils.shakapacker_version_requirement_met?("9.4.0")

📝 Documentation Review

i18n.md Updates (lines 24-46)

Excellent:

  • Clear before/after examples
  • Explains the idempotent behavior
  • Links to process-managers.md for details
  • Maintains backward compatibility guidance

process-managers.md Updates (lines 19-58)

Outstanding:

  • Comprehensive explanation of the integration
  • Clear before/after Procfile examples
  • Version compatibility warnings
  • Links back to i18n.md

CHANGELOG.md Entry

Good:

  • Clearly describes the feature
  • Explains the benefits
  • References the related issue
  • Mentions version compatibility

Minor observation: The entry is quite long (4 lines). Per CLAUDE.md changelog guidelines, this is acceptable for a significant feature, but could be slightly condensed if needed.


🧪 Test Quality

The test suite is comprehensive and well-structured:

Coverage:

  • ✅ Hook execution for all modes (dev, static, prod)
  • ✅ Environment variable setting
  • ✅ Skip logic for kill/help commands
  • ✅ Help flag handling (-h, --help)
  • ✅ Version-specific warnings
  • ✅ Error handling when hook fails
  • ✅ No-hook scenario

Test Quality:

  • ✅ Proper test isolation (ENV cleanup in before blocks)
  • ✅ Appropriate use of mocks/stubs
  • ✅ Clear test descriptions
  • ✅ Tests verify both positive and negative cases

Edge Cases Covered:

  • ✅ Hook configured vs. not configured
  • ✅ Hook success vs. failure
  • ✅ Different Shakapacker versions
  • ✅ Different command modes
  • ✅ Help flags in various positions

🔒 Security Review

Verdict: No security concerns for the intended use case.

Considerations:

  1. Command Injection via system(): Acceptable because:

    • Configuration comes from config/shakapacker.yml (version-controlled)
    • Development-only tool
    • Users already control the codebase
  2. Environment Variable Pollution: Intentional and documented

    • Tests properly clean up after themselves
    • The variable is meant to persist for spawned processes

⚡ Performance Impact

Positive:

  • Eliminates duplicate hook execution
  • Prevents wasted work in development
  • No performance regression

Neutral:

  • Adds minimal overhead (version checking, environment variable setting)
  • Hook execution is intentional and necessary

📋 Potential Improvements (Optional)

These are not blocking issues, just suggestions for future consideration:

  1. Add explanatory comment for version check logic (line 241-243)

    # Only warn for versions between 9.0.0 and 9.3.x
    # Versions below 9.0.0 don't support precompile_hook
    return unless PackerUtils.shakapacker_version_requirement_met?("9.0.0")
  2. Consider using safer command execution (line 226)
    While the current approach is acceptable, you could use Shellwords.split for additional safety:

    require "shellwords"
    unless system(*Shellwords.split(hook_value.to_s))

    This would prevent shell metacharacter interpretation, though it's not strictly necessary here.

  3. Test coverage for ENV cleanup (good practice)
    The tests already clean up properly, but you could add an after hook to ensure cleanup even if tests fail:

    after do
      ENV.delete("SHAKAPACKER_SKIP_PRECOMPILE_HOOK")
    end

🎓 Lessons from Bot Reviews

I reviewed the extensive bot feedback (coderabbitai and previous Claude reviews). Key points that were already addressed or are non-issues:

  1. CodeRabbit's command detection concern: The current logic is actually correct. Early command detection works properly for all use cases.

  2. ENV variable always set concern: This is intentional and correct as a defense-in-depth measure.

  3. Empty locale_files edge case: This PR focuses on bin/dev coordination, not locale generation internals. That's tracked in a separate PR (Make locale generation idempotent with force flag support #2093).


✅ Compliance with Project Guidelines

Per CLAUDE.md requirements:

  • Code Quality: Clean, readable, well-structured
  • Testing: Comprehensive test coverage
  • Documentation: Excellent user-facing docs
  • Formatting: Follows project conventions
  • Changelog: Properly updated
  • Backward Compatibility: No breaking changes

Note: Per CLAUDE.md, you should verify:

bundle exec rubocop

This should pass with zero offenses. (I couldn't run it without approval, but the code looks RuboCop-compliant.)


🎯 Final Recommendation

Status:APPROVED

This PR is ready to merge. The implementation is solid, well-tested, and solves a real problem that users face with duplicate precompile hook execution. The code quality is high, documentation is excellent, and there are no blocking issues.

Summary:

  • ✅ Solves the stated problem effectively
  • ✅ Excellent test coverage (161 new lines)
  • ✅ Clear, helpful documentation
  • ✅ No security concerns
  • ✅ No breaking changes
  • ✅ Follows project conventions

Great work on this feature! This will significantly improve the developer experience for users with expensive precompile hooks.


justin808 and others added 2 commits November 22, 2025 11:32
This commit enhances the bin/dev precompile hook feature with better
error reporting and clearer code documentation based on code review
feedback.

Key improvements:
- Enhanced error reporting: Hook failures now display stdout and stderr
  output to help users debug issues, using Open3.capture3 instead of
  system()
- Improved documentation: Added comments explaining why
  SHAKAPACKER_SKIP_PRECOMPILE_HOOK is always set (provides consistent
  signal for custom scripts)
- Better code clarity: Added descriptive variable names in version check
  (has_precompile_hook_support, has_skip_env_var_support)
- Structured changelog: Broke long entry into scannable sub-bullets
- Updated tests: All 7 test cases now use Open3.capture3 with proper
  status mocks
- Reduced complexity: Extracted error handling into separate method to
  fix cyclomatic complexity

All tests pass (35 examples, 0 failures) and RuboCop is clean (153
files, no offenses).

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit implements additional safety and robustness improvements
suggested during code review.

Improvements:
- Safer command execution: Use Shellwords.split to prevent shell
  metacharacter interpretation when executing precompile hooks
- Better test isolation: Add after hook to clean up
  SHAKAPACKER_SKIP_PRECOMPILE_HOOK env var even if tests fail
- Updated test mocks: All Open3.capture3 expectations now use split
  arguments matching Shellwords behavior

These changes are non-blocking improvements that enhance code safety
without changing user-facing behavior.

All tests pass (35 examples, 0 failures) and RuboCop is clean (153
files, no offenses).

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

Co-Authored-By: Claude <noreply@anthropic.com>
@justin808 justin808 merged commit d433348 into master Nov 22, 2025
19 checks passed
@justin808 justin808 deleted the jg-/coordinate-precompile-hooks branch November 22, 2025 22:48
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