react_on_rails 14.0.5 → 15.0.0.alpha.1

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: ca8ba0235b9256e1b24f5afc2c874f258cf239d78ad6490e86ce4d918b26ecf4
4
- data.tar.gz: 54796b7b8067b98180bf0e41eb20c4d6bb0a4ea54594af8365eb95c74158d1b7
3
+ metadata.gz: 7d60051000fbd817b3499b1de6f13c9abeaf5627bee250c50bf0d748ce3532a5
4
+ data.tar.gz: 4eccfe81e12b647503fa6e690c514d4d14d6704ab1e5c7f64282cafa7709d6e2
5
5
  SHA512:
6
- metadata.gz: c081c9e1b626d9c986f212c2c351fd1774fddaa9be301ae05d6c890b41c30f21871acb56002001b2bf64584da3932457ffc9da522463f3d49f7b73dd62852032
7
- data.tar.gz: 005f0dc9f274c6ba9edc8784efb679e6ba5bc7610b708c7c072f4301f4fe25d3a4bf887ad2e9449284f2bbce346dfec54f0e7bd54192ab8d3532c7d5b3004267
6
+ metadata.gz: 99004f6792fe209d21474c7b5b311ac8a02cce27740b69164e44079983a5852895aa99ed12530043ba64ad6920d189cb6b19ce3957aca83b6bab06f75bf702ee
7
+ data.tar.gz: 2be5eb3178a913c90c7b4e640812bccc479245d0052d95751317f543c80e7bf9429f9fd61cbc1c5edb917aafbead33502c75fb63fd8ab183480aaa44124f3dc5
data/CHANGELOG.md CHANGED
@@ -18,6 +18,25 @@ Please follow the recommendations outlined at [keepachangelog.com](http://keepac
18
18
  ### [Unreleased]
19
19
  Changes since the last non-beta release.
20
20
 
21
+ #### Added(https://github.com/AbanoubGhadban).
22
+ - Added streaming server rendering support:
23
+ - [PR #1633](https://github.com/shakacode/react_on_rails/pull/1633) by [AbanoubGhadban](https://github.com/AbanoubGhadban).
24
+ - New `stream_react_component` helper for adding streamed components to views
25
+ - New `streamServerRenderedReactComponent` function in the react-on-rails package that uses React 18's `renderToPipeableStream` API
26
+ - Enables progressive page loading and improved performance for server-rendered React components
27
+ - Added support for replaying console logs that occur during server rendering of streamed React components. This enables debugging of server-side rendering issues by capturing and displaying console output on the client and on the server output. [PR #1647](https://github.com/shakacode/react_on_rails/pull/1647) by [AbanoubGhadban](https://github.com/AbanoubGhadban).
28
+ - Added support for handling errors happening during server rendering of streamed React components. It handles errors that happen during the initial render and errors that happen inside suspense boundaries. [PR #1648](https://github.com/shakacode/react_on_rails/pull/1648) by [AbanoubGhadban](https://github.com/AbanoubGhadban).
29
+
30
+ #### Changed
31
+ - Console replay script generation now awaits the render request promise before generating, allowing it to capture console logs from asynchronous operations. This requires using a version of the Node renderer that supports replaying async console logs. [PR #1649](https://github.com/shakacode/react_on_rails/pull/1649) by [AbanoubGhadban](https://github.com/AbanoubGhadban).
32
+
33
+ #### Fixed
34
+ - Incorrect type and confusing name for `ReactOnRails.registerStore`, use `registerStoreGenerators` instead. [PR 1651](https://github.com/shakacode/react_on_rails/pull/1651) by [alexeyr-ci](https://github.com/alexeyr-ci).
35
+
36
+ ### [14.0.5] - 2024-08-20
37
+ #### Fixed
38
+ - Should force load react-components which send over turbo-stream [PR #1620](https://github.com/shakacode/react_on_rails/pull/1620) by [theforestvn88](https://github.com/theforestvn88).
39
+
21
40
  ### [14.0.4] - 2024-07-02
22
41
 
23
42
  #### Improved
@@ -1148,7 +1167,8 @@ Best done with Object destructing:
1148
1167
  ##### Fixed
1149
1168
  - Fix several generator-related issues.
1150
1169
 
1151
- [Unreleased]: https://github.com/shakacode/react_on_rails/compare/14.0.4...master
1170
+ [Unreleased]: https://github.com/shakacode/react_on_rails/compare/14.0.5...master
1171
+ [14.0.5]: https://github.com/shakacode/react_on_rails/compare/14.0.4...14.0.5
1152
1172
  [14.0.4]: https://github.com/shakacode/react_on_rails/compare/14.0.3...14.0.4
1153
1173
  [14.0.3]: https://github.com/shakacode/react_on_rails/compare/14.0.2...14.0.3
1154
1174
  [14.0.2]: https://github.com/shakacode/react_on_rails/compare/14.0.1...14.0.2
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- react_on_rails (14.0.4)
4
+ react_on_rails (14.0.5)
5
5
  addressable
6
6
  connection_pool
7
7
  execjs (~> 2.5)
data/README.md CHANGED
@@ -17,6 +17,7 @@
17
17
  [![Linting](https://github.com/shakacode/react_on_rails/actions/workflows/lint-js-and-ruby.yml/badge.svg)](https://github.com/shakacode/react_on_rails/actions/workflows/lint-js-and-ruby.yml)
18
18
 
19
19
  # News
20
+ * [React on Rails Pro](https://www.shakacode.com/react-on-rails-pro/) supports the latest features of React 18, including [React Server Components](https://react.dev/reference/rsc/server-components) and [streaming](https://react.dev/reference/react-dom/server/renderToPipeableStream). Contact [Justin Gordon](mailto:justin@shakacode.com) for more information.
20
21
  * ShakaCode now maintains the official successor to `rails/webpacker`, [`shakapacker`](https://github.com/shakacode/shakapacker).
21
22
  * Project is updated to support Rails 7 and Shakapacker v6+!
22
23
 
data/SUMMARY.md CHANGED
@@ -17,6 +17,7 @@ Here is the new link:
17
17
  + [How React on Rails Works](docs/outdated/how-react-on-rails-works.md)
18
18
  + [Client vs. Server Rendering](./docs/guides/client-vs-server-rendering.md)
19
19
  + [React Server Rendering](./docs/guides/react-server-rendering.md)
20
+ + [🚀 Next-Gen Server Rendering: Streaming with React 18's Latest APIs](./docs/guides/streaming-server-rendering.md)
20
21
  + [Render-Functions and the RailsContext](docs/guides/render-functions-and-railscontext.md)
21
22
  + [Caching and Performance: React on Rails Pro](https://github.com/shakacode/react_on_rails/wiki).
22
23
  + [Deployment](docs/guides/deployment.md).
@@ -91,6 +91,64 @@ module ReactOnRails
91
91
  end
92
92
  end
93
93
 
94
+ # Streams a server-side rendered React component using React's `renderToPipeableStream`.
95
+ # Supports React 18 features like Suspense, concurrent rendering, and selective hydration.
96
+ # Enables progressive rendering and improved performance for large components.
97
+ #
98
+ # Note: This function can only be used with React on Rails Pro.
99
+ # The view that uses this function must be rendered using the
100
+ # `stream_view_containing_react_components` method from the React on Rails Pro gem.
101
+ #
102
+ # Example of an async React component that can benefit from streaming:
103
+ #
104
+ # const AsyncComponent = async () => {
105
+ # const data = await fetchData();
106
+ # return <div>{data}</div>;
107
+ # };
108
+ #
109
+ # function App() {
110
+ # return (
111
+ # <Suspense fallback={<div>Loading...</div>}>
112
+ # <AsyncComponent />
113
+ # </Suspense>
114
+ # );
115
+ # }
116
+ #
117
+ # @param [String] component_name Name of your registered component
118
+ # @param [Hash] options Options for rendering
119
+ # @option options [Hash] :props Props to pass to the react component
120
+ # @option options [String] :dom_id DOM ID of the component container
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
+ # @option options [Boolean] :trace Set to true to add extra debugging information to the HTML
124
+ # @option options [Boolean] :raise_on_prerender_error Set to true to raise exceptions during server-side rendering
125
+ # Any other options are passed to the content tag, including the id.
126
+ 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"
135
+ end
136
+
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
142
+ 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
+ end
151
+
94
152
  # react_component_hash is used to return multiple HTML strings for server rendering, such as for
95
153
  # adding meta-tags to a page.
96
154
  # It is exactly like react_component except for the following:
@@ -330,6 +388,16 @@ module ReactOnRails
330
388
 
331
389
  private
332
390
 
391
+ def internal_stream_react_component(component_name, options = {})
392
+ options = options.merge(stream?: true)
393
+ result = internal_react_component(component_name, options)
394
+ build_react_component_result_for_server_streamed_content(
395
+ rendered_html_stream: result[:result],
396
+ component_specification_tag: result[:tag],
397
+ render_options: result[:render_options]
398
+ )
399
+ end
400
+
333
401
  def generated_components_pack_path(component_name)
334
402
  "#{ReactOnRails::PackerUtils.packer_source_entry_path}/generated/#{component_name}.js"
335
403
  end
@@ -361,6 +429,32 @@ module ReactOnRails
361
429
  prepend_render_rails_context(result)
362
430
  end
363
431
 
432
+ def build_react_component_result_for_server_streamed_content(
433
+ rendered_html_stream:,
434
+ component_specification_tag:,
435
+ render_options:
436
+ )
437
+ is_first_chunk = true
438
+ rendered_html_stream.transform do |chunk_json_result|
439
+ if is_first_chunk
440
+ is_first_chunk = false
441
+ build_react_component_result_for_server_rendered_string(
442
+ server_rendered_html: chunk_json_result["html"],
443
+ component_specification_tag: component_specification_tag,
444
+ console_script: chunk_json_result["consoleReplayScript"],
445
+ render_options: render_options
446
+ )
447
+ else
448
+ result_console_script = render_options.replay_console ? chunk_json_result["consoleReplayScript"] : ""
449
+ # No need to prepend component_specification_tag or add rails context again
450
+ # as they're already included in the first chunk
451
+ compose_react_component_html_with_spec_and_console(
452
+ "", chunk_json_result["html"], result_console_script
453
+ )
454
+ end
455
+ end
456
+ end
457
+
364
458
  def build_react_component_result_for_server_rendered_hash(
365
459
  server_rendered_html: required("server_rendered_html"),
366
460
  component_specification_tag: required("component_specification_tag"),
@@ -397,27 +491,30 @@ module ReactOnRails
397
491
 
398
492
  def compose_react_component_html_with_spec_and_console(component_specification_tag, rendered_output, console_script)
399
493
  # IMPORTANT: Ensure that we mark string as html_safe to avoid escaping.
400
- <<~HTML.html_safe
494
+ html_content = <<~HTML
401
495
  #{rendered_output}
402
496
  #{component_specification_tag}
403
497
  #{console_script}
404
498
  HTML
499
+ html_content.strip.html_safe
405
500
  end
406
501
 
407
- # prepend the rails_context if not yet applied
408
- def prepend_render_rails_context(render_value)
409
- return render_value if @rendered_rails_context
502
+ def rails_context_if_not_already_rendered
503
+ return "" if @rendered_rails_context
410
504
 
411
505
  data = rails_context(server_side: false)
412
506
 
413
507
  @rendered_rails_context = true
414
508
 
415
- rails_context_content = content_tag(:script,
416
- json_safe_and_pretty(data).html_safe,
417
- type: "application/json",
418
- id: "js-react-on-rails-context")
509
+ content_tag(:script,
510
+ json_safe_and_pretty(data).html_safe,
511
+ type: "application/json",
512
+ id: "js-react-on-rails-context")
513
+ end
419
514
 
420
- "#{rails_context_content}\n#{render_value}".html_safe
515
+ # prepend the rails_context if not yet applied
516
+ def prepend_render_rails_context(render_value)
517
+ "#{rails_context_if_not_already_rendered}\n#{render_value}".strip.html_safe
421
518
  end
422
519
 
423
520
  def internal_react_component(react_component_name, options = {})
@@ -473,6 +570,25 @@ ReactOnRails.reactOnRailsComponentLoaded('#{render_options.dom_id}');
473
570
  props.is_a?(String) ? props : props.to_json
474
571
  end
475
572
 
573
+ def raise_prerender_error(json_result, react_component_name, props, js_code)
574
+ raise ReactOnRails::PrerenderError.new(
575
+ component_name: react_component_name,
576
+ props: sanitized_props_string(props),
577
+ err: nil,
578
+ js_code: js_code,
579
+ console_messages: json_result["consoleReplayScript"]
580
+ )
581
+ end
582
+
583
+ def should_raise_streaming_prerender_error?(chunk_json_result, render_options)
584
+ chunk_json_result["hasErrors"] &&
585
+ (if chunk_json_result["isShellReady"]
586
+ render_options.raise_non_shell_server_rendering_errors
587
+ else
588
+ render_options.raise_on_prerender_error
589
+ end)
590
+ end
591
+
476
592
  # Returns object with values that are NOT html_safe!
477
593
  def server_rendered_react_component(render_options)
478
594
  return { "html" => "", "consoleReplayScript" => "" } unless render_options.prerender
@@ -520,16 +636,18 @@ ReactOnRails.reactOnRailsComponentLoaded('#{render_options.dom_id}');
520
636
  js_code: js_code)
521
637
  end
522
638
 
523
- if result["hasErrors"] && render_options.raise_on_prerender_error
524
- # We caught this exception on our backtrace handler
525
- raise ReactOnRails::PrerenderError.new(component_name: react_component_name,
526
- # Sanitize as this might be browser logged
527
- props: sanitized_props_string(props),
528
- err: nil,
529
- js_code: js_code,
530
- console_messages: result["consoleReplayScript"])
531
-
639
+ if render_options.stream?
640
+ result.transform do |chunk_json_result|
641
+ if should_raise_streaming_prerender_error?(chunk_json_result, render_options)
642
+ raise_prerender_error(chunk_json_result, react_component_name, props, js_code)
643
+ end
644
+ # It doesn't make any transformation, it listens and raises error if a chunk has errors
645
+ chunk_json_result
646
+ end
647
+ elsif result["hasErrors"] && render_options.raise_on_prerender_error
648
+ raise_prerender_error(result, react_component_name, props, js_code)
532
649
  end
650
+
533
651
  result
534
652
  end
535
653
 
@@ -87,6 +87,10 @@ module ReactOnRails
87
87
  retrieve_configuration_value_for(:raise_on_prerender_error)
88
88
  end
89
89
 
90
+ def raise_non_shell_server_rendering_errors
91
+ retrieve_react_on_rails_pro_config_value_for(:raise_non_shell_server_rendering_errors)
92
+ end
93
+
90
94
  def logging_on_server
91
95
  retrieve_configuration_value_for(:logging_on_server)
92
96
  end
@@ -107,6 +111,10 @@ module ReactOnRails
107
111
  options[key] = value
108
112
  end
109
113
 
114
+ def stream?
115
+ options[:stream?]
116
+ end
117
+
110
118
  private
111
119
 
112
120
  attr_reader :options
@@ -124,6 +132,14 @@ module ReactOnRails
124
132
  ReactOnRails.configuration.public_send(key)
125
133
  end
126
134
  end
135
+
136
+ def retrieve_react_on_rails_pro_config_value_for(key)
137
+ options.fetch(key) do
138
+ return nil unless ReactOnRails::Utils.react_on_rails_pro?
139
+
140
+ ReactOnRailsPro.configuration.public_send(key)
141
+ end
142
+ end
127
143
  end
128
144
  end
129
145
  end
@@ -46,7 +46,7 @@ module ReactOnRails
46
46
  # Note, js_code does not have to be based on React.
47
47
  # js_code MUST RETURN json stringify Object
48
48
  # Calling code will probably call 'html_safe' on return value before rendering to the view.
49
- # rubocop:disable Metrics/CyclomaticComplexity, Metrics/AbcSize, Metrics/PerceivedComplexity
49
+ # rubocop:disable Metrics/CyclomaticComplexity
50
50
  def exec_server_render_js(js_code, render_options, js_evaluator = nil)
51
51
  js_evaluator ||= self
52
52
  if render_options.trace
@@ -56,7 +56,11 @@ module ReactOnRails
56
56
  @file_index += 1
57
57
  end
58
58
  begin
59
- json_string = js_evaluator.eval_js(js_code, render_options)
59
+ result = if render_options.stream?
60
+ js_evaluator.eval_streaming_js(js_code, render_options)
61
+ else
62
+ js_evaluator.eval_js(js_code, render_options)
63
+ end
60
64
  rescue StandardError => err
61
65
  msg = <<~MSG
62
66
  Error evaluating server bundle. Check your webpack configuration.
@@ -71,26 +75,14 @@ module ReactOnRails
71
75
  end
72
76
  raise ReactOnRails::Error, msg, err.backtrace
73
77
  end
74
- result = nil
75
- begin
76
- result = JSON.parse(json_string)
77
- rescue JSON::ParserError => e
78
- raise ReactOnRails::JsonParseError.new(parse_error: e, json: json_string)
79
- end
80
78
 
81
- if render_options.logging_on_server
82
- console_script = result["consoleReplayScript"]
83
- console_script_lines = console_script.split("\n")
84
- console_script_lines = console_script_lines[2..-2]
85
- re = /console\.(?:log|error)\.apply\(console, \["\[SERVER\] (?<msg>.*)"\]\);/
86
- console_script_lines&.each do |line|
87
- match = re.match(line)
88
- Rails.logger.info { "[react_on_rails] #{match[:msg]}" } if match
89
- end
90
- end
91
- result
79
+ return parse_result_and_replay_console_messages(result, render_options) unless render_options.stream?
80
+
81
+ # Streamed component is returned as stream of strings.
82
+ # We need to parse each chunk and replay the console messages.
83
+ result.transform { |chunk| parse_result_and_replay_console_messages(chunk, render_options) }
92
84
  end
93
- # rubocop:enable Metrics/CyclomaticComplexity, Metrics/AbcSize, Metrics/PerceivedComplexity
85
+ # rubocop:enable Metrics/CyclomaticComplexity
94
86
 
95
87
  def trace_js_code_used(msg, js_code, file_name = "tmp/server-generated.js", force: false)
96
88
  return unless ReactOnRails.configuration.trace || force
@@ -233,6 +225,28 @@ module ReactOnRails
233
225
  msg = "file_url_to_string #{url} failed\nError is: #{e}"
234
226
  raise ReactOnRails::Error, msg
235
227
  end
228
+
229
+ def parse_result_and_replay_console_messages(result_string, render_options)
230
+ result = nil
231
+ begin
232
+ result = JSON.parse(result_string)
233
+ rescue JSON::ParserError => e
234
+ raise ReactOnRails::JsonParseError.new(parse_error: e, json: result_string)
235
+ end
236
+
237
+ if render_options.logging_on_server
238
+ console_script = result["consoleReplayScript"]
239
+ console_script_lines = console_script.split("\n")
240
+ # Regular expression to match console.log or console.error calls with SERVER prefix
241
+ re = /console\.(?:log|error)\.apply\(console, \["\[SERVER\] (?<msg>.*)"\]\);/
242
+ console_script_lines&.each do |line|
243
+ match = re.match(line)
244
+ # Log matched messages to Rails logger with react_on_rails prefix
245
+ Rails.logger.info { "[react_on_rails] #{match[:msg]}" } if match
246
+ end
247
+ end
248
+ result
249
+ end
236
250
  end
237
251
  # rubocop:enable Metrics/ClassLength
238
252
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ReactOnRails
4
- VERSION = "14.0.5"
4
+ VERSION = "15.0.0.alpha.1"
5
5
  end
data/tsconfig.json CHANGED
@@ -8,6 +8,7 @@
8
8
  "noImplicitAny": true,
9
9
  "outDir": "node_package/lib",
10
10
  "strict": true,
11
+ "incremental": true,
11
12
  "target": "es5"
12
13
  },
13
14
  "include": ["node_package/src/**/*"]
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: 14.0.5
4
+ version: 15.0.0.alpha.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Gordon
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-08-21 00:00:00.000000000 Z
11
+ date: 2024-11-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: addressable