Skip to content

Commit 514406f

Browse files
Initital Implementation of RSC
Signed-off-by: Khaled Emara <[email protected]>
1 parent 7dd8750 commit 514406f

File tree

16 files changed

+1205
-354
lines changed

16 files changed

+1205
-354
lines changed

Gemfile.development_dependencies

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
55

66
ruby '2.7.5'
77

8-
gem "react_on_rails", "13.3.4" # keep in sync with package.json files
8+
# gem "react_on_rails", "13.3.4" # keep in sync with package.json files
99

1010
# For local development
11-
# gem "react_on_rails", path: "../react_on_rails"
11+
gem "react_on_rails", path: "../react_on_rails"
1212

1313
gem "shakapacker", "6.5.6"
1414
gem "bootsnap", require: false
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# frozen_string_literal: true
2+
3+
module ReactOnRailsPro
4+
class RscController < ApplicationController
5+
include ReactOnRails::Helper
6+
7+
def render_rsc
8+
props = params[:props] || {}
9+
props_js = props_string(props).gsub("\u2028", '\u2028').gsub("\u2029", '\u2029')
10+
11+
render_options = ReactOnRails::ReactComponent::RenderOptions.new(
12+
react_component_name: params[:componentName],
13+
options: {
14+
trace: true,
15+
replay_console: true,
16+
raise_on_prerender_error: true
17+
}
18+
)
19+
20+
ReactOnRails::ServerRenderingPool.reset_pool_if_server_bundle_was_modified
21+
22+
js_code =
23+
<<-JS
24+
(function() {
25+
#{ReactOnRailsPro.configuration.ssr_pre_hook_js || ''}
26+
#{initialize_redux_stores}
27+
return ReactOnRails.renderReactServerComponent({
28+
name: '#{params[:componentName]}',
29+
props: #{props_js},
30+
throwJsErrors: #{ReactOnRailsPro.configuration.throw_js_errors},
31+
renderingReturnsPromises: #{ReactOnRailsPro.configuration.rendering_returns_promises},
32+
});
33+
})()
34+
JS
35+
36+
begin
37+
result = ReactOnRails::ServerRenderingPool.server_render_js_with_console_logging(js_code, render_options)
38+
rescue StandardError => e
39+
# This error came from the renderer
40+
raise ReactOnRails::PrerenderError.new(component_name: params[:componentName],
41+
# Sanitize as this might be browser logged
42+
props: sanitized_props_string(props),
43+
err: e,
44+
js_code: js_code)
45+
end
46+
47+
if result["hasErrors"] && render_options.raise_on_prerender_error
48+
# We caught this exception on our backtrace handler
49+
raise ReactOnRails::PrerenderError.new(component_name: params[:componentName],
50+
# Sanitize as this might be browser logged
51+
props: sanitized_props_string(props),
52+
err: nil,
53+
js_code: js_code,
54+
console_messages: result["consoleReplayScript"])
55+
56+
end
57+
58+
render plain: result["html"]
59+
end
60+
end
61+
end

config/routes.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# frozen_string_literal: true
2+
3+
Rails.application.routes.draw do
4+
get "rsc/:componentName" => "react_on_rails_pro/rsc#render_rsc", as: :rsc
5+
end

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ exports.buildVM = async function buildVM(filePath) {
6767
const { supportModules, includeTimerPolyfills } = getConfig();
6868
vmBundleFilePath = undefined;
6969
if (supportModules) {
70-
context = vm.createContext({ Buffer, process, setTimeout, setInterval, clearTimeout, clearInterval });
70+
context = vm.createContext({ Buffer, process, setTimeout, setInterval, setImmediate, clearTimeout, clearInterval, clearImmediate });
7171
} else {
7272
context = vm.createContext();
7373
}
@@ -111,8 +111,10 @@ exports.buildVM = async function buildVM(filePath) {
111111
// Define timer polyfills:
112112
vm.runInContext(`function setInterval() {}`, context);
113113
vm.runInContext(`function setTimeout() {}`, context);
114+
vm.runInContext(`function setImmediate() {}`, context);
114115
vm.runInContext(`function clearTimeout() {}`, context);
115116
vm.runInContext(`function clearInterval() {}`, context);
117+
vm.runInContext(`function clearImmediate() {}`, context);
116118
}
117119

118120
// Run bundle code in created context:

spec/dummy/Gemfile.lock

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,16 @@ GIT
66
byebug (~> 11.0)
77
pry (>= 0.13, < 0.15)
88

9+
PATH
10+
remote: ../../../react_on_rails
11+
specs:
12+
react_on_rails (13.4.0)
13+
addressable
14+
connection_pool
15+
execjs (~> 2.5)
16+
rails (>= 5.2)
17+
rainbow (~> 3.0)
18+
919
PATH
1020
remote: ../..
1121
specs:
@@ -250,12 +260,6 @@ GEM
250260
rb-fsevent (0.11.2)
251261
rb-inotify (0.10.1)
252262
ffi (~> 1.0)
253-
react_on_rails (13.3.4)
254-
addressable
255-
connection_pool
256-
execjs (~> 2.5)
257-
rails (>= 5.2)
258-
rainbow (~> 3.0)
259263
regexp_parser (2.6.0)
260264
rexml (3.2.5)
261265
rspec-core (3.11.0)
@@ -419,7 +423,7 @@ DEPENDENCIES
419423
pry-theme
420424
puma (>= 5.6.4)
421425
rails (>= 7.0.4)
422-
react_on_rails (= 13.3.4)
426+
react_on_rails!
423427
react_on_rails_pro!
424428
rspec-rails
425429
rspec-retry

spec/dummy/app/controllers/pages_controller.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ def apollo_graphql
4040
render "/pages/pro/apollo_graphql"
4141
end
4242

43+
def react_server_components_router
44+
render "/pages/pro/react_server_components_router"
45+
end
46+
4347
# See files in spec/dummy/app/views/pages
4448

4549
helper_method :calc_slow_app_props_server_render
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<%= react_component("ReactServerComponentsRouter", props: {}, prerender: false, trace: true, id: "ReactServerComponentsRouter-react-component-0") %>

spec/dummy/app/views/shared/_menu.erb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ end
2525
<li>
2626
<%= link_to("React 18 Apollo with GraphQL", apollo_graphql_path, class: cp(apollo_graphql_path)) %>
2727
</li>
28+
<li>
29+
<%= link_to("React Server Components", react_server_components_router_path, class: cp(react_server_components_router_path)) %>
30+
</li>
2831
</ul>
2932

3033
<h2 class="text-red-700 text-2xl mt-5">

spec/dummy/client/app/packs/server-bundle.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
// import statement added by react_on_rails:generate_packs rake task
2+
import "./../generated/server-bundle-generated.js"
13
// Shows the mapping from the exported object to the name used by the server rendering.
24
import ReactOnRails from 'react-on-rails';
35

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
'use server';
2+
3+
import React, { Suspense } from 'react';
4+
5+
import { marked } from 'marked'; // 35.9K (11.2K gzipped)
6+
import sanitizeHtml from 'sanitize-html'; // 206K (63.3K gzipped)
7+
8+
import { ApolloClient, InMemoryCache, gql } from '@apollo/client';
9+
10+
const allowedTags = sanitizeHtml.defaults.allowedTags.concat([
11+
'img',
12+
'h1',
13+
'h2',
14+
'h3',
15+
]);
16+
const allowedAttributes = Object.assign(
17+
{},
18+
sanitizeHtml.defaults.allowedAttributes,
19+
{
20+
img: ['alt', 'src'],
21+
}
22+
);
23+
24+
async function fetchLocations(client) {
25+
const result = await client
26+
.query({
27+
query: gql`
28+
query GetLocations {
29+
locations {
30+
id
31+
name
32+
description
33+
photo
34+
}
35+
}
36+
`,
37+
});
38+
39+
return result;
40+
}
41+
42+
export default async function ReactServerComponent({ markdownText }) {
43+
const client = new ApolloClient({
44+
ssrMode: true,
45+
uri: 'https://flyby-router-demo.herokuapp.com/',
46+
cache: new InMemoryCache(),
47+
});
48+
49+
return (
50+
<Suspense fallback={<div>Loading...</div>}>
51+
<div dangerouslySetInnerHTML={{
52+
__html: sanitizeHtml(marked(markdownText)),
53+
allowedTags,
54+
allowedAttributes,
55+
}} />
56+
<div>
57+
{JSON.stringify(await fetchLocations(client))}
58+
</div>
59+
</Suspense>
60+
);
61+
};

0 commit comments

Comments
 (0)