Pro RSC migration 2/3: switch SSR to the Pro Node renderer on webpack#728
Conversation
Second of three stacked sub-PRs in the Pro RSC migration. Routes all server rendering through the Pro Node renderer (port 3800) instead of ExecJS, and flips the asset bundler from rspack to webpack — scoped reversal of #702, needed because rspack 2.0.0-beta.7's webpack compatibility layer doesn't cover the APIs upstream RSCWebpackPlugin requires. We flip back to rspack once shakacode/react_on_rails_rsc#29 ships a native rspack RSC plugin. The bundler flip and NodeRenderer wiring ship atomically: the server bundle produced by the Pro webpack transforms (target: 'node' + libraryTarget: 'commonjs2') is not evaluable by ExecJS, so the initializer pointing server_renderer at the NodeRenderer must land at the same time. Key changes: - config/shakapacker.yml: assets_bundler: rspack → webpack - config/webpack/bundlerUtils.js: return @rspack/core or webpack based on the shakapacker setting (was rspack-only and threw otherwise); spec updated in parallel - config/webpack/serverWebpackConfig.js: Pro transforms per the :pro generator's update_webpack_config_for_pro and the marketplace/dummy references — target: 'node' + node: false, libraryTarget: 'commonjs2', extractLoader helper, babelLoader.options.caller = { ssr: true }, destructured module.exports so Sub-PR 3's rscWebpackConfig.js can derive from serverWebpackConfig(true). RSCWebpackPlugin({ isServer: true }) when !rscBundle emits the server manifest; inert until Sub-PR 3 activates RSC support - config/initializers/react_on_rails_pro.rb: NodeRenderer-only config (no RSC fields — those move in Sub-PR 3) - renderer/node-renderer.js: launcher with strict integer env parsing, CI worker cap, and additionalContext: { URL, AbortController } so react-router-dom's NavLink.encodeLocation does not throw "ReferenceError: URL is not defined" at SSR time - Procfile.dev: renderer: NODE_ENV=development node renderer/node-renderer.js - package.json: react-on-rails-pro-node-renderer 16.6.0 and react-on-rails-rsc ^19.0.4 (Pro peer dep; required for the RSCWebpackPlugin import) - .gitignore: /renderer/.node-renderer-bundles/ - .env.example: REACT_ON_RAILS_PRO_LICENSE, RENDERER_PASSWORD, and REACT_RENDERER_URL with dev vs prod guidance - .github/workflows/rspec_test.yml: start the Node renderer before rspec with PID liveness and port-ready checks plus log capture on failure Verified locally: webpack build compiles cleanly. `bin/rails s` on 3000 with `node renderer/node-renderer.js` on 3800 renders GET / at HTTP 200; Rails log shows "Node Renderer responded" and the renderer log emits "[SERVER] RENDERED Footer to dom node with id: ..." — confirming SSR went through the Pro path rather than falling back to ExecJS. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
🚀 Quick Review App CommandsWelcome! Here are the commands you can use in this PR:
|
Code Review — PR #728: Pro RSC migration 2/3 (SSR → Node renderer + webpack)OverviewThis PR wires up the Pro Node renderer as the SSR backend and switches the asset bundler from rspack back to webpack (scoped tactical reversal while the native rspack RSC plugin is in progress). The overall approach is sound and the PR is well-documented. A few targeted issues to address before merging. Issues🔴 Breaking change:
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 758a2c7bd4
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
Four fixes from auditing Sub-PR 2 against the Pro dummy, the :pro generator, and the Pro configuration docs. - config/initializers/react_on_rails_pro.rb: renderer_password now raises in non-local envs instead of falling back to the dev string. Previously any production deploy that forgot to set the env var would silently run with a known-public password; now it fails loudly. Matches the safer pattern from PR #723's final state. - config/webpack/serverWebpackConfig.js: pass clientReferences to RSCWebpackPlugin({ isServer: true }), matching the Pro dummy's serverWebpackConfig at `react_on_rails_pro/spec/dummy/config/webpack/ serverWebpackConfig.js`. Without it, the plugin may walk into node_modules and hit unlodaed .tsx source files and re-scan modules we don't need. Locks client-ref discovery to client/app/**. - config/webpack/serverWebpackConfig.js: drop publicPath from the server-bundle output. Server bundles are loaded by the Node renderer via the filesystem, never served over HTTP — the URL is unused. Matches the Pro dummy's comment. - package.json: pin react-on-rails-rsc to 19.0.4 stable (was ^19.0.4 range) and add "node-renderer" npm script as a convenience shortcut for `node renderer/node-renderer.js`. - .github/workflows/rspec_test.yml: set RENDERER_PASSWORD explicitly in CI to the shared dev default so both the initializer and the launcher use the same concrete value, avoiding silent drift if either side's default is ever touched. Re-verified: webpack build clean; renderer + Rails boot; GET / returns HTTP 200 with "Node Renderer responded" in the Rails log and "[SERVER] RENDERED" in the renderer log, confirming SSR still goes through the Pro path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
PR Review findings for Pro RSC Migration 2/3: (1) Bug in renderer/node-renderer.js - parseIntegerEnv uses min:0 for NODE_RENDERER_CONCURRENCY allowing workersCount:0 which would hang the renderer; bump to min:1. (2) serverWebpackConfig.js overwrites babelLoader.options.caller rather than merging - should spread existing value. (3) The module.exports={default:configureServer,extractLoader} pattern is unusual for CJS; prefer module.exports=configureServer; module.exports.extractLoader=extractLoader. (4) target:'node' without version pin could cause unexpected polyfills; consider 'node18'. (5) extractLoader helper has no unit tests. Minor: getBundler() silently falls back to webpack for unknown assets_bundler values; a warning would help. Security looks good - ENV.fetch(RENDERER_PASSWORD) correctly raises KeyError at boot in production rather than falling back to a dev default. |
Previous commit added Rails.env-branching + explicit raise in non-local envs. That duplicates what Pro's configuration.rb already does at boot (validate_renderer_password_for_production) and diverges from the documented pattern in pro/node-renderer.md and pro/installation.md. Revert to the simple ENV.fetch with a dev default. Pro handles the prod-enforcement itself — if RENDERER_PASSWORD is unset in a production-like env, Pro raises at boot with a helpful error message pointing at exactly this fix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code Review — PR #728: Pro RSC migration 2/3 (SSR → Node renderer + webpack)Overall this is a well-structured, well-commented atomic migration. The stacking rationale is clear, the Pro webpack transforms follow the upstream dummy pattern, and the CI wiring for the renderer is thoughtful. A few issues worth addressing before merge: Bugs / Correctness
Code Quality
Security
CI / Operations
Minor / Nits
|
Journey plan's Sub-PR 1 KEEP table explicitly called for the 19.0.4 pin from PR #723's final state — missed in the Sub-PR 1 migration commit. 19.0.4 is the minimum React version documented as required for React Server Components (per the Pro RSC tutorial doc, CVE-safe floor). Tightens from the inherited "^19.0.0" range. Boot-verified: webpack build clean; renderer + Rails boot; GET / returns HTTP 200 with "Node Renderer responded" in Rails log and "[SERVER] RENDERED" in renderer log — SSR path unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous comment restated in English what the code below already expressed. Cut to the one non-obvious WHY (why the prod branch exists despite Pro's own JS-side guard). Method names + the raise message cover the rest. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pro's own integration CI (react_on_rails_pro/.github/workflows/ pro-integration-tests.yml) runs the renderer with `pnpm run node-renderer &` — no log redirect — so renderer output interleaves with job stdout and is always visible without a special dump step. Our workflow inherited a redirect-to-/tmp/node-renderer.log pattern from PR #723 plus a follow-up if-failure cat step (commit 7dea094) that dumped the file after rspec. Both existed to work around the redirect; neither was in any reference. Drop the redirect and the associated cat calls (startup-failure cat, timeout cat, post-rspec failure dump). Renderer logs now appear inline with job output, same shape as Pro CI. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
"Tactical:" read as a weird label; "TODO:" is the standard signal that the current state is meant to be reverted once the upstream blocker (shakacode/react_on_rails_rsc#29) ships. Same content, leads with the action. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code Review: Pro RSC migration 2/3Overall this is a solid, well-documented PR. The architecture decisions are sound and the test plan is thorough. A few items worth addressing before merge: Signal handling in the single-container CMD (medium)The Dockerfile For a tutorial app this may be acceptable, but for staging/production it can cause dropped requests on redeploy. The fix is a small CMD ["bash", "-c", "
trap 'kill $RENDERER_PID $RAILS_PID 2>/dev/null' TERM INT;
node renderer/node-renderer.js & RENDERER_PID=$! ;
bundle exec thrust bin/rails server & RAILS_PID=$! ;
wait -n ; exit 1
"]Or better yet, extract this to No renderer log capture if it crashes mid-test (low)The CI "Start Node renderer" step captures crash-on-startup correctly, but exits (and loses
|
|
/deploy-review-app |
The guard added in 0eb94af raised at every Rails boot in production, which breaks the Docker build step that runs `bin/rails react_on_rails:locale` during image bake (RENDERER_PASSWORD isn't in the build environment — secrets are mounted at runtime only, not at build time). Deploy on Control Plane fails before it can even produce an image. Reference check on the guard: zero of Pro dummy, marketplace demo (react-on-rails-demo-marketplace-rsc), Justin's PR 723, and hichee have a "raise on literal dev-default value" guard. Each uses ENV.fetch with a default or hardcodes the password. The guard was a reviewer-driven divergence with no reference backing. Revert the prod branch. Keep the .presence fallback from 7254b1a (that one responds to our specific .env.example shape, not a reference divergence). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
/deploy-review-app |
🚀 Deploying to Control Plane...⏳ Waiting for deployment to be ready... |
🎉 ✨ Deploy Complete! 🚀🌐 ➡️ Open Review AppDeployment successful for PR #728, commit b000c55 🎮 Control Plane Console |
Code Review: PR #728 — Pro RSC migration 2/3: switch SSR to the Pro Node renderer on webpackOverviewThis PR wires the React on Rails Pro Node renderer into Rails SSR, flips the asset bundler back to webpack (tactical regression while awaiting rspack RSC plugin support), and updates dev/CI/production plumbing accordingly. The description is thorough and the implementation is well-commented. A few correctness and safety issues are worth addressing before merge. IssuesHigh priority1. 2. 3. 4. CI log capture on renderer startup failure is promised but not implemented Medium priority5. 6. 7. Low priority8. Removed test for invalid 9. Positives
|
| } | ||
|
|
||
| _cachedBundler = require('@rspack/core'); | ||
| _cachedBundler = config.assets_bundler === 'rspack' ? require('@rspack/core') : require('webpack'); |
There was a problem hiding this comment.
Any value that isn't 'rspack' — including typos like 'webapck' or an empty string — silently falls through to require('webpack'). The old code threw explicitly; the new code is less strict.
Consider guarding the two valid values:
| _cachedBundler = config.assets_bundler === 'rspack' ? require('@rspack/core') : require('webpack'); | |
| if (config.assets_bundler === 'rspack') { | |
| _cachedBundler = require('@rspack/core'); | |
| } else if (!config.assets_bundler || config.assets_bundler === 'webpack') { | |
| _cachedBundler = require('webpack'); | |
| } else { | |
| throw new Error( | |
| `Invalid assets_bundler: "${config.assets_bundler}". Expected "webpack" or "rspack".`, | |
| ); | |
| } |
| // Expose globals the VM sandbox doesn't auto-provide but that downstream | ||
| // deps rely on during SSR. Without URL, react-router-dom's NavLink throws | ||
| // `ReferenceError: URL is not defined` via encodeLocation. | ||
| additionalContext: { URL, AbortController }, |
There was a problem hiding this comment.
AbortController is only a Node.js global from v15.0+. Referencing it here (at module load time, not lazily) means an older Node.js runtime throws ReferenceError: AbortController is not defined before the helpful startup error messages can run.
Adding an engines field to package.json would make the minimum Node.js version explicit and surface this constraint early:
"engines": { "node": ">=18.0.0" }Alternatively, a guard at the top of this file:
if (typeof AbortController === 'undefined') {
throw new Error('Node.js >=15.0.0 is required (AbortController global missing)');
}| // Output to a private directory for SSR bundles (not in public/) | ||
| // Using the default React on Rails path: ssr-generated | ||
| if (!rscBundle) { | ||
| // Scope client-reference discovery to the app source dir. Without this, |
There was a problem hiding this comment.
The condition if (!rscBundle) means the RSCWebpackPlugin is added for the default SSR bundle and skipped when rscBundle = true. The parameter name implies the opposite — "if I'm building the RSC bundle, add the plugin". A clearer name (or a short comment) would help avoid confusion in Sub-PR 3:
| // Scope client-reference discovery to the app source dir. Without this, | |
| if (!rscBundle) { | |
| // RSCWebpackPlugin does server-side client-reference discovery. | |
| // Skip it for the dedicated RSC client bundle (Sub-PR 3) where the | |
| // client-side plugin variant is used instead. |
| fi | ||
| sleep 1 | ||
| done | ||
| echo "Node renderer failed to start within 30 seconds (see output above)." |
There was a problem hiding this comment.
The PR description promises "log capture on failure", but when the renderer exits unexpectedly or times out the error message is just "see output above" with no actual log capture. In GitHub Actions each step runs in a fresh shell, so "above" is only what the renderer printed to stdout/stderr before backgrounding — which may be empty if it died during startup.
Consider capturing renderer output to a file and uploading it as an artifact on failure:
| echo "Node renderer failed to start within 30 seconds (see output above)." | |
| echo "Node renderer failed to start within 30 seconds." | |
| kill $RENDERER_PID 2>/dev/null || true | |
| exit 1 |
And add a subsequent step:
- name: Upload renderer logs on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: renderer-logs
path: /tmp/node-renderer.log(requires redirecting node renderer/node-renderer.js output to /tmp/node-renderer.log at startup)
| # docs/oss/building-features/node-renderer/container-deployment.md). | ||
| # `wait -n` exits on first child failure → container exits → Control Plane | ||
| # restarts the whole unit. Keeps the existing Thruster HTTP/2 proxy for Rails. | ||
| CMD ["bash", "-c", "node renderer/node-renderer.js & RENDERER_PID=$! ; bundle exec thrust bin/rails server & RAILS_PID=$! ; wait -n ; exit 1"] |
There was a problem hiding this comment.
RENDERER_PID and RAILS_PID are captured but never read — wait -n waits for any background child, not those specific PIDs, so they do nothing today.
More importantly, wait -n requires bash ≥ 4.3. If the base image doesn't ship that version (or /bin/bash is dash/ash), the container will crash at startup with a confusing syntax error rather than a process failure. Worth verifying the base image guarantees bash 4.3+, or consider a portable init approach (e.g. tini).
Suggested cleanup (assuming bash 4.3+ is confirmed):
| CMD ["bash", "-c", "node renderer/node-renderer.js & RENDERER_PID=$! ; bundle exec thrust bin/rails server & RAILS_PID=$! ; wait -n ; exit 1"] | |
| CMD ["bash", "-c", "node renderer/node-renderer.js & bundle exec thrust bin/rails server & wait -n ; exit 1"] |
| expect(() => utils.getBundler()).toThrow('configured for Rspack only'); | ||
| expect(bundler.DefinePlugin).toBeDefined(); | ||
| }); | ||
|
|
There was a problem hiding this comment.
The test for an invalid assets_bundler value was removed without replacement. The new behavior — silently falling back to webpack for any non-'rspack' value — is a deliberate design decision, but it is now undocumented in the test suite. A future reader could reasonably re-add a throw guard thinking the lack of validation was an oversight.
Consider adding an explicit test after this block:
it('falls back to webpack when assets_bundler is an unrecognized value', () => {
mockConfig.assets_bundler = 'invalid-bundler';
jest.doMock('shakapacker', () => ({ config: mockConfig }));
const utils = require('../../../config/webpack/bundlerUtils');
// Intentional: any non-rspack value resolves to webpack (tactical, see shakapacker.yml TODO)
const bundler = utils.getBundler();
expect(bundler.DefinePlugin.name).toBe('MockWebpackDefinePlugin');
});(Or change this to toThrow if stricter validation is added to bundlerUtils.js.)
|
Thanks for the review. All 9 items are either reference-matched or previously-settled themes — running the same reference-check methodology used across this PR: High priority
Medium priority
Low priority
Deploy status: Review app built and deployed successfully to CP. End-to-end SSR validated via logs: License validated, Node renderer listening on both workers, |
767c513
into
ihabadham/feature/pro-rsc/base
|
✅ Review app for PR #728 was successfully deleted |
Summary
Routes server rendering through the Pro Node renderer and flips the asset bundler to webpack. Wires the renderer in dev (
Procfile.dev), CI (rspec_test.yml), and production (Control PlaneDockerfileCMD + env).Part 2 of a stacked-PR series. Targets
ihabadham/feature/pro-rsc/base, notmaster.References this PR follows
docs/oss/configuration/configuration-pro.mdfor initializer fields,docs/oss/building-features/node-renderer/container-deployment.mdfor the single-container deploy pattern,docs/oss/building-features/node-renderer/js-configuration.mdfor launcher options.react_on_rails_pro/spec/dummy/in shakacode/react_on_rails. Referenced for the initializer shape,serverWebpackConfig.jsPro transforms, and the launcher defaults.DockerfileCMD pattern and theapp.ymlrenderer env block.docs/secrets-and-env-values.mdguide andlib/core/template_parser.rbdocument the{{APP_SECRETS}}template variable used inapp.yml.Changes
Bundler
shakapacker.yml:rspack→webpack. Tactical reversal of Upgrade React on Rails/Shakapacker and standardize on Rspack v2 #702 — rspack 2.0.0-beta.7's webpack-compat layer doesn't support the APIs upstreamRSCWebpackPluginneeds. Flips back whenshakacode/react_on_rails_rsc#29ships a release.bundlerUtils.js: returns@rspack/coreorwebpackbased on config (was rspack-only).serverWebpackConfig.js: Pro transforms per the:progenerator'supdate_webpack_config_for_proand the Pro dummy'sserverWebpackConfig.js—target: 'node',node: false,libraryTarget: 'commonjs2',extractLoader, babel SSR caller, destructured exports,RSCWebpackPlugin({ isServer: true, clientReferences: [...] }).Pro runtime
initializers/react_on_rails_pro.rb: NodeRenderer-only config (RSC fields in Sub-PR 3).renderer_use_fallback_exec_js = falseso any renderer issue raises loudly instead of silently degrading to ExecJS.renderer/node-renderer.js: launcher with integer env parsing, CI worker cap,additionalContext: { URL, AbortController }(withoutURL,react-router-dom's NavLink throwsReferenceError: URL is not definedat SSR time).package.json:react-on-rails-pro-node-renderer 16.6.0,react-on-rails-rsc 19.0.4(Pro peer dep),react+react-dompinned to19.0.4per the RSC CVE floor.Dev / CI / Production wiring
Procfile.dev:node-rendererprocess name (matches the Pro dummy'sProcfile.dev)..github/workflows/rspec_test.yml: starts the renderer with PID liveness + port-ready checks + log capture on failure..controlplane/DockerfileCMD: runs the renderer and Rails in one container withwait -n ; exit 1. Option 1 "Single Container" fromcontainer-deployment.md; same pattern as the marketplace demo'sDockerfile..controlplane/templates/app.yml: renderer env (RENDERER_PORT,RENDERER_LOG_LEVEL,RENDERER_WORKERS_COUNT,RENDERER_URL) +cpln://secret/{{APP_SECRETS}}.*references forRENDERER_PASSWORDandREACT_ON_RAILS_PRO_LICENSE.{{APP_SECRETS}}is cpflow's template variable for{APP_PREFIX}-secrets..controlplane/templates/rails.yml: memory512Mi→1GiandcapacityAI: false→true. Gives the single container headroom for Rails + renderer without matching the marketplace demo's 2Gi static baseline (tutorial's Rails surface is smaller);capacityAIadjusts CPU/memory within the replica based on observed usage..env.example:RENDERER_URL,RENDERER_PASSWORD,REACT_ON_RAILS_PRO_LICENSE.Deploy prerequisite (manual, one-time)
Before deploying this change to Control Plane, create three Secrets across the two orgs with keys
RENDERER_PASSWORD(strong random) andREACT_ON_RAILS_PRO_LICENSE(JWT from https://pro.reactonrails.com/):shakacode-open-source-examples-productionreact-webpack-rails-tutorial-production-secretsshakacode-open-source-examples-stagingreact-webpack-rails-tutorial-staging-secretsshakacode-open-source-examples-stagingqa-react-webpack-rails-tutorial-secretsReview apps share one secret because
{{APP_SECRETS}}expands to{APP_PREFIX}-secretsand the qa-apps config hasmatch_if_app_name_starts_with: true, so the prefix isqa-react-webpack-rails-tutorialfor every PR.Without this, the workload resolves
cpln://secret/...→ fails → crashloops.Stack context
RSCWebpackPlugin({ isServer: false })on client,rscWebpackConfig.jsderived fromserverWebpackConfig(true), initializer RSC fields, server-components page + routes +'use client'directives).shakapacker.ymlback to rspack onceshakacode/react_on_rails_rsc#29ships a release.Test plan
bundle exec rake shakapacker:compileclean webpack buildGET /HTTP 200; Rails log hasNode Renderer responded; renderer log shows[SERVER] RENDEREDlines — SSR routes through the Pro path, not ExecJS.HTTPX::ConnectionError (Connection refused). No ExecJS fallback (confirmsrenderer_use_fallback_exec_js = falseis load-bearing).401: Wrong passwordfrom the renderer.🤖 Generated with Claude Code