Skip to content

Commit 4e3ad5b

Browse files
tmp
1 parent ed92489 commit 4e3ad5b

File tree

8 files changed

+187
-39
lines changed

8 files changed

+187
-39
lines changed

lib/react_on_rails/helper.rb

+17-1
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,15 @@ def react_component(component_name, options = {})
9191
end
9292
end
9393

94+
def rsc_react_component(component_name, options = {})
95+
res = internal_rsc_react_component(component_name, options)
96+
s = ""
97+
res.each_chunk do |chunk|
98+
s += chunk
99+
end
100+
s
101+
end
102+
94103
def stream_react_component(component_name, options = {})
95104
options = options.merge(stream?: true)
96105
result = internal_react_component(component_name, options)
@@ -468,6 +477,13 @@ def prepend_render_rails_context(render_value)
468477
"#{rails_context_if_not_already_rendered}\n#{render_value}".html_safe
469478
end
470479

480+
def internal_rsc_react_component(react_component_name, options = {})
481+
options = options.merge(rsc?: true)
482+
render_options = ReactOnRails::ReactComponent::RenderOptions.new(react_component_name: react_component_name,
483+
options: options)
484+
server_rendered_react_component(render_options)
485+
end
486+
471487
def internal_react_component(react_component_name, options = {})
472488
# Create the JavaScript and HTML to allow either client or server rendering of the
473489
# react_component.
@@ -561,7 +577,7 @@ def server_rendered_react_component(render_options)
561577
end
562578

563579
# TODO: handle errors for streams
564-
return result if render_options.stream?
580+
return result if render_options.stream? || render_options.rsc?
565581

566582
if result["hasErrors"] && render_options.raise_on_prerender_error
567583
# We caught this exception on our backtrace handler

lib/react_on_rails/react_component/render_options.rb

+4
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,10 @@ def stream?
107107
options[:stream?]
108108
end
109109

110+
def rsc?
111+
options[:rsc?]
112+
end
113+
110114
private
111115

112116
attr_reader :options

lib/react_on_rails/utils.rb

+29-16
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,32 +76,45 @@ 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::WebpackerUtils.using_webpacker?
79+
@server_bundle_path = if ReactOnRails::WebpackerUtils.using_webpacker? && bundle_name != "manifest.json"
8380
begin
84-
bundle_js_file_path(bundle_name)
81+
ReactOnRails::WebpackerUtils.bundle_js_uri_from_webpacker(bundle_name)
8582
rescue Webpacker::Manifest::MissingEntryError
8683
File.expand_path(
8784
File.join(ReactOnRails::WebpackerUtils.webpacker_public_output_path,
8885
bundle_name)
8986
)
9087
end
9188
else
92-
bundle_js_file_path(bundle_name)
89+
# Default to the non-hashed name in the specified output directory, which, for legacy
90+
# React on Rails, this is the output directory picked up by the asset pipeline.
91+
# For Webpacker, this is the public output path defined in the webpacker.yml file.
92+
File.join(generated_assets_full_path, bundle_name)
9393
end
9494
end
9595

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

107120
def self.running_on_windows?

node_package/src/ReactOnRails.ts

+9
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,15 @@ ctx.ReactOnRails = {
250250
return streamServerRenderedReactComponent(options);
251251
},
252252

253+
/**
254+
* Used by server rendering by Rails
255+
* @param options
256+
*/
257+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
258+
serverRenderRSCReactComponent(options: RenderParams): PassThrough {
259+
throw new Error('serverRenderRSCReactComponent is supported in RSC bundle only.');
260+
},
261+
253262
/**
254263
* Used by Rails to catch errors in rendering
255264
* @param options

node_package/src/ReactOnRailsRSC.ts

+79
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

+1
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ export interface ReactOnRails {
139139
getComponent(name: string): RegisteredComponent;
140140
serverRenderReactComponent(options: RenderParams): null | string | Promise<RenderResult>;
141141
streamServerRenderedReactComponent(options: RenderParams): PassThrough;
142+
serverRenderRSCReactComponent(options: RenderParams): PassThrough;
142143
handleError(options: ErrorOptions): string | undefined;
143144
buildConsoleReplay(): string;
144145
registeredComponents(): Map<string, RegisteredComponent>;

package.json

+9-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@
22
"name": "react-on-rails",
33
"version": "14.0.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
},
@@ -39,8 +44,9 @@
3944
"prettier": "^2.8.8",
4045
"prettier-eslint-cli": "^5.0.0",
4146
"prop-types": "^15.8.1",
42-
"react": "^18.2.0",
43-
"react-dom": "^18.2.0",
47+
"react": "18.3.0-canary-670811593-20240322",
48+
"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

+39-19
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"
@@ -5155,7 +5167,7 @@ loglevel@^1.4.1:
51555167
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.7.1.tgz#005fde2f5e6e47068f935ff28573e125ef72f197"
51565168
integrity sha512-Hesni4s5UkWkwCGJMQGAh71PaLUmKFM60dHvq0zi/vDhhrzuk+4GgNbTXJ12YYQJn6ZKBDNIjYcuQGKudvqrIw==
51575169

5158-
loose-envify@^1.1.0, loose-envify@^1.3.1, loose-envify@^1.4.0:
5170+
loose-envify@^1.3.1, loose-envify@^1.4.0:
51595171
version "1.4.0"
51605172
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
51615173
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
@@ -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"
@@ -5877,13 +5894,12 @@ react-deep-force-update@^1.0.0:
58775894
resolved "https://registry.yarnpkg.com/react-deep-force-update/-/react-deep-force-update-1.1.2.tgz#3d2ae45c2c9040cbb1772be52f8ea1ade6ca2ee1"
58785895
integrity sha512-WUSQJ4P/wWcusaH+zZmbECOk7H5N2pOIl0vzheeornkIMhu+qrNdGFm0bDZLCb0hSF0jf/kH1SgkNGfBdTc4wA==
58795896

5880-
react-dom@^18.2.0:
5881-
version "18.3.1"
5882-
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4"
5883-
integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==
5897+
react-dom@18.3.0-canary-670811593-20240322:
5898+
version "18.3.0-canary-670811593-20240322"
5899+
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.0-canary-670811593-20240322.tgz#ac677b164fd83050272bf985e740ed4ca65337be"
5900+
integrity sha512-AHxCnyDzZueXIHY4WA2Uba1yaL7/vbjhO3D3TWPQeruKD5MwgD0/xExZi0T104gBr6Thv6MEsLSxFjBAHhHKKg==
58845901
dependencies:
5885-
loose-envify "^1.1.0"
5886-
scheduler "^0.23.2"
5902+
scheduler "0.24.0-canary-670811593-20240322"
58875903

58885904
react-is@^16.13.1:
58895905
version "16.13.1"
@@ -5903,6 +5919,14 @@ react-proxy@^1.1.7:
59035919
lodash "^4.6.1"
59045920
react-deep-force-update "^1.0.0"
59055921

5922+
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+
59065930
react-transform-hmr@^1.0.4:
59075931
version "1.0.4"
59085932
resolved "https://registry.yarnpkg.com/react-transform-hmr/-/react-transform-hmr-1.0.4.tgz#e1a40bd0aaefc72e8dfd7a7cda09af85066397bb"
@@ -5911,12 +5935,10 @@ react-transform-hmr@^1.0.4:
59115935
global "^4.3.0"
59125936
react-proxy "^1.1.7"
59135937

5914-
react@^18.2.0:
5915-
version "18.3.1"
5916-
resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891"
5917-
integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==
5918-
dependencies:
5919-
loose-envify "^1.1.0"
5938+
5939+
version "18.3.0-canary-670811593-20240322"
5940+
resolved "https://registry.yarnpkg.com/react/-/react-18.3.0-canary-670811593-20240322.tgz#3735250b45468d313ed36121324452bb5a732e9b"
5941+
integrity sha512-EI6+q3tOT+0z4OkB2sz842Ra/n/yz7b3jOJhSK1HQwi4Ng29VJzLGngWmSuxQ94YfdE3EBhpUKDfgNgzoKM9Vg==
59205942

59215943
readdirp@~3.6.0:
59225944
version "3.6.0"
@@ -6213,12 +6235,10 @@ saxes@^6.0.0:
62136235
dependencies:
62146236
xmlchars "^2.2.0"
62156237

6216-
scheduler@^0.23.2:
6217-
version "0.23.2"
6218-
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3"
6219-
integrity sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==
6220-
dependencies:
6221-
loose-envify "^1.1.0"
6238+
6239+
version "0.24.0-canary-670811593-20240322"
6240+
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.24.0-canary-670811593-20240322.tgz#45c5c45f18a127ab4e3c805dd466bc231b20adf3"
6241+
integrity sha512-IGX6Fq969h1L0X7jV0sJ/EdI4fr+mRetbBNJl55nn+/RsCuQSVwgKnZG6Q3NByixDNbkRI8nRmWuhOm8NQowGQ==
62226242

62236243
62246244
version "5.5.0"

0 commit comments

Comments
 (0)