Skip to content

Commit 2f7d333

Browse files
ihabadhamclaude
andauthored
Pro RSC migration: tutorial on React on Rails Pro with a React Server Components demo
Migrates this tutorial to React on Rails Pro with the Pro Node Renderer for SSR, and adds a React Server Components demo at /server-components. RSC is a Pro-only feature (`enable_rsc_support` doesn't exist in the OSS gem), so demonstrating RSC inherently moves the whole app to Pro — that's the entry ticket, not scope drift. What changes: - Dependencies: gem and npm move from react_on_rails to react_on_rails_pro 16.6.0; React pinned to ~19.0.4 (RSC's minimum); adds react-on-rails-pro-node-renderer and react-on-rails-rsc. - SSR runtime: Server-side rendering moves from ExecJS to the Pro Node Renderer (renderer/node-renderer.js, port 3800). The renderer wires additionalContext: { URL, AbortController } for react-router-dom, stubTimers: false so React's RSC server renderer can use setTimeout for Flight-protocol yielding, and replayServerAsyncOperationLogs: true to surface async-server-component console output. - RSC demo: A new page at /server-components exercises React Server Components end-to-end: server-only library imports (Node os + lodash) that never reach the browser, async server components streamed via <Suspense>, the donut pattern (a 'use client' component nested in a server tree), and client-driven server-component re-fetching with react-error-boundary + useRSC().refetchComponent. - Bundler: shakapacker temporarily switched from rspack to webpack while waiting for shakacode/react_on_rails_rsc#29 to ship rspack support for the RSC plugin. Tracked by TODO in config/shakapacker.yml. Verification: - Each sub-PR (#726, #728, #729) reviewed independently before squashing into base. - Manually QA'd on the deployed review app. - Request and system specs cover the RSC payload endpoint and the demo page interactions (spec/requests/server_components_spec.rb, spec/system/server_components_demo_spec.rb). - Lint, RSpec, and JS test workflows ran for the first time on the base PR — sub-PRs target the base branch and skip those workflows by trigger filter. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 5cf245f commit 2f7d333

49 files changed

Lines changed: 1631 additions & 223 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.controlplane/Dockerfile

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,8 @@ RUN SECRET_KEY_BASE=precompile_placeholder yarn res:build && \
8080
# For Kubernetes and ControlPlane, this is the command on the workload.
8181
ENTRYPOINT ["./.controlplane/entrypoint.sh"]
8282

83-
# Default args to pass to the entry point that can be overridden
84-
# For Kubernetes and ControlPlane, these are the "workload args"
85-
# Use Thruster HTTP/2 proxy for optimized performance
86-
CMD ["bundle", "exec", "thrust", "bin/rails", "server"]
83+
# Run the Pro Node renderer and Rails in a single container (Option 1 from
84+
# docs/oss/building-features/node-renderer/container-deployment.md).
85+
# `wait -n` exits on first child failure → container exits → Control Plane
86+
# restarts the whole unit. Keeps the existing Thruster HTTP/2 proxy for Rails.
87+
CMD ["bash", "-c", "node renderer/node-renderer.js & RENDERER_PID=$! ; bundle exec thrust bin/rails server & RAILS_PID=$! ; wait -n ; exit 1"]

.controlplane/templates/app.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,29 @@ spec:
2424
# set to a secure random value using: openssl rand -hex 64
2525
# Production apps should configure this manually after app creation via a secret.
2626
value: 'placeholder_secret_key_base_for_test_apps_only'
27+
# Pro Node renderer settings. The renderer process runs in the same
28+
# container as Rails (see .controlplane/Dockerfile CMD).
29+
- name: RENDERER_PORT
30+
value: '3800'
31+
- name: RENDERER_LOG_LEVEL
32+
value: info
33+
- name: RENDERER_WORKERS_COUNT
34+
value: '2'
35+
- name: RENDERER_URL
36+
value: http://localhost:3800
37+
# Enable the artificial Suspense demo delay so the streaming fallback is
38+
# visible on the review-app. Off by default in production deployments.
39+
- name: RSC_SUSPENSE_DEMO_DELAY
40+
value: 'true'
41+
# RENDERER_PASSWORD and REACT_ON_RAILS_PRO_LICENSE must be created in the
42+
# Control Plane Secret named by {{APP_SECRETS}} before deploy. cpflow
43+
# resolves {{APP_SECRETS}} to `{APP_PREFIX}-secrets` — which means review
44+
# apps all share one `qa-react-webpack-rails-tutorial-secrets` (thanks to
45+
# match_if_app_name_starts_with: true on the qa template).
46+
- name: RENDERER_PASSWORD
47+
value: cpln://secret/{{APP_SECRETS}}.RENDERER_PASSWORD
48+
- name: REACT_ON_RAILS_PRO_LICENSE
49+
value: cpln://secret/{{APP_SECRETS}}.REACT_ON_RAILS_PRO_LICENSE
2750
# Part of standard configuration
2851
staticPlacement:
2952
locationLinks:

.controlplane/templates/org.yml

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,25 @@
44
# other sensitive information that is shared across multiple apps
55
# in the same organization.
66

7-
# This is how you apply this once (not during CI)
8-
# cpl apply-template secrets -a qa-react-webpack-rails-tutorial --org shakacode-open-source-examples-staging
7+
# The qa-* dictionary is bootstrapped via this template; prod and
8+
# staging dictionaries are created manually with real values.
9+
10+
# Initial bootstrap (once, manually, not in CI):
11+
# cpflow apply-template secrets -a qa-react-webpack-rails-tutorial --org shakacode-open-source-examples-staging
12+
#
13+
# Populate real values with `cpln apply -f <file>` or `cpln secret edit`.
14+
# Do NOT re-apply this template after real values are set: it will
15+
# overwrite them with the placeholders below.
916

1017
kind: secret
1118
name: {{APP_SECRETS}}
1219
type: dictionary
1320
data:
14-
SOME_ENV: "123456"
21+
# Both sides of the Rails/Node renderer handshake must match.
22+
# Generate with `openssl rand -hex 32`.
23+
RENDERER_PASSWORD: "replace-with-openssl-rand-hex-32"
24+
# JWT from https://pro.reactonrails.com/; same token across envs.
25+
REACT_ON_RAILS_PRO_LICENSE: "replace-with-pro-license-jwt"
1526

1627
---
1728

.controlplane/templates/rails.yml

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,11 @@ spec:
1515
# Inherit other ENV values from GVC
1616
inheritEnv: true
1717
image: {{APP_IMAGE_LINK}}
18-
# 512 corresponds to a standard 1x dyno type
19-
memory: 512Mi
18+
# 1Gi (up from 512Mi) gives the single container enough headroom for
19+
# Rails + the Pro Node renderer. Below the marketplace demo's 2Gi
20+
# because this tutorial's Rails surface is smaller; capacityAI below
21+
# adjusts upward if actual usage warrants it.
22+
memory: 1Gi
2023
ports:
2124
- number: 3000
2225
protocol: http
@@ -27,7 +30,9 @@ spec:
2730
autoscaling:
2831
# Max of 1 effectively disables autoscaling, so a like a Heroku dyno count of 1
2932
maxScale: 1
30-
capacityAI: false
33+
# CapacityAI adjusts CPU/memory within the single replica based on observed
34+
# usage, so the 1Gi/300m baseline can grow if Rails + renderer need more.
35+
capacityAI: true
3136
firewallConfig:
3237
external:
3338
# Default to allow public access to Rails server

.env.example

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# React on Rails Pro license (JWT token from https://pro.reactonrails.com/).
2+
# Required in production; optional in development/test.
3+
REACT_ON_RAILS_PRO_LICENSE=
4+
5+
# Shared secret between Rails and the Node renderer. Must match on both sides.
6+
# Dev/test default to `local-dev-renderer-password` if unset; production must
7+
# set this explicitly (Pro raises at boot otherwise).
8+
RENDERER_PASSWORD=
9+
10+
# Node renderer endpoint. Defaults to http://localhost:3800.
11+
# RENDERER_URL=http://localhost:3800
12+
13+
# Port the Node renderer listens on. Defaults to 3800.
14+
# RENDERER_PORT=3800
15+
16+
# Log verbosity for the Node renderer (debug, info, warn, error).
17+
# Defaults to info.
18+
# RENDERER_LOG_LEVEL=info
19+
20+
# Number of renderer worker processes. `0` is single-process mode, useful
21+
# for local debugging. Defaults to 3; the app's CI workflow caps to 2.
22+
# RENDERER_WORKERS_COUNT=3

.eslintrc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ rules:
3737
# currently deprecated https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/no-onchange.md
3838
jsx-a11y/no-onchange: 0
3939

40+
# React 19 removed runtime propTypes validation; the rule is still in
41+
# plugin:react/recommended (jsx-eslint/eslint-plugin-react#3753 tracks
42+
# removing it) but no longer relevant in this codebase.
43+
react/prop-types: 0
44+
4045
settings:
4146
import/core-modules:
4247
- react-redux

.github/workflows/rspec_test.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ jobs:
3333
DRIVER: selenium_chrome
3434
CHROME_BIN: /usr/bin/google-chrome
3535
USE_COVERALLS: true
36+
# Must match config.renderer_password in config/initializers/react_on_rails_pro.rb.
37+
# Setting explicitly avoids silent drift if the two-sided defaults ever diverge.
38+
RENDERER_PASSWORD: local-dev-renderer-password
3639

3740
steps:
3841
- name: Install Chrome
@@ -82,6 +85,25 @@ jobs:
8285
- name: Build shakapacker chunks
8386
run: NODE_ENV=development bundle exec bin/shakapacker
8487

88+
- name: Start Node renderer for SSR
89+
run: |
90+
node renderer/node-renderer.js &
91+
RENDERER_PID=$!
92+
echo "Waiting for Node renderer (PID $RENDERER_PID) on port 3800..."
93+
for i in $(seq 1 30); do
94+
if ! kill -0 $RENDERER_PID 2>/dev/null; then
95+
echo "Node renderer process exited unexpectedly (see output above)."
96+
exit 1
97+
fi
98+
if nc -z localhost 3800 2>/dev/null; then
99+
echo "Node renderer is ready"
100+
exit 0
101+
fi
102+
sleep 1
103+
done
104+
echo "Node renderer failed to start within 30 seconds (see output above)."
105+
exit 1
106+
85107
- name: Run rspec with xvfb
86108
uses: coactions/setup-xvfb@v1
87109
with:

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ client/app/bundles/comments/rescript/**/*.bs.js
5757
# Using React on Rails default directory
5858
/ssr-generated/
5959

60+
# Pro Node renderer bundle cache
61+
/renderer/.node-renderer-bundles/
62+
6063
# Generated React on Rails packs
6164
**/generated/**
6265

Gemfile

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

66
ruby "3.4.6"
77

8-
gem "react_on_rails", "16.6.0.rc.0"
9-
gem "shakapacker", "10.0.0.rc.0"
8+
gem "react_on_rails_pro", "16.6.0"
9+
gem "shakapacker", "10.0.0"
1010

1111
# Bundle edge Rails instead: gem "rails", github: "rails/rails"
1212
gem "rails", "~> 8.1.2"

Gemfile.lock

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,12 @@ GEM
7878
addressable (2.8.7)
7979
public_suffix (>= 2.0.2, < 7.0)
8080
ast (2.4.3)
81+
async (2.39.0)
82+
console (~> 1.29)
83+
fiber-annotation
84+
io-event (~> 1.11)
85+
metrics (~> 0.12)
86+
traces (~> 0.18)
8187
autoprefixer-rails (10.4.16.0)
8288
execjs (~> 2)
8389
awesome_print (1.9.2)
@@ -115,6 +121,10 @@ GEM
115121
coffee-script-source (1.12.2)
116122
concurrent-ruby (1.3.6)
117123
connection_pool (3.0.2)
124+
console (1.34.3)
125+
fiber-annotation
126+
fiber-local (~> 1.1)
127+
json
118128
coveralls_reborn (0.25.0)
119129
simplecov (>= 0.18.1, < 0.22.0)
120130
term-ansicolor (~> 1.6)
@@ -146,16 +156,24 @@ GEM
146156
railties (>= 5.0.0)
147157
ffi (1.17.2-arm64-darwin)
148158
ffi (1.17.2-x86_64-linux-gnu)
159+
fiber-annotation (0.2.0)
160+
fiber-local (1.1.0)
161+
fiber-storage
162+
fiber-storage (1.0.1)
149163
foreman (0.88.1)
150164
generator_spec (0.10.0)
151165
activesupport (>= 3.0.0)
152166
railties (>= 3.0.0)
153167
globalid (1.3.0)
154168
activesupport (>= 6.1)
169+
http-2 (1.1.3)
170+
httpx (1.7.6)
171+
http-2 (>= 1.1.3)
155172
i18n (1.14.8)
156173
concurrent-ruby (~> 1.0)
157174
interception (0.5)
158175
io-console (0.8.2)
176+
io-event (1.15.1)
159177
irb (1.17.0)
160178
pp (>= 0.6.0)
161179
prism (>= 1.3.0)
@@ -165,6 +183,8 @@ GEM
165183
actionview (>= 5.0.0)
166184
activesupport (>= 5.0.0)
167185
json (2.19.1)
186+
jwt (2.10.2)
187+
base64
168188
language_server-protocol (3.17.0.5)
169189
launchy (3.0.1)
170190
addressable (~> 2.8)
@@ -182,6 +202,7 @@ GEM
182202
marcel (1.1.0)
183203
matrix (0.4.2)
184204
method_source (1.1.0)
205+
metrics (0.15.0)
185206
mini_mime (1.1.5)
186207
minitest (6.0.2)
187208
drb (~> 2.0)
@@ -296,13 +317,23 @@ GEM
296317
erb
297318
psych (>= 4.0.0)
298319
tsort
299-
react_on_rails (16.6.0.rc.0)
320+
react_on_rails (16.6.0)
300321
addressable
301322
connection_pool
302323
execjs (~> 2.5)
303324
rails (>= 5.2)
304325
rainbow (~> 3.0)
305326
shakapacker (>= 6.0)
327+
react_on_rails_pro (16.6.0)
328+
addressable
329+
async (>= 2.29)
330+
connection_pool
331+
execjs (~> 2.9)
332+
http-2 (>= 1.1.1)
333+
httpx (~> 1.5)
334+
jwt (~> 2.7)
335+
rainbow
336+
react_on_rails (= 16.6.0)
306337
redcarpet (3.6.0)
307338
redis (5.3.0)
308339
redis-client (>= 0.22.0)
@@ -387,7 +418,7 @@ GEM
387418
websocket (~> 1.0)
388419
semantic_range (3.1.1)
389420
sexp_processor (4.17.1)
390-
shakapacker (10.0.0.rc.0)
421+
shakapacker (10.0.0)
391422
activesupport (>= 5.2)
392423
package_json
393424
rack-proxy (>= 0.6.1)
@@ -425,6 +456,7 @@ GEM
425456
tins (1.33.0)
426457
bigdecimal
427458
sync
459+
traces (0.18.2)
428460
tsort (0.2.0)
429461
turbo-rails (2.0.11)
430462
actionpack (>= 6.0.0)
@@ -486,7 +518,7 @@ DEPENDENCIES
486518
rails-html-sanitizer
487519
rails_best_practices
488520
rainbow
489-
react_on_rails (= 16.6.0.rc.0)
521+
react_on_rails_pro (= 16.6.0)
490522
redcarpet
491523
redis (~> 5.0)
492524
rspec-rails (~> 6.0.0)
@@ -498,7 +530,7 @@ DEPENDENCIES
498530
scss_lint
499531
sdoc
500532
selenium-webdriver (~> 4)
501-
shakapacker (= 10.0.0.rc.0)
533+
shakapacker (= 10.0.0)
502534
spring
503535
spring-commands-rspec
504536
stimulus-rails (~> 1.3)

0 commit comments

Comments
 (0)