react_on_rails 16.2.0.beta.10 → 16.2.0.beta.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +5 -8
  3. data/CLAUDE.md +136 -5
  4. data/CONTRIBUTING.md +3 -1
  5. data/Gemfile.lock +1 -1
  6. data/Steepfile +4 -0
  7. data/analysis/rake-task-duplicate-analysis.md +149 -0
  8. data/bin/ci-run-failed-specs +6 -4
  9. data/bin/ci-switch-config +4 -3
  10. data/lib/generators/react_on_rails/base_generator.rb +2 -1
  11. data/lib/generators/react_on_rails/generator_helper.rb +29 -0
  12. data/lib/generators/react_on_rails/js_dependency_manager.rb +29 -7
  13. data/lib/generators/react_on_rails/templates/base/base/config/initializers/react_on_rails.rb.tt +19 -0
  14. data/lib/generators/react_on_rails/templates/base/base/config/{shakapacker.yml → shakapacker.yml.tt} +9 -0
  15. data/lib/generators/react_on_rails/templates/base/base/config/webpack/serverWebpackConfig.js.tt +38 -4
  16. data/lib/react_on_rails/configuration.rb +82 -8
  17. data/lib/react_on_rails/dev/pack_generator.rb +1 -0
  18. data/lib/react_on_rails/dev/server_manager.rb +1 -0
  19. data/lib/react_on_rails/doctor.rb +94 -4
  20. data/lib/react_on_rails/system_checker.rb +7 -4
  21. data/lib/react_on_rails/utils.rb +54 -0
  22. data/lib/react_on_rails/version.rb +1 -1
  23. data/react_on_rails_pro/Gemfile.lock +3 -3
  24. data/react_on_rails_pro/lib/react_on_rails_pro/version.rb +1 -1
  25. data/react_on_rails_pro/package.json +1 -1
  26. data/react_on_rails_pro/spec/dummy/Gemfile.lock +3 -3
  27. data/sig/react_on_rails/dev/file_manager.rbs +15 -0
  28. data/sig/react_on_rails/dev/pack_generator.rbs +19 -0
  29. data/sig/react_on_rails/dev/process_manager.rbs +22 -0
  30. data/sig/react_on_rails/dev/server_manager.rbs +39 -0
  31. metadata +8 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: da10987e43490e2a8da60fccb2033038aa9fa6a9b5ccce9add3f9d0da795474c
4
- data.tar.gz: 9e7a3a46c8bb8e0fcc57142b5a1218ea58d5155d54614038d035e2e6b39fdf47
3
+ metadata.gz: 94e2c46e66774ffdeb5902068d74c22daf9b2eafd08b45f1d353be5de6df24b1
4
+ data.tar.gz: 49e2fc58fbc4e1df4d5a0573940b5bbe2c346c7bf226f6535e80b8e1fd82e216
5
5
  SHA512:
6
- metadata.gz: d9f6d5be2b5bf2437adc711b6ac77e64fb7c6540d34bdce6380e98bf919872ec59e897b87869e25221f1f461ac91b25fc146c48701eb161630cb9bea7c44c7a1
7
- data.tar.gz: 5ec66977d53c808a8983277c6534e24fe966123105c4738e20995a9bb45be34a139030fe3e70ffe3251bb9961470371841bea040e558deb3fc974c154d6bdb7d
6
+ metadata.gz: ca050de67f3ae49a02674b0130b1b2532d090605565cff6055fa51f779741109d2415a4069ceb470a819fdcb988fab51795b278eff644c122599c484a1e6aada
7
+ data.tar.gz: 99e95152c8015ae37ca106f70ac64bb1b64e52c006b0fd0836f84a091bf00db014d2f09c81199b19624ddc8f6db68ea75c4d74ace02445a7d25cf7db3d79f529
data/CHANGELOG.md CHANGED
@@ -23,11 +23,7 @@ After a release, please make sure to run `bundle exec rake update_changelog`. Th
23
23
 
24
24
  Changes since the last non-beta release.
25
25
 
26
- #### Fixed
27
-
28
- - **Duplicate Rake Task Execution**: Fixed rake tasks executing twice during asset precompilation and other rake operations. Rails Engine was loading task files twice: once via explicit `load` calls in the `rake_tasks` block (Railtie layer) and once via automatic file loading from `lib/tasks/` (Engine layer). This caused `react_on_rails:assets:webpack`, `react_on_rails:generate_packs`, and `react_on_rails:locale` tasks to run twice, significantly increasing build times. Removed explicit `load` calls and now rely on Rails Engine's standard auto-loading behavior. [PR 2052](https://github.com/shakacode/react_on_rails/pull/2052) by [justin808](https://github.com/justin808).
29
-
30
- ### [v16.2.0.beta.8] - 2025-11-16
26
+ ### [v16.2.0.beta.10] - 2025-11-18
31
27
 
32
28
  #### Added
33
29
 
@@ -73,6 +69,8 @@ Changes since the last non-beta release.
73
69
 
74
70
  #### Fixed
75
71
 
72
+ - **Duplicate Rake Task Execution**: Fixed rake tasks executing twice during asset precompilation and other rake operations. Rails Engine was loading task files twice: once via explicit `load` calls in the `rake_tasks` block (Railtie layer) and once via automatic file loading from `lib/tasks/` (Engine layer). This caused `react_on_rails:assets:webpack`, `react_on_rails:generate_packs`, and `react_on_rails:locale` tasks to run twice, significantly increasing build times. Removed explicit `load` calls and now rely on Rails Engine's standard auto-loading behavior. [PR 2052](https://github.com/shakacode/react_on_rails/pull/2052) by [justin808](https://github.com/justin808).
73
+
76
74
  - **Node Renderer Worker Restart**: Fixed "descriptor closed" error that occurred when the node renderer restarts while handling an in-progress request (especially streaming requests). Workers now perform graceful shutdowns: they disconnect from the cluster to stop receiving new requests, wait for active requests to complete, then shut down cleanly. A configurable `gracefulWorkerRestartTimeout` ensures workers are forcibly killed if they don't shut down in time. [PR 1970](https://github.com/shakacode/react_on_rails/pull/1970) by [AbanoubGhadban](https://github.com/AbanoubGhadban).
77
75
 
78
76
  - **Body Duplication Bug On Streaming**: Fixed a bug that happens while streaming if the node renderer connection closed after streaming some chunks to the client. [PR #1995](https://github.com/shakacode/react_on_rails/pull/1995) by [AbanoubGhadban](https://github.com/AbanoubGhadban).
@@ -1845,9 +1843,8 @@ such as:
1845
1843
 
1846
1844
  - Fix several generator-related issues.
1847
1845
 
1848
- [unreleased]: https://github.com/shakacode/react_on_rails/compare/v16.2.0.beta.8...master
1849
- [v16.2.0.beta.8]: https://github.com/shakacode/react_on_rails/compare/16.2.0.beta.4...v16.2.0.beta.8
1850
- [16.2.0.beta.4]: https://github.com/shakacode/react_on_rails/compare/16.1.1...16.2.0.beta.4
1846
+ [unreleased]: https://github.com/shakacode/react_on_rails/compare/v16.2.0.beta.10...master
1847
+ [v16.2.0.beta.10]: https://github.com/shakacode/react_on_rails/compare/16.1.1...v16.2.0.beta.10
1851
1848
  [16.1.1]: https://github.com/shakacode/react_on_rails/compare/16.1.0...16.1.1
1852
1849
  [16.1.0]: https://github.com/shakacode/react_on_rails/compare/16.0.0...16.1.0
1853
1850
  [16.0.0]: https://github.com/shakacode/react_on_rails/compare/14.2.0...16.0.0
data/CLAUDE.md CHANGED
@@ -2,6 +2,22 @@
2
2
 
3
3
  This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
4
 
5
+ ## Project Structure Guidelines
6
+
7
+ ### Analysis Documents
8
+
9
+ When creating analysis documents (deep dives, investigations, historical context):
10
+ - **Location**: Place in `/analysis` directory
11
+ - **Format**: Use Markdown (.md)
12
+ - **Naming**: Use descriptive kebab-case names (e.g., `rake-task-duplicate-analysis.md`)
13
+ - **Purpose**: Keep detailed analyses separate from top-level project files
14
+
15
+ Examples:
16
+ - `/analysis/rake-task-duplicate-analysis.md` - Historical analysis of duplicate rake task bug
17
+ - `/analysis/feature-investigation.md` - Investigation of a specific feature or issue
18
+
19
+ Top-level documentation (like README.md, CONTRIBUTING.md) should remain at the root.
20
+
5
21
  ## ⚠️ CRITICAL REQUIREMENTS
6
22
 
7
23
  **BEFORE EVERY COMMIT/PUSH:**
@@ -178,11 +194,24 @@ cd react_on_rails_pro && bundle exec rake rbs:validate
178
194
  ```
179
195
  ## Changelog
180
196
 
197
+ **IMPORTANT: This is a monorepo with TWO separate changelogs:**
198
+ - **Open Source**: `/CHANGELOG.md` - for react_on_rails gem and npm package
199
+ - **Pro**: `/react_on_rails_pro/CHANGELOG.md` - for react_on_rails_pro gem and npm packages
200
+
201
+ When making changes, update the **appropriate changelog(s)**:
202
+ - Open-source features/fixes → Update `/CHANGELOG.md`
203
+ - Pro-only features/fixes → Update `/react_on_rails_pro/CHANGELOG.md`
204
+ - Changes affecting both → Update **BOTH** changelogs
205
+
206
+ ### Changelog Guidelines
207
+
181
208
  - **Update CHANGELOG.md for user-visible changes only** (features, bug fixes, breaking changes, deprecations, performance improvements)
182
209
  - **Do NOT add entries for**: linting, formatting, refactoring, tests, or documentation fixes
183
210
  - **Format**: `[PR 1818](https://github.com/shakacode/react_on_rails/pull/1818) by [username](https://github.com/username)` (no hash in PR number)
184
211
  - **Use `/update-changelog` command** for guided changelog updates with automatic formatting
185
- - **Version management**: Run `bundle exec rake update_changelog` after releases to update version headers
212
+ - **Version management after releases**:
213
+ - Open source: `bundle exec rake update_changelog`
214
+ - Pro: `cd react_on_rails_pro && bundle exec rake update_changelog`
186
215
  - **Examples**: Run `grep -A 3 "^#### " CHANGELOG.md | head -30` to see real formatting examples
187
216
 
188
217
  ## ⚠️ FORMATTING RULES
@@ -198,12 +227,25 @@ cd react_on_rails_pro && bundle exec rake rbs:validate
198
227
  **CRITICAL**: When resolving merge conflicts, follow this exact sequence:
199
228
 
200
229
  1. **Resolve logical conflicts only** - don't worry about formatting
201
- 2. **Add resolved files**: `git add .` (or specific files)
202
- 3. **Auto-fix everything**: `rake autofix`
203
- 4. **Add any formatting changes**: `git add .`
204
- 5. **Continue rebase/merge**: `git rebase --continue` or `git commit`
230
+ 2. **VERIFY FILE PATHS** - if the conflict involved directory structure:
231
+ - Check if any hardcoded paths need updating
232
+ - Run: `grep -r "old/path" . --exclude-dir=node_modules`
233
+ - Pay special attention to package-scripts.yml, webpack configs, package.json
234
+ - **Test affected scripts:** If package-scripts.yml changed, run `yarn run prepack`
235
+ 3. **Add resolved files**: `git add .` (or specific files)
236
+ 4. **Auto-fix everything**: `rake autofix`
237
+ 5. **Add any formatting changes**: `git add .`
238
+ 6. **Continue rebase/merge**: `git rebase --continue` or `git commit`
239
+ 7. **TEST CRITICAL SCRIPTS if build configs changed:**
240
+ ```bash
241
+ yarn run prepack # Test prepack script
242
+ yarn run yalc.publish # Test yalc publish if package structure changed
243
+ rake run_rspec:gem # Run relevant test suites
244
+ ```
205
245
 
206
246
  **❌ NEVER manually format during conflict resolution** - this causes formatting wars between tools.
247
+ **❌ NEVER blindly accept path changes** - verify they're correct for current structure.
248
+ **❌ NEVER skip testing after resolving conflicts in build configs** - silent failures are dangerous.
207
249
 
208
250
  ### Debugging Formatting Issues
209
251
  - Check current formatting: `yarn start format.listDifferent`
@@ -223,6 +265,18 @@ cd react_on_rails_pro && bundle exec rake rbs:validate
223
265
  - **Gem-only tests**: `rake run_rspec:gem`
224
266
  - **All tests except examples**: `rake all_but_examples`
225
267
 
268
+ ## Testing Build and Package Scripts
269
+
270
+ @.claude/docs/testing-build-scripts.md
271
+
272
+ ## Master Branch Health Monitoring
273
+
274
+ @.claude/docs/master-health-monitoring.md
275
+
276
+ ## Managing File Paths in Configuration Files
277
+
278
+ @.claude/docs/managing-file-paths.md
279
+
226
280
  ## Project Architecture
227
281
 
228
282
  ### Monorepo Structure
@@ -504,6 +558,83 @@ Playwright E2E tests run automatically in CI via GitHub Actions (`.github/workfl
504
558
  - Uploads HTML reports as artifacts (available for 30 days)
505
559
  - Auto-starts Rails server before running tests
506
560
 
561
+ ## Rails Engine Development Nuances
562
+
563
+ React on Rails is a **Rails Engine**, which has important implications for development:
564
+
565
+ ### Automatic Rake Task Loading
566
+
567
+ **CRITICAL**: Rails::Engine automatically loads all `.rake` files from `lib/tasks/` directory. **DO NOT** use a `rake_tasks` block to explicitly load them, as this causes duplicate task execution.
568
+
569
+ ```ruby
570
+ # ❌ WRONG - Causes duplicate execution
571
+ module ReactOnRails
572
+ class Engine < ::Rails::Engine
573
+ rake_tasks do
574
+ load File.expand_path("../tasks/generate_packs.rake", __dir__)
575
+ load File.expand_path("../tasks/assets.rake", __dir__)
576
+ load File.expand_path("../tasks/locale.rake", __dir__)
577
+ end
578
+ end
579
+ end
580
+
581
+ # ✅ CORRECT - Rails::Engine loads lib/tasks/*.rake automatically
582
+ module ReactOnRails
583
+ class Engine < ::Rails::Engine
584
+ # Rake tasks are automatically loaded from lib/tasks/*.rake by Rails::Engine
585
+ # No explicit loading needed
586
+ end
587
+ end
588
+ ```
589
+
590
+ **When to use `rake_tasks` block:**
591
+ - Tasks are in a **non-standard location** (not `lib/tasks/`)
592
+ - You need to **programmatically generate** tasks
593
+ - You need to **pass context** to the tasks
594
+
595
+ **Historical Context**: PR #1770 added explicit rake task loading, causing webpack builds and pack generation to run twice during `rake assets:precompile`. This was fixed in PR #2052. See `analysis/rake-task-duplicate-analysis.md` for full details.
596
+
597
+ ### Engine Initializers and Hooks
598
+
599
+ Engines have specific initialization hooks that run at different times:
600
+
601
+ ```ruby
602
+ module ReactOnRails
603
+ class Engine < ::Rails::Engine
604
+ # Runs after Rails initializes but before routes are loaded
605
+ config.to_prepare do
606
+ ReactOnRails::ServerRenderingPool.reset_pool
607
+ end
608
+
609
+ # Runs during Rails initialization, use for validations
610
+ initializer "react_on_rails.validate_version" do
611
+ config.after_initialize do
612
+ # Validation logic here
613
+ end
614
+ end
615
+ end
616
+ end
617
+ ```
618
+
619
+ ### Engine vs Application Code
620
+
621
+ - **Engine code** (`lib/react_on_rails/`): Runs in the gem context, has limited access to host application
622
+ - **Host application code**: The Rails app that includes the gem
623
+ - **Generators** (`lib/generators/react_on_rails/`): Run in host app context during setup
624
+
625
+ ### Testing Engines
626
+
627
+ - **Dummy app** (`spec/dummy/`): Full Rails app for integration testing
628
+ - **Unit tests** (`spec/react_on_rails/`): Test gem code in isolation
629
+ - Always test both contexts: gem code alone and gem + host app integration
630
+
631
+ ### Common Pitfalls
632
+
633
+ 1. **Assuming host app structure**: Don't assume `app/javascript/` exists—it might not in older apps
634
+ 2. **Path resolution**: Use `Rails.root` for host app paths, not relative paths
635
+ 3. **Autoloading**: Engine code follows Rails autoloading rules but with a different load path
636
+ 4. **Configuration**: Engine config is separate from host app config—use `ReactOnRails.configure`
637
+
507
638
  ## IDE Configuration
508
639
 
509
640
  Exclude these directories to prevent IDE slowdowns:
data/CONTRIBUTING.md CHANGED
@@ -426,12 +426,14 @@ For more details, see [`docs/CI_OPTIMIZATION.md`](./docs/CI_OPTIMIZATION.md).
426
426
 
427
427
  React on Rails provides PR comment commands to control CI behavior:
428
428
 
429
- #### `/run-skipped-ci` - Enable Full CI Mode
429
+ #### `/run-skipped-ci` (or `/run-skipped-tests`) - Enable Full CI Mode
430
430
 
431
431
  Runs all skipped CI checks and enables full CI mode for the PR:
432
432
 
433
433
  ```
434
434
  /run-skipped-ci
435
+ # or use the shorter alias:
436
+ /run-skipped-tests
435
437
  ```
436
438
 
437
439
  **What it does:**
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- react_on_rails (16.2.0.beta.10)
4
+ react_on_rails (16.2.0.beta.11)
5
5
  addressable
6
6
  connection_pool
7
7
  execjs (~> 2.5)
data/Steepfile CHANGED
@@ -28,6 +28,10 @@ target :lib do
28
28
  check "lib/react_on_rails.rb"
29
29
  check "lib/react_on_rails/configuration.rb"
30
30
  check "lib/react_on_rails/controller.rb"
31
+ check "lib/react_on_rails/dev/file_manager.rb"
32
+ check "lib/react_on_rails/dev/pack_generator.rb"
33
+ check "lib/react_on_rails/dev/process_manager.rb"
34
+ check "lib/react_on_rails/dev/server_manager.rb"
31
35
  check "lib/react_on_rails/git_utils.rb"
32
36
  check "lib/react_on_rails/helper.rb"
33
37
  check "lib/react_on_rails/packer_utils.rb"
@@ -0,0 +1,149 @@
1
+ # Analysis: Why rake_tasks Block Was Added in PR #1770 and Caused Duplicate Execution
2
+
3
+ ## Summary
4
+
5
+ In PR #1770 (commit `8f3d178` - "Generator Overhaul & Developer Experience Enhancement"), Ihab added a `rake_tasks` block to `lib/react_on_rails/engine.rb` that explicitly loaded three rake task files. This was part of a **massive generator overhaul** that introduced new rake tasks for the file-system auto-registration feature. However, this caused those tasks to execute **twice** during operations like `rake assets:precompile`, which was fixed in PR #2052.
6
+
7
+ ## The Problem: Double Loading of Rake Tasks
8
+
9
+ ### What Was Added in PR #1770 (8f3d178)
10
+
11
+ ```ruby
12
+ # lib/react_on_rails/engine.rb
13
+ module ReactOnRails
14
+ class Engine < ::Rails::Engine
15
+ # ... existing code ...
16
+
17
+ rake_tasks do
18
+ load File.expand_path("../tasks/generate_packs.rake", __dir__)
19
+ load File.expand_path("../tasks/assets.rake", __dir__)
20
+ load File.expand_path("../tasks/locale.rake", __dir__)
21
+ end
22
+ end
23
+ end
24
+ ```
25
+
26
+ ### Why This Caused Duplicate Execution
27
+
28
+ Rails Engines have **two different mechanisms** for loading rake tasks, and this code inadvertently activated both:
29
+
30
+ 1. **Automatic Loading (Engine Layer)**: Rails::Engine automatically loads all `.rake` files from `lib/tasks/` directory
31
+ 2. **Manual Loading (Railtie Layer)**: The `rake_tasks` block explicitly loads specific files
32
+
33
+ Because the task files existed in `lib/tasks/`:
34
+
35
+ - `lib/tasks/assets.rake`
36
+ - `lib/tasks/generate_packs.rake`
37
+ - `lib/tasks/locale.rake`
38
+
39
+ They were being loaded **twice**:
40
+
41
+ - Once automatically by Rails::Engine from the `lib/tasks/` directory
42
+ - Once explicitly by the `rake_tasks` block
43
+
44
+ ## Why Was This Added in PR #1770?
45
+
46
+ PR #1770 was a **major overhaul** with 97 files changed. Looking at the context:
47
+
48
+ ### The Generator Overhaul Introduced:
49
+
50
+ 1. **File-system auto-registration**: New feature where components auto-register under `app/javascript/src/.../ror_components`
51
+ 2. **New `react_on_rails:generate_packs` rake task**: Critical new task for the auto-registration system
52
+ 3. **Enhanced dev tooling**: New `ReactOnRails::Dev` namespace with ServerManager, ProcessManager, PackGenerator
53
+ 4. **Shakapacker as required dependency**: Made Shakapacker mandatory (removed Webpacker support)
54
+
55
+ ### Why the Explicit Loading Was Added:
56
+
57
+ Based on the PR context and commit message, the most likely reasons:
58
+
59
+ 1. **Ensuring Critical Task Availability**: The `react_on_rails:generate_packs` task was brand new and absolutely critical to the file-system auto-registration feature. Ihab may have wanted to guarantee it would be loaded in all contexts.
60
+
61
+ 2. **Following Common Rails Engine Patterns**: The `rake_tasks` block is a well-documented pattern in Rails engines. Many gems use it explicitly, even when files are in `lib/tasks/`. Ihab likely followed this pattern as "best practice."
62
+
63
+ 3. **Massive PR Complexity**: With 97 files changed, this was a huge refactor. The `rake_tasks` block addition was a tiny part of the overall changes, and the duplicate loading issue was subtle enough that it wasn't caught during review.
64
+
65
+ 4. **Lack of Awareness About Automatic Loading**: Rails::Engine's automatic loading of `lib/tasks/*.rake` files is not as well-known as it should be. Many developers (even experienced ones) don't realize this happens automatically.
66
+
67
+ 5. **"Belt and Suspenders" Approach**: Given the criticality of the new auto-registration feature, Ihab may have intentionally added explicit loading as a safety measure, not realizing it would cause duplication.
68
+
69
+ **The commit message doesn't mention the rake_tasks addition at all**—it focuses on generator improvements, dev experience, and component architecture. This suggests the `rake_tasks` block was added as a routine implementation detail, not something Ihab thought needed explanation.
70
+
71
+ ## The Impact
72
+
73
+ Tasks affected by duplicate execution:
74
+
75
+ - `react_on_rails:assets:webpack` - Webpack builds ran twice
76
+ - `react_on_rails:generate_packs` - Pack generation ran twice
77
+ - `react_on_rails:locale` - Locale file generation ran twice
78
+
79
+ This meant:
80
+
81
+ - **2x build times** during asset precompilation
82
+ - **Slower CI** builds
83
+ - **Confusing console output** showing duplicate webpack compilation messages
84
+ - **Wasted resources** running the same expensive operations twice
85
+
86
+ ## The Fix (PR #2052)
87
+
88
+ The fix was simple—remove the redundant `rake_tasks` block and rely solely on Rails' automatic loading:
89
+
90
+ ```ruby
91
+ # lib/react_on_rails/engine.rb
92
+ module ReactOnRails
93
+ class Engine < ::Rails::Engine
94
+ # ... existing code ...
95
+
96
+ # Rake tasks are automatically loaded from lib/tasks/*.rake by Rails::Engine
97
+ # No need to explicitly load them here to avoid duplicate loading
98
+ end
99
+ end
100
+ ```
101
+
102
+ ## Key Lesson
103
+
104
+ **Rails::Engine Best Practice**: If your rake task files are in `lib/tasks/`, you don't need a `rake_tasks` block. Rails will load them automatically. Only use `rake_tasks do` if:
105
+
106
+ - Tasks are in a non-standard location
107
+ - You need to programmatically generate tasks
108
+ - You need to pass context to the tasks
109
+
110
+ ## Timeline
111
+
112
+ - **Sep 16, 2025** (PR #1770, commit 8f3d178): Ihab adds `rake_tasks` block as part of massive Generator Overhaul (97 files changed)
113
+ - **Nov 18, 2025** (PR #2052, commit 3f6df6be9): Justin discovers and fixes duplicate execution issue by removing the block (~2 months later)
114
+
115
+ ## What We Learned
116
+
117
+ ### For Code Reviews
118
+
119
+ This incident highlights the challenge of reviewing massive PRs:
120
+
121
+ - **97 files changed** made it nearly impossible to catch subtle issues
122
+ - The `rake_tasks` addition was 6 lines in a file that wasn't the focus of the PR
123
+ - The duplicate loading bug only manifested during asset precompilation, not during normal development
124
+ - Smaller, focused PRs would have made this easier to catch
125
+
126
+ ### For Testing
127
+
128
+ The duplicate execution bug was subtle:
129
+
130
+ - **Didn't cause failures**—just slower builds (2x time)
131
+ - **Hard to notice locally**—developers might not realize builds were taking twice as long
132
+ - **Only obvious in CI**—where build times are closely monitored
133
+ - **Needed production-like scenarios**—requires running `rake assets:precompile` to trigger
134
+
135
+ ### For Documentation
136
+
137
+ Better documentation of Rails::Engine automatic loading would help:
138
+
139
+ - Many Rails guides show `rake_tasks` blocks without mentioning automatic loading
140
+ - The Rails Engine guide doesn't clearly state when NOT to use `rake_tasks`
141
+ - This leads to cargo-culting of the pattern
142
+
143
+ ## References
144
+
145
+ - **Original PR**: [#1770 - "React on Rails Generator Overhaul & Developer Experience Enhancement"](https://github.com/shakacode/react_on_rails/pull/1770)
146
+ - **Original commit**: `8f3d178` - 97 files changed, massive refactor
147
+ - **Fix PR**: [#2052 - "Fix duplicate rake task execution by removing explicit task loading"](https://github.com/shakacode/react_on_rails/pull/2052)
148
+ - **Fix commit**: `3f6df6be9` - Simple 6-line removal
149
+ - **Rails Engine documentation**: https://guides.rubyonrails.org/engines.html#rake-tasks
@@ -139,10 +139,12 @@ echo ""
139
139
 
140
140
  # Determine the working directory (check if we need to be in spec/dummy)
141
141
  WORKING_DIR="."
142
- if [ ${#UNIQUE_SPECS[@]} -gt 0 ] && ([[ "${UNIQUE_SPECS[0]}" == *"spec/system"* ]] || [[ "${UNIQUE_SPECS[0]}" == *"spec/helpers"* ]]); then
143
- if [ -d "spec/dummy" ]; then
144
- WORKING_DIR="spec/dummy"
145
- echo -e "${BLUE}Running from spec/dummy directory${NC}"
142
+ if [ ${#UNIQUE_SPECS[@]} -gt 0 ]; then
143
+ if [[ "${UNIQUE_SPECS[0]}" == *"spec/system"* ]] || [[ "${UNIQUE_SPECS[0]}" == *"spec/helpers"* ]]; then
144
+ if [ -d "spec/dummy" ]; then
145
+ WORKING_DIR="spec/dummy"
146
+ echo -e "${BLUE}Running from spec/dummy directory${NC}"
147
+ fi
146
148
  fi
147
149
  fi
148
150
 
data/bin/ci-switch-config CHANGED
@@ -255,9 +255,10 @@ EOF
255
255
  set_node_version "20.18.1" "$VERSION_MANAGER"
256
256
 
257
257
  # Run conversion script
258
- # NOTE: This uses whatever 'ruby' is in PATH after version manager updates above.
259
- # The version manager may not have reloaded yet, so ensure your current Ruby is
260
- # compatible with script/convert (Ruby 2.6+ should work).
258
+ # NOTE: This executes 'ruby' before the version manager reloads in your current shell.
259
+ # The script/convert file requires Ruby 2.6+ and uses basic file I/O operations.
260
+ # Most modern Ruby versions (2.6+) are compatible. The version manager changes above
261
+ # only take effect after shell reload, so this uses your current Ruby installation.
261
262
  print_header "Running script/convert to downgrade dependencies"
262
263
  cd "$PROJECT_ROOT"
263
264
  ruby script/convert
@@ -101,7 +101,8 @@ module ReactOnRails
101
101
  puts "Adding Shakapacker #{ReactOnRails::PackerUtils.shakapacker_version} config"
102
102
  base_path = "base/base/"
103
103
  config = "config/shakapacker.yml"
104
- copy_file("#{base_path}#{config}", config)
104
+ # Use template to enable version-aware configuration
105
+ template("#{base_path}#{config}.tt", config)
105
106
  configure_rspack_in_shakapacker if options.rspack?
106
107
  end
107
108
 
@@ -95,4 +95,33 @@ module GeneratorHelper
95
95
  def component_extension(options)
96
96
  options.typescript? ? "tsx" : "jsx"
97
97
  end
98
+
99
+ # Check if Shakapacker 9.0 or higher is available
100
+ # Returns true if Shakapacker >= 9.0, false otherwise
101
+ #
102
+ # This method is used during code generation to determine which configuration
103
+ # patterns to use in generated files (e.g., config.privateOutputPath vs hardcoded paths).
104
+ #
105
+ # @return [Boolean] true if Shakapacker 9.0+ is available or likely to be installed
106
+ #
107
+ # @note Default behavior: Returns true when Shakapacker is not yet installed
108
+ # Rationale: During fresh installations, we optimistically assume users will install
109
+ # the latest Shakapacker version. This ensures new projects get best-practice configs.
110
+ # If users later install an older version, the generated webpack config includes
111
+ # fallback logic (e.g., `config.privateOutputPath || hardcodedPath`) that prevents
112
+ # breakage, and validation warnings guide them to fix any misconfigurations.
113
+ def shakapacker_version_9_or_higher?
114
+ return @shakapacker_version_9_or_higher if defined?(@shakapacker_version_9_or_higher)
115
+
116
+ @shakapacker_version_9_or_higher = begin
117
+ # If Shakapacker is not available yet (fresh install), default to true
118
+ # since we're likely installing the latest version
119
+ return true unless defined?(ReactOnRails::PackerUtils)
120
+
121
+ ReactOnRails::PackerUtils.shakapacker_version_requirement_met?("9.0.0")
122
+ rescue StandardError
123
+ # If we can't determine version, assume latest
124
+ true
125
+ end
126
+ end
98
127
  end
@@ -123,14 +123,36 @@ module ReactOnRails
123
123
  end
124
124
 
125
125
  def add_react_on_rails_package
126
- # Use exact version match between gem and npm package for stable releases
127
- # For pre-release versions (e.g., 16.1.0-rc.1), use latest to avoid installing
128
- # a version that may not exist in the npm registry
129
- major_minor_patch_only = /\A\d+\.\d+\.\d+\z/
130
- react_on_rails_pkg = if ReactOnRails::VERSION.match?(major_minor_patch_only)
131
- "react-on-rails@#{ReactOnRails::VERSION}"
126
+ # Use exact version match between gem and npm package for all versions including pre-releases
127
+ # Ruby gem versions use dots (16.2.0.beta.10) but npm requires hyphens (16.2.0-beta.10)
128
+ # This method converts between the two formats.
129
+ #
130
+ # The regex matches:
131
+ # - Stable: 16.2.0
132
+ # - Beta (Ruby): 16.2.0.beta.10 or (npm): 16.2.0-beta.10
133
+ # - RC (Ruby): 16.1.0.rc.1 or (npm): 16.1.0-rc.1
134
+ # - Alpha (Ruby): 16.0.0.alpha.5 or (npm): 16.0.0-alpha.5
135
+ # This ensures beta/rc versions use the exact version instead of "latest" which would
136
+ # install the latest stable release and cause version mismatches.
137
+
138
+ # Accept both dot and hyphen separators for pre-release versions
139
+ version_with_optional_prerelease = /\A(\d+\.\d+\.\d+)([-.]([a-zA-Z0-9.]+))?\z/
140
+
141
+ react_on_rails_pkg = if (match = ReactOnRails::VERSION.match(version_with_optional_prerelease))
142
+ base_version = match[1]
143
+ prerelease = match[3]
144
+
145
+ # Convert Ruby gem format (dot) to npm semver format (hyphen)
146
+ npm_version = if prerelease
147
+ "#{base_version}-#{prerelease}"
148
+ else
149
+ base_version
150
+ end
151
+
152
+ "react-on-rails@#{npm_version}"
132
153
  else
133
- puts "Adding the latest react-on-rails NPM module. " \
154
+ puts "WARNING: Unrecognized version format #{ReactOnRails::VERSION}. " \
155
+ "Adding the latest react-on-rails NPM module. " \
134
156
  "Double check this is correct in package.json"
135
157
  "react-on-rails"
136
158
  end
@@ -12,6 +12,25 @@ ReactOnRails.configure do |config|
12
12
  # Set to "" if you're not using server rendering
13
13
  config.server_bundle_js_file = "server-bundle.js"
14
14
 
15
+ # ⚠️ RECOMMENDED: Use Shakapacker 9.0+ private_output_path instead
16
+ #
17
+ # If using Shakapacker 9.0+, add to config/shakapacker.yml:
18
+ # private_output_path: ssr-generated
19
+ #
20
+ # React on Rails will auto-detect this value, eliminating the need to set it here.
21
+ # This keeps your webpack and Rails configs in sync automatically.
22
+ #
23
+ # For older Shakapacker versions or custom setups, manually configure:
24
+ # config.server_bundle_output_path = "ssr-generated"
25
+ #
26
+ # The path is relative to Rails.root and should point to a private directory
27
+ # (outside of public/) for security. Run 'rails react_on_rails:doctor' to verify.
28
+
29
+ # Enforce that server bundles are only loaded from private (non-public) directories.
30
+ # When true, server bundles will only be loaded from the configured server_bundle_output_path.
31
+ # This is recommended for production to prevent server-side code from being exposed.
32
+ config.enforce_private_server_bundles = true
33
+
15
34
  ################################################################################
16
35
  # Test Configuration (Optional)
17
36
  ################################################################################
@@ -29,6 +29,15 @@ default: &default
29
29
  # Location for manifest.json, defaults to {public_output_path}/manifest.json if unset
30
30
  # manifest_path: public/packs/manifest.json
31
31
 
32
+ # Location for private server-side bundles (e.g., for SSR)
33
+ # These bundles are not served publicly, unlike public_output_path
34
+ # Shakapacker 9.0+ feature - automatically detected by React on Rails
35
+ <% if shakapacker_version_9_or_higher? -%>
36
+ private_output_path: ssr-generated
37
+ <% else -%>
38
+ # private_output_path: ssr-generated # Uncomment to enable (requires Shakapacker 9.0+)
39
+ <% end -%>
40
+
32
41
  # Additional paths webpack should look up modules
33
42
  # ['app/assets', 'engine/foo/app/assets']
34
43
  additional_paths: []
@@ -44,19 +44,53 @@ const configureServer = () => {
44
44
  };
45
45
  serverWebpackConfig.plugins.unshift(new bundler.optimize.LimitChunkCountPlugin({ maxChunks: 1 }));
46
46
 
47
- // Custom output for the server-bundle that matches the config in
48
- // config/initializers/react_on_rails.rb
49
- // Server bundles are output to a private directory (not public) for security
47
+ // Custom output for the server-bundle
48
+ <% if shakapacker_version_9_or_higher? -%>
49
+ // Using Shakapacker 9.0+ privateOutputPath for automatic sync with shakapacker.yml
50
+ // This eliminates manual path configuration and keeps configs in sync.
51
+ // Falls back to hardcoded path if private_output_path is not configured.
52
+ const serverBundleOutputPath = config.privateOutputPath ||
53
+ require('path').resolve(__dirname, '../../ssr-generated');
54
+ <% else -%>
55
+ // Using hardcoded path (Shakapacker < 9.0)
56
+ // For Shakapacker 9.0+, consider using config.privateOutputPath instead
57
+ // to automatically sync with shakapacker.yml private_output_path.
58
+ const serverBundleOutputPath = require('path').resolve(__dirname, '../../ssr-generated');
59
+ <% end -%>
60
+
50
61
  serverWebpackConfig.output = {
51
62
  filename: 'server-bundle.js',
52
63
  globalObject: 'this',
53
64
  // If using the React on Rails Pro node server renderer, uncomment the next line
54
65
  // libraryTarget: 'commonjs2',
55
- path: require('path').resolve(__dirname, '../../ssr-generated'),
66
+ path: serverBundleOutputPath,
56
67
  // No publicPath needed since server bundles are not served via web
57
68
  // https://webpack.js.org/configuration/output/#outputglobalobject
58
69
  };
59
70
 
71
+ // Validate server bundle output path configuration
72
+ <% if shakapacker_version_9_or_higher? -%>
73
+ // For Shakapacker 9.0+, verify privateOutputPath is configured in shakapacker.yml
74
+ if (!config.privateOutputPath) {
75
+ console.warn('⚠️ Shakapacker 9.0+ detected but private_output_path not configured in shakapacker.yml');
76
+ console.warn(' Add to config/shakapacker.yml:');
77
+ console.warn(' private_output_path: ssr-generated');
78
+ console.warn(' Run: rails react_on_rails:doctor to validate your configuration');
79
+ }
80
+ <% else -%>
81
+ // For Shakapacker < 9.0, verify hardcoded path syncs with Rails config
82
+ // 1. Ensure config/initializers/react_on_rails.rb has: config.server_bundle_output_path = "ssr-generated"
83
+ // 2. Run: rails react_on_rails:doctor to verify configuration
84
+ const fs = require('fs');
85
+ if (!fs.existsSync(serverBundleOutputPath)) {
86
+ console.warn(`⚠️ Server bundle output directory does not exist: ${serverBundleOutputPath}`);
87
+ console.warn(' It will be created during build, but ensure React on Rails is configured:');
88
+ console.warn(' config.server_bundle_output_path = "ssr-generated" in config/initializers/react_on_rails.rb');
89
+ console.warn(' Run: rails react_on_rails:doctor to validate your configuration');
90
+ }
91
+ <% end -%>
92
+
93
+
60
94
  // Don't hash the server bundle b/c would conflict with the client manifest
61
95
  // And no need for the MiniCssExtractPlugin
62
96
  serverWebpackConfig.plugins = serverWebpackConfig.plugins.filter(
@@ -1,5 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_support/core_ext/enumerable"
4
+ require "active_support/core_ext/object/blank"
5
+
6
+ # Polyfill for compact_blank (added in Rails 6.1) to support Rails 5.2-6.0
7
+ unless [].respond_to?(:compact_blank)
8
+ module Enumerable
9
+ def compact_blank
10
+ reject(&:blank?)
11
+ end
12
+ end
13
+
14
+ class Array
15
+ def compact_blank
16
+ reject(&:blank?)
17
+ end
18
+ end
19
+ end
20
+
3
21
  # rubocop:disable Metrics/ClassLength
4
22
 
5
23
  module ReactOnRails
@@ -10,6 +28,7 @@ module ReactOnRails
10
28
 
11
29
  DEFAULT_GENERATED_ASSETS_DIR = File.join(%w[public webpack], Rails.env).freeze
12
30
  DEFAULT_COMPONENT_REGISTRY_TIMEOUT = 5000
31
+ DEFAULT_SERVER_BUNDLE_OUTPUT_PATH = "ssr-generated"
13
32
 
14
33
  def self.configuration
15
34
  @configuration ||= Configuration.new(
@@ -46,7 +65,7 @@ module ReactOnRails
46
65
  # Set to 0 to disable the timeout and wait indefinitely for component registration.
47
66
  component_registry_timeout: DEFAULT_COMPONENT_REGISTRY_TIMEOUT,
48
67
  generated_component_packs_loading_strategy: nil,
49
- server_bundle_output_path: "ssr-generated",
68
+ server_bundle_output_path: DEFAULT_SERVER_BUNDLE_OUTPUT_PATH,
50
69
  enforce_private_server_bundles: false
51
70
  )
52
71
  end
@@ -184,6 +203,7 @@ module ReactOnRails
184
203
  check_component_registry_timeout
185
204
  validate_generated_component_packs_loading_strategy
186
205
  validate_enforce_private_server_bundles
206
+ auto_detect_server_bundle_path_from_shakapacker
187
207
  end
188
208
 
189
209
  private
@@ -257,6 +277,57 @@ module ReactOnRails
257
277
  "the public directory. Please set it to a directory outside of public."
258
278
  end
259
279
 
280
+ # Auto-detect server_bundle_output_path from Shakapacker 9.0+ private_output_path
281
+ # Checks if user explicitly set a value and warns them to use auto-detection instead
282
+ def auto_detect_server_bundle_path_from_shakapacker
283
+ # Skip if Shakapacker is not available
284
+ return unless defined?(::Shakapacker)
285
+
286
+ # Check if Shakapacker config has private_output_path method (9.0+)
287
+ return unless ::Shakapacker.config.respond_to?(:private_output_path)
288
+
289
+ # Get the private_output_path from Shakapacker
290
+ private_path = ::Shakapacker.config.private_output_path
291
+ return unless private_path
292
+
293
+ relative_path = ReactOnRails::Utils.normalize_to_relative_path(private_path)
294
+
295
+ # Check if user explicitly configured server_bundle_output_path
296
+ if server_bundle_output_path != ReactOnRails::DEFAULT_SERVER_BUNDLE_OUTPUT_PATH
297
+ warn_about_explicit_configuration(relative_path)
298
+ return
299
+ end
300
+
301
+ apply_shakapacker_private_output_path(relative_path)
302
+ rescue StandardError => e
303
+ # Fail gracefully - if auto-detection fails, keep the default
304
+ Rails.logger&.debug("ReactOnRails: Could not auto-detect server bundle path from " \
305
+ "Shakapacker: #{e.message}")
306
+ end
307
+
308
+ def warn_about_explicit_configuration(shakapacker_path)
309
+ # Normalize both paths for comparison
310
+ normalized_config = server_bundle_output_path.to_s.chomp("/")
311
+ normalized_shakapacker = shakapacker_path.to_s.chomp("/")
312
+
313
+ # Only warn if there's a mismatch
314
+ return if normalized_config == normalized_shakapacker
315
+
316
+ Rails.logger&.warn(
317
+ "ReactOnRails: server_bundle_output_path is explicitly set to '#{server_bundle_output_path}' " \
318
+ "but shakapacker.yml private_output_path is '#{shakapacker_path}'. " \
319
+ "Consider removing server_bundle_output_path from your React on Rails initializer " \
320
+ "to use the auto-detected value from shakapacker.yml."
321
+ )
322
+ end
323
+
324
+ def apply_shakapacker_private_output_path(relative_path)
325
+ self.server_bundle_output_path = relative_path
326
+
327
+ Rails.logger&.debug("ReactOnRails: Auto-detected server_bundle_output_path from " \
328
+ "shakapacker.yml private_output_path: '#{relative_path}'")
329
+ end
330
+
260
331
  def check_minimum_shakapacker_version
261
332
  ReactOnRails::PackerUtils.raise_shakapacker_version_incompatible_for_basic_pack_generation unless
262
333
  ReactOnRails::PackerUtils.supports_basic_pack_generation?
@@ -360,13 +431,16 @@ module ReactOnRails
360
431
  def ensure_webpack_generated_files_exists
361
432
  return unless webpack_generated_files.empty?
362
433
 
363
- self.webpack_generated_files = [
364
- "manifest.json",
365
- server_bundle_js_file,
366
- rsc_bundle_js_file,
367
- react_client_manifest_file,
368
- react_server_client_manifest_file
369
- ].compact_blank
434
+ files = ["manifest.json", server_bundle_js_file]
435
+
436
+ if ReactOnRails::Utils.react_on_rails_pro?
437
+ pro_config = ReactOnRailsPro.configuration
438
+ files << pro_config.rsc_bundle_js_file
439
+ files << pro_config.react_client_manifest_file
440
+ files << pro_config.react_server_client_manifest_file
441
+ end
442
+
443
+ self.webpack_generated_files = files.compact_blank
370
444
  end
371
445
 
372
446
  def configure_skip_display_none_deprecation
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "English"
4
4
  require "stringio"
5
+ require_relative "../packer_utils"
5
6
 
6
7
  module ReactOnRails
7
8
  module Dev
@@ -3,6 +3,7 @@
3
3
  require "English"
4
4
  require "open3"
5
5
  require "rainbow"
6
+ require_relative "../packer_utils"
6
7
 
7
8
  module ReactOnRails
8
9
  module Dev
@@ -667,6 +667,7 @@ module ReactOnRails
667
667
  end
668
668
  end
669
669
 
670
+ # rubocop:disable Metrics/CyclomaticComplexity
670
671
  def analyze_server_rendering_config(content)
671
672
  checker.add_info("\n🖥️ Server Rendering:")
672
673
 
@@ -678,6 +679,19 @@ module ReactOnRails
678
679
  checker.add_info(" server_bundle_js_file: server-bundle.js (default)")
679
680
  end
680
681
 
682
+ # Server bundle output path
683
+ server_bundle_path_match = content.match(/config\.server_bundle_output_path\s*=\s*["']([^"']+)["']/)
684
+ default_path = ReactOnRails::DEFAULT_SERVER_BUNDLE_OUTPUT_PATH
685
+ rails_bundle_path = server_bundle_path_match ? server_bundle_path_match[1] : default_path
686
+ checker.add_info(" server_bundle_output_path: #{rails_bundle_path}")
687
+
688
+ # Enforce private server bundles
689
+ enforce_private_match = content.match(/config\.enforce_private_server_bundles\s*=\s*([^\s\n,]+)/)
690
+ checker.add_info(" enforce_private_server_bundles: #{enforce_private_match[1]}") if enforce_private_match
691
+
692
+ # Check Shakapacker integration and provide recommendations
693
+ check_shakapacker_private_output_path(rails_bundle_path)
694
+
681
695
  # RSC bundle file (Pro feature)
682
696
  rsc_bundle_match = content.match(/config\.rsc_bundle_js_file\s*=\s*["']([^"']+)["']/)
683
697
  if rsc_bundle_match
@@ -702,9 +716,9 @@ module ReactOnRails
702
716
 
703
717
  checker.add_info(" raise_on_prerender_error: #{raise_on_error_match[1]}")
704
718
  end
705
- # rubocop:enable Metrics/AbcSize
719
+ # rubocop:enable Metrics/CyclomaticComplexity
706
720
 
707
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
721
+ # rubocop:disable Metrics/CyclomaticComplexity
708
722
  def analyze_performance_config(content)
709
723
  checker.add_info("\n⚡ Performance & Loading:")
710
724
 
@@ -1387,9 +1401,85 @@ module ReactOnRails
1387
1401
  end
1388
1402
 
1389
1403
  def log_debug(message)
1390
- return unless defined?(Rails.logger) && Rails.logger
1404
+ Rails.logger&.debug(message)
1405
+ end
1406
+
1407
+ # Check Shakapacker private_output_path integration and provide recommendations
1408
+ def check_shakapacker_private_output_path(rails_bundle_path)
1409
+ return report_no_shakapacker unless defined?(::Shakapacker)
1410
+ return report_upgrade_shakapacker unless ::Shakapacker.config.respond_to?(:private_output_path)
1411
+
1412
+ check_shakapacker_9_private_output_path(rails_bundle_path)
1413
+ rescue StandardError => e
1414
+ checker.add_info("\n ℹ️ Could not check Shakapacker config: #{e.message}")
1415
+ end
1416
+
1417
+ def report_no_shakapacker
1418
+ checker.add_info("\n ℹ️ Shakapacker not detected - using manual configuration")
1419
+ end
1420
+
1421
+ def report_upgrade_shakapacker
1422
+ checker.add_info(<<~MSG.strip)
1423
+ \n 💡 Recommendation: Upgrade to Shakapacker 9.0+
1424
+
1425
+ Shakapacker 9.0+ adds 'private_output_path' in shakapacker.yml for server bundles.
1426
+ This eliminates the need to configure server_bundle_output_path separately.
1427
+
1428
+ Benefits:
1429
+ - Single source of truth in shakapacker.yml
1430
+ - Automatic detection by React on Rails
1431
+ - No configuration duplication
1432
+ MSG
1433
+ end
1434
+
1435
+ def check_shakapacker_9_private_output_path(rails_bundle_path)
1436
+ private_path = ::Shakapacker.config.private_output_path
1437
+
1438
+ if private_path
1439
+ report_shakapacker_path_status(private_path, rails_bundle_path)
1440
+ else
1441
+ report_configure_private_output_path(rails_bundle_path)
1442
+ end
1443
+ end
1444
+
1445
+ def report_shakapacker_path_status(private_path, rails_bundle_path)
1446
+ relative_path = ReactOnRails::Utils.normalize_to_relative_path(private_path)
1447
+ # Normalize both paths for comparison (remove trailing slashes)
1448
+ normalized_relative = relative_path.to_s.chomp("/")
1449
+ normalized_rails = rails_bundle_path.to_s.chomp("/")
1450
+
1451
+ if normalized_relative == normalized_rails
1452
+ checker.add_success("\n ✅ Using Shakapacker 9.0+ private_output_path: '#{relative_path}'")
1453
+ checker.add_info(" Auto-detected from shakapacker.yml - no manual config needed")
1454
+ else
1455
+ report_configuration_mismatch(relative_path, rails_bundle_path)
1456
+ end
1457
+ end
1458
+
1459
+ def report_configuration_mismatch(relative_path, rails_bundle_path)
1460
+ checker.add_warning(<<~MSG.strip)
1461
+ \n ⚠️ Configuration mismatch detected!
1462
+
1463
+ Shakapacker private_output_path: '#{relative_path}'
1464
+ React on Rails server_bundle_output_path: '#{rails_bundle_path}'
1465
+
1466
+ Recommendation: Remove server_bundle_output_path from your React on Rails
1467
+ initializer and let it auto-detect from shakapacker.yml private_output_path.
1468
+ MSG
1469
+ end
1470
+
1471
+ def report_configure_private_output_path(rails_bundle_path)
1472
+ checker.add_info(<<~MSG.strip)
1473
+ \n 💡 Recommendation: Configure private_output_path in shakapacker.yml
1474
+
1475
+ Add to config/shakapacker.yml:
1476
+ private_output_path: #{rails_bundle_path}
1391
1477
 
1392
- Rails.logger.debug(message)
1478
+ This will:
1479
+ - Keep webpack and Rails configs in sync automatically
1480
+ - Enable auto-detection by React on Rails
1481
+ - Serve as single source of truth for server bundle location
1482
+ MSG
1393
1483
  end
1394
1484
  end
1395
1485
  # rubocop:enable Metrics/ClassLength
@@ -213,17 +213,20 @@ module ReactOnRails
213
213
 
214
214
  return unless npm_version && defined?(ReactOnRails::VERSION)
215
215
 
216
- # Clean version strings for comparison (remove ^, ~, =, etc.)
217
- clean_npm_version = npm_version.gsub(/[^0-9.]/, "")
216
+ # Normalize NPM version format to Ruby gem format for comparison
217
+ # Uses existing VersionSyntaxConverter to handle dash/dot differences
218
+ # (e.g., "16.2.0-beta.10" → "16.2.0.beta.10")
219
+ converter = ReactOnRails::VersionSyntaxConverter.new
220
+ normalized_npm_version = converter.npm_to_rubygem(npm_version)
218
221
  gem_version = ReactOnRails::VERSION
219
222
 
220
- if clean_npm_version == gem_version
223
+ if normalized_npm_version == gem_version
221
224
  add_success("✅ React on Rails gem and NPM package versions match (#{gem_version})")
222
225
  check_version_patterns(npm_version, gem_version)
223
226
  else
224
227
  # Check for major version differences
225
228
  gem_major = gem_version.split(".")[0].to_i
226
- npm_major = clean_npm_version.split(".")[0].to_i
229
+ npm_major = normalized_npm_version.split(".")[0].to_i
227
230
 
228
231
  if gem_major != npm_major # rubocop:disable Style/NegatedIfElseCondition
229
232
  add_error(<<~MSG.strip)
@@ -443,6 +443,60 @@ module ReactOnRails
443
443
  end
444
444
  end
445
445
 
446
+ # Converts an absolute path (String or Pathname) to a path relative to Rails.root.
447
+ # If the path is already relative or doesn't contain Rails.root, returns it as-is.
448
+ #
449
+ # This method is used to normalize paths from Shakapacker's privateOutputPath (which is
450
+ # absolute) to relative paths suitable for React on Rails configuration.
451
+ #
452
+ # Note: Absolute paths that don't start with Rails.root are intentionally passed through
453
+ # unchanged. While there's no known use case for server bundles outside Rails.root,
454
+ # this behavior preserves the original path for debugging and error messages.
455
+ #
456
+ # @param path [String, Pathname] The path to normalize
457
+ # @return [String, nil] The relative path as a string, or nil if path is nil
458
+ #
459
+ # @example Converting absolute paths within Rails.root
460
+ # # Assuming Rails.root is "/app"
461
+ # normalize_to_relative_path("/app/ssr-generated") # => "ssr-generated"
462
+ # normalize_to_relative_path("/app/foo/bar") # => "foo/bar"
463
+ #
464
+ # @example Already relative paths pass through
465
+ # normalize_to_relative_path("ssr-generated") # => "ssr-generated"
466
+ # normalize_to_relative_path("./ssr-generated") # => "./ssr-generated"
467
+ #
468
+ # @example Absolute paths outside Rails.root (edge case)
469
+ # normalize_to_relative_path("/other/path/bundles") # => "/other/path/bundles"
470
+ # rubocop:disable Metrics/CyclomaticComplexity
471
+ def self.normalize_to_relative_path(path)
472
+ return nil if path.nil?
473
+
474
+ path_str = path.to_s
475
+ rails_root_str = Rails.root.to_s.chomp("/")
476
+
477
+ # Treat as "inside Rails.root" only for exact match or a subdirectory
478
+ inside_rails_root = rails_root_str.present? &&
479
+ (path_str == rails_root_str || path_str.start_with?("#{rails_root_str}/"))
480
+
481
+ # If path is within Rails.root, remove that prefix
482
+ if inside_rails_root
483
+ # Remove Rails.root and any leading slash
484
+ path_str.sub(%r{^#{Regexp.escape(rails_root_str)}/?}, "")
485
+ else
486
+ # Path is already relative or outside Rails.root
487
+ # Warn if it's an absolute path outside Rails.root (edge case)
488
+ if path_str.start_with?("/") && !inside_rails_root
489
+ Rails.logger&.warn(
490
+ "ReactOnRails: Detected absolute path outside Rails.root: '#{path_str}'. " \
491
+ "Server bundles are typically stored within Rails.root. " \
492
+ "Verify this is intentional."
493
+ )
494
+ end
495
+ path_str
496
+ end
497
+ end
498
+ # rubocop:enable Metrics/CyclomaticComplexity
499
+
446
500
  def self.default_troubleshooting_section
447
501
  <<~DEFAULT
448
502
  📞 Get Help & Support:
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ReactOnRails
4
- VERSION = "16.2.0.beta.10"
4
+ VERSION = "16.2.0.beta.11"
5
5
  end
@@ -9,7 +9,7 @@ GIT
9
9
  PATH
10
10
  remote: ..
11
11
  specs:
12
- react_on_rails (16.2.0.beta.10)
12
+ react_on_rails (16.2.0.beta.11)
13
13
  addressable
14
14
  connection_pool
15
15
  execjs (~> 2.5)
@@ -20,7 +20,7 @@ PATH
20
20
  PATH
21
21
  remote: .
22
22
  specs:
23
- react_on_rails_pro (16.2.0.beta.10)
23
+ react_on_rails_pro (16.2.0.beta.11)
24
24
  addressable
25
25
  async (>= 2.6)
26
26
  connection_pool
@@ -28,7 +28,7 @@ PATH
28
28
  httpx (~> 1.5)
29
29
  jwt (~> 2.7)
30
30
  rainbow
31
- react_on_rails (= 16.2.0.beta.10)
31
+ react_on_rails (= 16.2.0.beta.11)
32
32
 
33
33
  GEM
34
34
  remote: https://rubygems.org/
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ReactOnRailsPro
4
- VERSION = "16.2.0.beta.10"
4
+ VERSION = "16.2.0.beta.11"
5
5
  PROTOCOL_VERSION = "2.0.0"
6
6
  end
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-on-rails-pro-node-renderer",
3
- "version": "16.2.0-beta.10",
3
+ "version": "16.2.0-beta.11",
4
4
  "protocolVersion": "2.0.0",
5
5
  "description": "react-on-rails-pro JavaScript for react_on_rails_pro Ruby gem",
6
6
  "exports": {
@@ -9,7 +9,7 @@ GIT
9
9
  PATH
10
10
  remote: ../../..
11
11
  specs:
12
- react_on_rails (16.2.0.beta.10)
12
+ react_on_rails (16.2.0.beta.11)
13
13
  addressable
14
14
  connection_pool
15
15
  execjs (~> 2.5)
@@ -20,7 +20,7 @@ PATH
20
20
  PATH
21
21
  remote: ../..
22
22
  specs:
23
- react_on_rails_pro (16.2.0.beta.10)
23
+ react_on_rails_pro (16.2.0.beta.11)
24
24
  addressable
25
25
  async (>= 2.6)
26
26
  connection_pool
@@ -28,7 +28,7 @@ PATH
28
28
  httpx (~> 1.5)
29
29
  jwt (~> 2.7)
30
30
  rainbow
31
- react_on_rails (= 16.2.0.beta.10)
31
+ react_on_rails (= 16.2.0.beta.11)
32
32
 
33
33
  GEM
34
34
  remote: https://rubygems.org/
@@ -0,0 +1,15 @@
1
+ module ReactOnRails
2
+ module Dev
3
+ class FileManager
4
+ def self.cleanup_stale_files: () -> bool
5
+
6
+ private
7
+
8
+ def self.cleanup_overmind_sockets: () -> bool
9
+ def self.cleanup_rails_pid_file: () -> bool
10
+ def self.overmind_running?: () -> bool
11
+ def self.process_running?: (Integer) -> bool
12
+ def self.remove_file_if_exists: (String, String) -> bool
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,19 @@
1
+ module ReactOnRails
2
+ module Dev
3
+ class PackGenerator
4
+ def self.generate: (?verbose: bool) -> void
5
+
6
+ private
7
+
8
+ def self.run_pack_generation: (?silent: bool) -> bool
9
+ def self.should_run_directly?: () -> bool
10
+ def self.rails_available?: () -> bool
11
+ def self.run_rake_task_directly: (?silent: bool) -> bool
12
+ def self.load_rake_tasks: () -> void
13
+ def self.prepare_rake_task: () -> untyped
14
+ def self.capture_output: (bool) { () -> bool } -> bool
15
+ def self.handle_rake_error: (Exception, bool) -> void
16
+ def self.run_via_bundle_exec: (?silent: bool) -> (bool | nil)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,22 @@
1
+ module ReactOnRails
2
+ module Dev
3
+ class ProcessManager
4
+ VERSION_CHECK_TIMEOUT: Integer
5
+
6
+ def self.installed?: (String) -> bool
7
+ def self.ensure_procfile: (String) -> void
8
+ def self.run_with_process_manager: (String) -> void
9
+
10
+ private
11
+
12
+ def self.installed_in_current_context?: (String) -> bool
13
+ def self.version_flags_for: (String) -> Array[String]
14
+ def self.run_process_if_available: (String, Array[String]) -> bool
15
+ def self.run_process_outside_bundle: (String, Array[String]) -> bool
16
+ def self.process_available_in_system?: (String) -> bool
17
+ def self.with_unbundled_context: () { () -> untyped } -> untyped
18
+ def self.show_process_manager_installation_help: () -> void
19
+ def self.valid_procfile_path?: (String) -> bool
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,39 @@
1
+ module ReactOnRails
2
+ module Dev
3
+ class ServerManager
4
+ type mode = :development | :production_like | :static | :hmr
5
+
6
+ def self.start: (mode, String?, ?verbose: bool, ?route: String?, ?rails_env: String?) -> void
7
+ def self.kill_processes: () -> void
8
+ def self.development_processes: () -> Hash[String, String]
9
+ def self.kill_running_processes: () -> bool
10
+ def self.find_process_pids: (String) -> Array[Integer]
11
+ def self.terminate_processes: (Array[Integer]) -> void
12
+ def self.kill_port_processes: (Array[Integer]) -> bool
13
+ def self.find_port_pids: (Integer) -> Array[Integer]
14
+ def self.cleanup_socket_files: () -> bool
15
+ def self.print_kill_summary: (bool) -> void
16
+ def self.show_help: () -> void
17
+ def self.run_from_command_line: (?Array[String]) -> void
18
+
19
+ private
20
+
21
+ def self.help_usage: () -> String
22
+ def self.help_commands: () -> String
23
+ def self.help_options: () -> String
24
+ def self.help_customization: () -> String
25
+ def self.help_mode_details: () -> String
26
+ def self.help_troubleshooting: () -> String
27
+ def self.run_production_like: (?_verbose: bool, ?route: String?, ?rails_env: String?) -> void
28
+ def self.run_static_development: (String, ?verbose: bool, ?route: String?) -> void
29
+ def self.run_development: (String, ?verbose: bool, ?route: String?) -> void
30
+ def self.print_server_info: (String, Array[String], ?Integer, ?route: String?) -> void
31
+ def self.print_procfile_info: (String, ?route: String?) -> void
32
+ def self.procfile_port: (String) -> Integer
33
+ def self.box_border: (Integer) -> String
34
+ def self.box_bottom: (Integer) -> String
35
+ def self.box_empty_line: (Integer) -> String
36
+ def self.format_box_line: (String, Integer) -> String
37
+ end
38
+ end
39
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: react_on_rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 16.2.0.beta.10
4
+ version: 16.2.0.beta.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Gordon
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-11-19 00:00:00.000000000 Z
11
+ date: 2025-11-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: addressable
@@ -140,6 +140,7 @@ files:
140
140
  - Steepfile
141
141
  - TODO.md
142
142
  - WARP.md
143
+ - analysis/rake-task-duplicate-analysis.md
143
144
  - app/helpers/react_on_rails_helper.rb
144
145
  - bin/ci-local
145
146
  - bin/ci-rerun-failures
@@ -191,7 +192,7 @@ files:
191
192
  - lib/generators/react_on_rails/templates/base/base/bin/shakapacker-precompile-hook
192
193
  - lib/generators/react_on_rails/templates/base/base/bin/switch-bundler
193
194
  - lib/generators/react_on_rails/templates/base/base/config/initializers/react_on_rails.rb.tt
194
- - lib/generators/react_on_rails/templates/base/base/config/shakapacker.yml
195
+ - lib/generators/react_on_rails/templates/base/base/config/shakapacker.yml.tt
195
196
  - lib/generators/react_on_rails/templates/base/base/config/webpack/clientWebpackConfig.js.tt
196
197
  - lib/generators/react_on_rails/templates/base/base/config/webpack/commonWebpackConfig.js.tt
197
198
  - lib/generators/react_on_rails/templates/base/base/config/webpack/development.js.tt
@@ -974,6 +975,10 @@ files:
974
975
  - sig/react_on_rails.rbs
975
976
  - sig/react_on_rails/configuration.rbs
976
977
  - sig/react_on_rails/controller.rbs
978
+ - sig/react_on_rails/dev/file_manager.rbs
979
+ - sig/react_on_rails/dev/pack_generator.rbs
980
+ - sig/react_on_rails/dev/process_manager.rbs
981
+ - sig/react_on_rails/dev/server_manager.rbs
977
982
  - sig/react_on_rails/error.rbs
978
983
  - sig/react_on_rails/generators/js_dependency_manager.rbs
979
984
  - sig/react_on_rails/git_utils.rbs