-
-
Notifications
You must be signed in to change notification settings - Fork 629
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[WIP] Add support for RSC #1644
base: master
Are you sure you want to change the base?
Changes from all commits
c3739f8
9c6b763
2f7d3a5
c226cd1
c34fe34
b8d18e7
3e05fef
ac3263a
9b8de13
0c1de19
ed59054
ad75762
28c527c
1a1928a
65a0040
a331d39
62f19cb
40c89e4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -32,3 +32,6 @@ npm-debug.* | |
yalc.lock | ||
|
||
.byebug_history | ||
|
||
# IDE | ||
.idea/ |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
module.exports = { | ||
preset: 'ts-jest/presets/js-with-ts', | ||
testEnvironment: 'jsdom', | ||
setupFiles: ['<rootDir>/node_package/tests/jest.setup.js'], | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -91,6 +91,39 @@ def react_component(component_name, options = {}) | |
end | ||
end | ||
|
||
def rsc_react_component(component_name, options = {}) | ||
rendering_fiber = Fiber.new do | ||
res = internal_rsc_react_component(component_name, options) | ||
res.each_chunk do |chunk| | ||
Fiber.yield chunk | ||
end | ||
Fiber.yield nil | ||
end | ||
rendering_fiber | ||
end | ||
|
||
def stream_react_component(component_name, options = {}) | ||
rendering_fiber = Fiber.new do | ||
stream = stream_react_component_internal(component_name, options) | ||
stream.each_chunk do |chunk| | ||
Fiber.yield chunk | ||
end | ||
Fiber.yield nil | ||
end | ||
|
||
if @rorp_rendering_fibers.nil? | ||
raise ReactOnRails::Error, | ||
"You must call stream_view_containing_react_components to render the view containing the react component" | ||
end | ||
|
||
@rorp_rendering_fibers << rendering_fiber | ||
|
||
# return the first chunk of the fiber | ||
# It contains the initial html of the component | ||
# all updates will be appended to the stream sent to browser | ||
rendering_fiber.resume | ||
end | ||
|
||
# react_component_hash is used to return multiple HTML strings for server rendering, such as for | ||
# adding meta-tags to a page. | ||
# It is exactly like react_component except for the following: | ||
|
@@ -317,19 +350,30 @@ def load_pack_for_generated_component(react_component_name, render_options) | |
return unless render_options.auto_load_bundle | ||
|
||
ReactOnRails::PackerUtils.raise_nested_entries_disabled unless ReactOnRails::PackerUtils.nested_entries? | ||
if Rails.env.development? | ||
is_component_pack_present = File.exist?(generated_components_pack_path(react_component_name)) | ||
raise_missing_autoloaded_bundle(react_component_name) unless is_component_pack_present | ||
end | ||
append_javascript_pack_tag("generated/#{react_component_name}", | ||
defer: ReactOnRails.configuration.defer_generated_component_packs) | ||
append_stylesheet_pack_tag("generated/#{react_component_name}") | ||
append_javascript_pack_tag("client-bundle") | ||
# if Rails.env.development? | ||
# is_component_pack_present = File.exist?(generated_components_pack_path(react_component_name)) | ||
# raise_missing_autoloaded_bundle(react_component_name) unless is_component_pack_present | ||
# end | ||
# append_javascript_pack_tag("generated/#{react_component_name}", | ||
# defer: ReactOnRails.configuration.defer_generated_component_packs) | ||
# append_stylesheet_pack_tag("generated/#{react_component_name}") | ||
end | ||
|
||
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity | ||
|
||
private | ||
|
||
def stream_react_component_internal(component_name, options = {}) | ||
options = options.merge(stream?: true) | ||
result = internal_react_component(component_name, options) | ||
build_react_component_result_for_server_streamed_content( | ||
rendered_html_stream: result[:result], | ||
component_specification_tag: result[:tag], | ||
render_options: result[:render_options] | ||
) | ||
end | ||
|
||
def generated_components_pack_path(component_name) | ||
"#{ReactOnRails::PackerUtils.packer_source_entry_path}/generated/#{component_name}.js" | ||
end | ||
|
@@ -361,6 +405,33 @@ def build_react_component_result_for_server_rendered_string( | |
prepend_render_rails_context(result) | ||
end | ||
|
||
def build_react_component_result_for_server_streamed_content( | ||
rendered_html_stream: required("rendered_html_stream"), | ||
component_specification_tag: required("component_specification_tag"), | ||
render_options: required("render_options") | ||
) | ||
content_tag_options_html_tag = render_options.html_options[:tag] || "div" | ||
# The component_specification_tag is appended to the first chunk | ||
# We need to pass it early with the first chunk because it's needed in hydration | ||
# We need to make sure that client can hydrate the app early even before all components are streamed | ||
is_first_chunk = true | ||
rendered_html_stream = rendered_html_stream.transform do |chunk| | ||
if is_first_chunk | ||
is_first_chunk = false | ||
html_content = <<-HTML | ||
#{rails_context_if_not_already_rendered} | ||
#{component_specification_tag} | ||
<#{content_tag_options_html_tag} id="#{render_options.dom_id}">#{chunk}</#{content_tag_options_html_tag}> | ||
HTML | ||
next html_content.strip | ||
end | ||
chunk | ||
end | ||
|
||
rendered_html_stream.transform(&:html_safe) | ||
# TODO: handle console logs | ||
end | ||
|
||
def build_react_component_result_for_server_rendered_hash( | ||
server_rendered_html: required("server_rendered_html"), | ||
component_specification_tag: required("component_specification_tag"), | ||
|
@@ -404,20 +475,29 @@ def compose_react_component_html_with_spec_and_console(component_specification_t | |
HTML | ||
end | ||
|
||
# prepend the rails_context if not yet applied | ||
def prepend_render_rails_context(render_value) | ||
return render_value if @rendered_rails_context | ||
def rails_context_if_not_already_rendered | ||
return "" if @rendered_rails_context | ||
|
||
data = rails_context(server_side: false) | ||
|
||
@rendered_rails_context = true | ||
|
||
rails_context_content = content_tag(:script, | ||
json_safe_and_pretty(data).html_safe, | ||
type: "application/json", | ||
id: "js-react-on-rails-context") | ||
content_tag(:script, | ||
json_safe_and_pretty(data).html_safe, | ||
type: "application/json", | ||
id: "js-react-on-rails-context") | ||
end | ||
|
||
# prepend the rails_context if not yet applied | ||
def prepend_render_rails_context(render_value) | ||
"#{rails_context_if_not_already_rendered}\n#{render_value}".strip.html_safe | ||
end | ||
|
||
"#{rails_context_content}\n#{render_value}".html_safe | ||
def internal_rsc_react_component(react_component_name, options = {}) | ||
options = options.merge(rsc?: true) | ||
render_options = ReactOnRails::ReactComponent::RenderOptions.new(react_component_name: react_component_name, | ||
options: options) | ||
server_rendered_react_component(render_options) | ||
end | ||
|
||
def internal_react_component(react_component_name, options = {}) | ||
|
@@ -512,6 +592,9 @@ def server_rendered_react_component(render_options) | |
js_code: js_code) | ||
end | ||
|
||
# TODO: handle errors for streams | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Still left as TODO? |
||
return result if render_options.stream? || render_options.rsc? | ||
|
||
if result["hasErrors"] && render_options.raise_on_prerender_error | ||
# We caught this exception on our backtrace handler | ||
raise ReactOnRails::PrerenderError.new(component_name: react_component_name, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -66,7 +66,7 @@ def self.server_bundle_path_is_http? | |
server_bundle_js_file_path =~ %r{https?://} | ||
end | ||
|
||
def self.server_bundle_js_file_path | ||
def self.bundle_js_file_path(bundle_name) | ||
# Either: | ||
# 1. Using same bundle for both server and client, so server bundle will be hashed in manifest | ||
# 2. Using a different bundle (different Webpack config), so file is not hashed, and | ||
|
@@ -76,36 +76,49 @@ def self.server_bundle_js_file_path | |
# a. The webpack manifest plugin would have a race condition where the same manifest.json | ||
# is edited by both the webpack-dev-server | ||
# b. There is no good reason to hash the server bundle name. | ||
return @server_bundle_path if @server_bundle_path && !Rails.env.development? | ||
|
||
bundle_name = ReactOnRails.configuration.server_bundle_js_file | ||
@server_bundle_path = if ReactOnRails::PackerUtils.using_packer? | ||
begin | ||
bundle_js_file_path(bundle_name) | ||
rescue Object.const_get( | ||
ReactOnRails::PackerUtils.packer_type.capitalize | ||
)::Manifest::MissingEntryError | ||
File.expand_path( | ||
File.join(ReactOnRails::PackerUtils.packer_public_output_path, | ||
bundle_name) | ||
) | ||
end | ||
else | ||
bundle_js_file_path(bundle_name) | ||
end | ||
end | ||
|
||
def self.bundle_js_file_path(bundle_name) | ||
if ReactOnRails::PackerUtils.using_packer? && bundle_name != "manifest.json" | ||
ReactOnRails::PackerUtils.bundle_js_uri_from_packer(bundle_name) | ||
begin | ||
ReactOnRails::PackerUtils.bundle_js_uri_from_packer(bundle_name) | ||
rescue Object.const_get( | ||
ReactOnRails::PackerUtils.packer_type.capitalize | ||
)::Manifest::MissingEntryError | ||
Comment on lines
+82
to
+84
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This looks like it could easily raise an error itself. Any way around it? |
||
File.expand_path( | ||
File.join(ReactOnRails::PackerUtils.packer_public_output_path, | ||
bundle_name) | ||
) | ||
end | ||
else | ||
# Default to the non-hashed name in the specified output directory, which, for legacy | ||
# React on Rails, this is the output directory picked up by the asset pipeline. | ||
# For Shakapacker, this is the public output path defined in the (shaka/web)packer.yml file. | ||
# For Webpacker, this is the public output path defined in the webpacker.yml file. | ||
Comment on lines
-104
to
+93
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably should be reverted. |
||
File.join(generated_assets_full_path, bundle_name) | ||
end | ||
end | ||
|
||
def self.server_bundle_js_file_path | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the benefit from moving it here? The drawback is less clear diff. |
||
# Either: | ||
# 1. Using same bundle for both server and client, so server bundle will be hashed in manifest | ||
# 2. Using a different bundle (different Webpack config), so file is not hashed, and | ||
# bundle_js_path will throw so the default path is used without a hash. | ||
# 3. The third option of having the server bundle hashed and a different configuration than | ||
# the client bundle is not supported for 2 reasons: | ||
# a. The webpack manifest plugin would have a race condition where the same manifest.json | ||
# is edited by both the webpack-dev-server | ||
# b. There is no good reason to hash the server bundle name. | ||
return @server_bundle_path if @server_bundle_path && !Rails.env.development? | ||
|
||
bundle_name = ReactOnRails.configuration.server_bundle_js_file | ||
@server_bundle_path = bundle_js_file_path(bundle_name) | ||
end | ||
|
||
def self.rsc_bundle_js_file_path | ||
return @rsc_bundle_path if @rsc_bundle_path && !Rails.env.development? | ||
|
||
# TODO: make it configurable | ||
bundle_name = "rsc-bundle.js" | ||
@rsc_bundle_path = bundle_js_file_path(bundle_name) | ||
end | ||
|
||
def self.running_on_windows? | ||
(/cygwin|mswin|mingw|bccwin|wince|emx/ =~ RUBY_PLATFORM) != nil | ||
end | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,11 @@ | ||
import type { ReactElement } from 'react'; | ||
import type { PassThrough } from 'stream'; | ||
|
||
import * as ClientStartup from './clientStartup'; | ||
import handleError from './handleError'; | ||
import ComponentRegistry from './ComponentRegistry'; | ||
import StoreRegistry from './StoreRegistry'; | ||
import serverRenderReactComponent from './serverRenderReactComponent'; | ||
import serverRenderReactComponent, { streamServerRenderedReactComponent } from './serverRenderReactComponent'; | ||
import buildConsoleReplay from './buildConsoleReplay'; | ||
import createReactOutput from './createReactOutput'; | ||
import Authenticity from './Authenticity'; | ||
|
@@ -241,6 +242,23 @@ ctx.ReactOnRails = { | |
return serverRenderReactComponent(options); | ||
}, | ||
|
||
/** | ||
* Used by server rendering by Rails | ||
* @param options | ||
*/ | ||
streamServerRenderedReactComponent(options: RenderParams): PassThrough { | ||
return streamServerRenderedReactComponent(options); | ||
}, | ||
|
||
/** | ||
* Used by server rendering by Rails | ||
* @param options | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No use including this line without a description. |
||
*/ | ||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
serverRenderRSCReactComponent(options: RenderParams): PassThrough { | ||
throw new Error('serverRenderRSCReactComponent is supported in RSC bundle only.'); | ||
}, | ||
|
||
/** | ||
* Used by Rails to catch errors in rendering | ||
* @param options | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be removed or is the plan to revert (if so, better to add a comment)?