Skip to content

Commit 3cace71

Browse files
Use RSC payload t orender server components on server
1 parent a3ad230 commit 3cace71

17 files changed

+216
-35
lines changed

lib/react_on_rails/configuration.rb

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ def self.configure
1010

1111
DEFAULT_GENERATED_ASSETS_DIR = File.join(%w[public webpack], Rails.env).freeze
1212
DEFAULT_REACT_CLIENT_MANIFEST_FILE = "react-client-manifest.json"
13+
DEFAULT_REACT_SERVER_MANIFEST_FILE = "react-server-manifest.json"
1314
DEFAULT_COMPONENT_REGISTRY_TIMEOUT = 5000
1415

1516
def self.configuration
@@ -21,6 +22,7 @@ def self.configuration
2122
server_bundle_js_file: "",
2223
rsc_bundle_js_file: "",
2324
react_client_manifest_file: DEFAULT_REACT_CLIENT_MANIFEST_FILE,
25+
react_server_manifest_file: DEFAULT_REACT_SERVER_MANIFEST_FILE,
2426
prerender: false,
2527
auto_load_bundle: false,
2628
replay_console: true,
@@ -65,7 +67,7 @@ class Configuration
6567
:same_bundle_for_client_and_server, :rendering_props_extension,
6668
:make_generated_server_bundle_the_entrypoint,
6769
:defer_generated_component_packs, :force_load, :rsc_bundle_js_file,
68-
:react_client_manifest_file, :component_registry_timeout
70+
:react_client_manifest_file, :react_server_manifest_file, :component_registry_timeout
6971

7072
# rubocop:disable Metrics/AbcSize
7173
def initialize(node_modules_location: nil, server_bundle_js_file: nil, prerender: nil,
@@ -81,7 +83,8 @@ def initialize(node_modules_location: nil, server_bundle_js_file: nil, prerender
8183
i18n_dir: nil, i18n_yml_dir: nil, i18n_output_format: nil, i18n_yml_safe_load_options: nil,
8284
random_dom_id: nil, server_render_method: nil, rendering_props_extension: nil,
8385
components_subdirectory: nil, auto_load_bundle: nil, force_load: nil,
84-
rsc_bundle_js_file: nil, react_client_manifest_file: nil, component_registry_timeout: nil)
86+
rsc_bundle_js_file: nil, react_client_manifest_file: nil, react_server_manifest_file: nil,
87+
component_registry_timeout: nil)
8588
self.node_modules_location = node_modules_location.present? ? node_modules_location : Rails.root
8689
self.generated_assets_dirs = generated_assets_dirs
8790
self.generated_assets_dir = generated_assets_dir
@@ -111,6 +114,7 @@ def initialize(node_modules_location: nil, server_bundle_js_file: nil, prerender
111114
self.server_bundle_js_file = server_bundle_js_file
112115
self.rsc_bundle_js_file = rsc_bundle_js_file
113116
self.react_client_manifest_file = react_client_manifest_file
117+
self.react_server_manifest_file = react_server_manifest_file
114118
self.same_bundle_for_client_and_server = same_bundle_for_client_and_server
115119
self.server_renderer_pool_size = self.development_mode ? 1 : server_renderer_pool_size
116120
self.server_renderer_timeout = server_renderer_timeout # seconds
@@ -266,6 +270,7 @@ def ensure_webpack_generated_files_exists
266270
files << server_bundle_js_file if server_bundle_js_file.present?
267271
files << rsc_bundle_js_file if rsc_bundle_js_file.present?
268272
files << react_client_manifest_file if react_client_manifest_file.present?
273+
files << react_server_manifest_file if react_server_manifest_file.present?
269274

270275
self.webpack_generated_files = files
271276
end

lib/react_on_rails/packs_generator.rb

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,14 +129,24 @@ def generated_server_pack_file_content
129129
"import #{name} from '#{relative_path(generated_server_bundle_file_path, component_path)}';"
130130
end
131131

132-
components_to_register = component_for_server_registration_to_path.keys
132+
load_server_components = ReactOnRails::Utils.react_on_rails_pro? &&
133+
ReactOnRailsPro.configuration.enable_rsc_support
134+
server_components_to_register = component_for_server_registration_to_path.keys.delete_if do |name|
135+
next true unless load_server_components
136+
137+
component_path = component_for_server_registration_to_path[name]
138+
client_entrypoint?(component_path)
139+
end
140+
client_components_to_register = component_for_server_registration_to_path.keys - server_components_to_register
133141

134142
<<~FILE_CONTENT
135143
import ReactOnRails from 'react-on-rails';
144+
import registerServerComponent from 'react-on-rails/registerServerComponent';
136145
137146
#{server_component_imports.join("\n")}
138147
139-
ReactOnRails.register({#{components_to_register.join(",\n")}});
148+
ReactOnRails.register({#{client_components_to_register.join(",\n")}});
149+
registerServerComponent({#{server_components_to_register.join(",\n")}});
140150
FILE_CONTENT
141151
end
142152

lib/react_on_rails/test_helper/webpack_assets_status_checker.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ def all_compiled_assets
5252
webpack_generated_files = @webpack_generated_files.map do |bundle_name|
5353
if bundle_name == ReactOnRails.configuration.react_client_manifest_file
5454
ReactOnRails::Utils.react_client_manifest_file_path
55+
elsif bundle_name == ReactOnRails.configuration.react_server_manifest_file
56+
ReactOnRails::Utils.react_server_manifest_file_path
5557
else
5658
ReactOnRails::Utils.bundle_js_file_path(bundle_name)
5759
end

lib/react_on_rails/utils.rb

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,14 @@ def self.bundle_js_file_path(bundle_name)
9595
end
9696
end
9797

98+
def self.asset_file_path(asset_name)
99+
if ReactOnRails::PackerUtils.using_packer?
100+
ReactOnRails::PackerUtils.asset_uri_from_packer(asset_name)
101+
else
102+
File.join(generated_assets_full_path, asset_name)
103+
end
104+
end
105+
98106
def self.server_bundle_js_file_path
99107
return @server_bundle_path if @server_bundle_path && !Rails.env.development?
100108

@@ -112,12 +120,15 @@ def self.rsc_bundle_js_file_path
112120
def self.react_client_manifest_file_path
113121
return @react_client_manifest_path if @react_client_manifest_path && !Rails.env.development?
114122

115-
file_name = ReactOnRails.configuration.react_client_manifest_file
116-
@react_client_manifest_path = if ReactOnRails::PackerUtils.using_packer?
117-
ReactOnRails::PackerUtils.asset_uri_from_packer(file_name)
118-
else
119-
File.join(generated_assets_full_path, file_name)
120-
end
123+
asset_name = ReactOnRails.configuration.react_client_manifest_file
124+
@react_client_manifest_path = asset_file_path(asset_name)
125+
end
126+
127+
def self.react_server_manifest_file_path
128+
return @react_server_manifest_path if @react_server_manifest_path && !Rails.env.development?
129+
130+
asset_name = ReactOnRails.configuration.react_server_manifest_file
131+
@react_server_manifest_path = File.join(generated_assets_full_path, asset_name)
121132
end
122133

123134
def self.running_on_windows?

node_package/src/RSCServerRoot.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import fs from 'fs';
2+
import * as React from 'react';
3+
import RSDWClient from 'react-server-dom-webpack/client.node';
4+
import transformRSCStream from './transformRSCNodeStreamAndReplayConsoleLogs';
5+
import loadJsonFile from './loadJsonFile';
6+
7+
if (!('use' in React && typeof React.use === 'function')) {
8+
throw new Error('React.use is not defined. Please ensure you are using React 18 with experimental features enabled or React 19+ to use server components.');
9+
}
10+
11+
const { use } = React;
12+
13+
export type RSCServerRootProps = {
14+
getRscPromise: NodeJS.ReadableStream,
15+
reactClientManifestFileName: string,
16+
reactServerManifestFileName: string,
17+
}
18+
19+
const createFromFetch = (stream: NodeJS.ReadableStream, ssrManifest: Record<string, unknown>) => {
20+
const transformedStream = transformRSCStream(stream);
21+
return RSDWClient.createFromNodeStream(transformedStream, ssrManifest);
22+
}
23+
24+
const createSSRManifest = (reactServerManifestFileName: string, reactClientManifestFileName: string) => {
25+
const reactServerManifest = loadJsonFile(reactServerManifestFileName);
26+
const reactClientManifest = loadJsonFile(reactClientManifestFileName);
27+
28+
const ssrManifest = {
29+
moduleLoading: {
30+
prefix: "/webpack/development/",
31+
crossOrigin: null,
32+
},
33+
moduleMap: {} as Record<string, unknown>,
34+
};
35+
36+
Object.entries(reactClientManifest).forEach(([aboluteFileUrl, clientFileBundlingInfo]) => {
37+
const serverFileBundlingInfo = reactServerManifest[aboluteFileUrl];
38+
ssrManifest.moduleMap[(clientFileBundlingInfo as { id: string }).id] = {
39+
'*': {
40+
id: (serverFileBundlingInfo as { id: string }).id,
41+
chunks: (serverFileBundlingInfo as { chunks: string[] }).chunks,
42+
name: '*',
43+
}
44+
};
45+
});
46+
47+
return ssrManifest;
48+
}
49+
50+
const RSCServerRoot = ({
51+
getRscPromise,
52+
reactClientManifestFileName,
53+
reactServerManifestFileName,
54+
}: RSCServerRootProps) => {
55+
const ssrManifest = createSSRManifest(reactServerManifestFileName, reactClientManifestFileName);
56+
return use(createFromFetch(getRscPromise, ssrManifest));
57+
};
58+
59+
export default RSCServerRoot;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import ReactOnRails from './ReactOnRails';
22
import streamServerRenderedReactComponent from './streamServerRenderedReactComponent';
3+
import RSCServerRoot from './RSCServerRoot';
34

45
ReactOnRails.streamServerRenderedReactComponent = streamServerRenderedReactComponent;
6+
// @ts-expect-error eeee
7+
ReactOnRails.RSCServerRoot = RSCServerRoot;
58

69
export * from './ReactOnRails';
710
export { default } from './ReactOnRails';

node_package/src/ReactOnRails.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,8 @@ ctx.ReactOnRails = {
335335
resetOptions(): void {
336336
this.options = Object.assign({}, DEFAULT_OPTIONS);
337337
},
338+
339+
isRSCBundle: false,
338340
};
339341

340342
ctx.ReactOnRails.resetOptions();

node_package/src/ReactOnRailsRSC.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
streamServerRenderedComponent,
1616
transformRenderStreamChunksToResultObject,
1717
} from './streamServerRenderedReactComponent';
18-
import loadReactClientManifest from './loadReactClientManifest';
18+
import loadJsonFile from './loadJsonFile';
1919

2020
const stringToStream = (str: string) => {
2121
const stream = new PassThrough();
@@ -36,7 +36,7 @@ const streamRenderRSCComponent = (reactElement: ReactElement, options: RSCRender
3636
try {
3737
const rscStream = renderToPipeableStream(
3838
reactElement,
39-
loadReactClientManifest(reactClientManifestFileName),
39+
loadJsonFile(reactClientManifestFileName),
4040
{
4141
onError: (err) => {
4242
const error = convertToError(err);
@@ -69,5 +69,7 @@ ReactOnRails.serverRenderRSCReactComponent = (options: RSCRenderParams) => {
6969
}
7070
};
7171

72+
ReactOnRails.isRSCBundle = true;
73+
7274
export * from './types';
7375
export default ReactOnRails;

node_package/src/loadJsonFile.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import path from 'path';
2+
import fs from 'fs';
3+
4+
const loadedJsonFiles = new Map<string, { [key: string]: unknown; }>();
5+
6+
export default function loadJsonFile(fileName: string) {
7+
// Asset JSON files are uploaded to node renderer.
8+
// Renderer copies assets to the same place as the server-bundle.js and rsc-bundle.js.
9+
// Thus, the __dirname of this code is where we can find the manifest file.
10+
const filePath = path.resolve(__dirname, fileName);
11+
if (!loadedJsonFiles.has(filePath)) {
12+
try {
13+
const file = JSON.parse(fs.readFileSync(filePath, 'utf8'));
14+
loadedJsonFiles.set(filePath, file);
15+
} catch (error) {
16+
console.error(`Failed to load JSON file: ${filePath}`, error);
17+
throw error;
18+
}
19+
}
20+
21+
return loadedJsonFiles.get(filePath)!;
22+
}

node_package/src/loadReactClientManifest.ts

Lines changed: 0 additions & 17 deletions
This file was deleted.

0 commit comments

Comments
 (0)