Skip to content

Commit 746631c

Browse files
use rsc payload to render server components on server
1 parent c0d0c0d commit 746631c

File tree

7 files changed

+106
-67
lines changed

7 files changed

+106
-67
lines changed

lib/react_on_rails_pro/request.rb

Lines changed: 32 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,9 @@ def render_code_as_stream(path, js_code, is_rsc:)
2626
end
2727
end
2828

29-
# TODO: add support for uploading rsc assets
3029
def upload_assets
3130
Rails.logger.info { "[ReactOnRailsPro] Uploading assets" }
3231
perform_request("/upload-assets", form: form_with_assets_and_bundle)
33-
34-
return unless ReactOnRailsPro.configuration.enable_rsc_support
35-
36-
perform_request("/upload-assets", form: form_with_assets_and_bundle(is_rsc: true))
37-
# Explicitly return nil to ensure consistent return value regardless of whether
38-
# enable_rsc_support is true or false. Without this, the method would return nil
39-
# when RSC is disabled but return the response object when RSC is enabled.
40-
nil
4132
end
4233

4334
def asset_exists_on_vm_renderer?(filename)
@@ -116,39 +107,52 @@ def perform_request(path, **post_options) # rubocop:disable Metrics/AbcSize,Metr
116107
def form_with_code(js_code, send_bundle, is_rsc:)
117108
form = common_form_data
118109
form["renderingRequest"] = js_code
119-
populate_form_with_bundle_and_assets(form, is_rsc: is_rsc, check_bundle: false) if send_bundle
110+
unless is_rsc
111+
pool = ReactOnRailsPro::ServerRenderingPool::NodeRenderingPool
112+
form["rscBundleTimestamp"] = pool.rsc_bundle_hash
113+
end
114+
populate_form_with_bundle_and_assets(form, check_bundle: false) if send_bundle
120115
form
121116
end
122117

123-
def populate_form_with_bundle_and_assets(form, is_rsc:, check_bundle:)
124-
server_bundle_path = if is_rsc
125-
ReactOnRails::Utils.rsc_bundle_js_file_path
126-
else
127-
ReactOnRails::Utils.server_bundle_js_file_path
128-
end
118+
def populate_form_with_bundle_and_assets(form, check_bundle:)
119+
server_bundle_path = ReactOnRails::Utils.server_bundle_js_file_path
120+
rsc_support_enabled = ReactOnRailsPro.configuration.enable_rsc_support
121+
rsc_bundle_path = ReactOnRails::Utils.rsc_bundle_js_file_path
122+
129123
if check_bundle && !File.exist?(server_bundle_path)
130124
raise ReactOnRailsPro::Error, "Bundle not found #{server_bundle_path}"
131125
end
132126

127+
if check_bundle && rsc_support_enabled && !File.exist?(rsc_bundle_path)
128+
raise ReactOnRailsPro::Error, "RSC bundle not found #{rsc_bundle_path}"
129+
end
130+
133131
pool = ReactOnRailsPro::ServerRenderingPool::NodeRenderingPool
134-
renderer_bundle_file_name = if is_rsc
135-
pool.rsc_renderer_bundle_file_name
136-
else
137-
pool.renderer_bundle_file_name
138-
end
139132
form["bundle"] = {
140133
body: get_form_body_for_file(server_bundle_path),
141134
content_type: "text/javascript",
142-
filename: renderer_bundle_file_name
135+
filename: pool.renderer_bundle_file_name
143136
}
144137

145-
add_assets_to_form(form, is_rsc: is_rsc)
138+
if rsc_support_enabled
139+
form["rscBundle"] = {
140+
body: get_form_body_for_file(rsc_bundle_path),
141+
content_type: "text/javascript",
142+
filename: pool.rsc_renderer_bundle_file_name
143+
}
144+
end
145+
146+
add_assets_to_form(form)
146147
end
147148

148-
def add_assets_to_form(form, is_rsc:)
149+
def add_assets_to_form(form)
149150
assets_to_copy = ReactOnRailsPro.configuration.assets_to_copy || []
150-
# react_client_manifest file is needed to generate react server components payload
151-
assets_to_copy << ReactOnRails::Utils.react_client_manifest_file_path if is_rsc
151+
# react_client_manifest and react_server_manifest files are needed to generate react server components payload
152+
if ReactOnRailsPro.configuration.enable_rsc_support
153+
assets_to_copy << ReactOnRails::Utils.react_client_manifest_file_path
154+
assets_to_copy << ReactOnRails::Utils.react_server_manifest_file_path
155+
end
152156

153157
return form unless assets_to_copy.present?
154158

@@ -175,9 +179,9 @@ def add_assets_to_form(form, is_rsc:)
175179
form
176180
end
177181

178-
def form_with_assets_and_bundle(is_rsc: false)
182+
def form_with_assets_and_bundle
179183
form = common_form_data
180-
populate_form_with_bundle_and_assets(form, is_rsc: is_rsc, check_bundle: true)
184+
populate_form_with_bundle_and_assets(form, check_bundle: true)
181185
form
182186
end
183187

lib/react_on_rails_pro/server_rendering_js_code.rb

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,33 +9,41 @@ def ssr_pre_hook_js
99

1010
def render(props_string, rails_context, redux_stores, react_component_name, render_options)
1111
render_function_name = if render_options.rsc?
12-
"serverRenderRSCReactComponent"
12+
"'serverRenderRSCReactComponent'"
1313
elsif render_options.stream?
14-
"streamServerRenderedReactComponent"
14+
"ReactOnRails.isRSCBundle ? 'serverRenderRSCReactComponent' : 'streamServerRenderedReactComponent'"
1515
else
16-
"serverRenderReactComponent"
16+
"'serverRenderReactComponent'"
1717
end
18-
rsc_props_if_rsc_request = if render_options.rsc?
19-
manifest_file = ReactOnRails.configuration.react_client_manifest_file
20-
"reactClientManifestFileName: '#{manifest_file}',"
21-
else
22-
""
23-
end
18+
request_specific_props = if render_options.rsc?
19+
manifest_file = ReactOnRails.configuration.react_client_manifest_file
20+
"reactClientManifestFileName: '#{manifest_file}',"
21+
elsif render_options.stream?
22+
client_manifest_file = ReactOnRails.configuration.react_client_manifest_file
23+
server_manifest_file = ReactOnRails.configuration.react_server_manifest_file
24+
<<-JS
25+
reactClientManifestFileName: '#{client_manifest_file}',
26+
reactServerManifestFileName: '#{server_manifest_file}',
27+
JS
28+
else
29+
""
30+
end
2431
<<-JS
2532
(function() {
2633
var railsContext = #{rails_context};
2734
#{ssr_pre_hook_js}
2835
#{redux_stores}
2936
var props = #{props_string};
30-
return ReactOnRails.#{render_function_name}({
37+
return ReactOnRails[#{render_function_name}]({
3138
name: '#{react_component_name}',
3239
domNodeId: '#{render_options.dom_id}',
3340
props: props,
3441
trace: #{render_options.trace},
3542
railsContext: railsContext,
3643
throwJsErrors: #{ReactOnRailsPro.configuration.throw_js_errors},
3744
renderingReturnsPromises: #{ReactOnRailsPro.configuration.rendering_returns_promises},
38-
#{rsc_props_if_rsc_request}
45+
rscResult,
46+
#{request_specific_props}
3947
});
4048
})()
4149
JS

packages/node-renderer/src/worker.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,13 +191,16 @@ export default function run(config: Partial<Config>) {
191191
// await delay(100000);
192192
// }
193193

194-
const { renderingRequest } = req.body;
194+
const { renderingRequest, rscBundleTimestamp } = req.body;
195195
const { bundleTimestamp } = req.params;
196196
let providedNewBundle: Asset | undefined;
197+
let providedNewRscBundle: Asset | undefined;
197198
const assetsToCopy: Asset[] = [];
198199
Object.entries(req.body).forEach(([key, value]) => {
199200
if (key === 'bundle') {
200201
providedNewBundle = value as Asset;
202+
} else if (key === 'rscBundle') {
203+
providedNewRscBundle = value as Asset;
201204
} else if (isAsset(value)) {
202205
assetsToCopy.push(value);
203206
}
@@ -210,7 +213,9 @@ export default function run(config: Partial<Config>) {
210213
renderingRequest,
211214
bundleTimestamp,
212215
providedNewBundle,
216+
providedNewRscBundle,
213217
assetsToCopy,
218+
rscBundleTimestamp: rscBundleTimestamp as string | undefined,
214219
});
215220
await setResponse(result, res);
216221
} catch (err) {

packages/node-renderer/src/worker/handleRenderRequest.ts

Lines changed: 32 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,14 @@ import { buildVM, hasVMContextForBundle, runInVM } from './vm';
2929
async function prepareResult(
3030
renderingRequest: string,
3131
bundleFilePathPerTimestamp: string,
32+
rscBundleFilePathPerTimestamp?: string,
3233
): Promise<ResponseResult> {
3334
try {
34-
const result = await runInVM(renderingRequest, bundleFilePathPerTimestamp, cluster);
35+
let rscResult;
36+
if (rscBundleFilePathPerTimestamp) {
37+
rscResult = await runInVM(renderingRequest, rscBundleFilePathPerTimestamp, cluster);
38+
}
39+
const result = await runInVM(renderingRequest, bundleFilePathPerTimestamp, cluster, rscResult);
3540

3641
let exceptionMessage = null;
3742
if (!result) {
@@ -84,7 +89,7 @@ async function handleNewBundleProvided(
8489
providedNewBundle: Asset,
8590
renderingRequest: string,
8691
assetsToCopy: Asset[] | null | undefined,
87-
): Promise<ResponseResult> {
92+
): Promise<void> {
8893
log.info('Worker received new bundle: %s', bundleFilePathPerTimestamp);
8994

9095
let lockAcquired = false;
@@ -100,7 +105,7 @@ async function handleNewBundleProvided(
100105
errorMessage,
101106
`Failed to acquire lock ${lockfileName}. Worker: ${workerIdLabel()}.`,
102107
);
103-
return Promise.resolve(errorResponseResult(msg));
108+
throw new Error(msg);
104109
}
105110

106111
try {
@@ -123,28 +128,13 @@ async function handleNewBundleProvided(
123128
to ${bundleFilePathPerTimestamp})`,
124129
);
125130
log.error(msg);
126-
return Promise.resolve(errorResponseResult(msg));
131+
throw new Error(msg);
127132
}
128133
log.info(
129134
'File exists when trying to overwrite bundle %s. Assuming bundle written by other thread',
130135
bundleFilePathPerTimestamp,
131136
);
132137
}
133-
134-
try {
135-
// Either this process or another process placed the file. Because the lock is acquired, the
136-
// file must be fully written
137-
log.info('buildVM, bundleFilePathPerTimestamp', bundleFilePathPerTimestamp);
138-
await buildVM(bundleFilePathPerTimestamp);
139-
return prepareResult(renderingRequest, bundleFilePathPerTimestamp);
140-
} catch (error) {
141-
const msg = formatExceptionMessage(
142-
renderingRequest,
143-
error,
144-
`Unexpected error when building the VM ${bundleFilePathPerTimestamp}`,
145-
);
146-
return Promise.resolve(errorResponseResult(msg));
147-
}
148138
} finally {
149139
if (lockAcquired) {
150140
log.info('About to unlock %s from worker %i', lockfileName, workerIdLabel());
@@ -173,33 +163,48 @@ export = async function handleRenderRequest({
173163
renderingRequest,
174164
bundleTimestamp,
175165
providedNewBundle,
166+
providedNewRscBundle,
176167
assetsToCopy,
168+
rscBundleTimestamp,
177169
}: {
178170
renderingRequest: string;
179171
bundleTimestamp: string | number;
180172
providedNewBundle?: Asset | null;
173+
providedNewRscBundle?: Asset | null;
181174
assetsToCopy?: Asset[] | null;
175+
rscBundleTimestamp?: string | undefined;
182176
}): Promise<ResponseResult> {
183177
try {
184178
const bundleFilePathPerTimestamp = getRequestBundleFilePath(bundleTimestamp);
179+
const rscBundleFilePathPerTimestamp = rscBundleTimestamp && getRequestBundleFilePath(rscBundleTimestamp);
185180

186181
// If the current VM has the correct bundle and is ready
187-
if (hasVMContextForBundle(bundleFilePathPerTimestamp)) {
188-
return prepareResult(renderingRequest, bundleFilePathPerTimestamp);
182+
if (hasVMContextForBundle(bundleFilePathPerTimestamp) && (!rscBundleFilePathPerTimestamp || hasVMContextForBundle(rscBundleFilePathPerTimestamp))) {
183+
return prepareResult(renderingRequest, bundleFilePathPerTimestamp, rscBundleFilePathPerTimestamp);
189184
}
190185

191186
// If gem has posted updated bundle:
192187
if (providedNewBundle) {
193-
return handleNewBundleProvided(
188+
await handleNewBundleProvided(
194189
bundleFilePathPerTimestamp,
195190
providedNewBundle,
196191
renderingRequest,
197192
assetsToCopy,
198193
);
199194
}
200195

196+
if (providedNewRscBundle && rscBundleFilePathPerTimestamp) {
197+
await handleNewBundleProvided(
198+
rscBundleFilePathPerTimestamp,
199+
providedNewRscBundle,
200+
renderingRequest,
201+
[],
202+
);
203+
}
204+
201205
// Check if the bundle exists:
202-
const fileExists = await fileExistsAsync(bundleFilePathPerTimestamp);
206+
const fileExists = await fileExistsAsync(bundleFilePathPerTimestamp) &&
207+
(!rscBundleFilePathPerTimestamp || await fileExistsAsync(rscBundleFilePathPerTimestamp));
203208
if (!fileExists) {
204209
log.info(`No saved bundle ${bundleFilePathPerTimestamp}. Requesting a new bundle.`);
205210
return Promise.resolve({
@@ -213,8 +218,11 @@ export = async function handleRenderRequest({
213218
// Another worker must have written it or it was saved during deployment.
214219
log.info('Bundle %s exists. Building VM for worker %s.', bundleFilePathPerTimestamp, workerIdLabel());
215220
await buildVM(bundleFilePathPerTimestamp);
221+
if (rscBundleFilePathPerTimestamp) {
222+
await buildVM(rscBundleFilePathPerTimestamp);
223+
}
216224

217-
return prepareResult(renderingRequest, bundleFilePathPerTimestamp);
225+
return prepareResult(renderingRequest, bundleFilePathPerTimestamp, rscBundleFilePathPerTimestamp);
218226
} catch (error) {
219227
const msg = formatExceptionMessage(
220228
renderingRequest,

packages/node-renderer/src/worker/vm.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@ export async function runInVM(
246246
renderingRequest: string,
247247
filePath: string,
248248
vmCluster?: typeof cluster,
249+
rscResult?: RenderResult,
249250
): Promise<RenderResult> {
250251
const { bundlePath } = getConfig();
251252

@@ -273,7 +274,14 @@ ${smartTrim(renderingRequest)}`);
273274
}
274275

275276
let result = sharedConsoleHistory.trackConsoleHistoryInRenderRequest(
276-
() => vm.runInContext(renderingRequest, context) as RenderCodeResult,
277+
() => {
278+
try {
279+
context.rscResult = rscResult;
280+
return vm.runInContext(renderingRequest, context) as RenderCodeResult
281+
} finally {
282+
context.rscResult = undefined;
283+
}
284+
},
277285
);
278286

279287
if (isReadableStream(result)) {

spec/dummy/Gemfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ GIT
88

99
GIT
1010
remote: https://github.com/shakacode/react_on_rails.git
11-
revision: 544a93208d38d029be7810cd3f1f8468f9f5234c
11+
revision: 6307be98c88d06550cffb046807ffb9ab2b8c9d6
1212
branch: abanoubghadban/pro362-add-support-for-RSC
1313
specs:
1414
react_on_rails (14.1.1)

spec/dummy/config/webpack/serverWebpackConfig.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
const webpack = require('webpack');
12
const { config } = require('shakapacker');
3+
const RSDWPlugin = require('react-server-dom-webpack/plugin');
24
const commonWebpackConfig = require('./commonWebpackConfig');
3-
const webpack = require('webpack');
45

56
function extractLoader(rule, loaderName) {
67
return rule.use.find((item) => {
@@ -52,6 +53,11 @@ const configureServer = () => {
5253
minimize: false,
5354
};
5455

56+
serverWebpackConfig.plugins.push(new RSDWPlugin({
57+
isServer: false,
58+
clientManifestFilename: 'react-server-manifest.json',
59+
ssrManifestFilename: 'react-server-ssr-manifest.json',
60+
}));
5561
serverWebpackConfig.plugins.unshift(new webpack.optimize.LimitChunkCountPlugin({ maxChunks: 1 }));
5662
// Custom output for the server-bundle that matches the config in
5763
// config/initializers/react_on_rails.rb

0 commit comments

Comments
 (0)