Skip to content

Commit edad27b

Browse files
tmp
1 parent f5d4c26 commit edad27b

File tree

8 files changed

+174
-18
lines changed

8 files changed

+174
-18
lines changed

lib/react_on_rails/helper.rb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,15 @@ def stream_react_component(component_name, options = {})
149149
rendering_fiber.resume
150150
end
151151

152+
def rsc_react_component(component_name, options = {})
153+
res = internal_rsc_react_component(component_name, options)
154+
s = ""
155+
res.each_chunk do |chunk|
156+
s += chunk
157+
end
158+
s
159+
end
160+
152161
# react_component_hash is used to return multiple HTML strings for server rendering, such as for
153162
# adding meta-tags to a page.
154163
# It is exactly like react_component except for the following:
@@ -517,6 +526,13 @@ def prepend_render_rails_context(render_value)
517526
"#{rails_context_if_not_already_rendered}\n#{render_value}".strip.html_safe
518527
end
519528

529+
def internal_rsc_react_component(react_component_name, options = {})
530+
options = options.merge(rsc?: true)
531+
render_options = ReactOnRails::ReactComponent::RenderOptions.new(react_component_name: react_component_name,
532+
options: options)
533+
server_rendered_react_component(render_options)
534+
end
535+
520536
def internal_react_component(react_component_name, options = {})
521537
# Create the JavaScript and HTML to allow either client or server rendering of the
522538
# react_component.
@@ -636,6 +652,9 @@ def server_rendered_react_component(render_options)
636652
js_code: js_code)
637653
end
638654

655+
# TODO: handle errors for rsc streams
656+
return result if render_options.rsc?
657+
639658
if render_options.stream?
640659
result.transform do |chunk_json_result|
641660
if should_raise_streaming_prerender_error?(chunk_json_result, render_options)

lib/react_on_rails/react_component/render_options.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,10 @@ def stream?
115115
options[:stream?]
116116
end
117117

118+
def rsc?
119+
options[:rsc?]
120+
end
121+
118122
private
119123

120124
attr_reader :options

lib/react_on_rails/utils.rb

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ def self.server_bundle_path_is_http?
6666
server_bundle_js_file_path =~ %r{https?://}
6767
end
6868

69-
def self.server_bundle_js_file_path
69+
def self.bundle_js_file_path(bundle_name)
7070
# Either:
7171
# 1. Using same bundle for both server and client, so server bundle will be hashed in manifest
7272
# 2. Using a different bundle (different Webpack config), so file is not hashed, and
@@ -76,12 +76,9 @@ def self.server_bundle_js_file_path
7676
# a. The webpack manifest plugin would have a race condition where the same manifest.json
7777
# is edited by both the webpack-dev-server
7878
# b. There is no good reason to hash the server bundle name.
79-
return @server_bundle_path if @server_bundle_path && !Rails.env.development?
80-
81-
bundle_name = ReactOnRails.configuration.server_bundle_js_file
82-
@server_bundle_path = if ReactOnRails::PackerUtils.using_packer?
79+
@server_bundle_path ||= if ReactOnRails::PackerUtils.using_packer? && bundle_name != "manifest.json"
8380
begin
84-
bundle_js_file_path(bundle_name)
81+
ReactOnRails::PackerUtils.bundle_js_uri_from_packer(bundle_name)
8582
rescue Object.const_get(
8683
ReactOnRails::PackerUtils.packer_type.capitalize
8784
)::Manifest::MissingEntryError
@@ -91,19 +88,35 @@ def self.server_bundle_js_file_path
9188
)
9289
end
9390
else
94-
bundle_js_file_path(bundle_name)
91+
# Default to the non-hashed name in the specified output directory, which, for legacy
92+
# React on Rails, this is the output directory picked up by the asset pipeline.
93+
# For Webpacker, this is the public output path defined in the webpacker.yml file.
94+
File.join(generated_assets_full_path, bundle_name)
9595
end
9696
end
9797

98-
def self.bundle_js_file_path(bundle_name)
99-
if ReactOnRails::PackerUtils.using_packer? && bundle_name != "manifest.json"
100-
ReactOnRails::PackerUtils.bundle_js_uri_from_packer(bundle_name)
101-
else
102-
# Default to the non-hashed name in the specified output directory, which, for legacy
103-
# React on Rails, this is the output directory picked up by the asset pipeline.
104-
# For Shakapacker, this is the public output path defined in the (shaka/web)packer.yml file.
105-
File.join(generated_assets_full_path, bundle_name)
106-
end
98+
def self.server_bundle_js_file_path
99+
# Either:
100+
# 1. Using same bundle for both server and client, so server bundle will be hashed in manifest
101+
# 2. Using a different bundle (different Webpack config), so file is not hashed, and
102+
# bundle_js_path will throw so the default path is used without a hash.
103+
# 3. The third option of having the server bundle hashed and a different configuration than
104+
# the client bundle is not supported for 2 reasons:
105+
# a. The webpack manifest plugin would have a race condition where the same manifest.json
106+
# is edited by both the webpack-dev-server
107+
# b. There is no good reason to hash the server bundle name.
108+
return @server_bundle_path if @server_bundle_path && !Rails.env.development?
109+
110+
bundle_name = ReactOnRails.configuration.server_bundle_js_file
111+
bundle_js_file_path(bundle_name)
112+
end
113+
114+
def self.rsc_bundle_js_file_path
115+
return @rsc_bundle_path if @rsc_bundle_path && !Rails.env.development?
116+
117+
# TODO: make it configurable
118+
bundle_name = "rsc-bundle.js"
119+
@server_bundle_path = bundle_js_file_path(bundle_name)
107120
end
108121

109122
def self.running_on_windows?

node_package/src/ReactOnRails.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,15 @@ ctx.ReactOnRails = {
256256
return streamServerRenderedReactComponent(options);
257257
},
258258

259+
/**
260+
* Used by server rendering by Rails
261+
* @param options
262+
*/
263+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
264+
serverRenderRSCReactComponent(options: RenderParams): PassThrough {
265+
throw new Error('serverRenderRSCReactComponent is supported in RSC bundle only.');
266+
},
267+
259268
/**
260269
* Used by Rails to catch errors in rendering
261270
* @param options
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import type { ReactElement } from 'react';
2+
// @ts-expect-error will define this module types later
3+
import { renderToReadableStream } from 'react-server-dom-webpack/server.edge';
4+
import { PassThrough } from 'stream';
5+
6+
import { RenderParams } from './types';
7+
import ComponentRegistry from './ComponentRegistry';
8+
import createReactOutput from './createReactOutput';
9+
import { isPromise, isServerRenderHash } from './isServerRenderResult';
10+
import handleError from './handleError';
11+
import ReactOnRails from './ReactOnRails';
12+
13+
(async () => {
14+
try {
15+
// @ts-expect-error AsyncLocalStorage is not in the node types
16+
globalThis.AsyncLocalStorage = (await import('node:async_hooks')).AsyncLocalStorage;
17+
} catch (e) {
18+
console.log('AsyncLocalStorage not found');
19+
}
20+
})();
21+
22+
const stringToStream = (str: string) => {
23+
const stream = new PassThrough();
24+
stream.push(str);
25+
stream.push(null);
26+
return stream;
27+
};
28+
29+
ReactOnRails.serverRenderRSCReactComponent = (options: RenderParams) => {
30+
const { name, domNodeId, trace, props, railsContext, throwJsErrors } = options;
31+
32+
let renderResult: null | PassThrough = null;
33+
34+
try {
35+
const componentObj = ComponentRegistry.get(name);
36+
if (componentObj.isRenderer) {
37+
throw new Error(`\
38+
Detected a renderer while server rendering component '${name}'. \
39+
See https://github.com/shakacode/react_on_rails#renderer-functions`);
40+
}
41+
42+
const reactRenderingResult = createReactOutput({
43+
componentObj,
44+
domNodeId,
45+
trace,
46+
props,
47+
railsContext,
48+
});
49+
50+
if (isServerRenderHash(reactRenderingResult) || isPromise(reactRenderingResult)) {
51+
throw new Error('Server rendering of streams is not supported for server render hashes or promises.');
52+
}
53+
54+
renderResult = new PassThrough();
55+
const streamReader = renderToReadableStream(reactRenderingResult as ReactElement).getReader();
56+
const processStream = async () => {
57+
const { done, value } = await streamReader.read();
58+
if (done) {
59+
renderResult?.push(null);
60+
return;
61+
}
62+
63+
renderResult?.push(value);
64+
processStream();
65+
}
66+
processStream();
67+
} catch (e: unknown) {
68+
if (throwJsErrors) {
69+
throw e;
70+
}
71+
72+
renderResult = stringToStream(`Error: ${e}`);
73+
}
74+
75+
return renderResult;
76+
};
77+
78+
export * from './types';
79+
export default ReactOnRails;

node_package/src/types/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { ReactElement, ReactNode, Component, ComponentType } from 'react';
2-
import type { Readable } from 'stream';
2+
import type { Readable, PassThrough } from 'stream';
33

44
// Don't import redux just for the type definitions
55
// See https://github.com/shakacode/react_on_rails/issues/1321
@@ -171,6 +171,7 @@ export interface ReactOnRails {
171171
getComponent(name: string): RegisteredComponent;
172172
serverRenderReactComponent(options: RenderParams): null | string | Promise<RenderResult>;
173173
streamServerRenderedReactComponent(options: RenderParams): Readable;
174+
serverRenderRSCReactComponent(options: RenderParams): PassThrough;
174175
handleError(options: ErrorOptions): string | undefined;
175176
buildConsoleReplay(): string;
176177
registeredComponents(): Map<string, RegisteredComponent>;

package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@
22
"name": "react-on-rails",
33
"version": "15.0.0-alpha.1",
44
"description": "react-on-rails JavaScript for react_on_rails Ruby gem",
5-
"main": "node_package/lib/ReactOnRails.js",
5+
"exports": {
6+
".": {
7+
"rsc-server": "./node_package/lib/ReactOnRailsRSC.js",
8+
"default": "./node_package/lib/ReactOnRails.js"
9+
}
10+
},
611
"directories": {
712
"doc": "docs"
813
},
@@ -41,6 +46,7 @@
4146
"prop-types": "^15.8.1",
4247
"react": "18.3.0-canary-670811593-20240322",
4348
"react-dom": "18.3.0-canary-670811593-20240322",
49+
"react-server-dom-webpack": "18.3.0-canary-670811593-20240322",
4450
"react-transform-hmr": "^1.0.4",
4551
"redux": "^4.2.1",
4652
"ts-jest": "^29.1.0",

yarn.lock

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1972,6 +1972,13 @@ acorn-jsx@^5.3.1:
19721972
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b"
19731973
integrity sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==
19741974

1975+
acorn-loose@^8.3.0:
1976+
version "8.4.0"
1977+
resolved "https://registry.yarnpkg.com/acorn-loose/-/acorn-loose-8.4.0.tgz#26d3e219756d1e180d006f5bcc8d261a28530f55"
1978+
integrity sha512-M0EUka6rb+QC4l9Z3T0nJEzNOO7JcoJlYMrBlyBCiFSXRyxjLKayd4TbQs2FDRWQU1h9FR7QVNHt+PEaoNL5rQ==
1979+
dependencies:
1980+
acorn "^8.11.0"
1981+
19751982
acorn-walk@^8.0.2:
19761983
version "8.3.1"
19771984
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.1.tgz#2f10f5b69329d90ae18c58bf1fa8fccd8b959a43"
@@ -2002,6 +2009,11 @@ acorn@^8.1.0, acorn@^8.8.1:
20022009
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a"
20032010
integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==
20042011

2012+
acorn@^8.11.0:
2013+
version "8.12.1"
2014+
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248"
2015+
integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==
2016+
20052017
agent-base@6:
20062018
version "6.0.2"
20072019
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
@@ -5337,6 +5349,11 @@ natural-compare@^1.4.0:
53375349
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
53385350
integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
53395351

5352+
neo-async@^2.6.1:
5353+
version "2.6.2"
5354+
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
5355+
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
5356+
53405357
nice-try@^1.0.4:
53415358
version "1.0.5"
53425359
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
@@ -5902,6 +5919,14 @@ react-proxy@^1.1.7:
59025919
lodash "^4.6.1"
59035920
react-deep-force-update "^1.0.0"
59045921

5922+
react-server-dom-webpack@18.3.0-canary-670811593-20240322:
5923+
version "18.3.0-canary-670811593-20240322"
5924+
resolved "https://registry.yarnpkg.com/react-server-dom-webpack/-/react-server-dom-webpack-18.3.0-canary-670811593-20240322.tgz#e9b99b1f0179357e5acbf2fbacaee88dd1e8bf3b"
5925+
integrity sha512-YaCk3AvvOXcOo0FL7SlAY2GVBeuZKFQ/5FfAtE48IjpI6MvXTwMBu3QVnT/Ukk9Y4M9GzpIbLtuc8hPjfFAOaw==
5926+
dependencies:
5927+
acorn-loose "^8.3.0"
5928+
neo-async "^2.6.1"
5929+
59055930
react-transform-hmr@^1.0.4:
59065931
version "1.0.4"
59075932
resolved "https://registry.yarnpkg.com/react-transform-hmr/-/react-transform-hmr-1.0.4.tgz#e1a40bd0aaefc72e8dfd7a7cda09af85066397bb"

0 commit comments

Comments
 (0)