Skip to content

Add React Server Components with React on Rails Pro#722

Closed
justin808 wants to merge 11 commits intomasterfrom
jg/react-server-components
Closed

Add React Server Components with React on Rails Pro#722
justin808 wants to merge 11 commits intomasterfrom
jg/react-server-components

Conversation

@justin808
Copy link
Copy Markdown
Member

@justin808 justin808 commented Apr 4, 2026

Summary

  • Upgrades from react_on_rails to react_on_rails_pro (16.5.1) with React 19.0.4 and react-on-rails-rsc
  • Adds a three-bundle build pipeline: client, server, and RSC bundles via Rspack
  • Creates an RSC demo page at /server-components showcasing server components with async data fetching, Suspense streaming, and interactive client components
  • Includes a custom RspackRscPlugin for manifest generation since the standard RSCWebpackPlugin uses webpack internals incompatible with Rspack

RSC Demo Features

  • Server components using Node.js os module and lodash (never shipped to client browser)
  • Async data fetching with Suspense streaming — comments fetched directly from Rails API on server
  • Interactive client components ('use client' TogglePanel) mixed with server-rendered content (donut pattern)
  • Server-side markdown rendering with marked + sanitize-html (~200KB of libraries that stay server-side)

Infrastructure Changes

  • rscWebpackConfig.js — RSC bundle targeting Node.js with react-on-rails-rsc/WebpackLoader
  • rspackRscPlugin.js — Lightweight Rspack-compatible alternative to the webpack-only RSCWebpackPlugin
  • 'use client' directives added to all existing client component entry points
  • Webpack resolve alias maps react-on-railsreact-on-rails-pro for third-party compatibility
  • Dedicated rsc-bundle.js entry with registerServerComponent server/client registration
  • rsc_payload_route added to routes for RSC payload delivery

Test plan

  • All three bundles (client, server, RSC) compile successfully with Rspack
  • Existing server rendering specs pass (spec/requests/server_render_check_spec.rb)
  • RuboCop passes on all changed Ruby files
  • Manual: Visit /server-components to verify RSC demo page renders
  • Manual: Verify existing pages (home, simple, no-router, rescript) still work
  • Manual: Verify RSC payload is fetched from /rsc_payload/ServerComponentsPage

🤖 Generated with Claude Code


Note

High Risk
High risk because it changes the React on Rails integration and SSR execution model (introduces a Node renderer, new RSC payload route, and a third build output), which can break rendering/builds and affects server-side execution paths.

Overview
Adds React Server Components (RSC) support by migrating from react_on_rails to react_on_rails_pro and introducing a Node-based renderer process used for SSR and RSC payload generation.

Introduces a third build target (rsc-bundle) alongside client and SSR bundles, including a custom RspackRscPlugin to emit React Flight manifests and updates to webpack configs (aliases, fallbacks for Node builtins, Node-targeted RSC config, and SSR output commonjs2).

Adds an /server-components demo page wired into Rails routes/controller/views and the nav, plus new RSC client/server pack entrypoints and dev/CI plumbing (Procfile, npm script, CI step/env) to start the renderer and build the additional bundles.

Reviewed by Cursor Bugbot for commit bc41706. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • New Features

    • Added an "RSC Demo" page accessible from the nav: server-rendered server info, progressively streamed comments with server-rendered markdown, and client-interactive toggle panels.
    • New local RSC payload endpoint and a development renderer process to generate SSR/RSC payloads.
  • Chores

    • Migrated React integration to Pro/RSC packages and pinned related JS deps.
    • Updated build/dev tooling to support separate RSC bundling, watch mode, and dedicated renderer process.

Upgrade from react_on_rails to react_on_rails_pro gem (16.5.1) and
corresponding npm packages. Add RSC infrastructure with a demo page
at /server-components that showcases:

- Server components using Node.js os module and lodash (never shipped
  to client)
- Async data fetching with Suspense streaming (comments from Rails API)
- Interactive client components ('use client' TogglePanel) mixed with
  server-rendered content (donut pattern)
- Markdown rendering with marked + sanitize-html on server only

Key changes:
- Three-bundle build: client, server, and RSC bundles via Rspack
- Custom RspackRscPlugin for manifest generation (the standard
  RSCWebpackPlugin uses webpack internals incompatible with Rspack)
- 'use client' directives on all existing client component entry points
- Alias react-on-rails to react-on-rails-pro in webpack resolve to
  handle third-party packages (rescript-react-on-rails)
- Dedicated rsc-bundle.js entry with registerServerComponent
- RSC payload route and client-side registration

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 4, 2026

🚀 Quick Review App Commands

Welcome! Here are the commands you can use in this PR:

/deploy-review-app

Deploy your PR branch for testing

/delete-review-app

Remove the review app when done

/help

Show detailed instructions, environment setup, and configuration options.


@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 4, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds React Server Components (RSC) support: swaps to react_on_rails_pro, adds RSC webpack configs and an Rspack plugin, introduces a node renderer and RSC packs/entrypoints, adds a server-components demo (server + client pieces), and wires initializer, routes, and dev processes.

Changes

Cohort / File(s) Summary
Dependency & Dev Process
Gemfile, package.json, Procfile.dev
Replaced react_on_rails with react_on_rails_pro (gem/npm), added react-on-rails-pro-node-renderer and react-on-rails-rsc, pinned react/react-dom to exact versions, added node-renderer npm script and wp-rsc/node-renderer dev processes.
Rails Config & Routes
config/initializers/react_on_rails_pro.rb, config/routes.rb, app/controllers/pages_controller.rb
New initializer configuring ReactOnRailsPro (renderer URL/password, enable RSC, rsc bundle filename). Added rsc_payload_route and GET /server-componentspages#server_components; new server_components action.
Rails View
app/views/pages/server_components.html.erb
New view that appends the rsc-client-components pack and mounts ServerComponentsPage via react_component (prerender:false, auto_load_bundle:false, trace in dev, explicit id).
Webpack: RSC Build & Plugin
config/webpack/rscWebpackConfig.js, config/webpack/rspackRscPlugin.js, config/webpack/webpackConfig.js
Added RSC webpack config producing a single SSR-targeted RSC bundle (stripping CSS/extractors, adding RSC loader), new RspackRscPlugin to detect 'use client' modules and emit client/SSR manifests, and updated main webpack flow to support RSC-only and combined builds.
Webpack: Client/Server Configs
config/webpack/clientWebpackConfig.js, config/webpack/commonWebpackConfig.js, config/webpack/serverWebpackConfig.js
Client/server configs updated: alias react-on-railsreact-on-rails-pro, client registers RspackRscPlugin for client manifest, server config adds resolver aliases/fallbacks, exposes new extractLoader helper and changes module export shape.
Client Packs & Entrypoints
client/app/packs/rsc-bundle.js, client/app/packs/rsc-client-components.js, client/app/packs/stimulus-bundle.js, client/app/packs/stores-registration.js
Added RSC server bundle entry and client pack to register server components and client-side RSC components; updated packs to use react-on-rails-pro and register RSC payload generation URL.
Client Startup & Integration
client/app/bundles/.../startup/*, client/app/bundles/.../serverRegistration.jsx, client/app/libs/requestsManager.js
Replaced many react-on-rails imports with react-on-rails-pro, preserving registration/getStore/authenticity header calls but sourcing them from Pro package.
Client Components & 'use client'
client/app/bundles/.../*.{jsx,js}, client/app/bundles/comments/constants/paths.js, client/app/bundles/comments/components/NavigationBar/NavigationBar.jsx
Marked several modules with 'use client', added SERVER_COMPONENTS_PATH constant and a navigation link to the demo; minor client component adjustments to be client-executed.
New RSC Demo Components
client/app/bundles/server-components/ServerComponentsPage.jsx, .../components/ServerInfo.jsx, .../CommentsFeed.jsx, .../TogglePanel.jsx
Added server-rendered ServerComponentsPage composing ServerInfo (server runtime info), CommentsFeed (async fetch, dev delay, markdown via marked + sanitize-html), and TogglePanel (client interactive component).
Node Renderer Script & Gitignore
react-on-rails-pro-node-renderer.js, .gitignore
New Node renderer entry script with configurable port/workers/password and added .node-renderer-bundles/ to .gitignore.

Sequence Diagram(s)

sequenceDiagram
    participant Browser
    participant Rails
    participant NodeRSC as Node RSC Renderer
    participant ClientLoader as Client Loader

    Browser->>Rails: GET /server-components
    activate Rails
    Rails->>NodeRSC: Request RSC render for ServerComponentsPage
    deactivate Rails

    activate NodeRSC
    NodeRSC->>NodeRSC: Render ServerInfo (sync)
    NodeRSC->>NodeRSC: Await CommentsFeed (fetch, markdown/sanitize)
    NodeRSC->>Browser: Stream RSC/Flight payload
    deactivate NodeRSC

    Browser->>ClientLoader: Fetch client manifest & chunks
    ClientLoader->>Browser: Load TogglePanel chunk
    Browser->>Browser: Hydrate TogglePanel (interactive)
Loading
sequenceDiagram
    participant Bundler
    participant RscConfig
    participant Plugin as RspackRscPlugin
    participant Output

    Bundler->>RscConfig: Load rscWebpackConfig (RSC_BUNDLE_ONLY)
    RscConfig->>RscConfig: Strip CSS, set react-server condition, enforce single chunk
    Bundler->>Plugin: Run RspackRscPlugin during compilation
    Plugin->>Plugin: Scan chunks/modules for 'use client'
    Plugin->>Output: Emit react-client-manifest.json (and SSR manifest when applicable)
    Output->>Bundler: RSC manifests & bundle produced
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Poem

🐰 I nibble bytes and spin the wheel,

Server threads hum while client bits unseal,
"use client" hops out, manifests delight,
Bundles stream and TogglePanels take flight,
A rabbit cheers — RSC shines bright!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'Add React Server Components with React on Rails Pro' directly summarizes the main change: introducing React Server Components functionality via the react-on-rails-pro upgrade, which is the core objective reflected throughout the changeset.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch jg/react-server-components

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@claude
Copy link
Copy Markdown

claude bot commented Apr 4, 2026

PR Review: Add React Server Components with React on Rails Pro

This is a well-structured PR that introduces RSC support cleanly. The three-bundle pipeline design and the custom RspackRscPlugin are solid approaches. A few issues need attention before merging:

Critical

  • Hardcoded localhost:3000 in CommentsFeed.jsx — will silently fail in any non-local environment (staging, Docker, CI, production). Must be replaced with a configurable base URL or a relative path via the Rails host.

Bugs

  • No error handling around the fetch call in CommentsFeed.jsx — an unhandled rejection here will crash the RSC render stream with no graceful fallback for users.
  • useClientCache in rspackRscPlugin.js is never invalidated — the module-level Map persists across Rspack watch-mode rebuilds. Adding or removing a 'use client' directive from a file during development will not be reflected until the watcher is restarted.

Debugging artifacts left in production code

  • await new Promise((resolve) => setTimeout(resolve, 800)) in CommentsFeed.jsx simulates latency and should be removed.
  • trace: true in server_components.html.erb exposes internal rendering traces and should be gated to Rails.env.development?.

Minor / Cleanup

  • extractLoader is defined identically in both rscWebpackConfig.js and serverWebpackConfig.js. It could be extracted to a shared utility.
  • sanitize-html is configured to allow <img> tags without restricting the src attribute. In a demo this is low-risk, but it permits <img src="http://internal-host"> probes from user-controlled comment content — worth noting or restricting.
  • The SSR manifest emitted by the client-side plugin is always {}. If react-on-rails-pro ever reads it, this will silently produce wrong behavior. A comment explaining why it is intentionally empty would help.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4d09e13058

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Apr 4, 2026

Greptile Summary

This PR introduces React Server Components (RSC) support via react_on_rails_pro 16.5.1 and a three-bundle build pipeline (client, server, RSC) powered by Rspack. It adds a demo page at /server-components showcasing async data fetching, Suspense streaming, and the donut pattern, with a custom RspackRscPlugin for Rspack compatibility.

  • P1: CommentsFeed.jsx hardcodes http://localhost:3000/comments.json — this URL will fail on any non-local environment (staging, CI, Docker, production).
  • P1: The same fetch call has no response.ok guard; a non-2xx API response will cause the component to crash silently inside the Suspense boundary with no user-facing error.

Confidence Score: 3/5

Not safe to merge until the hardcoded localhost URL and missing error handling in CommentsFeed are fixed

Two P1 defects exist in CommentsFeed.jsx: a hardcoded localhost URL that breaks all non-local deployments and a missing response.ok guard that causes silent crashes on API errors. These are on the primary path of the new RSC demo feature. All other changes (webpack configs, plugin, Rails routes, component structure) are well-structured and low risk.

client/app/bundles/server-components/components/CommentsFeed.jsx requires fixes to the fetch call before merge

Important Files Changed

Filename Overview
client/app/bundles/server-components/components/CommentsFeed.jsx Async server component — hardcoded localhost URL and missing response.ok guard will break non-local deployments
config/webpack/rspackRscPlugin.js Custom Rspack-compatible RSC manifest plugin; module-level cache not invalidated between watch builds
client/app/bundles/server-components/ServerComponentsPage.jsx New RSC demo page composing ServerInfo, CommentsFeed (behind Suspense), and TogglePanel — clean structure
config/webpack/rscWebpackConfig.js RSC bundle config targeting Node.js, injecting RSC WebpackLoader, emitting RSC manifest
config/webpack/webpackConfig.js Three-bundle orchestration adding RSC_BUNDLE_ONLY env path alongside existing client/server paths
client/app/packs/rsc-bundle.js RSC entry point registering ServerComponentsPage as a server component
client/app/packs/rsc-client-components.js Client entry registering TogglePanel for the React flight client to resolve
config/initializers/react_on_rails_pro.rb Enables RSC support and sets rsc-bundle.js as the RSC bundle file name
config/routes.rb Adds rsc_payload_route and /server-components route — straightforward
app/controllers/pages_controller.rb Adds empty server_components action for the RSC demo page
client/app/bundles/server-components/components/ServerInfo.jsx Server-only component using Node.js os module and lodash — correct async server component pattern
client/app/bundles/server-components/components/TogglePanel.jsx 'use client' interactive toggle component; simple useState — no issues

Sequence Diagram

sequenceDiagram
    participant Browser
    participant Rails
    participant RSCRenderer as RSC Node Renderer
    participant API as Rails API

    Browser->>Rails: GET /server-components
    Rails-->>Browser: HTML shell + JS bundles (client + rsc-client-components)

    Browser->>Rails: GET /rsc_payload/ServerComponentsPage
    Rails->>RSCRenderer: Render ServerComponentsPage (rsc-bundle.js)
    RSCRenderer->>API: fetch /comments.json (server-side, hardcoded localhost:3000)
    API-->>RSCRenderer: JSON comments array
    RSCRenderer-->>Rails: React Flight payload (streamed chunks)
    Rails-->>Browser: Streamed RSC payload

    Browser->>Browser: Hydrate TogglePanel ('use client')
    Note over Browser: ServerInfo and CommentsFeed<br/>ship zero client JS
Loading

Reviews (1): Last reviewed commit: "Add React Server Components with React o..." | Re-trigger Greptile

- Move eslint-disable after 'use client' directive in SimpleCommentScreen
- Add no-promise-executor-return disable for setTimeout in CommentsFeed
- Replace array index key with semantic key in ServerInfo
- Add PropTypes to TogglePanel component
- Fix import ordering in stimulus-bundle.js

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Use single-line comment eslint-disable before 'use client' directive
  (file-level rules must be disabled before line 1)
- Suppress react/no-danger for sanitized HTML in CommentsFeed

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@claude
Copy link
Copy Markdown

claude bot commented Apr 4, 2026

Code Review

Great direction — RSC support on top of React on Rails Pro is a meaningful addition and the three-bundle architecture is well-structured. A few issues need attention before merging:

Bugs / Must-Fix

1. Hardcoded localhost:3000 in CommentsFeed.jsx (inline comment)
The fetch URL is baked in as http://localhost:3000/comments.json. This will silently fail on staging, production, CI, or any Docker setup. Use an env var (RAILS_INTERNAL_URL) or a Rails-internal API helper to make this environment-agnostic.

2. 'use client' on server-only files (inline comments on RouterApp.server.jsx and serverRegistration.jsx)
Both are traditional SSR entry points that have nothing to do with the RSC runtime. Adding 'use client' to them causes the RspackRscPlugin to misclassify them as client boundaries and potentially over-include modules in the RSC client manifest. Remove the directive from both files.

3. useClientCache never invalidated in watch mode (inline comment on rspackRscPlugin.js)
The module-level Map persists across incremental rebuilds. Adding/removing a 'use client' directive won't take effect until the build process is restarted. Hook into compiler.hooks.watchRun to evict modified files.

Code Quality

4. extractLoader duplicated (inline comment on rscWebpackConfig.js)
Identical function already defined and exported from serverWebpackConfig.js. Import it instead.

5. Artificial 800 ms delay in CommentsFeed (inline comment)
Good for a live demo; needs to be removed or gated behind an env flag before this is usable in any real environment.

Security / Minor

6. Unrestricted img tags in sanitizeHtml (inline comment on CommentsFeed.jsx)
sanitize-html correctly strips event handlers, but allowing arbitrary src on img enables tracking pixels and unintended outbound requests from the Node renderer. Consider restricting or omitting the src attribute.

7. SSR manifest always emitted as {} (inline comment on rspackRscPlugin.js)
Needs a comment (or fix) clarifying whether react-on-rails-pro actually reads this file at runtime — an empty manifest could silently break client component hydration.

Observations (no action needed)

  • The version shift from react_on_rails 16.6.0-rc.0 to react_on_rails_pro 16.5.1 (which pins react_on_rails 16.5.1) is effectively a downgrade of the underlying core gem from an RC to a stable but older release. This is presumably intentional since Pro 16.5.1 is the stable build, but worth confirming that no rc.0 features are being relied on elsewhere.
  • react-dom is now pinned to 19.0.4 (exact) rather than ^19.0.0; the previous lockfile was resolving to 19.2.0. The downgrade is intentional for RSC API stability, but pinning an exact patch version in package.json means consumers won't get security patches automatically — consider using ~19.0.4 at minimum.
  • The rscWebpackConfig.js correctly sets conditionNames: ['react-server', '...'] and targets Node — this is the right setup for RSC bundles.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🧹 Nitpick comments (8)
config/webpack/commonWebpackConfig.js (1)

11-17: Consider using a broader alias pattern for complete coverage.

The current aliases handle exact imports and the /node_package subpath, but third-party packages might import other subpaths (e.g., react-on-rails/someOtherPath). A regex-based alias could provide more comprehensive coverage.

♻️ Alternative: broader alias pattern

If you encounter issues with other subpath imports, consider:

     alias: {
       'react-on-rails$': 'react-on-rails-pro',
-      'react-on-rails/node_package': 'react-on-rails-pro/node_package',
+      'react-on-rails/': 'react-on-rails-pro/',
     },

Note: The trailing slash pattern behavior varies between webpack/rspack versions. Test this if you encounter additional subpath resolution issues.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@config/webpack/commonWebpackConfig.js` around lines 11 - 17, The current
webpack alias object only maps exact imports ('react-on-rails$' and
'react-on-rails/node_package') and misses other subpath imports; update the
alias entries in the alias object to use a regex/broader pattern so any import
of react-on-rails and its subpaths (e.g., react-on-rails/... ) is redirected to
react-on-rails-pro and its corresponding subpath, replacing or supplementing the
existing keys in the alias object to ensure complete coverage across imports;
test subpath resolution behavior in your webpack/rspack version after changing
the alias.
config/webpack/rspackRscPlugin.js (1)

20-24: Synchronous file I/O may impact build performance at scale.

Using fs.openSync/readSync/closeSync blocks the event loop. While caching mitigates repeated reads, the initial scan of many files could slow builds. This is acceptable for most projects but worth noting for large codebases.

For future optimization, consider using fs.promises with Promise.all if build times become a concern, though this would require restructuring the processAssets hook to be async.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@config/webpack/rspackRscPlugin.js` around lines 20 - 24, The current
synchronous file reads (fs.openSync, fs.readSync, fs.closeSync) in the RSC
scanning logic block that reads the first ~200 bytes of filePath block the event
loop and can degrade large builds; change this to use fs.promises (e.g.,
fs.promises.open andfilehandle.read or fs.promises.readFile with a slice) and
perform reads concurrently with Promise.all so many files are scanned in
parallel, and update the surrounding processAssets hook (or the function that
calls this scan) to be async/await-compatible so it can await the promise-based
reads without blocking; keep the existing cache semantics and only switch the
low-level I/O to the promise-based APIs while preserving the logic that checks
for 'use client' at the top of filePath.
package.json (1)

132-132: Upgrade eslint-plugin-react-hooks to the latest version for React 19 compatibility.

The current version ^4.6.0 does not fully support React 19's new hooks like useActionState, useFormStatus, and useOptimistic. The latest stable version (7.0.1) provides complete support for React 19, including validation rules for React Compiler features introduced in React 19.

📦 Suggested upgrade
-    "eslint-plugin-react-hooks": "^4.6.0",
+    "eslint-plugin-react-hooks": "^7.0.0",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` at line 132, Update the eslint-plugin-react-hooks dependency in
package.json to the latest compatible release (replace the
"eslint-plugin-react-hooks": "^4.6.0" entry) so the project uses the React
19–compatible validator (e.g., 7.0.1); modify the package.json dependency line
for eslint-plugin-react-hooks and then run your package manager (npm/yarn/pnpm)
to install and update lockfiles, and run the linter to confirm no new rule
errors from the updated plugin.
app/controllers/pages_controller.rb (1)

41-41: Skip set_comments for the new server_components action.

Line 41 introduces an action with no use of @comments, but it still incurs before_action :set_comments. Consider excluding this action to avoid unnecessary DB work.

♻️ Proposed change
-  before_action :set_comments
+  before_action :set_comments, except: %i[server_components]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/pages_controller.rb` at line 41, The new controller action
server_components is being run through the before_action :set_comments even
though it does not use `@comments`; update the before_action declaration to skip
set_comments for this action by adding an except/only exemption (e.g., modify
the before_action :set_comments line to exclude :server_components) so the
database work is avoided when server_components is invoked.
app/views/pages/server_components.html.erb (1)

2-4: Gate tracing by environment.

Line 4 uses trace: true unconditionally. Prefer enabling trace only in development to reduce noise and overhead.

♻️ Proposed change
 <%= react_component("ServerComponentsPage",
     prerender: false,
-    trace: true,
+    trace: Rails.env.development?,
     id: "ServerComponentsPage-react-component-0") %>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/views/pages/server_components.html.erb` around lines 2 - 4, The
react_component call currently hardcodes trace: true; change it to enable
tracing only in development by replacing the literal with a conditional using
the Rails environment (e.g., set trace to Rails.env.development? or equivalent),
updating the react_component("ServerComponentsPage", prerender: false, trace:
...) invocation so trace is true only when Rails.env.development? and false
otherwise.
client/app/bundles/server-components/components/TogglePanel.jsx (1)

10-29: Expose expanded/collapsed state to assistive tech.

Please add aria-expanded (and ideally aria-controls) on the toggle button so screen readers get the panel state.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/app/bundles/server-components/components/TogglePanel.jsx` around lines
10 - 29, The TogglePanel component's toggle button needs ARIA attributes so
assistive tech can read its state: update the button (where setIsOpen is used
and isOpen is referenced) to include aria-expanded={isOpen} and aria-controls
pointing to the panel's id, and give the panel div (the conditional container
that renders {children}) a matching id; implement the id either by accepting a
prop (e.g., panelId) or generating a stable id inside the component (e.g.,
React's useId) and use that id on the div so aria-controls links to it.
config/webpack/rscWebpackConfig.js (2)

89-90: Don't replace the inherited optimization object.

This drops whatever commonWebpackConfig() already set up. Only minimize needs to change here, so merge into the existing object instead of resetting it wholesale.

Suggested diff
-  rscConfig.optimization = { minimize: false };
+  rscConfig.optimization = {
+    ...(rscConfig.optimization || {}),
+    minimize: false,
+  };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@config/webpack/rscWebpackConfig.js` around lines 89 - 90, The code currently
overwrites the inherited optimization object (rscConfig.optimization = {
minimize: false }), dropping settings established by commonWebpackConfig();
instead merge into the existing object by preserving rscConfig.optimization and
only changing minimize (e.g., assign rscConfig.optimization = {
...rscConfig.optimization, minimize: false } or set
rscConfig.optimization.minimize = false) so other optimization defaults remain
intact; update the rscWebpackConfig code that sets rscConfig.optimization
accordingly.

29-33: Mirror the server-bundle path diagnostics here.

config/webpack/serverWebpackConfig.js:52-68 derives the expected bundle path from config.source_path and config.source_entry_path. Hard-coding client/app/packs/rsc-bundle.js makes this failure misleading for any non-default Shakapacker layout.

Suggested diff
   const rscEntry = rscConfig.entry['rsc-bundle'];
   if (!rscEntry) {
+    const sourcePath = config.source_path || 'client/app';
+    const entryPath = config.source_entry_path || 'packs';
+    const fullPath = `${sourcePath}/${entryPath}/rsc-bundle.js`;
     throw new Error(
-      'RSC bundle entry not found. Ensure client/app/packs/rsc-bundle.js exists.',
+      `RSC bundle entry 'rsc-bundle' not found.\n` +
+      `Expected file: ${fullPath}\n` +
+      `Current source_path: ${config.source_path}\n` +
+      `Current source_entry_path: ${config.source_entry_path}\n` +
+      `Verify:\n` +
+      `1. The rsc-bundle.js file exists at the expected location\n` +
+      `2. nested_entries is configured correctly in shakapacker.yml\n` +
+      `3. The file is properly exported from your entry point`,
     );
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@config/webpack/rscWebpackConfig.js` around lines 29 - 33, The error thrown
when rscEntry is missing should mirror the server-bundle diagnostics by deriving
the expected RSC bundle path from the same config values instead of hard-coding
"client/app/packs/rsc-bundle.js"; update the check around rscEntry (rscEntry and
rscConfig.entry) to compute the expected path using config.source_path and
config.source_entry_path (same approach used in serverWebpackConfig's bundle
path logic) and include that computed path in the thrown Error message so the
failure message accurately reflects non-default Shakapacker layouts.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@client/app/bundles/comments/components/SimpleCommentScreen/ror_components/SimpleCommentScreen.jsx`:
- Line 3: ESLint is still failing because the single-line disable at the top
doesn't cover both class declarations in SimpleCommentScreen.jsx; either add a
file-level directive (/* eslint-disable max-classes-per-file */) at the very top
of SimpleCommentScreen.jsx so it applies to all class declarations in the file,
or move one of the class declarations into its own module and import it (i.e.,
split the two class declarations into separate files) and remove the inline
disable; locate the two class declarations in SimpleCommentScreen.jsx and choose
one of these fixes to resolve the rule failure.

In
`@client/app/bundles/comments/startup/RouterApp/ror_components/RouterApp.server.jsx`:
- Line 1: Remove the top-level 'use client' directive from RouterApp.server.jsx
so the module is treated as a server component (it currently imports server-only
APIs like react-router-dom/server); open the file and delete the "'use client'"
line and ensure no client-only hooks or APIs are used elsewhere in the module
(check for references inside RouterApp or any exported server functions) so the
import of react-router-dom/server remains valid.

In `@client/app/bundles/server-components/components/CommentsFeed.jsx`:
- Around line 19-21: In CommentsFeed.jsx replace the hardcoded
'http://localhost:3000/comments.json' fetch with an environment-aware or
relative URL (e.g., use a relative path '/comments.json' or a runtime config
variable like process.env.NEXT_PUBLIC_API_URL) and add response status handling
before parsing: check response.ok in the function that calls fetch and throw or
handle an error when it's false so you don't call response.json() on error
responses; adjust any surrounding error-handling logic in the CommentsFeed
component to surface or recover from that thrown error.
- Line 16: The Promise executor in CommentsFeed.jsx uses an implicit-return
arrow that returns setTimeout (await new Promise((resolve) =>
setTimeout(resolve, 800))), triggering the no-promise-executor-return lint rule;
change the executor to a block-bodied function and call setTimeout inside it
(i.e., replace the inline implicit-return with a brace-enclosed function that
invokes setTimeout and calls resolve), so the Promise executor does not return
the timer value.

In `@client/app/bundles/server-components/components/ServerInfo.jsx`:
- Around line 43-45: Replace the unstable array-index key used in the
grouped.map call (key={gi}) with a stable identifier derived from the group's
contents: inside the ServerInfo component replace the gi key with a
deterministic value (for example build a key from the group's item keys like
group.map(([k])=>k).join('-') or use the first item's key group[0][0] if
present). Update the grouped.map(...) element (where gi is currently used) to
use this computed stableKey so React reconciliation is stable and lint warnings
are resolved.

In `@client/app/bundles/server-components/components/TogglePanel.jsx`:
- Around line 5-34: Import PropTypes from 'prop-types' and add propTypes on the
TogglePanel component to satisfy the react/prop-types lint rule: add
"TogglePanel.propTypes = { title: PropTypes.node.isRequired, children:
PropTypes.node }" (and optionally defaultProps if desired). This change should
be made near the TogglePanel definition (reference: TogglePanel component, props
title and children) and ensure the PropTypes import is included at the top of
the file.

In `@client/app/packs/rsc-client-components.js`:
- Around line 7-8: The ESLint import/order violation is caused by importing the
relative module TogglePanel before the external package ReactOnRails; fix it by
reordering the imports so ReactOnRails is imported first and TogglePanel second
(i.e., move the ReactOnRails import above the TogglePanel import in
rsc-client-components.js), then re-run the linter to confirm the import/order
rule passes.

In `@config/webpack/rscWebpackConfig.js`:
- Around line 65-66: The guard that checks for url/file loaders in
rscWebpackConfig.js is too strict (it compares rule.use.loader using ===), so
emitFile=false is not applied when loader identifiers are resolved to full
paths; update the condition in the block that sets rule.use.options.emitFile to
use substring matching (e.g., use .includes('url-loader') ||
.includes('file-loader')) or call the existing extractLoader() helper to detect
loader identity, ensuring the check matches resolved loader paths and reliably
sets emitFile=false for the url-loader/file-loader cases.

In `@config/webpack/rspackRscPlugin.js`:
- Around line 12-35: The module-level cache useClientCache can become stale
during watch/dev builds; update the plugin's apply method to clear
useClientCache at the start of each compilation (e.g., register a hook on
compiler.hooks.compilation or compiler.hooks.watchRun depending on the plugin
entry) so hasUseClientDirective always re-reads changed files between rebuilds;
locate useClientCache and hasUseClientDirective and add the cache.clear() call
inside the apply's compilation/watch hook to invalidate cached results on each
new build.

In `@config/webpack/webpackConfig.js`:
- Around line 23-33: The RSC-only branch never applies env-specific adjustments
and doesn't expose the rsc config for modification; update the RSC_BUNDLE_ONLY
branch so after creating rscConfig via rscWebpackConfig() you call envSpecific
appropriately (e.g., envSpecific(null, null, rscConfig) or extend envSpecific to
accept a third rscConfig arg) before assigning result = rscConfig, ensuring
rscConfig is passed to envSpecific exactly like clientConfig and serverConfig
are in the multi-config path (symbols: rscWebpackConfig, envSpecific,
clientWebpackConfig, serverWebpackConfig, result).

---

Nitpick comments:
In `@app/controllers/pages_controller.rb`:
- Line 41: The new controller action server_components is being run through the
before_action :set_comments even though it does not use `@comments`; update the
before_action declaration to skip set_comments for this action by adding an
except/only exemption (e.g., modify the before_action :set_comments line to
exclude :server_components) so the database work is avoided when
server_components is invoked.

In `@app/views/pages/server_components.html.erb`:
- Around line 2-4: The react_component call currently hardcodes trace: true;
change it to enable tracing only in development by replacing the literal with a
conditional using the Rails environment (e.g., set trace to
Rails.env.development? or equivalent), updating the
react_component("ServerComponentsPage", prerender: false, trace: ...) invocation
so trace is true only when Rails.env.development? and false otherwise.

In `@client/app/bundles/server-components/components/TogglePanel.jsx`:
- Around line 10-29: The TogglePanel component's toggle button needs ARIA
attributes so assistive tech can read its state: update the button (where
setIsOpen is used and isOpen is referenced) to include aria-expanded={isOpen}
and aria-controls pointing to the panel's id, and give the panel div (the
conditional container that renders {children}) a matching id; implement the id
either by accepting a prop (e.g., panelId) or generating a stable id inside the
component (e.g., React's useId) and use that id on the div so aria-controls
links to it.

In `@config/webpack/commonWebpackConfig.js`:
- Around line 11-17: The current webpack alias object only maps exact imports
('react-on-rails$' and 'react-on-rails/node_package') and misses other subpath
imports; update the alias entries in the alias object to use a regex/broader
pattern so any import of react-on-rails and its subpaths (e.g.,
react-on-rails/... ) is redirected to react-on-rails-pro and its corresponding
subpath, replacing or supplementing the existing keys in the alias object to
ensure complete coverage across imports; test subpath resolution behavior in
your webpack/rspack version after changing the alias.

In `@config/webpack/rscWebpackConfig.js`:
- Around line 89-90: The code currently overwrites the inherited optimization
object (rscConfig.optimization = { minimize: false }), dropping settings
established by commonWebpackConfig(); instead merge into the existing object by
preserving rscConfig.optimization and only changing minimize (e.g., assign
rscConfig.optimization = { ...rscConfig.optimization, minimize: false } or set
rscConfig.optimization.minimize = false) so other optimization defaults remain
intact; update the rscWebpackConfig code that sets rscConfig.optimization
accordingly.
- Around line 29-33: The error thrown when rscEntry is missing should mirror the
server-bundle diagnostics by deriving the expected RSC bundle path from the same
config values instead of hard-coding "client/app/packs/rsc-bundle.js"; update
the check around rscEntry (rscEntry and rscConfig.entry) to compute the expected
path using config.source_path and config.source_entry_path (same approach used
in serverWebpackConfig's bundle path logic) and include that computed path in
the thrown Error message so the failure message accurately reflects non-default
Shakapacker layouts.

In `@config/webpack/rspackRscPlugin.js`:
- Around line 20-24: The current synchronous file reads (fs.openSync,
fs.readSync, fs.closeSync) in the RSC scanning logic block that reads the first
~200 bytes of filePath block the event loop and can degrade large builds; change
this to use fs.promises (e.g., fs.promises.open andfilehandle.read or
fs.promises.readFile with a slice) and perform reads concurrently with
Promise.all so many files are scanned in parallel, and update the surrounding
processAssets hook (or the function that calls this scan) to be
async/await-compatible so it can await the promise-based reads without blocking;
keep the existing cache semantics and only switch the low-level I/O to the
promise-based APIs while preserving the logic that checks for 'use client' at
the top of filePath.

In `@package.json`:
- Line 132: Update the eslint-plugin-react-hooks dependency in package.json to
the latest compatible release (replace the "eslint-plugin-react-hooks": "^4.6.0"
entry) so the project uses the React 19–compatible validator (e.g., 7.0.1);
modify the package.json dependency line for eslint-plugin-react-hooks and then
run your package manager (npm/yarn/pnpm) to install and update lockfiles, and
run the linter to confirm no new rule errors from the updated plugin.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 362cf633-c476-4648-9bdf-6c7c981ca0ac

📥 Commits

Reviewing files that changed from the base of the PR and between 5cf245f and 4d09e13.

⛔ Files ignored due to path filters (2)
  • Gemfile.lock is excluded by !**/*.lock
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (33)
  • Gemfile
  • Procfile.dev
  • app/controllers/pages_controller.rb
  • app/views/pages/server_components.html.erb
  • client/app/bundles/comments/components/Footer/ror_components/Footer.jsx
  • client/app/bundles/comments/components/NavigationBar/NavigationBar.jsx
  • client/app/bundles/comments/components/SimpleCommentScreen/ror_components/SimpleCommentScreen.jsx
  • client/app/bundles/comments/constants/paths.js
  • client/app/bundles/comments/rescript/ReScriptShow/ror_components/RescriptShow.jsx
  • client/app/bundles/comments/startup/App/ror_components/App.jsx
  • client/app/bundles/comments/startup/ClientRouterAppExpress.jsx
  • client/app/bundles/comments/startup/NavigationBarApp/ror_components/NavigationBarApp.jsx
  • client/app/bundles/comments/startup/RouterApp/ror_components/RouterApp.client.jsx
  • client/app/bundles/comments/startup/RouterApp/ror_components/RouterApp.server.jsx
  • client/app/bundles/comments/startup/serverRegistration.jsx
  • client/app/bundles/server-components/ServerComponentsPage.jsx
  • client/app/bundles/server-components/components/CommentsFeed.jsx
  • client/app/bundles/server-components/components/ServerInfo.jsx
  • client/app/bundles/server-components/components/TogglePanel.jsx
  • client/app/libs/requestsManager.js
  • client/app/packs/rsc-bundle.js
  • client/app/packs/rsc-client-components.js
  • client/app/packs/stimulus-bundle.js
  • client/app/packs/stores-registration.js
  • config/initializers/react_on_rails_pro.rb
  • config/routes.rb
  • config/webpack/clientWebpackConfig.js
  • config/webpack/commonWebpackConfig.js
  • config/webpack/rscWebpackConfig.js
  • config/webpack/rspackRscPlugin.js
  • config/webpack/serverWebpackConfig.js
  • config/webpack/webpackConfig.js
  • package.json

@claude
Copy link
Copy Markdown

claude bot commented Apr 5, 2026

Review: Add React Server Components with React on Rails Pro

Solid implementation overall — the three-bundle pipeline, custom RspackRscPlugin, and RSC demo page are well-structured. A few issues worth addressing before merge:

Security

  • Hardcoded default password — Both config/initializers/react_on_rails_pro.rb and react-on-rails-pro-node-renderer.js fall back to the literal string "devPassword", which is also committed in Procfile.dev. Any deployment that omits RENDERER_PASSWORD silently exposes a known credential. The initializer uses ENV.fetch with a default, which does not raise; the renderer uses ||. Both should raise in non-development environments. See inline comments on both files.

  • data: URI in allowedSchemes (CommentsFeed.jsx:59) — Permitting data: globally allows data:text/html and data:application/javascript URIs in user-supplied markdown, which can be exploited in certain browser/CSP configurations. Restrict to ['https', 'http'] or use allowedSchemesByTag to scope data: to img only.

Correctness

  • Artificial delay affects all non-production environments (CommentsFeed.jsx:16-20) — The NODE_ENV !== 'production' guard fires in CI, staging, and any NODE_ENV=development environment, not just local dev. This will slow or flake automated tests and degrade staging. Gate it behind an explicit opt-in flag instead (e.g. RSC_SUSPENSE_DEMO_DELAY=true).

  • Module-level useClientCache shared across parallel compilations (rspackRscPlugin.js:13) — When all three bundles compile together, concurrent compilations share the same Map and each clears it at thisCompilation. A clear from one compiler can evict entries mid-read in another. Move the cache into the plugin instance.

Code Quality

  • extractLoader duplicated (rscWebpackConfig.js:16-22) — Identical function already defined and exported in serverWebpackConfig.js. Import it rather than copy it.

🤖 Generated with Claude Code

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3f7e452b77

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

…ation

Three root causes for the 37/38 rspec test failures:

1. CI missing Node renderer: The RSC branch switched SSR from ExecJS to
   the react-on-rails-pro NodeRenderer service (port 3800). CI never
   started this service, causing Net::ReadTimeout on all SSR requests.
   Added renderer startup step and RENDERER_PASSWORD env var to CI.

2. Server bundle externals broke in VM sandbox: The previous commit
   externalized Node builtins (path/fs/stream) as CommonJS requires,
   but the Node renderer runs bundles in a vm.createContext() sandbox
   where require() is unavailable. Reverted to resolve.fallback: false
   which stubs these unused code paths at build time instead.

3. MessageChannel undefined in VM: react-dom/server.browser.js
   instantiates MessageChannel at module load time. The Node renderer's
   VM sandbox lacks this browser global (unlike Bun/ExecJS on master).
   Added a BannerPlugin polyfill injected at bundle top.

4. RouterApp.server.jsx misclassified as RSC: The auto-bundling system
   registered it via registerServerComponent() because it lacked 'use
   client'. But it's a traditional SSR component (StaticRouter), not an
   RSC. Added 'use client' directive so it registers via
   ReactOnRails.register() instead.

All 38 rspec tests now pass locally.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@claude
Copy link
Copy Markdown

claude bot commented Apr 6, 2026

Review: React Server Components with React on Rails Pro

Good work overall - the three-bundle pipeline is well-structured and the RSC demo clearly illustrates the server/client component model. A few issues need attention before merging.

Security (must fix)

  1. Hardcoded renderer password fallback (config/initializers/react_on_rails_pro.rb line 8)
    ENV.fetch with a devPassword default silently uses a well-known default in any environment where the var is unset. Use ENV.fetch without a default in production so it raises loudly if missing.

  2. data: URI scheme in sanitize-html config (CommentsFeed.jsx line 53)
    allowedSchemes including "data" permits img src=data:text/html payloads. Remove data from the global scheme list; use allowedSchemesByTag if inline images are truly needed.

  3. Hostname disclosure in server-rendered output (ServerInfo.jsx)
    os.hostname() is rendered into HTML sent to end users, leaking infrastructure details. Redact for a public demo.

  4. Unauthenticated internal API call (CommentsFeed.jsx line 22)
    The fetch to localhost:3000/comments.json has no authentication. Add an internal API token and derive the base URL from render context rather than a hardcoded fallback.

Code quality

  1. Duplicated extractLoader function (rscWebpackConfig.js vs serverWebpackConfig.js)
    The function is already exported from serverWebpackConfig.js. Import it - do not redefine it.

  2. Artificial 800ms delay (CommentsFeed.jsx line 15)
    The NODE_ENV !== production guard fires in CI and staging. Remove it before merge.

Minor

  • The MessageChannel polyfill in the server bundle banner plugin is a noteworthy workaround. A comment explaining why it is needed (Node VM sandbox lacks browser globals) would help future maintainers.
  • react-on-rails-rsc is pinned to an rc version (19.0.5-rc.1). Worth tracking when stable lands.

@justin808
Copy link
Copy Markdown
Member Author

Review follow-up completed in bc41706

Implemented the requested set (items 1-12), pushed, and replied on all outstanding review threads (including skipped items with rationale). All review threads are now resolved.

Implemented fixes

  • Hardened renderer secret handling:
    • Removed non-local default password behavior (config/initializers/react_on_rails_pro.rb, react-on-rails-pro-node-renderer.js)
    • Removed inline password from Procfile.dev
  • Scoped NodeRenderer usage:
    • NodeRenderer is now gated (development or REACT_USE_NODE_RENDERER=true) in react_on_rails_pro.rb
  • CommentsFeed reliability/security updates:
    • Explicit opt-in demo delay (RSC_SUSPENSE_DEMO_DELAY=true)
    • Safer base URL resolution (RAILS_INTERNAL_URL; localhost fallback only for dev/test)
    • Graceful fallback UI on fetch failures
    • Tightened sanitize config to only allow http/https schemes
  • Webpack/RSC updates:
    • rscWebpackConfig devtool is env-aware (source-map in production)
    • RspackRscPlugin cache moved to plugin instance scope and cleared per compilation
    • Restored CJS-compatible serverWebpackConfig export shape while keeping extractLoader export
    • Enabled libraryTarget: 'commonjs2' on server bundle output

Validation run

  • yarn -s prettier --check on changed JS/webpack files
  • bundle exec rubocop config/initializers/react_on_rails_pro.rb
  • bundle exec rspec spec/requests/server_render_check_spec.rb

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

There are 3 total unresolved issues (including 1 from previous review).

Fix All in Cursor

Bugbot Autofix is ON, but it could not run because on-demand usage is turned off. To enable Bugbot Autofix, turn on on-demand usage and set a spend limit in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit bc41706. Configure here.

sleep 1
done
echo "Node renderer failed to start within 30 seconds"
exit 1
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CI starts node renderer but Rails won't use it

Medium Severity

The CI workflow starts the node renderer process and waits for it, but the Rails initializer only enables the node renderer when Rails.env.development? or ENV["REACT_USE_NODE_RENDERER"] == "true". In CI, RAILS_ENV=test so development? is false, and REACT_USE_NODE_RENDERER is never set. The renderer starts but Rails never connects to it, meaning any test relying on RSC or node-based SSR will silently fall back to non-node rendering.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit bc41706. Configure here.

const testValue = typeof item === 'string' ? item : item?.loader;
return testValue && testValue.includes(loaderName);
});
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicated extractLoader function across webpack config files

Low Severity

extractLoader is defined identically in both rscWebpackConfig.js and serverWebpackConfig.js. The server config already exports it via module.exports.extractLoader = extractLoader, but the RSC config defines its own copy instead of importing it. This duplication risks divergent fixes if the function is ever updated.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit bc41706. Configure here.

@claude
Copy link
Copy Markdown

claude bot commented Apr 6, 2026

PR Review: Add React Server Components with React on Rails Pro

This is an impressive and well-structured PR that adds RSC support via a clean three-bundle pipeline. The custom RspackRscPlugin, the Node renderer wiring, and the donut-pattern demo are all solid. A few issues worth addressing before merge:

Security

  1. Server information disclosureServerInfo.jsx renders os.hostname(), process.version, total/free RAM, CPU count, and platform to any visitor of /server-components. Hostname and memory specifics are useful recon for an attacker. Consider redacting the hostname or gating the route behind authentication.

  2. HTTP allowed in markdown renderingCommentsFeed.jsx passes allowedSchemes: ['https', 'http'] to sanitize-html. Allowing plain http:// in <img src> enables mixed-content warnings and can leak the visitor's IP to an attacker-controlled server via image load. Restrict to ['https'] only.

  3. Weak hardcoded password in CIRENDERER_PASSWORD: devPassword is committed in plain text in the workflow YAML. Prefer a GitHub Actions secret (${{ secrets.RENDERER_PASSWORD }}).

Correctness / Bugs

  1. RSC manifest chunks format — In rspackRscPlugin.js _processModule, the chunks array is built with chunks.push(chunk.id, file), which alternates IDs and filenames in a flat array. The React Flight protocol expects chunks to be an array of chunk filenames (or [id, filename] tuples). The current output may cause client-side component resolution failures. Verify against what RSCWebpackPlugin emits.

  2. 'use client' on RouterApp.server.jsx — This is a traditional SSR component (StaticRouter, server-only rendering path). Adding 'use client' reclassifies it as a client reference in the RSC graph. Since rsc-bundle.js explicitly excludes it anyway, the directive is dead code at best and misleading at worst. Remove it or add a comment explaining why it is intentionally present.

Code Quality

  1. Duplicate extractLoader — The function is defined identically in both serverWebpackConfig.js (and exported as module.exports.extractLoader) and rscWebpackConfig.js (re-defined locally). rscWebpackConfig.js should import it from serverWebpackConfig.js.

  2. Missing libraryTarget: 'commonjs2' in RSC bundle output — The server bundle sets this explicitly because the Node renderer loads it as a CommonJS module. The RSC bundle also targets Node and is loaded by the same renderer, but its output config omits this setting. Verify the renderer can load the RSC bundle correctly without it.

DRIVER: selenium_chrome
CHROME_BIN: /usr/bin/google-chrome
USE_COVERALLS: true
RENDERER_PASSWORD: devPassword
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Weak password hardcoded in version control. Use a GitHub Actions secret instead:

Suggested change
RENDERER_PASSWORD: devPassword
RENDERER_PASSWORD: ${{ secrets.RENDERER_PASSWORD }}

Add a fallback (e.g. devPassword) only in the secret's default value, not in the YAML.

freeMemory: (os.freemem() / (1024 * 1024 * 1024)).toFixed(1),
cpus: os.cpus().length,
hostname: os.hostname(),
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security: hostname disclosure. Exposing the server's hostname to any page visitor is useful reconnaissance for an attacker. If this is a public demo page, consider omitting the hostname or replacing it with a masked value (e.g. os.hostname().replace(/[a-z0-9]+/gi, '*')).

img: ['src', 'alt', 'title', 'width', 'height'],
},
allowedSchemes: ['https', 'http'],
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security: http:// allows mixed-content and IP-leak via <img> tags. User-submitted markdown can contain ![](http://attacker.example/pixel.gif) which will fire a request from the server (during RSC render) or from the browser (after hydration), leaking the server/visitor IP. Restrict to HTTPS only:

Suggested change
});
allowedSchemes: ['https'],

@@ -1,8 +1,10 @@
'use client';
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Misleading 'use client' directive. This is a traditional SSR component that uses StaticRouter and is only included in the server bundle — the RSC bundle explicitly excludes it (see the comment in rsc-bundle.js). The 'use client' directive here is dead code that contradicts the filename and could mislead future maintainers into thinking this file participates in the RSC graph. Remove the directive, or add a comment explaining why it is intentionally present.

const testValue = typeof item === 'string' ? item : item?.loader;
return testValue && testValue.includes(loaderName);
});
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate function. extractLoader is already defined and exported from serverWebpackConfig.js. Import it instead of re-defining it:

Suggested change
}
const { extractLoader } = require('./serverWebpackConfig');

manifest[key] = {};
}
manifest[key]['*'] = ssrEntry;
manifest[key][''] = ssrEntry;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Likely incorrect chunks format. chunks.push(chunk.id, file) pushes two separate elements per file, producing a flat alternating array like [chunkId, "file.js", chunkId, "file2.js"]. The React Flight manifest format expects chunks to be an array of filenames (strings), not an interleaved id/name array. Compare against what RSCWebpackPlugin emits — the format is typically ["file.js"]. If chunk IDs are needed, they belong in a separate field or as [id, file] tuples depending on the React version.

@justin808
Copy link
Copy Markdown
Member Author

Closing in favor of PR #723 — the review fixes commit (bc41706) has been cherry-picked there.

@justin808 justin808 closed this Apr 7, 2026
@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 7, 2026

✅ Review app for PR #722 was successfully deleted

View Completed Delete Logs

Control Plane Organization

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant