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.
@@ -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
- unless ReactOnRails::Utils.react_on_rails_pro?
128
- raise ReactOnRails::Error,
129
- "You must use React on Rails Pro to use the stream_react_component method."
130
- end
131
-
132
- if @rorp_rendering_fibers.nil?
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
- rendering_fiber = Fiber.new do
138
- stream = internal_stream_react_component(component_name, options)
139
- stream.each_chunk do |chunk|
140
- Fiber.yield chunk
141
- end
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
- def redux_store(store_name, props: {}, defer: false)
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
- @registered_stores_defer_render ||= []
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
- @registered_stores ||= []
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 @registered_stores_defer_render.blank?
274
+ return if registered_stores_defer_render.blank?
233
275
 
234
- @registered_stores_defer_render.reduce(+"") do |accum, redux_store_data|
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(stream?: true)
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, console_script)
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
- html_content = <<~HTML
495
- #{rendered_output}
496
- #{component_specification_tag}
497
- #{console_script}
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
- "#{rails_context_if_not_already_rendered}\n#{render_value}".strip.html_safe
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 = ReactOnRails::ReactComponent::RenderOptions.new(react_component_name: react_component_name,
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
- result = content_tag(:script,
562
- json_safe_and_pretty(redux_store_data[:props]).html_safe,
563
- type: "application/json",
564
- "data-js-react-on-rails-store" => redux_store_data[:store_name].html_safe)
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(result)
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.stream?
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
- return result unless @registered_stores.present? || @registered_stores_defer_render.present?
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
- all_stores = (@registered_stores || []) + (@registered_stores_defer_render || [])
776
+ store_objects = registered_stores_including_deferred.select do |store|
777
+ store_dependencies.include?(store[:store_name])
778
+ end
663
779
 
664
- result << all_stores.each_with_object(declarations) do |redux_store_data, memo|
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
- translation = YAML.safe_load(File.open(f))
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
- is_server_bundle = bundle_name == ReactOnRails.configuration.server_bundle_js_file
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? && (!is_server_bundle ||
84
+ if packer.dev_server.running? && (!is_bundle_running_on_server ||
80
85
  ReactOnRails.configuration.same_bundle_for_client_and_server)
81
- "#{packer.dev_server.protocol}://#{packer.dev_server.host_with_port}#{hashed_bundle_name}"
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
- <<~FILE_CONTENT
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 '#{relative_component_path_from_generated_pack(file_path)}';
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
- server_component_imports = component_for_server_registration_to_path.map do |name, component_path|
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
- components_to_register = component_for_server_registration_to_path.keys
73
-
74
- <<~FILE_CONTENT
75
- import ReactOnRails from 'react-on-rails';
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
- #{server_component_imports.join("\n")}
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
- ReactOnRails.register({#{components_to_register.join(",\n")}});
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 = err.backtrace.first(15).join("\n")
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 stream?
115
- options[:stream?]
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.stream?
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.stream?
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.server_bundle_js_file
54
- ReactOnRails::Utils.server_bundle_js_file_path
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