react_on_rails_pro 16.6.0 → 16.7.0.rc.0
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 +4 -4
- data/.controlplane/rails.yml +2 -2
- data/CLAUDE.md +10 -0
- data/CONTRIBUTING.md +2 -2
- data/Gemfile.development_dependencies +20 -13
- data/Gemfile.lock +12 -28
- data/Rakefile +0 -5
- data/app/helpers/react_on_rails_pro_helper.rb +28 -1
- data/lib/react_on_rails_pro/assets_precompile.rb +170 -1
- data/lib/react_on_rails_pro/async_props_emitter.rb +80 -0
- data/lib/react_on_rails_pro/concerns/stream.rb +1 -1
- data/lib/react_on_rails_pro/configuration.rb +114 -17
- data/lib/react_on_rails_pro/engine.rb +10 -0
- data/lib/react_on_rails_pro/httpx_stream_bidi_patch.rb +42 -0
- data/lib/react_on_rails_pro/js_code_builder.rb +121 -0
- data/lib/react_on_rails_pro/pre_seed_renderer_cache.rb +148 -0
- data/lib/react_on_rails_pro/prepare_node_renderer_bundles.rb +33 -28
- data/lib/react_on_rails_pro/renderer_cache_helpers.rb +276 -0
- data/lib/react_on_rails_pro/renderer_cache_path.rb +74 -0
- data/lib/react_on_rails_pro/rendering_strategy/node_strategy.rb +29 -0
- data/lib/react_on_rails_pro/request.rb +135 -8
- data/lib/react_on_rails_pro/rolling_deploy_cache_stager.rb +516 -0
- data/lib/react_on_rails_pro/server_rendering_js_code.rb +47 -10
- data/lib/react_on_rails_pro/server_rendering_pool/node_rendering_pool.rb +35 -10
- data/lib/react_on_rails_pro/stream_request.rb +42 -49
- data/lib/react_on_rails_pro/utils.rb +7 -9
- data/lib/react_on_rails_pro/version.rb +1 -1
- data/lib/react_on_rails_pro.rb +8 -0
- data/lib/tasks/assets.rake +36 -3
- data/rakelib/run_rspec.rake +6 -6
- data/react_on_rails_pro.gemspec +9 -4
- data/sig/react_on_rails_pro/configuration.rbs +2 -0
- metadata +35 -22
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c4063509561a2dee0f173ad74812beea1ae6014f59388e29d54299b0a4644071
|
|
4
|
+
data.tar.gz: 84ac0c7d8c79a65f42e3651c543d74723662001584df47bb4923cceca2e4c10d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f37225bab9df92d9fa7dbc9bd136b88daedf9f913f0fae2cd46f0ab16bc7362a4e9060bf8e0cd81b5ecb39bf8ef6ffd262572afda5a3fa962cc7f1168a0b352e
|
|
7
|
+
data.tar.gz: a466711a910920add21ce39477319a8ab9cd94e94bf9e75f04dbbfdfefaae891fb7d6fb3e024efd1db0c7f5756273ac87dc2a613c744c82743fcef08b7b3c2da
|
data/.controlplane/rails.yml
CHANGED
|
@@ -20,7 +20,7 @@ spec:
|
|
|
20
20
|
protocol: http
|
|
21
21
|
- name: node-renderer
|
|
22
22
|
args:
|
|
23
|
-
-
|
|
23
|
+
- renderer/node-renderer.js
|
|
24
24
|
command: node
|
|
25
25
|
cpu: 512m
|
|
26
26
|
env:
|
|
@@ -46,4 +46,4 @@ spec:
|
|
|
46
46
|
- 0.0.0.0/0
|
|
47
47
|
# Could configure outbound for more security
|
|
48
48
|
outboundAllowCIDR:
|
|
49
|
-
- 0.0.0.0/0
|
|
49
|
+
- 0.0.0.0/0
|
data/CLAUDE.md
CHANGED
|
@@ -58,6 +58,16 @@ The node renderer is a standalone Fastify HTTP server (separate Node.js process)
|
|
|
58
58
|
- Integrations: Sentry, Honeybadger (optional peer deps)
|
|
59
59
|
- Protocol versioning: `protocolVersion` in package.json must match gem expectations
|
|
60
60
|
|
|
61
|
+
**Validating source changes against the dummy app:** the dummy consumes the _built_
|
|
62
|
+
`packages/react-on-rails-pro-node-renderer/lib/**`, so edits under `src/**` are not
|
|
63
|
+
picked up until the package is rebuilt. Use one of:
|
|
64
|
+
|
|
65
|
+
- `pnpm --filter react-on-rails-pro-node-renderer run build` (one-shot)
|
|
66
|
+
- `cd react_on_rails_pro/spec/dummy && pnpm run node-renderer:fresh` (rebuild + start)
|
|
67
|
+
- `pnpm --filter react-on-rails-pro-node-renderer run build-watch` (watch in another shell)
|
|
68
|
+
|
|
69
|
+
See `.claude/docs/validating-node-renderer-changes.md` for the full checklist.
|
|
70
|
+
|
|
61
71
|
### Yalc Dependency Chain
|
|
62
72
|
|
|
63
73
|
Pro dummy's preinstall builds and links packages in this order:
|
data/CONTRIBUTING.md
CHANGED
|
@@ -55,7 +55,7 @@ For links from docs pages to non-doc files, use full GitHub URLs so links resolv
|
|
|
55
55
|
`[Installation Guide](../docs/pro/installation.md)`
|
|
56
56
|
|
|
57
57
|
- When making references to source code files, use a full url path like:
|
|
58
|
-
`[spec/dummy/config/initializers/react_on_rails.rb](https://github.com/shakacode/react_on_rails/
|
|
58
|
+
`[spec/dummy/config/initializers/react_on_rails.rb](https://github.com/shakacode/react_on_rails/blob/main/react_on_rails_pro/spec/dummy/config/initializers/react_on_rails.rb)`
|
|
59
59
|
|
|
60
60
|
## To run tests:
|
|
61
61
|
|
|
@@ -347,7 +347,7 @@ bundle exec rspec
|
|
|
347
347
|
|
|
348
348
|
If you run `rspec` at the top level, you'll see this message: `require': cannot load such file -- rails_helper (LoadError)`
|
|
349
349
|
|
|
350
|
-
|
|
350
|
+
If you run tests with `COVERAGE=true`, you can view the SimpleCov report at `coverage/index.html`.
|
|
351
351
|
|
|
352
352
|
### Debugging
|
|
353
353
|
|
|
@@ -33,32 +33,39 @@ gem "amazing_print"
|
|
|
33
33
|
|
|
34
34
|
group :development do
|
|
35
35
|
# Access an interactive console on exception pages or by calling 'console' anywhere in the code.
|
|
36
|
-
gem
|
|
37
|
-
gem
|
|
36
|
+
gem "web-console"
|
|
37
|
+
gem "listen"
|
|
38
38
|
# Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
|
|
39
|
-
gem
|
|
40
|
-
gem
|
|
39
|
+
gem "spring"
|
|
40
|
+
gem "spring-watcher-listen"
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
group :development, :test do
|
|
44
|
-
gem
|
|
45
|
-
gem
|
|
46
|
-
gem
|
|
44
|
+
gem "faker"
|
|
45
|
+
gem "graphiql-rails"
|
|
46
|
+
gem "pry", ">= 0.14.1" # Console with powerful introspection capabilities
|
|
47
47
|
# Need to use master of pry-byebug to use latest pry version
|
|
48
|
-
|
|
49
|
-
gem
|
|
50
|
-
gem
|
|
51
|
-
gem
|
|
48
|
+
# Loaded manually in spec_helper.rb so specs can boot on readline-less Ruby builds.
|
|
49
|
+
gem "pry-byebug", github: "shakacode/pry-byebug", require: false # Integrates pry with byebug
|
|
50
|
+
gem "pry-doc" # Provide MRI Core documentation
|
|
51
|
+
gem "pry-rails" # Causes rails console to open pry. `DISABLE_PRY_RAILS=1 rails c` can still open with IRB
|
|
52
|
+
gem "pry-theme" # An easy way to customize Pry colors via theme files
|
|
52
53
|
|
|
53
54
|
gem "rbs", require: false
|
|
54
55
|
gem "scss_lint", require: false
|
|
55
|
-
gem
|
|
56
|
+
gem "fakefs", require: "fakefs/safe"
|
|
57
|
+
|
|
58
|
+
# Ruby 3.5+ removed these from the default gem set; they must now be declared explicitly
|
|
59
|
+
# to avoid `cannot load such file` errors from gems that lazy-require them (e.g. jbuilder).
|
|
60
|
+
gem "benchmark", require: false
|
|
61
|
+
gem "logger", require: false
|
|
62
|
+
gem "ostruct", require: false
|
|
56
63
|
end
|
|
57
64
|
|
|
58
65
|
group :test do
|
|
59
66
|
gem "capybara", ">= 3.38.0"
|
|
60
67
|
gem "capybara-screenshot"
|
|
61
|
-
gem "
|
|
68
|
+
gem "simplecov", "~> 0.16.1", require: false
|
|
62
69
|
gem "equivalent-xml"
|
|
63
70
|
gem "generator_spec"
|
|
64
71
|
gem "launchy"
|
data/Gemfile.lock
CHANGED
|
@@ -9,7 +9,7 @@ GIT
|
|
|
9
9
|
PATH
|
|
10
10
|
remote: ..
|
|
11
11
|
specs:
|
|
12
|
-
react_on_rails (16.
|
|
12
|
+
react_on_rails (16.7.0.rc.0)
|
|
13
13
|
addressable
|
|
14
14
|
connection_pool
|
|
15
15
|
execjs (~> 2.5)
|
|
@@ -20,16 +20,16 @@ PATH
|
|
|
20
20
|
PATH
|
|
21
21
|
remote: .
|
|
22
22
|
specs:
|
|
23
|
-
react_on_rails_pro (16.
|
|
23
|
+
react_on_rails_pro (16.7.0.rc.0)
|
|
24
24
|
addressable
|
|
25
25
|
async (>= 2.29)
|
|
26
26
|
connection_pool
|
|
27
27
|
execjs (~> 2.9)
|
|
28
28
|
http-2 (>= 1.1.1)
|
|
29
29
|
httpx (~> 1.5)
|
|
30
|
-
jwt (
|
|
30
|
+
jwt (>= 3.2.0)
|
|
31
31
|
rainbow
|
|
32
|
-
react_on_rails (= 16.
|
|
32
|
+
react_on_rails (= 16.7.0.rc.0)
|
|
33
33
|
|
|
34
34
|
GEM
|
|
35
35
|
remote: https://rubygems.org/
|
|
@@ -149,12 +149,6 @@ GEM
|
|
|
149
149
|
fiber-annotation
|
|
150
150
|
fiber-local (~> 1.1)
|
|
151
151
|
json
|
|
152
|
-
coveralls (0.8.23)
|
|
153
|
-
json (>= 1.8, < 3)
|
|
154
|
-
simplecov (~> 0.16.1)
|
|
155
|
-
term-ansicolor (~> 1.3)
|
|
156
|
-
thor (>= 0.19.4, < 2.0)
|
|
157
|
-
tins (~> 1.6)
|
|
158
152
|
crack (1.0.0)
|
|
159
153
|
bigdecimal
|
|
160
154
|
rexml
|
|
@@ -188,7 +182,7 @@ GEM
|
|
|
188
182
|
railties
|
|
189
183
|
hashdiff (1.1.0)
|
|
190
184
|
http-2 (1.1.1)
|
|
191
|
-
httpx (1.
|
|
185
|
+
httpx (1.7.0)
|
|
192
186
|
http-2 (>= 1.0.0)
|
|
193
187
|
i18n (1.14.8)
|
|
194
188
|
concurrent-ruby (~> 1.0)
|
|
@@ -207,7 +201,7 @@ GEM
|
|
|
207
201
|
railties (>= 4.2.0)
|
|
208
202
|
thor (>= 0.14, < 2.0)
|
|
209
203
|
json (2.17.1)
|
|
210
|
-
jwt (2.
|
|
204
|
+
jwt (3.2.0)
|
|
211
205
|
base64
|
|
212
206
|
language_server-protocol (3.17.0.5)
|
|
213
207
|
launchy (3.0.1)
|
|
@@ -234,8 +228,6 @@ GEM
|
|
|
234
228
|
minitest (6.0.2)
|
|
235
229
|
drb (~> 2.0)
|
|
236
230
|
prism (~> 1.5)
|
|
237
|
-
mize (0.4.1)
|
|
238
|
-
protocol (~> 2.0)
|
|
239
231
|
msgpack (1.7.2)
|
|
240
232
|
net-http (0.4.1)
|
|
241
233
|
uri
|
|
@@ -255,6 +247,7 @@ GEM
|
|
|
255
247
|
racc (~> 1.4)
|
|
256
248
|
nokogiri (1.19.2-x86_64-linux-gnu)
|
|
257
249
|
racc (~> 1.4)
|
|
250
|
+
ostruct (0.6.3)
|
|
258
251
|
package_json (0.2.0)
|
|
259
252
|
parallel (1.27.0)
|
|
260
253
|
parser (3.3.10.0)
|
|
@@ -265,8 +258,6 @@ GEM
|
|
|
265
258
|
prettyprint
|
|
266
259
|
prettyprint (0.2.0)
|
|
267
260
|
prism (1.9.0)
|
|
268
|
-
protocol (2.0.0)
|
|
269
|
-
ruby_parser (~> 3.0)
|
|
270
261
|
pry (0.14.2)
|
|
271
262
|
coderay (~> 1.1)
|
|
272
263
|
method_source (~> 1.0)
|
|
@@ -390,9 +381,6 @@ GEM
|
|
|
390
381
|
rubocop-rspec_rails (2.29.1)
|
|
391
382
|
rubocop (~> 1.61)
|
|
392
383
|
ruby-progressbar (1.13.0)
|
|
393
|
-
ruby_parser (3.21.0)
|
|
394
|
-
racc (~> 1.5)
|
|
395
|
-
sexp_processor (~> 4.16)
|
|
396
384
|
rubyzip (2.3.2)
|
|
397
385
|
sass (3.7.4)
|
|
398
386
|
sass-listen (~> 4.0.0)
|
|
@@ -417,7 +405,6 @@ GEM
|
|
|
417
405
|
rubyzip (>= 1.2.2, < 3.0)
|
|
418
406
|
websocket (~> 1.0)
|
|
419
407
|
semantic_range (3.1.1)
|
|
420
|
-
sexp_processor (4.17.1)
|
|
421
408
|
shakapacker (9.6.1)
|
|
422
409
|
activesupport (>= 5.2)
|
|
423
410
|
package_json
|
|
@@ -444,16 +431,9 @@ GEM
|
|
|
444
431
|
sqlite3 (2.9.2-x86_64-darwin)
|
|
445
432
|
sqlite3 (2.9.2-x86_64-linux-gnu)
|
|
446
433
|
stringio (3.2.0)
|
|
447
|
-
sync (0.5.0)
|
|
448
|
-
term-ansicolor (1.10.2)
|
|
449
|
-
mize
|
|
450
|
-
tins (~> 1.0)
|
|
451
434
|
thor (1.5.0)
|
|
452
435
|
tilt (2.4.0)
|
|
453
436
|
timeout (0.4.4)
|
|
454
|
-
tins (1.33.0)
|
|
455
|
-
bigdecimal
|
|
456
|
-
sync
|
|
457
437
|
traces (0.18.2)
|
|
458
438
|
tsort (0.2.0)
|
|
459
439
|
turbolinks (5.2.1)
|
|
@@ -488,6 +468,7 @@ GEM
|
|
|
488
468
|
zeitwerk (2.7.5)
|
|
489
469
|
|
|
490
470
|
PLATFORMS
|
|
471
|
+
arm64-darwin-23
|
|
491
472
|
arm64-darwin-24
|
|
492
473
|
arm64-darwin-25
|
|
493
474
|
x86_64-darwin-24
|
|
@@ -495,12 +476,12 @@ PLATFORMS
|
|
|
495
476
|
|
|
496
477
|
DEPENDENCIES
|
|
497
478
|
amazing_print
|
|
479
|
+
benchmark
|
|
498
480
|
bootsnap
|
|
499
481
|
bundler
|
|
500
482
|
capybara (>= 3.38.0)
|
|
501
483
|
capybara-screenshot
|
|
502
484
|
commonmarker
|
|
503
|
-
coveralls
|
|
504
485
|
equivalent-xml
|
|
505
486
|
fakefs
|
|
506
487
|
faker
|
|
@@ -511,9 +492,11 @@ DEPENDENCIES
|
|
|
511
492
|
jquery-rails
|
|
512
493
|
launchy
|
|
513
494
|
listen
|
|
495
|
+
logger
|
|
514
496
|
net-http
|
|
515
497
|
net-imap
|
|
516
498
|
net-smtp
|
|
499
|
+
ostruct
|
|
517
500
|
pg
|
|
518
501
|
pry (>= 0.14.1)
|
|
519
502
|
pry-byebug!
|
|
@@ -535,6 +518,7 @@ DEPENDENCIES
|
|
|
535
518
|
scss_lint
|
|
536
519
|
selenium-webdriver (= 4.9.0)
|
|
537
520
|
shakapacker (= 9.6.1)
|
|
521
|
+
simplecov (~> 0.16.1)
|
|
538
522
|
spring
|
|
539
523
|
spring-watcher-listen
|
|
540
524
|
sprockets
|
data/Rakefile
CHANGED
|
@@ -3,11 +3,6 @@
|
|
|
3
3
|
# Rake will automatically load any *.rake files inside of the "rakelib" folder
|
|
4
4
|
# See rakelib/
|
|
5
5
|
tasks = %w[run_rspec lint]
|
|
6
|
-
if ENV["USE_COVERALLS"] == "TRUE"
|
|
7
|
-
require "coveralls/rake/task"
|
|
8
|
-
Coveralls::RakeTask.new
|
|
9
|
-
tasks << "coveralls:push"
|
|
10
|
-
end
|
|
11
6
|
|
|
12
7
|
desc "Run all tests and linting"
|
|
13
8
|
task default: tasks
|
|
@@ -141,6 +141,30 @@ module ReactOnRailsProHelper
|
|
|
141
141
|
end
|
|
142
142
|
end
|
|
143
143
|
|
|
144
|
+
def stream_react_component_with_async_props(component_name, options = {}, &props_block)
|
|
145
|
+
unless ReactOnRailsPro.configuration.enable_rsc_support
|
|
146
|
+
raise ReactOnRailsPro::Error,
|
|
147
|
+
"stream_react_component_with_async_props requires enable_rsc_support to be true. " \
|
|
148
|
+
"Async props depend on React Server Components. " \
|
|
149
|
+
"Set `config.enable_rsc_support = true` in your ReactOnRailsPro configuration."
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
options[:async_props_block] = props_block
|
|
153
|
+
stream_react_component(component_name, options)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def rsc_payload_react_component_with_async_props(component_name, options = {}, &props_block)
|
|
157
|
+
unless ReactOnRailsPro.configuration.enable_rsc_support
|
|
158
|
+
raise ReactOnRailsPro::Error,
|
|
159
|
+
"rsc_payload_react_component_with_async_props requires enable_rsc_support to be true. " \
|
|
160
|
+
"Async props depend on React Server Components. " \
|
|
161
|
+
"Set `config.enable_rsc_support = true` in your ReactOnRailsPro configuration."
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
options[:async_props_block] = props_block
|
|
165
|
+
rsc_payload_react_component(component_name, options)
|
|
166
|
+
end
|
|
167
|
+
|
|
144
168
|
# Renders the React Server Component (RSC) payload for a given component. This helper generates
|
|
145
169
|
# a special format designed by React for serializing server components and transmitting them
|
|
146
170
|
# to the client.
|
|
@@ -516,7 +540,10 @@ module ReactOnRailsProHelper
|
|
|
516
540
|
render_options = create_render_options(react_component_name, options)
|
|
517
541
|
json_stream = server_rendered_react_component(render_options)
|
|
518
542
|
json_stream.transform do |chunk|
|
|
519
|
-
|
|
543
|
+
html = chunk.delete("html") || ""
|
|
544
|
+
metadata = chunk.to_json
|
|
545
|
+
content_bytes = html.bytesize.to_s(16).rjust(8, "0")
|
|
546
|
+
"#{metadata}\t#{content_bytes}\n#{html}".html_safe
|
|
520
547
|
end
|
|
521
548
|
end
|
|
522
549
|
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "timeout"
|
|
4
|
+
|
|
3
5
|
module ReactOnRailsPro
|
|
4
6
|
class AssetsPrecompile # rubocop:disable Metrics/ClassLength
|
|
5
7
|
include Singleton
|
|
6
8
|
|
|
9
|
+
# Per-hash upload budget during assets:precompile. With RSC enabled this can
|
|
10
|
+
# be spent twice per deploy (server bundle + RSC bundle).
|
|
11
|
+
UPLOAD_TIMEOUT_SECONDS = 120
|
|
12
|
+
|
|
7
13
|
def remote_bundle_cache_adapter
|
|
8
14
|
unless ReactOnRailsPro.configuration.remote_bundle_cache_adapter.is_a?(Module)
|
|
9
15
|
raise ReactOnRailsPro::Error, "config.remote_bundle_cache_adapter must have a module assigned"
|
|
@@ -62,9 +68,172 @@ module ReactOnRailsPro
|
|
|
62
68
|
|
|
63
69
|
def self.call
|
|
64
70
|
instance.build_or_fetch_bundles
|
|
71
|
+
return unless ReactOnRailsPro.configuration.node_renderer?
|
|
72
|
+
|
|
73
|
+
# Symlink is the same-filesystem default (local dev, CI, Heroku-style same-dyno
|
|
74
|
+
# deploys, bundle-caching restores). Docker image builds that run assets:precompile
|
|
75
|
+
# should set ASSETS_PRECOMPILE_RENDERER_CACHE_MODE=copy to bake the cache into the
|
|
76
|
+
# immutable artifact, or invoke `rake react_on_rails_pro:pre_seed_renderer_cache`
|
|
77
|
+
# directly (which defaults to copy mode).
|
|
78
|
+
ReactOnRailsPro::PreSeedRendererCache.call(mode: pre_seed_renderer_cache_mode)
|
|
79
|
+
|
|
80
|
+
publish_current_bundle_if_configured
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Best-effort publication of the just-built bundles + assets to the configured
|
|
84
|
+
# rolling_deploy_adapter so that the *next* deploy can fetch these hashes as
|
|
85
|
+
# "previous" bundles. Runs only in production-like environments. Errors are
|
|
86
|
+
# warned per-hash, not raised, because a failed upload degrades the next
|
|
87
|
+
# deploy's rolling-deploy seeding — not this deploy's correctness.
|
|
88
|
+
#
|
|
89
|
+
# Protocol: each hash is one bundle's cache entry — when RSC is enabled,
|
|
90
|
+
# upload is called once for the server bundle (under server_bundle_hash)
|
|
91
|
+
# and once for the RSC bundle (under rsc_bundle_hash).
|
|
92
|
+
def self.publish_current_bundle_if_configured
|
|
93
|
+
adapter = ReactOnRailsPro.configuration.rolling_deploy_adapter
|
|
94
|
+
return if adapter.nil?
|
|
95
|
+
# NodeRendererPool.server_bundle_hash is only available under the NodeRenderer
|
|
96
|
+
# renderer mode. With ExecJS, skip publication rather than crash.
|
|
97
|
+
return unless ReactOnRailsPro.configuration.node_renderer?
|
|
98
|
+
return if Rails.env.development? || Rails.env.test?
|
|
99
|
+
|
|
100
|
+
publish_bundles(adapter)
|
|
101
|
+
rescue StandardError => e
|
|
102
|
+
# Outer rescue catches anything raised by the setup-side calls below
|
|
103
|
+
# (collect_assets, server_bundle_hash, rsc_bundle_js_file_path). Per the
|
|
104
|
+
# rolling-deploy contract, a failed upload must degrade the next deploy's
|
|
105
|
+
# seeding — not fail *this* deploy's assets:precompile.
|
|
106
|
+
warn "[ReactOnRailsPro] rolling_deploy_adapter publication failed: #{e.class}: #{e.message}. " \
|
|
107
|
+
"Next deploy's rolling-deploy seeding may degrade; precompile continuing."
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def self.publish_bundles(adapter)
|
|
111
|
+
pool = ReactOnRailsPro::ServerRenderingPool::NodeRenderingPool
|
|
112
|
+
# Companion manifests are generated for the deploy as a whole, so server
|
|
113
|
+
# and RSC hashes from the same build intentionally share this asset set.
|
|
114
|
+
assets = filter_existing_assets(ReactOnRailsPro::RendererCacheHelpers.collect_assets.map(&:to_s))
|
|
65
115
|
|
|
66
|
-
|
|
116
|
+
# Defer the hash computation behind a block: `bundle_hash` reads the bundle
|
|
117
|
+
# file (`File.mtime` in dev/test, `Digest::MD5.file` for non-content-hashed
|
|
118
|
+
# names), so evaluating it eagerly as an argument would let a missing
|
|
119
|
+
# bundle raise and bypass the per-bundle warning path.
|
|
120
|
+
server_bundle = ReactOnRails::Utils.server_bundle_js_file_path
|
|
121
|
+
publish_bundle_if_present(adapter, server_bundle, assets, "server") { pool.server_bundle_hash }
|
|
122
|
+
|
|
123
|
+
return unless ReactOnRailsPro.configuration.enable_rsc_support
|
|
124
|
+
|
|
125
|
+
rsc_bundle = ReactOnRailsPro::Utils.rsc_bundle_js_file_path
|
|
126
|
+
publish_bundle_if_present(adapter, rsc_bundle, assets, "RSC") { pool.rsc_bundle_hash }
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Some collected companion assets may be absent or point at non-file paths.
|
|
130
|
+
# Typical adapters iterate the list and `cp`/open each entry, so forwarding
|
|
131
|
+
# an invalid path would raise and abort the whole hash upload, leaving the
|
|
132
|
+
# next deploy unable to fetch this hash (→ cold 410 retries). Drop invalid
|
|
133
|
+
# entries with a warning so publication still covers the existing assets.
|
|
134
|
+
#
|
|
135
|
+
# Mirrors RendererCacheHelpers.each_stageable_asset: skip URL-backed assets
|
|
136
|
+
# (dev server) and expand relative paths against Rails.root before checking
|
|
137
|
+
# existence. Without the expansion, `assets:precompile` invoked from a
|
|
138
|
+
# non-Rails.root cwd would silently drop relative entries in `assets_to_copy`.
|
|
139
|
+
def self.filter_existing_assets(assets)
|
|
140
|
+
resolvable = assets.reject { |path| ReactOnRailsPro::RendererCacheHelpers.http_url?(path) }
|
|
141
|
+
resolved = resolvable.map { |path| File.expand_path(path.to_s, Rails.root) }
|
|
142
|
+
|
|
143
|
+
existing, invalid = resolved.partition { |path| File.file?(path) }
|
|
144
|
+
return existing if invalid.empty?
|
|
145
|
+
|
|
146
|
+
missing, non_files = invalid.partition { |path| !File.exist?(path) }
|
|
147
|
+
warn_skipped_invalid_assets(existing, missing, non_files)
|
|
148
|
+
warn_if_unavailable_required_rsc_assets(invalid)
|
|
149
|
+
existing
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Combine missing-vs-non-file reasons into a single warning so operators see
|
|
153
|
+
# one entry per skipped batch instead of two near-identical lines. The
|
|
154
|
+
# reason breakdown (missing vs non-file) still appears so adapter authors
|
|
155
|
+
# can tell a deleted asset apart from one that resolved to e.g. a directory.
|
|
156
|
+
def self.warn_skipped_invalid_assets(existing, missing, non_files)
|
|
157
|
+
reasons = []
|
|
158
|
+
reasons << "missing: #{missing.inspect}" unless missing.empty?
|
|
159
|
+
reasons << "not a file: #{non_files.inspect}" unless non_files.empty?
|
|
160
|
+
warn "[ReactOnRailsPro] Skipping invalid assets for rolling_deploy_adapter upload " \
|
|
161
|
+
"(some may be required for RSC) — #{reasons.join('; ')}. " \
|
|
162
|
+
"Continuing with #{existing.length} existing asset(s)."
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Match by full expanded path (filter_existing_assets passes expanded paths
|
|
166
|
+
# in `unavailable_assets`) rather than basename, so an unrelated missing
|
|
167
|
+
# entry in `assets_to_copy` that happens to share a basename with a required
|
|
168
|
+
# RSC manifest can't false-positive this warning when the real required
|
|
169
|
+
# file is present elsewhere.
|
|
170
|
+
def self.warn_if_unavailable_required_rsc_assets(unavailable_assets)
|
|
171
|
+
required_paths = ReactOnRailsPro::RendererCacheHelpers.required_rsc_asset_paths_for_current_config
|
|
172
|
+
missing_required_paths = unavailable_assets.select { |path| required_paths.include?(path) }
|
|
173
|
+
return if missing_required_paths.empty?
|
|
174
|
+
|
|
175
|
+
missing_required = missing_required_paths.map { |path| File.basename(path) }
|
|
176
|
+
warn "[ReactOnRailsPro] WARNING: unavailable assets include required RSC companion file(s) " \
|
|
177
|
+
"#{missing_required.inspect}. The partial entry will be rejected on every subsequent rolling " \
|
|
178
|
+
"deploy that tries to seed this bundle hash for RSC (falling back to 410-retry) until a " \
|
|
179
|
+
"complete precompile with all required RSC companion files overwrites this hash."
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def self.publish_bundle(adapter, hash, bundle, assets, bundle_label)
|
|
183
|
+
if hash.to_s.empty?
|
|
184
|
+
warn "[ReactOnRailsPro] Skipping rolling_deploy_adapter publication for #{bundle_label} bundle " \
|
|
185
|
+
"#{bundle.inspect} because its bundle hash is blank."
|
|
186
|
+
return
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
upload_bundle(adapter, hash, bundle, assets)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def self.publish_bundle_if_present(adapter, bundle, assets, bundle_label)
|
|
193
|
+
if File.file?(bundle)
|
|
194
|
+
publish_bundle(adapter, yield, bundle, assets, bundle_label)
|
|
195
|
+
else
|
|
196
|
+
display_label = bundle_label == "RSC" ? "RSC" : bundle_label.capitalize
|
|
197
|
+
reason = File.exist?(bundle) ? "is not a file" : "does not exist"
|
|
198
|
+
# Use `bundle_label` (the caller's original casing) for the second
|
|
199
|
+
# reference so the warning reads consistently (e.g. "RSC bundle ...
|
|
200
|
+
# skipping ... for RSC bundle" rather than mixing "RSC" and "rsc").
|
|
201
|
+
warn "[ReactOnRailsPro] #{display_label} bundle #{bundle.inspect} #{reason}; " \
|
|
202
|
+
"skipping rolling_deploy_adapter publication for #{bundle_label} bundle."
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def self.upload_bundle(adapter, hash, bundle, assets)
|
|
207
|
+
Timeout.timeout(UPLOAD_TIMEOUT_SECONDS) do
|
|
208
|
+
adapter.upload(hash, bundle: bundle, assets: assets)
|
|
209
|
+
end
|
|
210
|
+
puts "[ReactOnRailsPro] Published bundle hash #{hash} via rolling_deploy_adapter"
|
|
211
|
+
rescue Timeout::Error
|
|
212
|
+
warn "[ReactOnRailsPro] rolling_deploy_adapter#upload for #{hash} timed out after " \
|
|
213
|
+
"#{UPLOAD_TIMEOUT_SECONDS}s. Next deploy's rolling-deploy seeding for this hash may degrade."
|
|
214
|
+
rescue StandardError => e
|
|
215
|
+
warn "[ReactOnRailsPro] rolling_deploy_adapter#upload for #{hash} raised #{e.class}: " \
|
|
216
|
+
"#{e.message}. Next deploy's rolling-deploy seeding for this hash may degrade."
|
|
217
|
+
end
|
|
218
|
+
private_class_method :publish_current_bundle_if_configured,
|
|
219
|
+
:publish_bundles,
|
|
220
|
+
:filter_existing_assets,
|
|
221
|
+
:warn_skipped_invalid_assets,
|
|
222
|
+
:warn_if_unavailable_required_rsc_assets,
|
|
223
|
+
:publish_bundle,
|
|
224
|
+
:publish_bundle_if_present,
|
|
225
|
+
:upload_bundle
|
|
226
|
+
|
|
227
|
+
def self.pre_seed_renderer_cache_mode
|
|
228
|
+
raw = ENV.fetch("ASSETS_PRECOMPILE_RENDERER_CACHE_MODE", "symlink").to_s.downcase
|
|
229
|
+
mode = raw.to_sym
|
|
230
|
+
return mode if ReactOnRailsPro::PreSeedRendererCache::VALID_MODES.include?(mode)
|
|
231
|
+
|
|
232
|
+
valid = ReactOnRailsPro::PreSeedRendererCache::VALID_MODES.map(&:to_s).join(", ")
|
|
233
|
+
raise ReactOnRailsPro::Error,
|
|
234
|
+
"ASSETS_PRECOMPILE_RENDERER_CACHE_MODE must be one of: #{valid} (got #{raw.inspect})"
|
|
67
235
|
end
|
|
236
|
+
private_class_method :pre_seed_renderer_cache_mode
|
|
68
237
|
|
|
69
238
|
def build_or_fetch_bundles
|
|
70
239
|
if disable_precompile_cache?
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ReactOnRailsPro
|
|
4
|
+
# Emitter class for sending async props incrementally during streaming render.
|
|
5
|
+
# Used by stream_react_component_with_async_props helper.
|
|
6
|
+
#
|
|
7
|
+
# PROTOCOL:
|
|
8
|
+
# Each call to `emit.call(prop_name, value)` sends an NDJSON line to the Node renderer:
|
|
9
|
+
# {"bundleTimestamp": "abc123", "updateChunk": "(function(){...})()"}
|
|
10
|
+
#
|
|
11
|
+
# The updateChunk JavaScript accesses the AsyncPropsManager via sharedExecutionContext
|
|
12
|
+
# and resolves the promise for that prop, allowing React to continue rendering.
|
|
13
|
+
#
|
|
14
|
+
# WHY NOT USE GLOBAL VARIABLES?
|
|
15
|
+
# Global variables in Node.js VM persist across requests, causing data leakage.
|
|
16
|
+
# sharedExecutionContext is scoped to a single HTTP request (ExecutionContext).
|
|
17
|
+
#
|
|
18
|
+
# @example Usage in view
|
|
19
|
+
# stream_react_component_with_async_props("Dashboard") do |emit|
|
|
20
|
+
# emit.call("users", User.all.to_a) # Sends immediately
|
|
21
|
+
# emit.call("posts", Post.recent.to_a) # Sends when ready
|
|
22
|
+
# end
|
|
23
|
+
class AsyncPropsEmitter
|
|
24
|
+
def initialize(bundle_timestamp, request_stream)
|
|
25
|
+
@bundle_timestamp = bundle_timestamp
|
|
26
|
+
@request_stream = request_stream
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Sends an async prop to the Node renderer.
|
|
30
|
+
# The prop value is JSON-serialized and sent as an NDJSON line.
|
|
31
|
+
# On the Node side, this triggers asyncPropsManager.setProp(propName, value).
|
|
32
|
+
def call(prop_name, prop_value)
|
|
33
|
+
update_chunk = generate_update_chunk(prop_name, prop_value)
|
|
34
|
+
@request_stream << "#{update_chunk.to_json}\n"
|
|
35
|
+
rescue StandardError => e
|
|
36
|
+
Rails.logger.error do
|
|
37
|
+
backtrace = e.backtrace&.first(5)&.join("\n")
|
|
38
|
+
"[ReactOnRailsPro::AsyncProps] Failed to send async prop '#{prop_name}': " \
|
|
39
|
+
"#{e.class} - #{e.message}\n#{backtrace}"
|
|
40
|
+
end
|
|
41
|
+
# Continue - don't abort entire render because one prop failed
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Generates the chunk that should be executed when the request stream closes
|
|
45
|
+
# This tells the asyncPropsManager to end the stream
|
|
46
|
+
def end_stream_chunk
|
|
47
|
+
{
|
|
48
|
+
bundleTimestamp: @bundle_timestamp,
|
|
49
|
+
updateChunk: generate_end_stream_js
|
|
50
|
+
}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def generate_update_chunk(prop_name, value)
|
|
56
|
+
{
|
|
57
|
+
bundleTimestamp: @bundle_timestamp,
|
|
58
|
+
updateChunk: generate_set_prop_js(prop_name, value)
|
|
59
|
+
}
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def generate_set_prop_js(prop_name, value)
|
|
63
|
+
<<~JS.strip
|
|
64
|
+
(function(){
|
|
65
|
+
var asyncPropsManager = ReactOnRails.getOrCreateAsyncPropsManager(sharedExecutionContext);
|
|
66
|
+
asyncPropsManager.setProp(#{prop_name.to_json}, #{value.to_json});
|
|
67
|
+
})()
|
|
68
|
+
JS
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def generate_end_stream_js
|
|
72
|
+
<<~JS.strip
|
|
73
|
+
(function(){
|
|
74
|
+
var asyncPropsManager = ReactOnRails.getOrCreateAsyncPropsManager(sharedExecutionContext);
|
|
75
|
+
asyncPropsManager.endStream();
|
|
76
|
+
})()
|
|
77
|
+
JS
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -62,7 +62,7 @@ module ReactOnRailsPro
|
|
|
62
62
|
# is when ActionController::Live commits headers. render_to_string itself
|
|
63
63
|
# never writes to response.stream, so this assignment is always safe.
|
|
64
64
|
response.content_type = content_type if content_type
|
|
65
|
-
response.stream.write(template_string)
|
|
65
|
+
response.stream.write(template_string.lstrip)
|
|
66
66
|
|
|
67
67
|
drain_streams_concurrently(parent_task)
|
|
68
68
|
# Do not close the response stream in an ensure block.
|