react_on_rails 15.0.0.alpha.1 → 15.0.0.alpha.2
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/CHANGELOG.md +46 -5
- data/CONTRIBUTING.md +10 -14
- data/Gemfile.lock +1 -1
- data/README.md +1 -1
- data/knip.ts +91 -0
- data/lib/react_on_rails/configuration.rb +35 -10
- data/lib/react_on_rails/engine.rb +1 -3
- data/lib/react_on_rails/helper.rb +169 -53
- data/lib/react_on_rails/locales/base.rb +7 -1
- data/lib/react_on_rails/packer_utils.rb +23 -3
- data/lib/react_on_rails/packs_generator.rb +89 -10
- data/lib/react_on_rails/prerender_error.rb +10 -2
- data/lib/react_on_rails/react_component/render_options.rb +26 -2
- data/lib/react_on_rails/server_rendering_pool/ruby_embedded_java_script.rb +6 -2
- data/lib/react_on_rails/test_helper/webpack_assets_status_checker.rb +2 -2
- data/lib/react_on_rails/utils.rb +44 -23
- data/lib/react_on_rails/version.rb +1 -1
- data/lib/react_on_rails/version_checker.rb +34 -23
- data/react_on_rails.gemspec +2 -2
- data/tsconfig.json +9 -6
- metadata +6 -5
@@ -119,34 +119,74 @@ module ReactOnRails
|
|
119
119
|
# @option options [Hash] :props Props to pass to the react component
|
120
120
|
# @option options [String] :dom_id DOM ID of the component container
|
121
121
|
# @option options [Hash] :html_options Options passed to content_tag
|
122
|
-
# @option options [Boolean] :prerender Set to false to disable server-side rendering
|
123
122
|
# @option options [Boolean] :trace Set to true to add extra debugging information to the HTML
|
124
123
|
# @option options [Boolean] :raise_on_prerender_error Set to true to raise exceptions during server-side rendering
|
125
124
|
# Any other options are passed to the content tag, including the id.
|
126
125
|
def stream_react_component(component_name, options = {})
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
raise ReactOnRails::Error,
|
134
|
-
"You must call stream_view_containing_react_components to render the view containing the react component"
|
126
|
+
# stream_react_component doesn't have the prerender option
|
127
|
+
# Because setting prerender to false is equivalent to calling react_component with prerender: false
|
128
|
+
options[:prerender] = true
|
129
|
+
options = options.merge(force_load: true) unless options.key?(:force_load)
|
130
|
+
run_stream_inside_fiber do
|
131
|
+
internal_stream_react_component(component_name, options)
|
135
132
|
end
|
133
|
+
end
|
136
134
|
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
135
|
+
# Renders the React Server Component (RSC) payload for a given component. This helper generates
|
136
|
+
# a special format designed by React for serializing server components and transmitting them
|
137
|
+
# to the client.
|
138
|
+
#
|
139
|
+
# @return [String] Returns a Newline Delimited JSON (NDJSON) stream where each line contains a JSON object with:
|
140
|
+
# - html: The RSC payload containing the rendered server components and client component references
|
141
|
+
# - consoleReplayScript: JavaScript to replay server-side console logs in the client
|
142
|
+
# - hasErrors: Boolean indicating if any errors occurred during rendering
|
143
|
+
# - isShellReady: Boolean indicating if the initial shell is ready for hydration
|
144
|
+
#
|
145
|
+
# Example NDJSON stream:
|
146
|
+
# {"html":"<RSC Payload>","consoleReplayScript":"","hasErrors":false,"isShellReady":true}
|
147
|
+
# {"html":"<RSC Payload>","consoleReplayScript":"console.log('Loading...')","hasErrors":false,"isShellReady":true}
|
148
|
+
#
|
149
|
+
# The RSC payload within the html field contains:
|
150
|
+
# - The component's rendered output from the server
|
151
|
+
# - References to client components that need hydration
|
152
|
+
# - Data props passed to client components
|
153
|
+
#
|
154
|
+
# @param component_name [String] The name of the React component to render. This component should
|
155
|
+
# be a server component or a mixed component tree containing both server and client components.
|
156
|
+
#
|
157
|
+
# @param options [Hash] Options for rendering the component
|
158
|
+
# @option options [Hash] :props Props to pass to the component (default: {})
|
159
|
+
# @option options [Boolean] :trace Enable tracing for debugging (default: false)
|
160
|
+
# @option options [String] :id Custom DOM ID for the component container (optional)
|
161
|
+
#
|
162
|
+
# @example Basic usage with a server component
|
163
|
+
# <%= rsc_payload_react_component("ReactServerComponentPage") %>
|
164
|
+
#
|
165
|
+
# @example With props and tracing enabled
|
166
|
+
# <%= rsc_payload_react_component("RSCPostsPage",
|
167
|
+
# props: { artificialDelay: 1000 },
|
168
|
+
# trace: true) %>
|
169
|
+
#
|
170
|
+
# @note This helper requires React Server Components support to be enabled in your configuration:
|
171
|
+
# ReactOnRailsPro.configure do |config|
|
172
|
+
# config.enable_rsc_support = true
|
173
|
+
# end
|
174
|
+
#
|
175
|
+
# @raise [ReactOnRailsPro::Error] if RSC support is not enabled in configuration
|
176
|
+
#
|
177
|
+
# @note You don't have to deal directly with this helper function - it's used internally by the
|
178
|
+
# `rsc_payload_route` helper function. The returned data from this function is used internally by
|
179
|
+
# components registered using the `registerServerComponent` function. Don't use it unless you need
|
180
|
+
# more control over the RSC payload generation. To know more about RSC payload, see the following link:
|
181
|
+
# @see https://www.shakacode.com/react-on-rails-pro/docs/how-react-server-components-works.md
|
182
|
+
# for technical details about the RSC payload format
|
183
|
+
def rsc_payload_react_component(component_name, options = {})
|
184
|
+
# rsc_payload_react_component doesn't have the prerender option
|
185
|
+
# Because setting prerender to false will not do anything
|
186
|
+
options[:prerender] = true
|
187
|
+
run_stream_inside_fiber do
|
188
|
+
internal_rsc_payload_react_component(component_name, options)
|
142
189
|
end
|
143
|
-
|
144
|
-
@rorp_rendering_fibers << rendering_fiber
|
145
|
-
|
146
|
-
# return the first chunk of the fiber
|
147
|
-
# It contains the initial html of the component
|
148
|
-
# all updates will be appended to the stream sent to browser
|
149
|
-
rendering_fiber.resume
|
150
190
|
end
|
151
191
|
|
152
192
|
# react_component_hash is used to return multiple HTML strings for server rendering, such as for
|
@@ -207,17 +247,19 @@ module ReactOnRails
|
|
207
247
|
# props: Ruby Hash or JSON string which contains the properties to pass to the redux store.
|
208
248
|
# Options
|
209
249
|
# defer: false -- pass as true if you wish to render this below your component.
|
210
|
-
|
250
|
+
# force_load: false -- pass as true if you wish to hydrate this store immediately instead of
|
251
|
+
# waiting for the page to load.
|
252
|
+
def redux_store(store_name, props: {}, defer: false, force_load: nil)
|
253
|
+
force_load = ReactOnRails.configuration.force_load if force_load.nil?
|
211
254
|
redux_store_data = { store_name: store_name,
|
212
|
-
props: props
|
255
|
+
props: props,
|
256
|
+
force_load: force_load }
|
213
257
|
if defer
|
214
|
-
|
215
|
-
@registered_stores_defer_render << redux_store_data
|
258
|
+
registered_stores_defer_render << redux_store_data
|
216
259
|
"YOU SHOULD NOT SEE THIS ON YOUR VIEW -- Uses as a code block, like <% redux_store %> " \
|
217
260
|
"and not <%= redux store %>"
|
218
261
|
else
|
219
|
-
|
220
|
-
@registered_stores << redux_store_data
|
262
|
+
registered_stores << redux_store_data
|
221
263
|
result = render_redux_store_data(redux_store_data)
|
222
264
|
prepend_render_rails_context(result)
|
223
265
|
end
|
@@ -229,9 +271,9 @@ module ReactOnRails
|
|
229
271
|
# client side rendering of this hydration data, which is a hidden div with a matching class
|
230
272
|
# that contains a data props.
|
231
273
|
def redux_store_hydration_data
|
232
|
-
return if
|
274
|
+
return if registered_stores_defer_render.blank?
|
233
275
|
|
234
|
-
|
276
|
+
registered_stores_defer_render.reduce(+"") do |accum, redux_store_data|
|
235
277
|
accum << render_redux_store_data(redux_store_data)
|
236
278
|
end.html_safe
|
237
279
|
end
|
@@ -321,6 +363,7 @@ module ReactOnRails
|
|
321
363
|
# ALERT: Keep in sync with node_package/src/types/index.ts for the properties of RailsContext
|
322
364
|
@rails_context ||= begin
|
323
365
|
result = {
|
366
|
+
componentRegistryTimeout: ReactOnRails.configuration.component_registry_timeout,
|
324
367
|
railsEnv: Rails.env,
|
325
368
|
inMailer: in_mailer?,
|
326
369
|
# Locale settings
|
@@ -388,8 +431,56 @@ module ReactOnRails
|
|
388
431
|
|
389
432
|
private
|
390
433
|
|
434
|
+
def run_stream_inside_fiber
|
435
|
+
unless ReactOnRails::Utils.react_on_rails_pro?
|
436
|
+
raise ReactOnRails::Error,
|
437
|
+
"You must use React on Rails Pro to use the stream_react_component method."
|
438
|
+
end
|
439
|
+
|
440
|
+
if @rorp_rendering_fibers.nil?
|
441
|
+
raise ReactOnRails::Error,
|
442
|
+
"You must call stream_view_containing_react_components to render the view containing the react component"
|
443
|
+
end
|
444
|
+
|
445
|
+
rendering_fiber = Fiber.new do
|
446
|
+
stream = yield
|
447
|
+
stream.each_chunk do |chunk|
|
448
|
+
Fiber.yield chunk
|
449
|
+
end
|
450
|
+
end
|
451
|
+
|
452
|
+
@rorp_rendering_fibers << rendering_fiber
|
453
|
+
|
454
|
+
# return the first chunk of the fiber
|
455
|
+
# It contains the initial html of the component
|
456
|
+
# all updates will be appended to the stream sent to browser
|
457
|
+
rendering_fiber.resume
|
458
|
+
end
|
459
|
+
|
460
|
+
def registered_stores
|
461
|
+
@registered_stores ||= []
|
462
|
+
end
|
463
|
+
|
464
|
+
def registered_stores_defer_render
|
465
|
+
@registered_stores_defer_render ||= []
|
466
|
+
end
|
467
|
+
|
468
|
+
def registered_stores_including_deferred
|
469
|
+
registered_stores + registered_stores_defer_render
|
470
|
+
end
|
471
|
+
|
472
|
+
def create_render_options(react_component_name, options)
|
473
|
+
# If no store dependencies are passed, default to all registered stores up till now
|
474
|
+
unless options.key?(:store_dependencies)
|
475
|
+
store_dependencies = registered_stores_including_deferred.map { |store| store[:store_name] }
|
476
|
+
options = options.merge(store_dependencies: store_dependencies.presence)
|
477
|
+
end
|
478
|
+
ReactOnRails::ReactComponent::RenderOptions.new(react_component_name: react_component_name,
|
479
|
+
options: options)
|
480
|
+
end
|
481
|
+
|
391
482
|
def internal_stream_react_component(component_name, options = {})
|
392
|
-
options = options.merge(
|
483
|
+
options = options.merge(render_mode: :html_streaming)
|
393
484
|
result = internal_react_component(component_name, options)
|
394
485
|
build_react_component_result_for_server_streamed_content(
|
395
486
|
rendered_html_stream: result[:result],
|
@@ -398,6 +489,15 @@ module ReactOnRails
|
|
398
489
|
)
|
399
490
|
end
|
400
491
|
|
492
|
+
def internal_rsc_payload_react_component(react_component_name, options = {})
|
493
|
+
options = options.merge(render_mode: :rsc_payload_streaming)
|
494
|
+
render_options = create_render_options(react_component_name, options)
|
495
|
+
json_stream = server_rendered_react_component(render_options)
|
496
|
+
json_stream.transform do |chunk|
|
497
|
+
"#{chunk.to_json}\n".html_safe
|
498
|
+
end
|
499
|
+
end
|
500
|
+
|
401
501
|
def generated_components_pack_path(component_name)
|
402
502
|
"#{ReactOnRails::PackerUtils.packer_source_entry_path}/generated/#{component_name}.js"
|
403
503
|
end
|
@@ -489,14 +589,13 @@ module ReactOnRails
|
|
489
589
|
)
|
490
590
|
end
|
491
591
|
|
492
|
-
def compose_react_component_html_with_spec_and_console(component_specification_tag, rendered_output,
|
592
|
+
def compose_react_component_html_with_spec_and_console(component_specification_tag, rendered_output,
|
593
|
+
console_script)
|
493
594
|
# IMPORTANT: Ensure that we mark string as html_safe to avoid escaping.
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
HTML
|
499
|
-
html_content.strip.html_safe
|
595
|
+
added_html = "#{component_specification_tag}\n#{console_script}".strip
|
596
|
+
added_html = added_html.present? ? "\n#{added_html}" : ""
|
597
|
+
|
598
|
+
"#{rendered_output}#{added_html}".html_safe
|
500
599
|
end
|
501
600
|
|
502
601
|
def rails_context_if_not_already_rendered
|
@@ -514,7 +613,9 @@ module ReactOnRails
|
|
514
613
|
|
515
614
|
# prepend the rails_context if not yet applied
|
516
615
|
def prepend_render_rails_context(render_value)
|
517
|
-
|
616
|
+
rails_context_content = rails_context_if_not_already_rendered
|
617
|
+
rails_context_content = rails_context_content.present? ? "#{rails_context_content}\n" : ""
|
618
|
+
"#{rails_context_content}#{render_value}".html_safe
|
518
619
|
end
|
519
620
|
|
520
621
|
def internal_react_component(react_component_name, options = {})
|
@@ -525,8 +626,7 @@ module ReactOnRails
|
|
525
626
|
# (re-hydrate the data). This enables react rendered on the client to see that the
|
526
627
|
# server has already rendered the HTML.
|
527
628
|
|
528
|
-
render_options =
|
529
|
-
options: options)
|
629
|
+
render_options = create_render_options(react_component_name, options)
|
530
630
|
|
531
631
|
# Setup the page_loaded_js, which is the same regardless of prerendering or not!
|
532
632
|
# The reason is that React is smart about not doing extra work if the server rendering did its job.
|
@@ -534,14 +634,17 @@ module ReactOnRails
|
|
534
634
|
json_safe_and_pretty(render_options.client_props).html_safe,
|
535
635
|
type: "application/json",
|
536
636
|
class: "js-react-on-rails-component",
|
637
|
+
id: "js-react-on-rails-component-#{render_options.dom_id}",
|
537
638
|
"data-component-name" => render_options.react_component_name,
|
538
639
|
"data-trace" => (render_options.trace ? true : nil),
|
539
|
-
"data-dom-id" => render_options.dom_id
|
640
|
+
"data-dom-id" => render_options.dom_id,
|
641
|
+
"data-store-dependencies" => render_options.store_dependencies&.to_json,
|
642
|
+
"data-force-load" => (render_options.force_load ? true : nil))
|
540
643
|
|
541
644
|
if render_options.force_load
|
542
645
|
component_specification_tag.concat(
|
543
646
|
content_tag(:script, %(
|
544
|
-
ReactOnRails.reactOnRailsComponentLoaded('#{render_options.dom_id}');
|
647
|
+
typeof ReactOnRails === 'object' && ReactOnRails.reactOnRailsComponentLoaded('#{render_options.dom_id}');
|
545
648
|
).html_safe)
|
546
649
|
)
|
547
650
|
end
|
@@ -558,12 +661,22 @@ ReactOnRails.reactOnRailsComponentLoaded('#{render_options.dom_id}');
|
|
558
661
|
end
|
559
662
|
|
560
663
|
def render_redux_store_data(redux_store_data)
|
561
|
-
|
562
|
-
|
563
|
-
|
564
|
-
|
664
|
+
store_hydration_data = content_tag(:script,
|
665
|
+
json_safe_and_pretty(redux_store_data[:props]).html_safe,
|
666
|
+
type: "application/json",
|
667
|
+
"data-js-react-on-rails-store" => redux_store_data[:store_name].html_safe,
|
668
|
+
"data-force-load" => (redux_store_data[:force_load] ? true : nil))
|
669
|
+
|
670
|
+
if redux_store_data[:force_load]
|
671
|
+
store_hydration_data.concat(
|
672
|
+
content_tag(:script, <<~JS.strip_heredoc.html_safe
|
673
|
+
typeof ReactOnRails === 'object' && ReactOnRails.reactOnRailsStoreLoaded('#{redux_store_data[:store_name]}');
|
674
|
+
JS
|
675
|
+
)
|
676
|
+
)
|
677
|
+
end
|
565
678
|
|
566
|
-
prepend_render_rails_context(
|
679
|
+
prepend_render_rails_context(store_hydration_data)
|
567
680
|
end
|
568
681
|
|
569
682
|
def props_string(props)
|
@@ -620,7 +733,7 @@ ReactOnRails.reactOnRailsComponentLoaded('#{render_options.dom_id}');
|
|
620
733
|
js_code = ReactOnRails::ServerRenderingJsCode.server_rendering_component_js_code(
|
621
734
|
props_string: props_string(props).gsub("\u2028", '\u2028').gsub("\u2029", '\u2029'),
|
622
735
|
rails_context: rails_context(server_side: true).to_json,
|
623
|
-
redux_stores: initialize_redux_stores,
|
736
|
+
redux_stores: initialize_redux_stores(render_options),
|
624
737
|
react_component_name: react_component_name,
|
625
738
|
render_options: render_options
|
626
739
|
)
|
@@ -636,7 +749,7 @@ ReactOnRails.reactOnRailsComponentLoaded('#{render_options.dom_id}');
|
|
636
749
|
js_code: js_code)
|
637
750
|
end
|
638
751
|
|
639
|
-
if render_options.
|
752
|
+
if render_options.streaming?
|
640
753
|
result.transform do |chunk_json_result|
|
641
754
|
if should_raise_streaming_prerender_error?(chunk_json_result, render_options)
|
642
755
|
raise_prerender_error(chunk_json_result, react_component_name, props, js_code)
|
@@ -651,17 +764,20 @@ ReactOnRails.reactOnRailsComponentLoaded('#{render_options.dom_id}');
|
|
651
764
|
result
|
652
765
|
end
|
653
766
|
|
654
|
-
def initialize_redux_stores
|
767
|
+
def initialize_redux_stores(render_options)
|
655
768
|
result = +<<-JS
|
656
769
|
ReactOnRails.clearHydratedStores();
|
657
770
|
JS
|
658
771
|
|
659
|
-
|
772
|
+
store_dependencies = render_options.store_dependencies
|
773
|
+
return result unless store_dependencies.present?
|
660
774
|
|
661
775
|
declarations = +"var reduxProps, store, storeGenerator;\n"
|
662
|
-
|
776
|
+
store_objects = registered_stores_including_deferred.select do |store|
|
777
|
+
store_dependencies.include?(store[:store_name])
|
778
|
+
end
|
663
779
|
|
664
|
-
result <<
|
780
|
+
result << store_objects.each_with_object(declarations) do |redux_store_data, memo|
|
665
781
|
store_name = redux_store_data[:store_name]
|
666
782
|
props = props_string(redux_store_data[:props])
|
667
783
|
memo << <<-JS.strip_heredoc
|
@@ -115,11 +115,17 @@ module ReactOnRails
|
|
115
115
|
translations = {}
|
116
116
|
defaults = {}
|
117
117
|
locale_files.each do |f|
|
118
|
-
|
118
|
+
safe_load_options = ReactOnRails.configuration.i18n_yml_safe_load_options || {}
|
119
|
+
translation = YAML.safe_load(File.open(f), **safe_load_options)
|
119
120
|
key = translation.keys[0]
|
120
121
|
val = flatten(translation[key])
|
121
122
|
translations = translations.deep_merge(key => val)
|
122
123
|
defaults = defaults.deep_merge(flatten_defaults(val)) if key == default_locale
|
124
|
+
rescue Psych::Exception => e
|
125
|
+
raise ReactOnRails::Error, <<~MSG
|
126
|
+
Error parsing #{f}: #{e.message}
|
127
|
+
Consider fixing unsafe YAML or permitting with config.i18n_yml_safe_load_options
|
128
|
+
MSG
|
123
129
|
end
|
124
130
|
[translations.to_json, defaults.to_json]
|
125
131
|
end
|
@@ -45,6 +45,10 @@ module ReactOnRails
|
|
45
45
|
packer.dev_server.running?
|
46
46
|
end
|
47
47
|
|
48
|
+
def self.dev_server_url
|
49
|
+
"#{packer.dev_server.protocol}://#{packer.dev_server.host_with_port}"
|
50
|
+
end
|
51
|
+
|
48
52
|
def self.shakapacker_version
|
49
53
|
return @shakapacker_version if defined?(@shakapacker_version)
|
50
54
|
return nil unless ReactOnRails::Utils.gem_available?("shakapacker")
|
@@ -74,16 +78,32 @@ module ReactOnRails
|
|
74
78
|
# the webpack-dev-server is provided by the config value
|
75
79
|
# "same_bundle_for_client_and_server" where a value of true
|
76
80
|
# would mean that the bundle is created by the webpack-dev-server
|
77
|
-
|
81
|
+
is_bundle_running_on_server = (bundle_name == ReactOnRails.configuration.server_bundle_js_file) ||
|
82
|
+
(bundle_name == ReactOnRails.configuration.rsc_bundle_js_file)
|
78
83
|
|
79
|
-
if packer.dev_server.running? && (!
|
84
|
+
if packer.dev_server.running? && (!is_bundle_running_on_server ||
|
80
85
|
ReactOnRails.configuration.same_bundle_for_client_and_server)
|
81
|
-
"#{
|
86
|
+
"#{dev_server_url}#{hashed_bundle_name}"
|
82
87
|
else
|
83
88
|
File.expand_path(File.join("public", hashed_bundle_name)).to_s
|
84
89
|
end
|
85
90
|
end
|
86
91
|
|
92
|
+
def self.public_output_uri_path
|
93
|
+
"#{packer.config.public_output_path.relative_path_from(packer.config.public_path)}/"
|
94
|
+
end
|
95
|
+
|
96
|
+
# The function doesn't ensure that the asset exists.
|
97
|
+
# - It just returns url to the asset if dev server is running
|
98
|
+
# - Otherwise it returns file path to the asset
|
99
|
+
def self.asset_uri_from_packer(asset_name)
|
100
|
+
if dev_server_running?
|
101
|
+
"#{dev_server_url}/#{public_output_uri_path}#{asset_name}"
|
102
|
+
else
|
103
|
+
File.join(packer_public_output_path, asset_name).to_s
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
87
107
|
def self.precompile?
|
88
108
|
return ::Webpacker.config.webpacker_precompile? if using_webpacker_const?
|
89
109
|
return ::Shakapacker.config.shakapacker_precompile? if using_shakapacker_const?
|
@@ -44,11 +44,71 @@ module ReactOnRails
|
|
44
44
|
puts(Rainbow("Generated Packs: #{output_path}").yellow)
|
45
45
|
end
|
46
46
|
|
47
|
+
def first_js_statement_in_code(content) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
|
48
|
+
return "" if content.nil? || content.empty?
|
49
|
+
|
50
|
+
start_index = 0
|
51
|
+
content_length = content.length
|
52
|
+
|
53
|
+
while start_index < content_length
|
54
|
+
# Skip whitespace
|
55
|
+
start_index += 1 while start_index < content_length && content[start_index].match?(/\s/)
|
56
|
+
|
57
|
+
break if start_index >= content_length
|
58
|
+
|
59
|
+
current_chars = content[start_index, 2]
|
60
|
+
|
61
|
+
case current_chars
|
62
|
+
when "//"
|
63
|
+
# Single-line comment
|
64
|
+
newline_index = content.index("\n", start_index)
|
65
|
+
return "" if newline_index.nil?
|
66
|
+
|
67
|
+
start_index = newline_index + 1
|
68
|
+
when "/*"
|
69
|
+
# Multi-line comment
|
70
|
+
comment_end = content.index("*/", start_index)
|
71
|
+
return "" if comment_end.nil?
|
72
|
+
|
73
|
+
start_index = comment_end + 2
|
74
|
+
else
|
75
|
+
# Found actual content
|
76
|
+
next_line_index = content.index("\n", start_index)
|
77
|
+
return next_line_index ? content[start_index...next_line_index].strip : content[start_index..].strip
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
""
|
82
|
+
end
|
83
|
+
|
84
|
+
def client_entrypoint?(file_path)
|
85
|
+
content = File.read(file_path)
|
86
|
+
# has "use client" directive. It can be "use client" or 'use client'
|
87
|
+
first_js_statement_in_code(content).match?(/^["']use client["'](?:;|\s|$)/)
|
88
|
+
end
|
89
|
+
|
47
90
|
def pack_file_contents(file_path)
|
48
91
|
registered_component_name = component_name(file_path)
|
49
|
-
|
92
|
+
load_server_components = ReactOnRails::Utils.react_on_rails_pro? &&
|
93
|
+
ReactOnRailsPro.configuration.enable_rsc_support
|
94
|
+
|
95
|
+
if load_server_components && !client_entrypoint?(file_path)
|
96
|
+
rsc_payload_generation_url_path = ReactOnRailsPro.configuration.rsc_payload_generation_url_path
|
97
|
+
|
98
|
+
return <<~FILE_CONTENT.strip
|
99
|
+
import registerServerComponent from 'react-on-rails/registerServerComponent/client';
|
100
|
+
|
101
|
+
registerServerComponent({
|
102
|
+
rscPayloadGenerationUrlPath: "#{rsc_payload_generation_url_path}",
|
103
|
+
}, "#{registered_component_name}")
|
104
|
+
FILE_CONTENT
|
105
|
+
end
|
106
|
+
|
107
|
+
relative_component_path = relative_component_path_from_generated_pack(file_path)
|
108
|
+
|
109
|
+
<<~FILE_CONTENT.strip
|
50
110
|
import ReactOnRails from 'react-on-rails';
|
51
|
-
import #{registered_component_name} from '#{
|
111
|
+
import #{registered_component_name} from '#{relative_component_path}';
|
52
112
|
|
53
113
|
ReactOnRails.register({#{registered_component_name}});
|
54
114
|
FILE_CONTENT
|
@@ -61,23 +121,42 @@ module ReactOnRails
|
|
61
121
|
puts(Rainbow("Generated Server Bundle: #{generated_server_bundle_file_path}").orange)
|
62
122
|
end
|
63
123
|
|
124
|
+
def build_server_pack_content(component_on_server_imports, server_components, client_components)
|
125
|
+
content = <<~FILE_CONTENT
|
126
|
+
import ReactOnRails from 'react-on-rails';
|
127
|
+
|
128
|
+
#{component_on_server_imports.join("\n")}\n
|
129
|
+
FILE_CONTENT
|
130
|
+
|
131
|
+
if server_components.any?
|
132
|
+
content += <<~FILE_CONTENT
|
133
|
+
import registerServerComponent from 'react-on-rails/registerServerComponent/server';
|
134
|
+
registerServerComponent({#{server_components.join(",\n")}});\n
|
135
|
+
FILE_CONTENT
|
136
|
+
end
|
137
|
+
|
138
|
+
content + "ReactOnRails.register({#{client_components.join(",\n")}});"
|
139
|
+
end
|
140
|
+
|
64
141
|
def generated_server_pack_file_content
|
65
142
|
common_components_for_server_bundle = common_component_to_path.delete_if { |k| server_component_to_path.key?(k) }
|
66
143
|
component_for_server_registration_to_path = common_components_for_server_bundle.merge(server_component_to_path)
|
67
144
|
|
68
|
-
|
145
|
+
component_on_server_imports = component_for_server_registration_to_path.map do |name, component_path|
|
69
146
|
"import #{name} from '#{relative_path(generated_server_bundle_file_path, component_path)}';"
|
70
147
|
end
|
71
148
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
149
|
+
load_server_components = ReactOnRails::Utils.react_on_rails_pro? &&
|
150
|
+
ReactOnRailsPro.configuration.enable_rsc_support
|
151
|
+
server_components = component_for_server_registration_to_path.keys.delete_if do |name|
|
152
|
+
next true unless load_server_components
|
76
153
|
|
77
|
-
|
154
|
+
component_path = component_for_server_registration_to_path[name]
|
155
|
+
client_entrypoint?(component_path)
|
156
|
+
end
|
157
|
+
client_components = component_for_server_registration_to_path.keys - server_components
|
78
158
|
|
79
|
-
|
80
|
-
FILE_CONTENT
|
159
|
+
build_server_pack_content(component_on_server_imports, server_components, client_components)
|
81
160
|
end
|
82
161
|
|
83
162
|
def add_generated_pack_to_server_bundle
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "rainbow"
|
4
|
+
|
3
5
|
# rubocop:disable: Layout/IndentHeredoc
|
4
6
|
module ReactOnRails
|
5
7
|
class PrerenderError < ::ReactOnRails::Error
|
@@ -51,11 +53,17 @@ module ReactOnRails
|
|
51
53
|
message << <<~MSG
|
52
54
|
Encountered error:
|
53
55
|
|
54
|
-
#{err}
|
56
|
+
#{err.inspect}
|
55
57
|
|
56
58
|
MSG
|
57
59
|
|
58
|
-
backtrace =
|
60
|
+
backtrace = if Utils.full_text_errors_enabled?
|
61
|
+
err.backtrace.join("\n")
|
62
|
+
else
|
63
|
+
"#{Rails.backtrace_cleaner.clean(err.backtrace).join("\n")}\n" +
|
64
|
+
Rainbow("The rest of the backtrace is hidden. " \
|
65
|
+
"To see the full backtrace, set FULL_TEXT_ERRORS=true.").red
|
66
|
+
end
|
59
67
|
else
|
60
68
|
backtrace = nil
|
61
69
|
end
|
@@ -111,8 +111,32 @@ module ReactOnRails
|
|
111
111
|
options[key] = value
|
112
112
|
end
|
113
113
|
|
114
|
-
def
|
115
|
-
|
114
|
+
def render_mode
|
115
|
+
# Determines the React rendering strategy:
|
116
|
+
# - :sync: Synchronous SSR using renderToString (blocking and rendering in one shot)
|
117
|
+
# - :html_streaming: Progressive SSR using renderToPipeableStream (non-blocking and rendering incrementally)
|
118
|
+
# - :rsc_payload_streaming: Server Components serialized in React flight format
|
119
|
+
# (non-blocking and rendering incrementally).
|
120
|
+
options.fetch(:render_mode, :sync)
|
121
|
+
end
|
122
|
+
|
123
|
+
def streaming?
|
124
|
+
# Returns true if the component should be rendered incrementally
|
125
|
+
%i[html_streaming rsc_payload_streaming].include?(render_mode)
|
126
|
+
end
|
127
|
+
|
128
|
+
def rsc_payload_streaming?
|
129
|
+
# Returns true if the component should be rendered as a React Server Component
|
130
|
+
render_mode == :rsc_payload_streaming
|
131
|
+
end
|
132
|
+
|
133
|
+
def html_streaming?
|
134
|
+
# Returns true if the component should be rendered incrementally
|
135
|
+
render_mode == :html_streaming
|
136
|
+
end
|
137
|
+
|
138
|
+
def store_dependencies
|
139
|
+
options[:store_dependencies]
|
116
140
|
end
|
117
141
|
|
118
142
|
private
|
@@ -19,6 +19,10 @@ module ReactOnRails
|
|
19
19
|
def reset_pool_if_server_bundle_was_modified
|
20
20
|
return unless ReactOnRails.configuration.development_mode
|
21
21
|
|
22
|
+
# RSC (React Server Components) bundle changes are not monitored here since:
|
23
|
+
# 1. RSC is only supported in the Pro version of React on Rails
|
24
|
+
# 2. This RubyEmbeddedJavaScript pool is used exclusively in the non-Pro version
|
25
|
+
# 3. This pool uses ExecJS for JavaScript evaluation which does not support RSC
|
22
26
|
if ReactOnRails::Utils.server_bundle_path_is_http?
|
23
27
|
return if @server_bundle_url == ReactOnRails::Utils.server_bundle_js_file_path
|
24
28
|
|
@@ -56,7 +60,7 @@ module ReactOnRails
|
|
56
60
|
@file_index += 1
|
57
61
|
end
|
58
62
|
begin
|
59
|
-
result = if render_options.
|
63
|
+
result = if render_options.streaming?
|
60
64
|
js_evaluator.eval_streaming_js(js_code, render_options)
|
61
65
|
else
|
62
66
|
js_evaluator.eval_js(js_code, render_options)
|
@@ -76,7 +80,7 @@ module ReactOnRails
|
|
76
80
|
raise ReactOnRails::Error, msg, err.backtrace
|
77
81
|
end
|
78
82
|
|
79
|
-
return parse_result_and_replay_console_messages(result, render_options) unless render_options.
|
83
|
+
return parse_result_and_replay_console_messages(result, render_options) unless render_options.streaming?
|
80
84
|
|
81
85
|
# Streamed component is returned as stream of strings.
|
82
86
|
# We need to parse each chunk and replay the console messages.
|
@@ -50,8 +50,8 @@ module ReactOnRails
|
|
50
50
|
def all_compiled_assets
|
51
51
|
@all_compiled_assets ||= begin
|
52
52
|
webpack_generated_files = @webpack_generated_files.map do |bundle_name|
|
53
|
-
if bundle_name == ReactOnRails.configuration.
|
54
|
-
ReactOnRails::Utils.
|
53
|
+
if bundle_name == ReactOnRails.configuration.react_client_manifest_file
|
54
|
+
ReactOnRails::Utils.react_client_manifest_file_path
|
55
55
|
else
|
56
56
|
ReactOnRails::Utils.bundle_js_file_path(bundle_name)
|
57
57
|
end
|