react_on_rails 16.2.0.beta.11 → 16.2.0.beta.12

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 94e2c46e66774ffdeb5902068d74c22daf9b2eafd08b45f1d353be5de6df24b1
4
- data.tar.gz: 49e2fc58fbc4e1df4d5a0573940b5bbe2c346c7bf226f6535e80b8e1fd82e216
3
+ metadata.gz: 1f1f3592776a1d9cc69f832cdd3df34f4f1b696be6dd681d341614830d97a3d3
4
+ data.tar.gz: d7563d35d02ccd555be46984c21be8bf2cc9b9998d0ad70d0aefd4588940cda5
5
5
  SHA512:
6
- metadata.gz: ca050de67f3ae49a02674b0130b1b2532d090605565cff6055fa51f779741109d2415a4069ceb470a819fdcb988fab51795b278eff644c122599c484a1e6aada
7
- data.tar.gz: 99e95152c8015ae37ca106f70ac64bb1b64e52c006b0fd0836f84a091bf00db014d2f09c81199b19624ddc8f6db68ea75c4d74ace02445a7d25cf7db3d79f529
6
+ metadata.gz: b7284eb5e6ef0133546d405f441ae7c0f2099108353d7f6de9d9eff889c0d9d411c2952b37d98c905fb31dd4dc88cb7b3b8ef1dab5d12f6b5856c0172973d0b1
7
+ data.tar.gz: df238dcf4c71cd74547ff523c52974d752aa1bb53d53de50ec3d9c7227b3d55c4d96102c508561991dc41bba3aa3077eb5b3d81f1c08c0e53fa7ebff2ea1430c
data/CHANGELOG.md CHANGED
@@ -23,10 +23,20 @@ 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
- ### [v16.2.0.beta.10] - 2025-11-18
26
+ #### Added
27
+
28
+ - **CSP Nonce Support for Console Replay**: Added Content Security Policy (CSP) nonce support for the `consoleReplay` script generated during server-side rendering. When Rails CSP is configured, the console replay script will automatically include the nonce attribute, allowing it to execute under restrictive CSP policies like `script-src: 'self'`. The implementation includes cross-version Rails compatibility (5.2-7.x) and defense-in-depth nonce sanitization to prevent attribute injection attacks. [PR 2059](https://github.com/shakacode/react_on_rails/pull/2059) by [justin808](https://github.com/justin808).
29
+
30
+ #### Bug Fixes
31
+
32
+ - [PR 2085](https://github.com/shakacode/react_on_rails/pull/2085) by [justin808](https://github.com/justin808): Fix pack generation in bin/dev when running from Bundler context. Pack generation was failing with "Could not find command 'react_on_rails:generate_packs'" because Bundler was intercepting the subprocess call. The fix wraps the bundle exec call with `Bundler.with_unbundled_env` to prevent interception.
33
+
34
+ ### [v16.2.0.beta.11] - 2025-11-19
27
35
 
28
36
  #### Added
29
37
 
38
+ - **Shakapacker 9.0+ Private Output Path Integration**: Added automatic detection and integration of Shakapacker 9.0+ `private_output_path` configuration. React on Rails now automatically reads `private_output_path` from `shakapacker.yml` and sets server bundle paths, eliminating manual configuration synchronization. Includes version-aware generator templates, enhanced doctor command diagnostics for configuration validation and upgrade recommendations, and improved security with `enforce_private_server_bundles` option. [PR 2028](https://github.com/shakacode/react_on_rails/pull/2028) by [justin808](https://github.com/justin808).
39
+
30
40
  - **Rspack Support**: Added `--rspack` flag to `react_on_rails:install` generator for significantly faster builds (~20x improvement with SWC). Includes unified webpack/rspack configuration templates and `bin/switch-bundler` utility to switch between bundlers post-installation. [PR #1852](https://github.com/shakacode/react_on_rails/pull/1852) by [justin808](https://github.com/justin808).
31
41
 
32
42
  - **Attribution Comment**: Added HTML comment attribution to Rails views containing React on Rails functionality. The comment automatically displays which version is in use (open source React on Rails or React on Rails Pro) and, for Pro users, shows the license status. This helps identify React on Rails usage across your application. [PR #1857](https://github.com/shakacode/react_on_rails/pull/1857) by [AbanoubGhadban](https://github.com/AbanoubGhadban).
@@ -37,8 +47,6 @@ Changes since the last non-beta release.
37
47
 
38
48
  - **Improved RSC Payload Error Handling**: Errors that happen during generation of RSC payload are transferred properly to rails side and logs the error message and stack. [PR #1888](https://github.com/shakacode/react_on_rails/pull/1888) by [AbanoubGhadban](https://github.com/AbanoubGhadban).
39
49
 
40
- - **Use as Git dependency**: All packages can now be installed as Git dependencies. This is useful for development and testing purposes. See [CONTRIBUTING.md](./CONTRIBUTING.md#git-dependencies) for documentation. [PR #1873](https://github.com/shakacode/react_on_rails/pull/1873) by [alexeyr-ci2](https://github.com/alexeyr-ci2).
41
-
42
50
  - **Doctor Checks for :async Loading Strategy**: Added proactive diagnostic checks to the React on Rails doctor tool to detect usage of the `:async` loading strategy in projects without React on Rails Pro. The feature scans view files and initializer configuration, providing clear guidance to either upgrade to Pro or use alternative loading strategies like `:defer` or `:sync` to avoid component registration race conditions. [PR 2010](https://github.com/shakacode/react_on_rails/pull/2010) by [justin808](https://github.com/justin808).
43
51
 
44
52
  #### Changed
@@ -63,25 +71,13 @@ Changes since the last non-beta release.
63
71
 
64
72
  - **Simplified Configuration Files**: Improved configuration documentation and generator template for better clarity and usability. Reduced generator template from 67 to 42 lines (37% reduction). Added comprehensive testing configuration guide. Reorganized configuration docs into Essential vs Advanced sections. Enhanced Doctor program with diagnostics for server rendering and test compilation consistency. [PR #2011](https://github.com/shakacode/react_on_rails/pull/2011) by [justin808](https://github.com/justin808).
65
73
 
66
- #### Deprecated
67
-
68
- - **Node Renderer Configuration**: Renamed `bundlePath` configuration option to `serverBundleCachePath` in the node renderer to better describe its purpose and avoid confusion with Shakapacker's public bundle path. The old `bundlePath` option continues to work with deprecation warnings. Both `RENDERER_SERVER_BUNDLE_CACHE_PATH` (new) and `RENDERER_BUNDLE_PATH` (deprecated) environment variables are supported. [PR #2008](https://github.com/shakacode/react_on_rails/pull/2008) by [justin808](https://github.com/justin808).
69
-
70
74
  #### Fixed
71
75
 
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
-
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).
75
-
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).
76
+ - **Doctor Command Version Mismatch Detection**: Fixed false version mismatch warnings in `rake react_on_rails:doctor` when using beta/prerelease versions. The command now correctly recognizes that gem version `16.2.0.beta.10` matches npm version `16.2.0-beta.10` using proper semver conversion instead of string normalization that stripped prerelease identifiers. [PR 2064](https://github.com/shakacode/react_on_rails/pull/2064) by [ihabadham](https://github.com/ihabadham).
77
77
 
78
- - **bin/dev --verbose Option**: Fixed `OptionParser::InvalidOption` error that occurred when using the `--verbose`/`-v` flag. The flag was documented in help text but not functional. Now properly implemented with flag parsing and passing verbose option through method calls. [PR 2023](https://github.com/shakacode/react_on_rails/pull/2023) by [justin808](https://github.com/justin808).
78
+ - **Rails 5.2-6.0 Compatibility**: Fixed compatibility with Rails 5.2-6.0 by adding polyfill for `compact_blank` method (introduced in Rails 6.1). Also refactored webpack asset handling to conditionally include React on Rails Pro files only when available, preventing NoMethodErrors in open-source installations. [PR 2058](https://github.com/shakacode/react_on_rails/pull/2058) by [justin808](https://github.com/justin808).
79
79
 
80
- - **Shakapacker Template Configuration**: Fixed shakapacker.yml template to prevent unnecessary "Slow setup for development" warnings by setting `compile: false` as the default configuration. This allows development environments to inherit the setting instead of forcing on-demand compilation when using Procfiles with `bin/dev` that already run the shakapacker dev server or watch mode. [PR 2021](https://github.com/shakacode/react_on_rails/pull/2021) by [justin808](https://github.com/justin808).
81
-
82
- #### Improved
83
-
84
- - **Concurrent Streaming Performance**: Implemented concurrent draining of streamed React components using the async gem. Instead of processing components sequentially, the system now uses a producer-consumer pattern with bounded buffering to allow multiple components to stream simultaneously while maintaining per-component chunk ordering. [PR 2015](https://github.com/shakacode/react_on_rails/pull/2015) by [ihabadham](https://github.com/ihabadham).
80
+ - **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).
85
81
 
86
82
  #### Breaking Changes
87
83
 
@@ -1843,8 +1839,8 @@ such as:
1843
1839
 
1844
1840
  - Fix several generator-related issues.
1845
1841
 
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
1842
+ [unreleased]: https://github.com/shakacode/react_on_rails/compare/v16.2.0.beta.11...master
1843
+ [v16.2.0.beta.11]: https://github.com/shakacode/react_on_rails/compare/16.1.1...v16.2.0.beta.11
1848
1844
  [16.1.1]: https://github.com/shakacode/react_on_rails/compare/16.1.0...16.1.1
1849
1845
  [16.1.0]: https://github.com/shakacode/react_on_rails/compare/16.0.0...16.1.0
1850
1846
  [16.0.0]: https://github.com/shakacode/react_on_rails/compare/14.2.0...16.0.0
data/CLAUDE.md CHANGED
@@ -214,6 +214,50 @@ When making changes, update the **appropriate changelog(s)**:
214
214
  - Pro: `cd react_on_rails_pro && bundle exec rake update_changelog`
215
215
  - **Examples**: Run `grep -A 3 "^#### " CHANGELOG.md | head -30` to see real formatting examples
216
216
 
217
+ ### Beta Release Changelog Curation
218
+
219
+ When consolidating beta versions into a stable release, carefully curate entries to include only user-facing changes:
220
+
221
+ **Remove these types of entries:**
222
+
223
+ 1. **Developer-only tooling**:
224
+ - yalc publish fixes (local development tool)
225
+ - Git dependency support (contributor workflow)
226
+ - CI/build script improvements
227
+ - Internal tooling changes
228
+
229
+ 2. **Beta-specific fixes**:
230
+ - Bugs introduced during the beta cycle (not present in last stable)
231
+ - Fixes for new beta-only features (e.g., bin/dev in 16.2.0.beta)
232
+ - Generator handling of beta/RC version formats
233
+
234
+ 3. **Pro-specific features** (move to Pro changelog):
235
+ - Node renderer fixes/improvements
236
+ - Streaming-related changes
237
+ - Async loading features (Pro-exclusive)
238
+
239
+ **Keep these types of entries:**
240
+
241
+ 1. **User-facing fixes**:
242
+ - Bugs that existed in previous stable release (e.g., 16.1.x)
243
+ - Compatibility fixes (Rails version support, etc.)
244
+ - Performance improvements affecting all users
245
+
246
+ 2. **Breaking changes**:
247
+ - API changes requiring migration
248
+ - Removed methods/features
249
+ - Configuration changes
250
+
251
+ **Investigation process:**
252
+
253
+ For each suspicious entry:
254
+ 1. Check git history: `git log --oneline <last_stable>..<current_beta> -- <file>`
255
+ 2. Determine when bug was introduced (stable vs beta cycle)
256
+ 3. Verify whether fix applies to stable users or only beta users
257
+ 4. Check PR description for context about what was broken
258
+
259
+ **Example reference:** See [PR #2072](https://github.com/shakacode/react_on_rails/pull/2072) for a complete example of beta changelog curation with detailed investigation notes.
260
+
217
261
  ## ⚠️ FORMATTING RULES
218
262
 
219
263
  **Prettier is the SOLE authority for formatting non-Ruby files, and RuboCop for formatting Ruby files. NEVER manually format code.**
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.11)
4
+ react_on_rails (16.2.0.beta.12)
5
5
  addressable
6
6
  connection_pool
7
7
  execjs (~> 2.5)
@@ -0,0 +1,148 @@
1
+ # V8 Crash Retry Solution for CI
2
+
3
+ ## Problem
4
+
5
+ CI jobs occasionally fail with a transient V8 bytecode deserialization crash during the Node.js setup phase. The error manifests as:
6
+
7
+ ```
8
+ Fatal error in , line 0
9
+ Check failed: ReadSingleBytecodeData( source_.Get(), SlotAccessorForHandle<IsolateT>(&ret, isolate())) == 1.
10
+ ```
11
+
12
+ This error occurs during the `yarn cache dir` command execution within the `actions/setup-node@v4` action.
13
+
14
+ ## Root Cause
15
+
16
+ This is a known bug in Node.js/V8 that occurs sporadically:
17
+
18
+ - **Node.js Issue**: https://github.com/nodejs/node/issues/56010
19
+ - **Setup-node Issue**: https://github.com/actions/setup-node/issues/1028
20
+
21
+ The crash happens when V8 attempts to deserialize cached bytecode and encounters corrupted or incompatible data. It's a transient issue that typically resolves on retry.
22
+
23
+ ## Previous Workarounds
24
+
25
+ Before this fix, the codebase used two workarounds:
26
+
27
+ 1. **Completely disable yarn caching** in `examples.yml`:
28
+
29
+ ```yaml
30
+ # TODO: Re-enable yarn caching once Node.js V8 cache crash is fixed
31
+ # Tracking: https://github.com/actions/setup-node/issues/1028
32
+ # cache: yarn
33
+ # cache-dependency-path: '**/yarn.lock'
34
+ ```
35
+
36
+ 2. **Conditionally disable caching for Node 22** in `integration-tests.yml`:
37
+ ```yaml
38
+ cache: ${{ matrix.node-version != '22' && 'yarn' || '' }}
39
+ ```
40
+
41
+ Both workarounds significantly slowed down CI by preventing yarn dependency caching.
42
+
43
+ ## Solution
44
+
45
+ Created a custom composite GitHub action at `.github/actions/setup-node-with-retry/` that:
46
+
47
+ ### Key Features
48
+
49
+ 1. **Pre-validation**: Tests `yarn cache dir` works before running `setup-node`
50
+ 2. **Automatic retry**: Retries up to 3 times when V8 crashes are detected
51
+ 3. **Smart error detection**: Only retries on V8 crashes, fails fast on other errors
52
+ 4. **Clear diagnostics**: Provides warning annotations in CI logs
53
+ 5. **Configurable**: Allows customizing max retries (defaults to 3)
54
+ 6. **Backward compatible**: Drop-in replacement for `actions/setup-node@v4`
55
+
56
+ ### How It Works
57
+
58
+ ```yaml
59
+ - name: Setup Node.js with retry
60
+ shell: bash
61
+ run: |
62
+ # Pre-validate yarn cache dir works
63
+ if timeout 30 yarn cache dir > "$TEMP_OUTPUT" 2>&1; then
64
+ echo "Yarn cache dir command succeeded"
65
+ else
66
+ # Check for V8 crash signature
67
+ if grep -q "Fatal error in.*Check failed: ReadSingleBytecodeData" "$TEMP_OUTPUT"; then
68
+ echo "::warning::V8 bytecode deserialization error detected"
69
+ # Retry logic...
70
+ fi
71
+ fi
72
+
73
+ - name: Actually setup Node.js
74
+ uses: actions/setup-node@v4
75
+ # ... standard setup-node configuration
76
+ ```
77
+
78
+ ### Usage
79
+
80
+ ```yaml
81
+ - name: Setup Node
82
+ uses: ./.github/actions/setup-node-with-retry
83
+ with:
84
+ node-version: 22
85
+ cache: yarn
86
+ cache-dependency-path: '**/yarn.lock'
87
+ max-retries: 3 # Optional, defaults to 3
88
+ ```
89
+
90
+ ## Changes Made
91
+
92
+ Updated all 8 CI workflow files to use the new action:
93
+
94
+ 1. ✅ `examples.yml` - **Re-enabled yarn caching**
95
+ 2. ✅ `integration-tests.yml` - **Re-enabled yarn caching for Node 22**
96
+ 3. ✅ `lint-js-and-ruby.yml`
97
+ 4. ✅ `package-js-tests.yml`
98
+ 5. ✅ `playwright.yml`
99
+ 6. ✅ `pro-integration-tests.yml`
100
+ 7. ✅ `pro-lint.yml`
101
+ 8. ✅ `pro-test-package-and-gem.yml`
102
+
103
+ ## Benefits
104
+
105
+ 1. **Improved reliability**: CI no longer fails due to transient V8 crashes
106
+ 2. **Better performance**: Yarn caching re-enabled across all workflows
107
+ 3. **Clear diagnostics**: Warning annotations show when retries occur
108
+ 4. **Maintainable**: Centralized retry logic in a reusable action
109
+ 5. **Future-proof**: Can be updated independently if V8 crash patterns change
110
+
111
+ ## Monitoring
112
+
113
+ To verify the retry logic is working when V8 crashes occur:
114
+
115
+ 1. Watch CI logs for these warning messages:
116
+
117
+ ```
118
+ ::warning::V8 bytecode deserialization error detected (attempt 1/3)
119
+ Retrying in 5 seconds...
120
+ ```
121
+
122
+ 2. Check that jobs succeed after retry instead of failing
123
+
124
+ 3. If a job exhausts all retries, it will show:
125
+ ```
126
+ ::error::All 3 retry attempts failed
127
+ ```
128
+
129
+ ## Implementation Details
130
+
131
+ - **Timeout**: Each retry attempt has a 30-second timeout for `yarn cache dir`
132
+ - **Retry delay**: 5 seconds between attempts to allow transient issues to clear
133
+ - **Max retries**: Defaults to 3, configurable via input
134
+ - **Error detection**: Regex pattern matches V8 crash signature in stderr/stdout
135
+
136
+ ## Future Improvements
137
+
138
+ If the V8 crash persists even with retries, consider:
139
+
140
+ 1. Updating Node.js to a version with the fix (when available)
141
+ 2. Increasing max-retries for particularly flaky environments
142
+ 3. Adding exponential backoff between retries
143
+ 4. Implementing cache clearing before retry
144
+
145
+ ## Pull Request
146
+
147
+ - **PR**: https://github.com/shakacode/react_on_rails/pull/2082
148
+ - **Branch**: `jg-/ci-retry-v8-crash`
@@ -429,18 +429,23 @@ module ReactOnRails
429
429
  end
430
430
 
431
431
  def ensure_webpack_generated_files_exists
432
- return unless webpack_generated_files.empty?
433
-
434
- files = ["manifest.json", server_bundle_js_file]
432
+ all_required_files = ["manifest.json", server_bundle_js_file]
435
433
 
436
434
  if ReactOnRails::Utils.react_on_rails_pro?
437
435
  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
436
+ all_required_files << pro_config.rsc_bundle_js_file
437
+ all_required_files << pro_config.react_client_manifest_file
438
+ all_required_files << pro_config.react_server_client_manifest_file
441
439
  end
442
440
 
443
- self.webpack_generated_files = files.compact_blank
441
+ all_required_files = all_required_files.compact_blank
442
+
443
+ if webpack_generated_files.empty?
444
+ self.webpack_generated_files = all_required_files
445
+ else
446
+ missing_files = all_required_files.reject { |file| webpack_generated_files.include?(file) }
447
+ self.webpack_generated_files += missing_files if missing_files.any?
448
+ end
444
449
  end
445
450
 
446
451
  def configure_skip_display_none_deprecation
@@ -38,29 +38,40 @@ module ReactOnRails
38
38
 
39
39
  if verbose
40
40
  puts "📦 Generating React on Rails packs..."
41
- success = run_pack_generation
41
+ success = run_pack_generation(silent: false, verbose: true)
42
42
  else
43
43
  print "📦 Generating packs... "
44
- success = run_pack_generation(silent: true)
44
+ success = run_pack_generation(silent: true, verbose: false)
45
45
  puts success ? "✅" : "❌"
46
46
  end
47
47
 
48
48
  return if success
49
49
 
50
50
  puts "❌ Pack generation failed"
51
+ unless verbose
52
+ puts ""
53
+ puts "💡 Run with #{Rainbow('--verbose').cyan.bold} flag for detailed output:"
54
+ puts " #{Rainbow('bin/dev --verbose').green.bold}"
55
+ end
51
56
  exit 1
52
57
  end
53
58
 
54
59
  private
55
60
 
56
- def run_pack_generation(silent: false)
61
+ def run_pack_generation(silent: false, verbose: false)
62
+ # Set environment variable for child processes to respect verbose mode
63
+ ENV["REACT_ON_RAILS_VERBOSE"] = verbose ? "true" : "false"
64
+
57
65
  # If we're already inside a Bundler context AND Rails is available (e.g., called from bin/dev),
58
66
  # we can directly require and run the task. Otherwise, use bundle exec.
59
67
  if should_run_directly?
60
68
  run_rake_task_directly(silent: silent)
61
69
  else
62
- run_via_bundle_exec(silent: silent)
70
+ run_via_bundle_exec(silent: silent, verbose: verbose)
63
71
  end
72
+ ensure
73
+ # Clean up environment variable
74
+ ENV.delete("REACT_ON_RAILS_VERBOSE")
64
75
  end
65
76
 
66
77
  def should_run_directly?
@@ -140,14 +151,40 @@ module ReactOnRails
140
151
  # rubocop:enable Style/StderrPuts, Style/GlobalStdStream
141
152
  end
142
153
 
143
- def run_via_bundle_exec(silent: false)
144
- if silent
145
- system(
146
- "bundle", "exec", "rake", "react_on_rails:generate_packs",
147
- out: File::NULL, err: File::NULL
148
- )
154
+ def run_via_bundle_exec(silent: false, verbose: false)
155
+ # Environment variable is already set in run_pack_generation, but we make it explicit here
156
+ # for clarity and to ensure it's passed to the subprocess
157
+ env = { "REACT_ON_RAILS_VERBOSE" => verbose ? "true" : "false" }
158
+
159
+ # Need to unbundle to prevent Bundler from intercepting our bundle exec call
160
+ # when already running inside a Bundler context (e.g., from bin/dev)
161
+ with_unbundled_context do
162
+ if silent
163
+ system(
164
+ env,
165
+ "bundle", "exec", "rake", "react_on_rails:generate_packs",
166
+ out: File::NULL, err: File::NULL
167
+ )
168
+ else
169
+ system(env, "bundle", "exec", "rake", "react_on_rails:generate_packs")
170
+ end
171
+ end
172
+ end
173
+
174
+ # DRY helper method for Bundler context switching with API compatibility
175
+ # Supports both new (with_unbundled_env) and legacy (with_clean_env) Bundler APIs
176
+ def with_unbundled_context(&block)
177
+ if defined?(Bundler)
178
+ if Bundler.respond_to?(:with_unbundled_env)
179
+ Bundler.with_unbundled_env(&block)
180
+ elsif Bundler.respond_to?(:with_clean_env)
181
+ Bundler.with_clean_env(&block)
182
+ else
183
+ # Fallback if neither method is available (very old Bundler versions)
184
+ yield
185
+ end
149
186
  else
150
- system("bundle", "exec", "rake", "react_on_rails:generate_packs")
187
+ yield
151
188
  end
152
189
  end
153
190
  end
@@ -226,7 +226,7 @@ module ReactOnRails
226
226
  }
227
227
  }
228
228
 
229
- consoleReplayScript = ReactOnRails.buildConsoleReplay();
229
+ consoleReplayScript = ReactOnRails.getConsoleReplayScript();
230
230
 
231
231
  return JSON.stringify({
232
232
  html: htmlResult,
@@ -242,8 +242,9 @@ module ReactOnRails
242
242
  .server_render_js_with_console_logging(js_code, render_options)
243
243
 
244
244
  html = result["html"]
245
- console_log_script = result["consoleLogScript"]
246
- raw("#{html}#{console_log_script if render_options.replay_console}")
245
+ console_script = result["consoleReplayScript"]
246
+ console_script_tag = wrap_console_script_with_nonce(console_script) if render_options.replay_console
247
+ raw("#{html}#{console_script_tag}")
247
248
  rescue ExecJS::ProgramError => err
248
249
  raise ReactOnRails::PrerenderError.new(component_name: "N/A (server_render_js called)",
249
250
  err: err,
@@ -394,7 +395,7 @@ module ReactOnRails
394
395
  server_rendered_html.html_safe,
395
396
  content_tag_options)
396
397
 
397
- result_console_script = render_options.replay_console ? console_script : ""
398
+ result_console_script = render_options.replay_console ? wrap_console_script_with_nonce(console_script) : ""
398
399
  result = compose_react_component_html_with_spec_and_console(
399
400
  component_specification_tag, rendered_output, result_console_script
400
401
  )
@@ -419,7 +420,7 @@ module ReactOnRails
419
420
  server_rendered_html[COMPONENT_HTML_KEY].html_safe,
420
421
  content_tag_options)
421
422
 
422
- result_console_script = render_options.replay_console ? console_script : ""
423
+ result_console_script = render_options.replay_console ? wrap_console_script_with_nonce(console_script) : ""
423
424
  result = compose_react_component_html_with_spec_and_console(
424
425
  component_specification_tag, rendered_output, result_console_script
425
426
  )
@@ -436,6 +437,32 @@ module ReactOnRails
436
437
  )
437
438
  end
438
439
 
440
+ # Wraps console replay JavaScript code in a script tag with CSP nonce if available.
441
+ # The console_script_code is already sanitized by scriptSanitizedVal() in the JavaScript layer,
442
+ # so using html_safe here is secure.
443
+ def wrap_console_script_with_nonce(console_script_code)
444
+ return "" if console_script_code.blank?
445
+
446
+ # Get the CSP nonce if available (Rails 5.2+)
447
+ # Rails 5.2-6.0 use content_security_policy_nonce with no arguments
448
+ # Rails 6.1+ accept an optional directive argument
449
+ nonce = if respond_to?(:content_security_policy_nonce)
450
+ begin
451
+ content_security_policy_nonce(:script)
452
+ rescue ArgumentError
453
+ # Fallback for Rails versions that don't accept arguments
454
+ content_security_policy_nonce
455
+ end
456
+ end
457
+
458
+ # Build the script tag with nonce if available
459
+ script_options = { id: "consoleReplayLog" }
460
+ script_options[:nonce] = nonce if nonce.present?
461
+
462
+ # Safe to use html_safe because content is pre-sanitized via scriptSanitizedVal()
463
+ content_tag(:script, console_script_code.html_safe, script_options)
464
+ end
465
+
439
466
  def compose_react_component_html_with_spec_and_console(component_specification_tag, rendered_output,
440
467
  console_script)
441
468
  # IMPORTANT: Ensure that we mark string as html_safe to avoid escaping.
@@ -25,40 +25,42 @@ module ReactOnRails
25
25
  def generate_packs_if_stale
26
26
  return unless ReactOnRails.configuration.auto_load_bundle
27
27
 
28
+ verbose = ENV["REACT_ON_RAILS_VERBOSE"] == "true"
29
+
28
30
  add_generated_pack_to_server_bundle
29
31
 
30
32
  # Clean any non-generated files from directories
31
- clean_non_generated_files_with_feedback
33
+ clean_non_generated_files_with_feedback(verbose: verbose)
32
34
 
33
35
  are_generated_files_present_and_up_to_date = Dir.exist?(generated_packs_directory_path) &&
34
36
  File.exist?(generated_server_bundle_file_path) &&
35
37
  !stale_or_missing_packs?
36
38
 
37
39
  if are_generated_files_present_and_up_to_date
38
- puts Rainbow("✅ Generated packs are up to date, no regeneration needed").green
40
+ puts Rainbow("✅ Generated packs are up to date, no regeneration needed").green if verbose
39
41
  return
40
42
  end
41
43
 
42
- clean_generated_directories_with_feedback
43
- generate_packs
44
+ clean_generated_directories_with_feedback(verbose: verbose)
45
+ generate_packs(verbose: verbose)
44
46
  end
45
47
 
46
48
  private
47
49
 
48
- def generate_packs
49
- common_component_to_path.each_value { |component_path| create_pack(component_path) }
50
- client_component_to_path.each_value { |component_path| create_pack(component_path) }
50
+ def generate_packs(verbose: false)
51
+ common_component_to_path.each_value { |component_path| create_pack(component_path, verbose: verbose) }
52
+ client_component_to_path.each_value { |component_path| create_pack(component_path, verbose: verbose) }
51
53
 
52
- create_server_pack if ReactOnRails.configuration.server_bundle_js_file.present?
54
+ create_server_pack(verbose: verbose) if ReactOnRails.configuration.server_bundle_js_file.present?
53
55
  end
54
56
 
55
- def create_pack(file_path)
57
+ def create_pack(file_path, verbose: false)
56
58
  output_path = generated_pack_path(file_path)
57
59
  content = pack_file_contents(file_path)
58
60
 
59
61
  File.write(output_path, content)
60
62
 
61
- puts(Rainbow("Generated Packs: #{output_path}").yellow)
63
+ puts(Rainbow("Generated Packs: #{output_path}").yellow) if verbose
62
64
  end
63
65
 
64
66
  def first_js_statement_in_code(content) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
@@ -126,11 +128,11 @@ module ReactOnRails
126
128
  FILE_CONTENT
127
129
  end
128
130
 
129
- def create_server_pack
131
+ def create_server_pack(verbose: false)
130
132
  File.write(generated_server_bundle_file_path, generated_server_pack_file_content)
131
133
 
132
134
  add_generated_pack_to_server_bundle
133
- puts(Rainbow("Generated Server Bundle: #{generated_server_bundle_file_path}").orange)
135
+ puts(Rainbow("Generated Server Bundle: #{generated_server_bundle_file_path}").orange) if verbose
134
136
  end
135
137
 
136
138
  def build_server_pack_content(component_on_server_imports, server_components, client_components)
@@ -200,17 +202,17 @@ module ReactOnRails
200
202
  "#{generated_nonentrypoints_path}/#{generated_server_bundle_file_name}.js"
201
203
  end
202
204
 
203
- def clean_non_generated_files_with_feedback
205
+ def clean_non_generated_files_with_feedback(verbose: false)
204
206
  directories_to_clean = [generated_packs_directory_path, generated_server_bundle_directory_path].compact.uniq
205
207
  expected_files = build_expected_files_set
206
208
 
207
- puts Rainbow("🧹 Cleaning non-generated files...").yellow
209
+ puts Rainbow("🧹 Cleaning non-generated files...").yellow if verbose
208
210
 
209
211
  total_deleted = directories_to_clean.sum do |dir_path|
210
- clean_unexpected_files_from_directory(dir_path, expected_files)
212
+ clean_unexpected_files_from_directory(dir_path, expected_files, verbose: verbose)
211
213
  end
212
214
 
213
- display_cleanup_summary(total_deleted)
215
+ display_cleanup_summary(total_deleted, verbose: verbose) if verbose
214
216
  end
215
217
 
216
218
  def build_expected_files_set
@@ -225,17 +227,17 @@ module ReactOnRails
225
227
  { pack_files: expected_pack_files, server_bundle: expected_server_bundle }
226
228
  end
227
229
 
228
- def clean_unexpected_files_from_directory(dir_path, expected_files)
230
+ def clean_unexpected_files_from_directory(dir_path, expected_files, verbose: false)
229
231
  return 0 unless Dir.exist?(dir_path)
230
232
 
231
233
  existing_files = Dir.glob("#{dir_path}/**/*").select { |f| File.file?(f) }
232
234
  unexpected_files = find_unexpected_files(existing_files, dir_path, expected_files)
233
235
 
234
236
  if unexpected_files.any?
235
- delete_unexpected_files(unexpected_files, dir_path)
237
+ delete_unexpected_files(unexpected_files, dir_path, verbose: verbose)
236
238
  unexpected_files.length
237
239
  else
238
- puts Rainbow(" No unexpected files found in #{dir_path}").cyan
240
+ puts Rainbow(" No unexpected files found in #{dir_path}").cyan if verbose
239
241
  0
240
242
  end
241
243
  end
@@ -250,15 +252,21 @@ module ReactOnRails
250
252
  end
251
253
  end
252
254
 
253
- def delete_unexpected_files(unexpected_files, dir_path)
254
- puts Rainbow(" Deleting #{unexpected_files.length} unexpected files from #{dir_path}:").cyan
255
- unexpected_files.each do |file|
256
- puts Rainbow(" - #{File.basename(file)}").blue
257
- File.delete(file)
255
+ def delete_unexpected_files(unexpected_files, dir_path, verbose: false)
256
+ if verbose
257
+ puts Rainbow(" Deleting #{unexpected_files.length} unexpected files from #{dir_path}:").cyan
258
+ unexpected_files.each do |file|
259
+ puts Rainbow(" - #{File.basename(file)}").blue
260
+ File.delete(file)
261
+ end
262
+ else
263
+ unexpected_files.each { |file| File.delete(file) }
258
264
  end
259
265
  end
260
266
 
261
- def display_cleanup_summary(total_deleted)
267
+ def display_cleanup_summary(total_deleted, verbose: false)
268
+ return unless verbose
269
+
262
270
  if total_deleted.positive?
263
271
  puts Rainbow("🗑️ Deleted #{total_deleted} unexpected files total").red
264
272
  else
@@ -266,15 +274,17 @@ module ReactOnRails
266
274
  end
267
275
  end
268
276
 
269
- def clean_generated_directories_with_feedback
277
+ def clean_generated_directories_with_feedback(verbose: false)
270
278
  directories_to_clean = [
271
279
  generated_packs_directory_path,
272
280
  generated_server_bundle_directory_path
273
281
  ].compact.uniq
274
282
 
275
- puts Rainbow("🧹 Cleaning generated directories...").yellow
283
+ puts Rainbow("🧹 Cleaning generated directories...").yellow if verbose
284
+
285
+ total_deleted = directories_to_clean.sum { |dir_path| clean_directory_with_feedback(dir_path, verbose: verbose) }
276
286
 
277
- total_deleted = directories_to_clean.sum { |dir_path| clean_directory_with_feedback(dir_path) }
287
+ return unless verbose
278
288
 
279
289
  if total_deleted.positive?
280
290
  puts Rainbow("🗑️ Deleted #{total_deleted} generated files total").red
@@ -283,27 +293,29 @@ module ReactOnRails
283
293
  end
284
294
  end
285
295
 
286
- def clean_directory_with_feedback(dir_path)
287
- return create_directory_with_feedback(dir_path) unless Dir.exist?(dir_path)
296
+ def clean_directory_with_feedback(dir_path, verbose: false)
297
+ return create_directory_with_feedback(dir_path, verbose: verbose) unless Dir.exist?(dir_path)
288
298
 
289
299
  files = Dir.glob("#{dir_path}/**/*").select { |f| File.file?(f) }
290
300
 
291
301
  if files.any?
292
- puts Rainbow(" Deleting #{files.length} files from #{dir_path}:").cyan
293
- files.each { |file| puts Rainbow(" - #{File.basename(file)}").blue }
302
+ if verbose
303
+ puts Rainbow(" Deleting #{files.length} files from #{dir_path}:").cyan
304
+ files.each { |file| puts Rainbow(" - #{File.basename(file)}").blue }
305
+ end
294
306
  FileUtils.rm_rf(dir_path)
295
307
  FileUtils.mkdir_p(dir_path)
296
308
  files.length
297
309
  else
298
- puts Rainbow(" Directory #{dir_path} is already empty").cyan
310
+ puts Rainbow(" Directory #{dir_path} is already empty").cyan if verbose
299
311
  FileUtils.rm_rf(dir_path)
300
312
  FileUtils.mkdir_p(dir_path)
301
313
  0
302
314
  end
303
315
  end
304
316
 
305
- def create_directory_with_feedback(dir_path)
306
- puts Rainbow(" Directory #{dir_path} does not exist, creating...").cyan
317
+ def create_directory_with_feedback(dir_path, verbose: false)
318
+ puts Rainbow(" Directory #{dir_path} does not exist, creating...").cyan if verbose
307
319
  FileUtils.mkdir_p(dir_path)
308
320
  0
309
321
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ReactOnRails
4
- VERSION = "16.2.0.beta.11"
4
+ VERSION = "16.2.0.beta.12"
5
5
  end
@@ -17,18 +17,24 @@ namespace :react_on_rails do
17
17
  DESC
18
18
 
19
19
  task generate_packs: :environment do
20
- puts Rainbow("🚀 Starting React on Rails pack generation...").bold
21
- puts Rainbow("📁 Auto-load bundle: #{ReactOnRails.configuration.auto_load_bundle}").cyan
22
- puts Rainbow("📂 Components subdirectory: #{ReactOnRails.configuration.components_subdirectory}").cyan
23
- puts ""
20
+ verbose = ENV["REACT_ON_RAILS_VERBOSE"] == "true"
21
+
22
+ if verbose
23
+ puts Rainbow("🚀 Starting React on Rails pack generation...").bold
24
+ puts Rainbow("📁 Auto-load bundle: #{ReactOnRails.configuration.auto_load_bundle}").cyan
25
+ puts Rainbow("📂 Components subdirectory: #{ReactOnRails.configuration.components_subdirectory}").cyan
26
+ puts ""
27
+ end
24
28
 
25
29
  begin
26
30
  start_time = Time.now
27
31
  ReactOnRails::PacksGenerator.instance.generate_packs_if_stale
28
32
  end_time = Time.now
29
33
 
30
- puts ""
31
- puts Rainbow("✨ Pack generation completed in #{((end_time - start_time) * 1000).round(1)}ms").green
34
+ if verbose
35
+ puts ""
36
+ puts Rainbow("✨ Pack generation completed in #{((end_time - start_time) * 1000).round(1)}ms").green
37
+ end
32
38
  rescue ReactOnRails::Error => e
33
39
  handle_react_on_rails_error(e)
34
40
  exit 1
@@ -20,13 +20,12 @@ You can find the **package** version numbers from this repo's tags and below in
20
20
  _Add changes in master not yet tagged._
21
21
 
22
22
  ### Improved
23
- - Significantly improved streaming performance by processing React components concurrently instead of sequentially. This reduces latency and improves responsiveness when using `stream_view_containing_react_components`.
24
23
 
25
- ### Added
26
- - Added `config.concurrent_component_streaming_buffer_size` configuration option to control the memory buffer size for concurrent component streaming (defaults to 64). This allows fine-tuning of memory usage vs. performance for streaming applications.
24
+ - **Concurrent Streaming Performance**: Implemented concurrent draining of streamed React components using the async gem. Instead of processing components sequentially, the system now uses a producer-consumer pattern with bounded buffering to allow multiple components to stream simultaneously while maintaining per-component chunk ordering. This significantly reduces latency and improves responsiveness when using `stream_view_containing_react_components`. [PR 2015](https://github.com/shakacode/react_on_rails/pull/2015) by [ihabadham](https://github.com/ihabadham).
27
25
 
28
26
  ### Added
29
27
 
28
+ - Added `config.concurrent_component_streaming_buffer_size` configuration option to control the memory buffer size for concurrent component streaming (defaults to 64). This allows fine-tuning of memory usage vs. performance for streaming applications.
30
29
  - Added `cached_stream_react_component` helper method, similar to `cached_react_component` but for streamed components.
31
30
  - **License Validation System**: Implemented comprehensive JWT-based license validation with offline verification using RSA-256 signatures. License validation occurs at startup in both Ruby and Node.js environments. Supports required fields (`sub`, `iat`, `exp`) and optional fields (`plan`, `organization`, `iss`). FREE evaluation licenses are available for 3 months at [shakacode.com/react-on-rails-pro](https://shakacode.com/react-on-rails-pro). [PR #1857](https://github.com/shakacode/react_on_rails/pull/1857) by [AbanoubGhadban](https://github.com/AbanoubGhadban).
32
31
  - **Pro-Specific Configurations Moved from Open-Source**: The following React Server Components (RSC) configurations are now exclusively in the Pro gem and should be configured in `ReactOnRailsPro.configure`:
@@ -46,9 +45,17 @@ _Add changes in master not yet tagged._
46
45
 
47
46
  - **Node Renderer Gem Version Validation**: The node renderer now validates that the Ruby gem version (`react_on_rails_pro`) matches the node renderer package version (`@shakacode-tools/react-on-rails-pro-node-renderer`) on every render request. Environment-aware: strict enforcement in development (returns 412 Precondition Failed on mismatch), permissive in production (allows with warning). Includes version normalization to handle Ruby gem vs NPM format differences (e.g., `4.0.0.rc.1` vs `4.0.0-rc.1`). [PR #1881](https://github.com/shakacode/react_on_rails/pull/1881) by [AbanoubGhadban](https://github.com/AbanoubGhadban).
48
47
 
48
+ ### Fixed
49
+
50
+ - **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).
51
+
52
+ - **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).
53
+
49
54
  ### Changed
50
55
 
51
- - Renamed Node Renderer configuration option `bundlePath` to `serverBundleCachePath` to better clarify its purpose as a cache directory for uploaded server bundles, distinct from Shakapacker's public asset directory. The old `bundlePath` property and `RENDERER_BUNDLE_PATH` environment variable continue to work with deprecation warnings. [PR 2008](https://github.com/shakacode/react_on_rails/pull/2008) by [justin808](https://github.com/justin808).
56
+ ### Deprecated
57
+
58
+ - **Node Renderer Configuration**: Renamed `bundlePath` configuration option to `serverBundleCachePath` in the node renderer to better describe its purpose and avoid confusion with Shakapacker's public bundle path. The old `bundlePath` option continues to work with deprecation warnings. Both `RENDERER_SERVER_BUNDLE_CACHE_PATH` (new) and `RENDERER_BUNDLE_PATH` (deprecated) environment variables are supported. [PR 2008](https://github.com/shakacode/react_on_rails/pull/2008) by [justin808](https://github.com/justin808).
52
59
 
53
60
  ### Changed (Breaking)
54
61
 
@@ -9,7 +9,7 @@ GIT
9
9
  PATH
10
10
  remote: ..
11
11
  specs:
12
- react_on_rails (16.2.0.beta.11)
12
+ react_on_rails (16.2.0.beta.12)
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.11)
23
+ react_on_rails_pro (16.2.0.beta.12)
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.11)
31
+ react_on_rails (= 16.2.0.beta.12)
32
32
 
33
33
  GEM
34
34
  remote: https://rubygems.org/
@@ -347,7 +347,8 @@ module ReactOnRailsProHelper
347
347
  render_options: render_options
348
348
  )
349
349
  else
350
- result_console_script = render_options.replay_console ? chunk_json_result["consoleReplayScript"] : ""
350
+ console_script = chunk_json_result["consoleReplayScript"]
351
+ result_console_script = render_options.replay_console ? wrap_console_script_with_nonce(console_script) : ""
351
352
  # No need to prepend component_specification_tag or add rails context again
352
353
  # as they're already included in the first chunk
353
354
  compose_react_component_html_with_spec_and_console(
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ReactOnRailsPro
4
- VERSION = "16.2.0.beta.11"
4
+ VERSION = "16.2.0.beta.12"
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.11",
3
+ "version": "16.2.0-beta.12",
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.11)
12
+ react_on_rails (16.2.0.beta.12)
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.11)
23
+ react_on_rails_pro (16.2.0.beta.12)
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.11)
31
+ react_on_rails (= 16.2.0.beta.12)
32
32
 
33
33
  GEM
34
34
  remote: https://rubygems.org/
@@ -295,13 +295,13 @@ describe ReactOnRailsProHelper do
295
295
  let(:chunks) do
296
296
  [
297
297
  { html: "<div>Chunk 1: Stream React Server Components</div>",
298
- consoleReplayScript: "<script>console.log.apply(console, " \
299
- "['Chunk 1: Console Message'])</script>" },
298
+ consoleReplayScript: "console.log.apply(console, " \
299
+ "['Chunk 1: Console Message'])" },
300
300
  { html: "<div>Chunk 2: More content</div>",
301
- consoleReplayScript: "<script>console.log.apply(console, " \
301
+ consoleReplayScript: "console.log.apply(console, " \
302
302
  "['Chunk 2: Console Message']);\n" \
303
303
  "console.error.apply(console, " \
304
- "['Chunk 2: Console Error']);</script>" },
304
+ "['Chunk 2: Console Error']);" },
305
305
  { html: "<div>Chunk 3: Final content</div>", consoleReplayScript: "" }
306
306
  ]
307
307
  end
@@ -373,7 +373,12 @@ describe ReactOnRailsProHelper do
373
373
  mock_request_and_response
374
374
  initial_result = stream_react_component(component_name, props: props, **component_options)
375
375
  expect(initial_result).to include(react_component_div_with_initial_chunk)
376
- expect(initial_result).to include(chunks.first[:consoleReplayScript])
376
+ # consoleReplayScript is now wrapped in a script tag with id="consoleReplayLog"
377
+ if chunks.first[:consoleReplayScript].present?
378
+ script = chunks.first[:consoleReplayScript]
379
+ wrapped = "<script id=\"consoleReplayLog\">#{script}</script>"
380
+ expect(initial_result).to include(wrapped)
381
+ end
377
382
  expect(initial_result).not_to include("More content", "Final content")
378
383
  expect(chunks_read.count).to eq(1)
379
384
  end
@@ -386,9 +391,12 @@ describe ReactOnRailsProHelper do
386
391
  expect(fiber).to be_alive
387
392
 
388
393
  second_result = fiber.resume
389
- # regex that matches the html and consoleReplayScript and allows for any amount of whitespace between them
394
+ # regex that matches the html and wrapped consoleReplayScript
395
+ # Note: consoleReplayScript is now wrapped in a script tag with id="consoleReplayLog"
396
+ script = chunks[1][:consoleReplayScript]
397
+ wrapped = script.present? ? "<script id=\"consoleReplayLog\">#{script}</script>" : ""
390
398
  expect(second_result).to match(
391
- /#{Regexp.escape(chunks[1][:html])}\s+#{Regexp.escape(chunks[1][:consoleReplayScript])}/
399
+ /#{Regexp.escape(chunks[1][:html])}\s+#{Regexp.escape(wrapped)}/
392
400
  )
393
401
  expect(second_result).not_to include("Stream React Server Components", "Final content")
394
402
  expect(chunks_read.count).to eq(2)
@@ -21,15 +21,20 @@ describe "Console logging from server" do
21
21
  console.log.apply(console, ["[SERVER] Script4\\"</div>\\"(/script <script>alert('WTF4')(/script>"]);
22
22
  console.log.apply(console, ["[SERVER] Script5:\\"</div>\\"(/script> <script>alert('WTF5')(/script>"]);
23
23
  console.log.apply(console, ["[SERVER] railsContext.serverSide is ","true"]);
24
+ console.log.apply(console, ["[SERVER] RENDERED ReduxSharedStoreApp to dom node with id: ReduxSharedStoreApp-react-component-1"]);
24
25
  JS
25
26
 
26
27
  expected_lines = expected.split("\n")
27
28
 
28
- script_node = html_nodes.css("script#consoleReplayLog")
29
- script_lines = script_node.text.split("\n")
29
+ # When multiple components with replay_console are rendered, each creates its own script tag
30
+ # with id="consoleReplayLog". Nokogiri's .text concatenates them without separators, which
31
+ # breaks parsing. Instead, we explicitly join them with newlines.
32
+ script_nodes = html_nodes.css("script#consoleReplayLog")
33
+ script_text = script_nodes.map(&:text).join("\n")
34
+ script_lines = script_text.split("\n")
30
35
 
31
- # First item is a blank line since expected script starts form "\n":
32
- script_lines.shift
36
+ # Remove leading blank line if present (old format had it, new format doesn't)
37
+ script_lines.shift if script_lines.first && script_lines.first.empty?
33
38
 
34
39
  # Create external iterators for expected and found console replay script lines:
35
40
  expected_lines_iterator = expected_lines.to_enum
@@ -5,7 +5,7 @@ module ReactOnRails
5
5
 
6
6
  private
7
7
 
8
- def self.run_pack_generation: (?silent: bool) -> bool
8
+ def self.run_pack_generation: (?silent: bool, ?verbose: bool) -> bool
9
9
  def self.should_run_directly?: () -> bool
10
10
  def self.rails_available?: () -> bool
11
11
  def self.run_rake_task_directly: (?silent: bool) -> bool
@@ -13,7 +13,7 @@ module ReactOnRails
13
13
  def self.prepare_rake_task: () -> untyped
14
14
  def self.capture_output: (bool) { () -> bool } -> bool
15
15
  def self.handle_rake_error: (Exception, bool) -> void
16
- def self.run_via_bundle_exec: (?silent: bool) -> (bool | nil)
16
+ def self.run_via_bundle_exec: (?silent: bool, ?verbose: bool) -> (bool | nil)
17
17
  end
18
18
  end
19
19
  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.11
4
+ version: 16.2.0.beta.12
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-20 00:00:00.000000000 Z
11
+ date: 2025-11-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: addressable
@@ -141,6 +141,7 @@ files:
141
141
  - TODO.md
142
142
  - WARP.md
143
143
  - analysis/rake-task-duplicate-analysis.md
144
+ - analysis/v8-crash-retry-solution.md
144
145
  - app/helpers/react_on_rails_helper.rb
145
146
  - bin/ci-local
146
147
  - bin/ci-rerun-failures