Skip to content

Add React Server Components with React on Rails Pro#722

Open
justin808 wants to merge 4 commits intomasterfrom
jg/react-server-components
Open

Add React Server Components with React on Rails Pro#722
justin808 wants to merge 4 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

Medium Risk
Adds a new RSC runtime path and introduces a third (Node-targeted) bundle plus custom manifest generation, which can break builds/SSR if misconfigured. Also switches core React-on-Rails integration to react_on_rails_pro, affecting app-wide component registration and bundling.

Overview
Adds React Server Components support using react_on_rails_pro. The Rails app now configures RSC support, exposes an rsc_payload_route, and adds a new /server-components demo page rendered via react_component("ServerComponentsPage") plus a dedicated client chunk for RSC client components.

Upgrades and refactors the JS/Ruby integration. Replaces react_on_rails with react_on_rails_pro across the app, pins React to 19.0.4, adds react-on-rails-rsc, and updates many components/entrypoints with 'use client' directives for RSC compatibility.

Introduces a 3-bundle build pipeline (client/server/RSC). Adds rscWebpackConfig.js, extends webpackConfig.js and Procfile.dev to build/watch an RSC bundle, and adds a custom RspackRscPlugin to emit RSC manifests under Rspack, plus resolver aliasing/fallbacks to keep client builds from pulling Node-only deps.

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

Summary by CodeRabbit

  • New Features

    • Added an "RSC Demo" page reachable from the navigation that showcases React Server Components: server-rendered server info, progressively streamed comments with markdown preview, and client-interactive toggle panels.
    • New dev-facing endpoint to serve RSC payloads for the demo.
  • Chores

    • Switched React-on-Rails integration to the Pro/RSC packages and updated JS deps.
    • Build system and dev Procfile updated to support separate RSC bundling and watch mode.

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

Walkthrough

Adds React Server Components (RSC) support: swaps to react_on_rails_pro/RSC packages, introduces RSC webpack configs and plugin, registers server/client RSC bundles, adds a demo server component page with client hydration, and updates Rails routes and initializer for RSC payload handling.

Changes

Cohort / File(s) Summary
Dependency & Dev Proc
Gemfile, package.json, Procfile.dev
Replaced react_on_rails with react_on_rails_pro (gem) and added react-on-rails-pro/react-on-rails-rsc (npm). Pinned react/react-dom to 19.0.4. Added wp-rsc: RSC_BUNDLE_ONLY=true bin/shakapacker --watch to Procfile.dev.
Rails Config & Routes
config/initializers/react_on_rails_pro.rb, config/routes.rb, app/controllers/pages_controller.rb
Added ReactOnRailsPro initializer enabling RSC and setting rsc_bundle_js_file. Added rsc_payload_route and GET /server-componentspages#server_components. Added server_components action to controller.
Rails View
app/views/pages/server_components.html.erb
New view mounts ServerComponentsPage and appends rsc-client-components JS pack; prerender disabled, tracing enabled, explicit id provided.
Webpack: RSC Build & Plugin
config/webpack/rscWebpackConfig.js, config/webpack/rspackRscPlugin.js, config/webpack/webpackConfig.js
Added dedicated RSC webpack config (single-chunk, react-server condition, CSS stripping, loader injection) and RspackRscPlugin to generate RSC manifests by scanning for 'use client'. Main webpack flow extended to support RSC-only builds via RSC_BUNDLE_ONLY.
Webpack: Client/Server Configs
config/webpack/clientWebpackConfig.js, config/webpack/commonWebpackConfig.js, config/webpack/serverWebpackConfig.js
Mapped react-on-rails imports to react-on-rails-pro via resolve.alias. Integrated RspackRscPlugin({ isServer: true }) into server config. Added Node fallback exclusions and exported new extractLoader helper from server config.
Client Entrypoints & Packs
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 registering ServerComponentsPage and client pack registering TogglePanel. Stimulus and stores packs updated for client runtime and RSC client registration.
Client Components & Startup
client/app/bundles/.../*.{jsx,js}, client/app/packs/*
Marked multiple modules with 'use client', updated imports from react-on-railsreact-on-rails-pro across client startup and component files, and updated store/registration usage accordingly.
Navigation & Paths
client/app/bundles/comments/constants/paths.js, client/app/bundles/comments/components/NavigationBar/NavigationBar.jsx, client/app/libs/requestsManager.js
Added SERVER_COMPONENTS_PATH constant and navigation link to RSC demo. Updated requestsManager and other imports to react-on-rails-pro.
New RSC Demo Components
client/app/bundles/server-components/ServerComponentsPage.jsx, client/app/bundles/server-components/components/ServerInfo.jsx, .../CommentsFeed.jsx, .../TogglePanel.jsx
Added ServerComponentsPage (server component) composing ServerInfo (server runtime details), CommentsFeed (async server fetch, markdown sanitization, list rendering), and TogglePanel (client interactive toggle). CommentsFeed delays ~800ms and sanitizes marked HTML with sanitize-html.

Sequence Diagram(s)

sequenceDiagram
    participant Browser
    participant Rails
    participant RSCServer as RSC Renderer (Node)
    participant ClientBundle as Client Loader

    Browser->>Rails: GET /server-components
    activate Rails
    Rails->>Rails: pages#server_components
    Rails->>RSCServer: Render ServerComponentsPage (RSC)
    deactivate Rails

    activate RSCServer
    RSCServer->>RSCServer: Render ServerInfo (sync)
    RSCServer->>RSCServer: Await CommentsFeed (fetch, parse)
    RSCServer->>RSCServer: Mark TogglePanel as client component
    RSCServer->>Browser: Stream RSC payload (serialized)
    deactivate RSCServer

    Browser->>ClientBundle: Fetch client manifest & chunks
    Browser->>ClientBundle: Load TogglePanel chunk
    ClientBundle->>Browser: Hydrate TogglePanel (interactive)
    Browser->>Browser: Display fully interactive page
Loading
sequenceDiagram
    participant Build as Bundler
    participant RscConfig as rscWebpackConfig
    participant Common as commonWebpackConfig
    participant Plugin as RspackRscPlugin
    participant Output as Build Output

    Build->>RscConfig: Load RSC config (RSC_BUNDLE_ONLY)
    RscConfig->>Common: Derive base config & adjust rules
    RscConfig->>RscConfig: Remove CSS emitters, set react-server condition
    RscConfig->>RscConfig: Inject react-on-rails-rsc loader
    Build->>Plugin: Run RspackRscPlugin during compilation
    Plugin->>Plugin: Scan modules for 'use client' directive
    Plugin->>Output: Emit react-client-manifest.json (and SSR manifest)
    Output->>Build: RSC bundle and manifests produced
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Poem

🐰 Hopping bytes and server streams,

Server renders, client redeems,
"use client" scrawled to split the seam,
Manifests hum and webpack dreams,
A rabbit cheers for RSC beams.

🚥 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 pull request title accurately summarizes the main change: adding React Server Components support via the React on Rails Pro integration.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 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.

await new Promise((resolve) => setTimeout(resolve, 800));

// Fetch comments directly from the Rails API — no client-side fetch needed
const response = await fetch('http://localhost:3000/comments.json');
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Critical: hardcoded localhost URL will break in all non-local environments.

This URL is baked into the server component bundle and will fail in staging, Docker, CI, and production. The server-side fetch should use a relative path (if the RSC renderer runs inside the same Rails process) or read the host from an environment variable:

Suggested change
const response = await fetch('http://localhost:3000/comments.json');
const baseUrl = process.env.RAILS_INTERNAL_URL || 'http://localhost:3000';
const response = await fetch(`${baseUrl}/comments.json`);

Or, if the RSC renderer is co-located with Rails, consider calling the model/service directly instead of going through HTTP.

Comment on lines +19 to +20
const response = await fetch('http://localhost:3000/comments.json');
const comments = await response.json();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Missing error handling — if the Rails server is unreachable or returns a non-OK status, this will throw an unhandled error that crashes the RSC render stream. Wrap in try/catch and return a meaningful error UI:

Suggested change
const response = await fetch('http://localhost:3000/comments.json');
const comments = await response.json();
let comments = [];
try {
const baseUrl = process.env.RAILS_INTERNAL_URL || 'http://localhost:3000';
const response = await fetch(`${baseUrl}/comments.json`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
comments = await response.json();
} catch (err) {
return (
<div className="bg-red-50 border border-red-200 rounded-lg p-6 text-center">
<p className="text-red-700">Could not load comments: {err.message}</p>
</div>
);
}


async function CommentsFeed() {
// Simulate network latency to demonstrate streaming
await new Promise((resolve) => setTimeout(resolve, 800));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Debugging artifact — artificial delay should be removed before merge.

This 800 ms setTimeout was added to simulate streaming but it runs in every environment, including production. It adds latency for all users.

Suggested change
await new Promise((resolve) => setTimeout(resolve, 800));

(Delete this line entirely.)

<%= append_javascript_pack_tag('rsc-client-components') %>
<%= react_component("ServerComponentsPage",
prerender: false,
trace: true,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

trace: true emits internal rendering diagnostics visible in the HTML response. This should be gated to development only:

Suggested change
trace: true,
trace: Rails.env.development?,

const { sources } = require('@rspack/core');

// Cache for file 'use client' checks
const useClientCache = new Map();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale cache bug in watch mode. This module-level Map is allocated once when the plugin module is first require()d and is never cleared between incremental builds. If a developer adds or removes a 'use client' directive from a file while the watcher is running, the cached result will shadow the change until the process is restarted.

Clear the cache on each compilation start so watch mode stays correct:

Suggested change
const useClientCache = new Map();
const useClientCache = new Map();

And inside apply, before iterating chunks:

compiler.hooks.thisCompilation.tap('RspackRscPlugin', (compilation) => {
  // Clear per-build so watch-mode changes to 'use client' are picked up
  useClientCache.clear();
  // ... rest of hook

@@ -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.

Semantically contradictory directive. This file is the server-side SSR variant of RouterApp (note .server.jsx) and is imported by the server bundle for ExecJS rendering — it should never run on the client. Adding 'use client' here tells the RSC bundler to treat it as a client component boundary, which conflicts with its purpose.

If the intent is only to satisfy the RSC bundler scanning all files, consider whether this file actually needs the directive at all, or whether it should remain server-only and be excluded from RSC manifest scanning.

Comment on lines +45 to +47
const safeHtml = sanitizeHtml(rawHtml, {
allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img']),
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Allowing <img> without restricting the src attribute means user-supplied comment text can render <img src="http://internal-service">, which probes internal network hosts from the server side during RSC rendering (SSRF). Since comment text is user-controlled, consider either dropping img from the allowlist or adding an allowedAttributes restriction:

Suggested change
const safeHtml = sanitizeHtml(rawHtml, {
allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img']),
});
const safeHtml = sanitizeHtml(rawHtml, {
allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img']),
allowedAttributes: {
...sanitizeHtml.defaults.allowedAttributes,
img: ['alt'], // omit src to prevent SSRF from server-side rendering
},
});

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".

Comment on lines +172 to +173
// RSC: Generate react-server-client-manifest.json for SSR component resolution
serverWebpackConfig.plugins.push(new RspackRscPlugin({ isServer: true }));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Emit server client manifest from only one bundle

This adds RspackRscPlugin({ isServer: true }) to the SSR server config even though the RSC config also emits the same react-server-client-manifest.json into ssr-generated, and the default build runs both compilers together (config/webpack/webpackConfig.js). Because the two compilers produce different module sets (the SSR bundle does not include TogglePanel), whichever compiler writes last wins, so RSC requests can intermittently fail to resolve client components after a full build/watch cycle.

Useful? React with 👍 / 👎.

await new Promise((resolve) => setTimeout(resolve, 800));

// Fetch comments directly from the Rails API — no client-side fetch needed
const response = await fetch('http://localhost:3000/comments.json');
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Remove hardcoded localhost URL in server component fetch

The comments feed fetches from http://localhost:3000/comments.json, which only works in a local dev setup on that exact port. In production, CI, or any non-default port/container deployment, this will fail with connection errors and break the streamed comments section. The server component should derive the base URL from runtime request/environment config instead of hardcoding localhost.

Useful? React with 👍 / 👎.

@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

await new Promise((resolve) => setTimeout(resolve, 800));

// Fetch comments directly from the Rails API — no client-side fetch needed
const response = await fetch('http://localhost:3000/comments.json');
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Hardcoded localhost URL breaks non-local environments

fetch('http://localhost:3000/comments.json') will fail in any environment other than a local dev machine on port 3000 — staging, CI, Docker, and production will all break. The base URL should come from an environment variable or Rails configuration.

Suggested change
const response = await fetch('http://localhost:3000/comments.json');
const response = await fetch(`${process.env.RAILS_BASE_URL || 'http://localhost:3000'}/comments.json`);

Comment on lines +19 to +20
const response = await fetch('http://localhost:3000/comments.json');
const comments = await response.json();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 No error handling on fetch — non-2xx responses silently corrupt rendering

If the Rails API returns a non-200 status (e.g. 500, 503), response.json() is still called and may return an error body object rather than an array, causing _.orderBy to receive invalid input and the component to throw inside the Suspense boundary with no user-visible message.

Suggested change
const response = await fetch('http://localhost:3000/comments.json');
const comments = await response.json();
const response = await fetch('http://localhost:3000/comments.json');
if (!response.ok) {
throw new Error(`Failed to fetch comments: ${response.status} ${response.statusText}`);
}
const comments = await response.json();

const { sources } = require('@rspack/core');

// Cache for file 'use client' checks
const useClientCache = new Map();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Module-level cache never invalidated between incremental watch builds

useClientCache is a module-level Map that lives for the full lifetime of the Rspack process. In watch/HMR mode, if a developer adds or removes a 'use client' directive, the stale cached value will be used for all subsequent incremental builds and the manifest won't update until the watcher is restarted. Consider hooking into compilation.fileDependencies to invalidate affected entries, or at minimum document this limitation.

Comment on lines +45 to +47
const safeHtml = sanitizeHtml(rawHtml, {
allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img']),
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 img allowed in sanitized HTML without restricting src

Adding img to allowedTags without also restricting the src attribute lets any commenter embed arbitrary external image URLs (tracking pixels, SSRF probes, mixed-content issues). Provide allowedAttributes to constrain which attributes img may carry.

Suggested change
const safeHtml = sanitizeHtml(rawHtml, {
allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img']),
});
const safeHtml = sanitizeHtml(rawHtml, {
allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img']),
allowedAttributes: {
...sanitizeHtml.defaults.allowedAttributes,
img: ['src', 'alt', 'title', 'width', 'height'],
},
allowedSchemes: ['https', 'http'],
});

await new Promise((resolve) => setTimeout(resolve, 800));

// Fetch comments directly from the Rails API — no client-side fetch needed
const response = await fetch('http://localhost:3000/comments.json');
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hardcoded localhost URL breaks non-local deployments

High Severity

The CommentsFeed server component fetches from a hardcoded http://localhost:3000/comments.json URL. This project is live at www.reactrails.com per the README, so this fetch will fail in any deployed environment where the server isn't reachable at localhost:3000. The RSC server component would get a connection error or wrong response, causing the comments section to break entirely.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4d09e13. Configure here.

const { sources } = require('@rspack/core');

// Cache for file 'use client' checks
const useClientCache = new Map();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Module-level cache causes stale results in watch mode

Medium Severity

The useClientCache Map is module-scoped and never cleared between compilations. In watch mode (used by wp-client, wp-server, and wp-rsc in Procfile.dev), adding or removing a 'use client' directive from a file won't be reflected in the RSC manifest because the stale cached value persists. This would cause confusing development issues where client/server component boundaries don't update until the watcher is restarted.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4d09e13. Configure here.

- 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>
await new Promise((resolve) => setTimeout(resolve, 800));

// Fetch comments directly from the Rails API — no client-side fetch needed
const response = await fetch('http://localhost:3000/comments.json');
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: hardcoded localhost URL will break in any non-local environment.

The URL is baked in as http://localhost:3000, so this component will fail on staging, production, CI, or any Docker/container setup where Rails isn't running on that exact host/port. The RSC bundle runs in the Node renderer process on the same server as Rails, so an internal/relative call is appropriate here.

Suggested change
const response = await fetch('http://localhost:3000/comments.json');
const baseUrl = process.env.RAILS_INTERNAL_URL || 'http://localhost:3000';
const response = await fetch(`${baseUrl}/comments.json`);

Or, if react_on_rails_pro exposes a Rails API helper, use that directly to avoid the network hop altogether.

marked.use(gfmHeadingId());

async function CommentsFeed() {
// Simulate network latency to demonstrate streaming
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Demo-only delay left in the component — should not ship as-is.

An 800 ms forced delay every time this component renders is fine for a live demo but would noticeably degrade real-world page load. Consider removing it before merging, or guarding it behind an env flag:

Suggested change
// Simulate network latency to demonstrate streaming
// Simulate network latency to demonstrate streaming (remove in production)
if (process.env.NODE_ENV === 'development' && process.env.RSC_SIMULATE_LATENCY) {
// eslint-disable-next-line no-promise-executor-return
await new Promise((resolve) => setTimeout(resolve, 800));
}

@@ -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.

'use client' on a server-only file is semantically wrong and may misclassify this module.

RouterApp.server.jsx is the SSR rendering entry; it runs in the Node renderer (ExecJS / server bundle), not in the React Server Components runtime. Adding 'use client' causes the RSC WebpackLoader and RspackRscPlugin to treat it as a client boundary, which is the opposite of its intent.

This also contradicts the .server.jsx naming convention. If the goal was just to satisfy a linter that flags missing directives on RSC-adjacent files, this file should be left without any directive — or annotated with a comment explaining it is a legacy SSR file unrelated to RSC.

@@ -1,5 +1,7 @@
'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.

'use client' on the server bundle registration file is a semantic mismatch.

serverRegistration.jsx is the entry point for the server bundle — its whole job is to register components for server-side rendering. Marking it 'use client' tells the RSC WebpackLoader that every module reachable from this file is a client boundary, which is incorrect and could cause the RSC manifest to over-include modules that should stay server-only.

Remove this directive; it should not be present here.

const { sources } = require('@rspack/core');

// Cache for file 'use client' checks
const useClientCache = new Map();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Module-level cache is never invalidated — stale results during --watch mode.

useClientCache persists across all compilations within the same process lifetime. If a developer adds or removes a 'use client' directive from a file during a watch session, the cached result will never be refreshed and the manifest will silently reflect the old state until the process is restarted.

Hook into the compiler to clear the cache entry when a file changes:

compiler.hooks.watchRun.tap('RspackRscPlugin', (compiler) => {
  const changedFiles = compiler.modifiedFiles || new Set();
  for (const filePath of changedFiles) {
    useClientCache.delete(filePath);
  }
});

Place this inside apply() alongside the thisCompilation hook.

const { getBundler } = require('./bundlerUtils');
const { RspackRscPlugin } = require('./rspackRscPlugin');

function extractLoader(rule, 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.

extractLoader is duplicated — it already exists (and is exported) in serverWebpackConfig.js.

serverWebpackConfig.js exports this exact same function:

module.exports = { default: configureServer, extractLoader };

Import it instead of redefining it:

Suggested change
function extractLoader(rule, loaderName) {
const { extractLoader } = require('./serverWebpackConfig');

Then remove lines 16–22 here.

- 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>
Comment on lines +88 to +90
// Emit SSR manifest (maps module IDs to SSR module data)
if (!this.isServer) {
compilation.emitAsset(
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SSR manifest is always emitted as an empty object — is this intentional?

The react-ssr-manifest.json is supposed to map module IDs to their SSR bundle equivalents so the React flight client can resolve client components during hydration. Emitting {} is a valid placeholder if react-on-rails-pro doesn't read this file at runtime, but if it does rely on it the empty manifest will silently break client component hydration without a clear error.

Please add a comment clarifying whether this is a known limitation/TODO, or whether react-on-rails-pro derives this information from the client manifest alone.

// These libraries (combined ~200KB) never reach the client.
const rawHtml = marked.parse(comment.text || '');
const safeHtml = sanitizeHtml(rawHtml, {
allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img']),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

img added to allowedTags without restricting attributes — allows external image loads.

sanitize-html strips event handler attributes (onerror, etc.) by default, so this isn't a direct XSS vector. However, allowing unrestricted img tags lets comment authors embed arbitrary external images, which can be used for IP/user-agent tracking pixels, or to trigger unintended outbound requests from the server when the RSC renderer fetches the HTML.

Consider restricting img to a allowlist of safe origins, or at minimum also setting allowedAttributes to block src on img so only data URIs or relative paths are permitted:

const safeHtml = sanitizeHtml(rawHtml, {
  allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img']),
  allowedAttributes: {
    ...sanitizeHtml.defaults.allowedAttributes,
    img: ['alt', 'title'], // omit src to block external images
  },
});

@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

@@ -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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "1) .server files that declare 'use client' (expected: no matches):"
rg -n --iglob '*.server.js' --iglob '*.server.jsx' --iglob '*.server.ts' --iglob '*.server.tsx' "^\s*['\"]use client['\"]\s*;?"

echo
echo "2) Server-only imports in .server files (context check):"
rg -n --iglob '*.server.js' --iglob '*.server.jsx' --iglob '*.server.ts' --iglob '*.server.tsx' "react-router-dom/server|node:|\\bos\\b"

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 444


Remove 'use client' from this server entry file.

The 'use client' directive on line 1 causes this module to be treated as a client component during RSC processing, but the file imports server-only APIs like react-router-dom/server (line 6), creating an incompatibility.

Proposed fix
-'use client';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
'use client';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@client/app/bundles/comments/startup/RouterApp/ror_components/RouterApp.server.jsx`
at 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.


async function CommentsFeed() {
// Simulate network latency to demonstrate streaming
await new Promise((resolve) => setTimeout(resolve, 800));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix lint failure in Promise executor callback.

Line 16 triggers no-promise-executor-return; wrap setTimeout in braces.

Proposed fix
-  await new Promise((resolve) => setTimeout(resolve, 800));
+  await new Promise((resolve) => {
+    setTimeout(resolve, 800);
+  });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await new Promise((resolve) => setTimeout(resolve, 800));
await new Promise((resolve) => {
setTimeout(resolve, 800);
});
🧰 Tools
🪛 ESLint

[error] 16-16: Return values from promise executor functions cannot be read.

(no-promise-executor-return)

🪛 GitHub Check: test (22.x, 3.4.6)

[failure] 16-16:
Return values from promise executor functions cannot be read

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

In `@client/app/bundles/server-components/components/CommentsFeed.jsx` at 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.

Comment on lines +19 to +21
const response = await fetch('http://localhost:3000/comments.json');
const comments = await response.json();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid hardcoded localhost and handle non-2xx responses.

Line 19 hardcodes a dev-only origin, and Line 20 parses JSON without checking response.ok. This will fail in non-local environments and produce brittle runtime errors.

Proposed fix
-  const response = await fetch('http://localhost:3000/comments.json');
-  const comments = await response.json();
+  const commentsUrl = process.env.RSC_COMMENTS_URL;
+  if (!commentsUrl) {
+    throw new Error('RSC_COMMENTS_URL is required for server component fetches.');
+  }
+  const response = await fetch(commentsUrl);
+  if (!response.ok) {
+    throw new Error(`Failed to fetch comments: ${response.status} ${response.statusText}`);
+  }
+  const comments = await response.json();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const response = await fetch('http://localhost:3000/comments.json');
const comments = await response.json();
const commentsUrl = process.env.RSC_COMMENTS_URL;
if (!commentsUrl) {
throw new Error('RSC_COMMENTS_URL is required for server component fetches.');
}
const response = await fetch(commentsUrl);
if (!response.ok) {
throw new Error(`Failed to fetch comments: ${response.status} ${response.statusText}`);
}
const comments = await response.json();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/app/bundles/server-components/components/CommentsFeed.jsx` around
lines 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.

Comment on lines +7 to +8
import TogglePanel from '../bundles/server-components/components/TogglePanel';
import ReactOnRails from 'react-on-rails-pro';
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix import/order lint violation.

Line 8 package import should come before the relative import to satisfy ESLint ordering.

♻️ Proposed change
-import TogglePanel from '../bundles/server-components/components/TogglePanel';
 import ReactOnRails from 'react-on-rails-pro';
+import TogglePanel from '../bundles/server-components/components/TogglePanel';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import TogglePanel from '../bundles/server-components/components/TogglePanel';
import ReactOnRails from 'react-on-rails-pro';
import ReactOnRails from 'react-on-rails-pro';
import TogglePanel from '../bundles/server-components/components/TogglePanel';
🧰 Tools
🪛 ESLint

[error] 8-8: react-on-rails-pro import should occur before import of ../bundles/server-components/components/TogglePanel

(import/order)

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

In `@client/app/packs/rsc-client-components.js` around lines 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.

Comment on lines +65 to +66
} else if (rule.use && (rule.use.loader === 'url-loader' || rule.use.loader === 'file-loader')) {
rule.use.options.emitFile = false;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's read the rscWebpackConfig.js file around lines 65-66
cat -n config/webpack/rscWebpackConfig.js | sed -n '55,75p'

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 1136


🏁 Script executed:

# Find and examine the extractLoader function
fd -t f rscWebpackConfig.js -x cat -n {} | head -50

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 2381


🏁 Script executed:

# Check the commonWebpackConfig.js file around lines 64-67
cat -n config/webpack/commonWebpackConfig.js | sed -n '60,70p'

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 488


🏁 Script executed:

# Search for extractLoader function definition to understand loader detection
rg -n "extractLoader" config/webpack/ -A 10 -B 2

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 3038


🏁 Script executed:

# Search for how loaders are being matched/detected across webpack configs
rg -n "\.includes\(" config/webpack/ | grep -i loader

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 928


The file/url-loader guard is too strict to fire reliably.

extractLoader() here and other loader checks throughout the config use .includes() for loader matching, which indicates loader identifiers are resolved paths, not bare names. Using strict equality === 'url-loader' / === 'file-loader' here will fail to match, leaving emitFile = false unapplied so the RSC build continues emitting assets.

Suggested diff
-    } else if (rule.use && (rule.use.loader === 'url-loader' || rule.use.loader === 'file-loader')) {
-      rule.use.options.emitFile = false;
+    } else if (
+      rule.use?.loader &&
+      (rule.use.loader.includes('url-loader') || rule.use.loader.includes('file-loader'))
+    ) {
+      rule.use.options = { ...(rule.use.options || {}), emitFile: false };
     }
🧰 Tools
🪛 ESLint

[error] 66-66: Assignment to property of function parameter 'rule'.

(no-param-reassign)

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

In `@config/webpack/rscWebpackConfig.js` around lines 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.

Comment on lines +12 to +35
// Cache for file 'use client' checks
const useClientCache = new Map();

function hasUseClientDirective(filePath) {
if (useClientCache.has(filePath)) return useClientCache.get(filePath);

let result = false;
try {
// Read the first ~200 bytes — 'use client' must be at the very top of the file
const fd = fs.openSync(filePath, 'r');
const buf = Buffer.alloc(200);
fs.readSync(fd, buf, 0, 200, 0);
fs.closeSync(fd);

const head = buf.toString('utf-8');
// Check for 'use client' as the first statement (with or without semicolons/quotes)
result = /^(?:\s*\/\/[^\n]*\n)*\s*['"]use client['"]/.test(head);
} catch (_) {
// file doesn't exist or can't be read
}

useClientCache.set(filePath, result);
return result;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Cache may return stale results during watch/dev mode.

The useClientCache is a module-level Map that persists across rebuilds. If a developer adds or removes a 'use client' directive during development, the cached result won't reflect the change until the process restarts.

🔧 Suggested fix: Clear cache on new compilation

Add cache invalidation in the apply method:

   apply(compiler) {
+    // Clear cache at the start of each compilation to handle file changes in watch mode
+    compiler.hooks.thisCompilation.tap('RspackRscPlugin-ClearCache', () => {
+      useClientCache.clear();
+    });
+
     compiler.hooks.thisCompilation.tap('RspackRscPlugin', (compilation) => {

Alternatively, integrate the cache clearing into the existing hook:

     compiler.hooks.thisCompilation.tap('RspackRscPlugin', (compilation) => {
+      useClientCache.clear();
       compilation.hooks.processAssets.tap(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@config/webpack/rspackRscPlugin.js` around lines 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.

Comment on lines +23 to 33
} else if (process.env.RSC_BUNDLE_ONLY) {
const rscConfig = rscWebpackConfig();
// eslint-disable-next-line no-console
console.log('[React on Rails] Creating only the RSC bundle.');
result = rscConfig;
} else {
// default is the standard client and server build
const clientConfig = clientWebpackConfig();
const serverConfig = serverWebpackConfig();
const rscConfig = rscWebpackConfig();
if (envSpecific) envSpecific(clientConfig, serverConfig);
// eslint-disable-next-line no-console
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

envSpecific is skipped for RSC config paths.

Line 23-27 never calls envSpecific, and Line 32 does not expose rscConfig. This makes RSC bundles miss environment-specific adjustments.

Proposed fix (backward-compatible)
   } else if (process.env.RSC_BUNDLE_ONLY) {
     const rscConfig = rscWebpackConfig();
+    if (envSpecific) envSpecific(rscConfig, null, rscConfig);
     // eslint-disable-next-line no-console
     console.log('[React on Rails] Creating only the RSC bundle.');
     result = rscConfig;
   } else {
@@
-    if (envSpecific) envSpecific(clientConfig, serverConfig);
+    if (envSpecific) envSpecific(clientConfig, serverConfig, rscConfig);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} else if (process.env.RSC_BUNDLE_ONLY) {
const rscConfig = rscWebpackConfig();
// eslint-disable-next-line no-console
console.log('[React on Rails] Creating only the RSC bundle.');
result = rscConfig;
} else {
// default is the standard client and server build
const clientConfig = clientWebpackConfig();
const serverConfig = serverWebpackConfig();
const rscConfig = rscWebpackConfig();
if (envSpecific) envSpecific(clientConfig, serverConfig);
// eslint-disable-next-line no-console
} else if (process.env.RSC_BUNDLE_ONLY) {
const rscConfig = rscWebpackConfig();
if (envSpecific) envSpecific(rscConfig, null, rscConfig);
// eslint-disable-next-line no-console
console.log('[React on Rails] Creating only the RSC bundle.');
result = rscConfig;
} else {
const clientConfig = clientWebpackConfig();
const serverConfig = serverWebpackConfig();
const rscConfig = rscWebpackConfig();
if (envSpecific) envSpecific(clientConfig, serverConfig, rscConfig);
// eslint-disable-next-line no-console
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@config/webpack/webpackConfig.js` around lines 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).

await new Promise((resolve) => setTimeout(resolve, 800));

// Fetch comments directly from the Rails API — no client-side fetch needed
const response = await fetch('http://localhost:3000/comments.json');
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: hardcoded localhost URL breaks all non-local environments

This URL is hardcoded to localhost:3000, which will fail in staging, production, CI, or any environment where the Rails server is not on port 3000 of the same host.

In a server component, the URL should be constructed from an environment variable or the railsContext (which includes host/port). For example:

Suggested change
const response = await fetch('http://localhost:3000/comments.json');
const apiBase = process.env.RAILS_RELATIVE_URL_ROOT
? `http://localhost:${process.env.PORT || 3000}`
: `http://localhost:${process.env.PORT || 3000}`;
const response = await fetch(`${apiBase}/comments.json`);

Or more robustly, pass the base URL via an environment variable (API_BASE_URL) so it can be overridden per environment.


// Fetch comments directly from the Rails API — no client-side fetch needed
const response = await fetch('http://localhost:3000/comments.json');
const comments = await response.json();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: missing HTTP error handling

response.json() will throw a parse error on non-2xx responses (e.g. Rails returns an HTML 500 page). The component will crash without a user-visible error, bypassing the Suspense fallback.

Suggested change
const comments = await response.json();
if (!response.ok) {
throw new Error(`Failed to fetch comments: ${response.status} ${response.statusText}`);
}
const comments = await response.json();

async function CommentsFeed() {
// Simulate network latency to demonstrate streaming
// eslint-disable-next-line no-promise-executor-return
await new Promise((resolve) => setTimeout(resolve, 800));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Demo artifact: artificial 800ms delay should not ship

This delay was added to demonstrate Suspense streaming, which is a reasonable tutorial goal, but it will make every page load noticeably slower in any real use. If this demo is meant to be a tutorial reference that people copy from, an unconditional 800ms penalty is a bad pattern to model.

Consider making it conditional on a query param or dev-only env var, or at minimum include a prominent warning comment that this should be removed in real usage.

// These libraries (combined ~200KB) never reach the client.
const rawHtml = marked.parse(comment.text || '');
const safeHtml = sanitizeHtml(rawHtml, {
allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img']),
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: allowing img tags enables remote content injection

Adding img to allowedTags means comment authors can embed arbitrary remote images, which:

  1. Leaks the reader's IP address to any third-party host (pixel tracking)
  2. In combination with src attributes pointing to internal services, can be used as a probe for SSRF — the server fetches the RSC payload, not the browser, so an attacker-controlled <img src="http://169.254.169.254/..."> may trigger a server-side request in some RSC runtime configurations

Unless image embedding in comments is an intentional feature, remove img from the allowed list. If images are needed, allowlist only relative paths or a known CDN via allowedAttributes.

@@ -1,5 +1,7 @@
'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.

Semantically incorrect 'use client' on a server registration file

serverRegistration.jsx is the SSR entry point — it runs in the Node/ExecJS server renderer to register React components for server-side rendering. Adding 'use client' here tells the RSC bundler to treat this module as a client boundary and include it in the client manifest, which is the opposite of what a server registration file should be.

The same concern applies to RouterApp.server.jsx (also marked 'use client' in this PR) — a file explicitly named .server. should not carry a client directive.

These directives appear to have been added globally to all entry points, but the server registration files should be excluded. The RSC bundle's rsc-bundle.js already handles the correct server/client split.

const { sources } = require('@rspack/core');

// Cache for file 'use client' checks
const useClientCache = new Map();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: module-level cache causes stale results in watch mode

useClientCache is a module-level Map. In --watch mode, the plugin instance is reused across incremental builds, so the cache is never cleared. If a developer removes the 'use client' directive from a file, the cache will return the stale true result from the prior build, and the component will incorrectly remain in the client manifest until the build process is restarted.

Move the cache inside the apply method (per-build scope), or clear it in a compiler.hooks.watchRun or compiler.hooks.beforeCompile hook:

apply(compiler) {
  compiler.hooks.beforeCompile.tap('RspackRscPlugin', () => {
    useClientCache.clear();
  });
  // ...
}

const { getBundler } = require('./bundlerUtils');
const { RspackRscPlugin } = require('./rspackRscPlugin');

function extractLoader(rule, 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.

Duplication: extractLoader is copy-pasted from serverWebpackConfig.js

The identical function was added to serverWebpackConfig.js in this PR (with a JSDoc comment) and is now duplicated here without the doc. Extract it to a shared utility, e.g. config/webpack/loaderUtils.js, and import it in both configs.

File-level ESLint rules require block comment /* */ syntax, not
single-line //. Update RspackRscPlugin regex to also recognize
block comments before 'use client' directives.

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

claude bot commented Apr 4, 2026

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

Overall this is a solid RSC integration — the three-bundle pipeline, custom RspackRscPlugin, and donut-pattern demo are well-structured. A few issues need attention before merge.

Bugs / Security

1. Hardcoded localhost:3000 in CommentsFeed.jsx (line 20)
The fetch URL http://localhost:3000/comments.json is hardcoded and will fail in every non-local environment (CI, staging, production). The base URL needs to come from an environment variable or the Rails context.

2. Missing HTTP error check in CommentsFeed.jsx (line 21)
response.json() is called without checking response.ok. A non-2xx response (e.g., a 500 HTML error page) will throw a parse exception and crash the component tree without a meaningful error message.

3. img tags allowed in sanitized HTML (CommentsFeed.jsx line 47)
Allowing <img> in user-generated comment content lets any commenter embed arbitrary remote images, leaking reader IPs via pixel tracking and potentially triggering server-side requests in RSC rendering contexts. Remove img from the allowlist unless image embedding is an intentional feature.

4. useClientCache module-level cache goes stale in watch mode (rspackRscPlugin.js line 13)
The Map is never cleared between incremental builds. Removing a 'use client' directive from a file during watch mode won't take effect until the watcher is restarted. Add a compiler.hooks.beforeCompile tap to clear the cache.

Code Quality

5. 'use client' on server-side files (serverRegistration.jsx, RouterApp.server.jsx)
The 'use client' directive was added broadly across all entry points, but server registration / SSR files should not carry it. This signals to the RSC bundler that these modules are client boundaries, conflicting with their purpose.

6. extractLoader duplicated (rscWebpackConfig.js line 16)
Identical to the version added to serverWebpackConfig.js in this same PR. Extract to a shared loaderUtils.js.

7. Unconditional 800ms artificial delay (CommentsFeed.jsx line 17)
Useful for the demo, but it will slow every real page load. Add a warning comment that this should be removed in production usage, or guard it with an env var (process.env.NODE_ENV === 'development').

Minor / Informational

  • Breaking export change: serverWebpackConfig.js changed from module.exports = configureServer to module.exports = { default: configureServer, extractLoader }. The webpackConfig.js was updated to match, but any external consumer of this file importing it directly would silently get undefined instead of the config function.
  • Empty SSR manifest: react-ssr-manifest.json is emitted as {}. This is fine for the initial RSC demo, but worth documenting as a known limitation if the react-on-rails-pro runtime queries it.

// Fetch comments directly from the Rails API — no client-side fetch needed
const response = await fetch('http://localhost:3000/comments.json');
const comments = await response.json();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: hard-coded localhost URL breaks non-local environments.

This URL is baked in at build time, so the RSC demo will silently fail in staging, CI, or any Docker-based setup where Rails is not on localhost:3000.

Consider using a relative URL (Rails server-side fetch can resolve relative paths via the react_on_rails_pro request context), or at minimum an environment variable:

Suggested change
const baseUrl = process.env.RAILS_API_BASE_URL || 'http://localhost:3000';
const response = await fetch(`${baseUrl}/comments.json`);

If react-on-rails-rsc injects a request context you can derive the base URL from, that would be the cleanest approach.

const response = await fetch('http://localhost:3000/comments.json');
const comments = await response.json();

// Use lodash to process (stays on server)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Missing fetch error handling — if the Rails server is down or returns a non-2xx status, response.json() will throw (or return an error body). The unhandled rejection will crash the RSC render rather than showing a graceful fallback.

Suggested change
// Use lodash to process (stays on server)
if (!response.ok) {
throw new Error(`Failed to fetch comments: ${response.status} ${response.statusText}`);
}
const comments = await response.json();

Alternatively, wrap the entire fetch block in a try/catch and return a fallback UI.

Comment on lines +52 to +56
key={comment.id}
className="bg-white border border-slate-200 rounded-lg p-4 shadow-sm hover:shadow-md transition-shadow"
>
<div className="flex items-center justify-between mb-2">
<span className="font-semibold text-slate-800">{comment.author}</span>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

sanitizeHtml adds img tags with no src restriction.

sanitizeHtml.defaults.allowedTags + img uses the default allowed attributes for img, which includes an unrestricted src. This allows commenters to embed arbitrary external images — both a tracking-pixel vector and a potential SSRF indicator (the server fetches nothing here, but browsers will).

If the goal is just to render user markdown safely, dropping img from the allowed list is the safer default for a tutorial:

Suggested change
key={comment.id}
className="bg-white border border-slate-200 rounded-lg p-4 shadow-sm hover:shadow-md transition-shadow"
>
<div className="flex items-center justify-between mb-2">
<span className="font-semibold text-slate-800">{comment.author}</span>
const safeHtml = sanitizeHtml(rawHtml);

Or, if images are intentional, restrict src to safe origins:

allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img']),
allowedAttributes: {
  ...sanitizeHtml.defaults.allowedAttributes,
  img: [{ name: 'src', multiple: false, values: [] }], // or a specific allowlist
},

@@ -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.

'use client' on a .server.jsx file is semantically contradictory.

RouterApp.server.jsx is used for SSR (server-side rendering via ExecJS). Adding 'use client' here means the RSC bundler will treat it as a client boundary and refuse to import server-only modules from it. The intent of this directive was presumably to prevent the RSC webpack loader from incorrectly treating it as a server component — but the right fix is to ensure the RSC bundle's WebpackLoader excludes files not under the server-components tree, rather than adding 'use client' to existing SSR entry points.

The same concern applies to serverRegistration.jsx — a file named "server registration" that now carries 'use client'.

Comment on lines +17 to +25
if (!Array.isArray(rule.use)) return null;
return rule.use.find((item) => {
const testValue = typeof item === 'string' ? item : item?.loader;
return testValue && testValue.includes(loaderName);
});
}

const configureRsc = () => {
const bundler = getBundler();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

extractLoader is duplicated verbatim in serverWebpackConfig.js.

Both files define the same function (lines 1120–1130 in the diff for serverWebpackConfig.js). serverWebpackConfig.js already exports it — this file should import it instead:

Suggested change
if (!Array.isArray(rule.use)) return null;
return rule.use.find((item) => {
const testValue = typeof item === 'string' ? item : item?.loader;
return testValue && testValue.includes(loaderName);
});
}
const configureRsc = () => {
const bundler = getBundler();
const { extractLoader } = require('./serverWebpackConfig');

(The default export wrapping added to serverWebpackConfig.js was presumably done in anticipation of this, but the import never happened.)

const { sources } = require('@rspack/core');

// Cache for file 'use client' checks
const useClientCache = new Map();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Module-level useClientCache is never cleared between watch rebuilds.

The Map lives at module scope, so across incremental rebuilds in --watch mode a file that had its 'use client' directive added or removed after the first build will continue to use the cached (stale) result.

The simplest fix is to clear the cache at the start of each compilation:

apply(compiler) {
  compiler.hooks.thisCompilation.tap('RspackRscPlugin', (compilation) => {
    useClientCache.clear(); // invalidate per build
    // ...
  });
}

Alternatively, scope the cache to the RspackRscPlugin instance rather than the module.

@claude
Copy link
Copy Markdown

claude bot commented Apr 4, 2026

Review: Add React Server Components with React on Rails Pro

Solid work adding RSC support — the three-bundle pipeline, the custom RspackRscPlugin, and the overall architecture are well thought out. The demo page does a nice job illustrating the donut pattern and the streaming Suspense boundary. A few issues worth addressing before merge:

Bugs / Correctness

  • Hard-coded localhost:3000 in CommentsFeed.jsx (see inline comment) — this is the most impactful: the demo will fail silently in any non-local environment (staging, CI, Docker). The fetch URL needs to be environment-configurable or derived from the request context.
  • No response.ok check after fetch (see inline comment) — a non-2xx response causes response.json() to return an error body or throw rather than showing a graceful fallback.

Security

  • sanitizeHtml adds img with unrestricted src (see inline comment) — allows arbitrary external image embedding (tracking pixels; potentially unexpected for a tutorial's comment system). Omit img from allowedTags or constrain its src attribute.

Architecture / Maintainability

  • extractLoader is duplicated between rscWebpackConfig.js and serverWebpackConfig.js (see inline comment) — serverWebpackConfig.js already exports it; rscWebpackConfig.js should import it instead.
  • 'use client' on .server.jsx and serverRegistration.jsx (see inline comment) — adding the client boundary directive to files whose names and purpose are server-side is confusing and could cause subtle issues as the RSC tooling matures. The right fix is scoping the WebpackLoader to the server-components directory rather than annotating unrelated SSR entry points.
  • useClientCache never cleared between watch builds (see inline comment) — stale cache entries mean 'use client' additions/removals mid-watch session won't be picked up until the process restarts.

Minor

  • The 800ms artificial setTimeout in CommentsFeed.jsx is clearly intentional for the demo, but a comment noting it should be removed before any production use would help readers distinguish demo scaffolding from real patterns.
  • react_on_rails is effectively downgraded from 16.6.0.rc.016.5.1 (the react_on_rails_pro gem pins react_on_rails = 16.5.1). Worth confirming this is intentional and that no features from the RC are relied on elsewhere.

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: 2

♻️ Duplicate comments (1)
config/webpack/rspackRscPlugin.js (1)

13-16: ⚠️ Potential issue | 🟡 Minor

Clear useClientCache on each compilation to avoid stale manifest decisions.

useClientCache persists across rebuilds, so flipping 'use client' during watch mode can leave stale results until restart.

🔧 Suggested patch
   apply(compiler) {
     compiler.hooks.thisCompilation.tap('RspackRscPlugin', (compilation) => {
+      useClientCache.clear();
       compilation.hooks.processAssets.tap(

Also applies to: 50-52

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

In `@config/webpack/rspackRscPlugin.js` around lines 13 - 16, The Map
useClientCache is persisting across rebuilds causing stale results; modify the
plugin to clear useClientCache at the start of each compilation/rebuild (e.g.,
in the plugin hook that runs per compilation/watch run) so hasUseClientDirective
reads fresh data; locate the useClientCache and hasUseClientDirective symbols
and add a call to useClientCache.clear() in the per-compilation hook
(compiler/compilation/watchRun) so toggling 'use client' during watch mode
updates correctly.
🧹 Nitpick comments (1)
config/webpack/rspackRscPlugin.js (1)

104-106: Avoid blanket exclusion of node_modules in client-reference discovery.

This can drop valid third-party components that correctly declare 'use client', causing missing manifest entries when used from server components.

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

In `@config/webpack/rspackRscPlugin.js` around lines 104 - 106, The current
unconditional skip "if (resource.includes('node_modules')) return;" drops
third-party files that may legitimately be client components; change it to only
skip node_modules files that do NOT declare the client directive by reading the
file contents (using the existing resource path) and checking for a top-of-file
"use client" directive (e.g., a regex for /^(['"])use client\1/ at the start).
In other words, in the client-reference discovery logic replace the blanket
exclusion of resource.includes('node_modules') with a conditional that reads the
file and only returns when the file is in node_modules and does not contain a
"use client" directive (keep referencing the same resource variable used in this
snippet).
🤖 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/server-components/components/ServerInfo.jsx`:
- Around line 37-48: Prettier flagged formatting in the ServerInfo component:
the paragraph text wrapping around the em-dash and a JSX attribute line-break in
the mapped elements (the JSX that renders grouped.map and the inner div/span
attributes) are misformatted; run your formatter (npx prettier --write
client/app/bundles/server-components/components/ServerInfo.jsx) or reflow the
paragraph so the em-dash and inline <code> elements are on the same logical line
and ensure each JSX attribute stays on the same line for the small spans/divs
(keys: grouped, labels, the mapped arrow functions rendering group.map and the
inner divs/spans) to satisfy ESLint/Prettier rules.

In `@config/webpack/rspackRscPlugin.js`:
- Around line 19-25: The current try block opens a file descriptor (fd) then
calls fs.readSync and fs.closeSync, but if fs.readSync throws the fd is not
closed; refactor the logic in rspackRscPlugin.js to declare fd in an outer
scope, wrap the open/read in try and ensure fs.closeSync(fd) is invoked in a
finally block (only if fd is a valid descriptor), so the file descriptor is
always cleaned up even on errors when reading; update the block around fd,
fs.openSync, fs.readSync and fs.closeSync accordingly.

---

Duplicate comments:
In `@config/webpack/rspackRscPlugin.js`:
- Around line 13-16: The Map useClientCache is persisting across rebuilds
causing stale results; modify the plugin to clear useClientCache at the start of
each compilation/rebuild (e.g., in the plugin hook that runs per
compilation/watch run) so hasUseClientDirective reads fresh data; locate the
useClientCache and hasUseClientDirective symbols and add a call to
useClientCache.clear() in the per-compilation hook
(compiler/compilation/watchRun) so toggling 'use client' during watch mode
updates correctly.

---

Nitpick comments:
In `@config/webpack/rspackRscPlugin.js`:
- Around line 104-106: The current unconditional skip "if
(resource.includes('node_modules')) return;" drops third-party files that may
legitimately be client components; change it to only skip node_modules files
that do NOT declare the client directive by reading the file contents (using the
existing resource path) and checking for a top-of-file "use client" directive
(e.g., a regex for /^(['"])use client\1/ at the start). In other words, in the
client-reference discovery logic replace the blanket exclusion of
resource.includes('node_modules') with a conditional that reads the file and
only returns when the file is in node_modules and does not contain a "use
client" directive (keep referencing the same resource variable used in this
snippet).
🪄 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: 0d9fdfe1-ad04-455f-bcbc-3f1073f7bdd1

📥 Commits

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

📒 Files selected for processing (6)
  • client/app/bundles/comments/components/SimpleCommentScreen/ror_components/SimpleCommentScreen.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/packs/stimulus-bundle.js
  • config/webpack/rspackRscPlugin.js
✅ Files skipped from review due to trivial changes (1)
  • client/app/bundles/comments/components/SimpleCommentScreen/ror_components/SimpleCommentScreen.jsx

Comment on lines +37 to +48
<p className="text-xs text-emerald-600 mb-4 font-medium">
This data comes from the Node.js <code className="bg-emerald-100 px-1 rounded">os</code> module
— it runs only on the server. The <code className="bg-emerald-100 px-1 rounded">lodash</code> library
used to format it never reaches the browser.
</p>
<div className="grid md:grid-cols-2 gap-x-8 gap-y-1">
{grouped.map((group) => (
<div key={group.map(([k]) => k).join('-')} className="space-y-1">
{group.map(([key, value]) => (
<div key={key} className="flex justify-between py-1.5 border-b border-emerald-100 last:border-0">
<span className="text-sm text-emerald-700 font-medium">{labels[key] || key}</span>
<span className="text-sm text-emerald-900 font-mono">{value}</span>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix Prettier formatting violations.

ESLint/Prettier flagged formatting issues on lines 38-40 (text wrapping around em-dash) and line 46 (JSX attribute formatting). Run your formatter to resolve these before merging.

Suggested formatting fix
       <p className="text-xs text-emerald-600 mb-4 font-medium">
-        This data comes from the Node.js <code className="bg-emerald-100 px-1 rounded">os</code> module
-        — it runs only on the server. The <code className="bg-emerald-100 px-1 rounded">lodash</code> library
-        used to format it never reaches the browser.
+        This data comes from the Node.js{' '}
+        <code className="bg-emerald-100 px-1 rounded">os</code> module — it runs only on the server.
+        The <code className="bg-emerald-100 px-1 rounded">lodash</code> library used to format it
+        never reaches the browser.
       </p>
       <div className="grid md:grid-cols-2 gap-x-8 gap-y-1">
         {grouped.map((group) => (
           <div key={group.map(([k]) => k).join('-')} className="space-y-1">
             {group.map(([key, value]) => (
-              <div key={key} className="flex justify-between py-1.5 border-b border-emerald-100 last:border-0">
+              <div
+                key={key}
+                className="flex justify-between py-1.5 border-b border-emerald-100 last:border-0"
+              >
                 <span className="text-sm text-emerald-700 font-medium">{labels[key] || key}</span>

Alternatively, run npx prettier --write client/app/bundles/server-components/components/ServerInfo.jsx to auto-fix.

🧰 Tools
🪛 ESLint

[error] 38-38: Insert ·—·it

(prettier/prettier)


[error] 39-39: Replace ·—·it·runs·only·on·the·server.·The·<code·className="bg-emerald-100·px-1·rounded">lodash</code>·library with ·runs·only·on·the·server.·The·<code·className="bg-emerald-100·px-1·rounded">lodash</code>·library·used

(prettier/prettier)


[error] 40-40: Delete ·used

(prettier/prettier)


[error] 46-46: Replace ·key={key}·className="flex·justify-between·py-1.5·border-b·border-emerald-100·last:border-0" with ⏎················key={key}⏎················className="flex·justify-between·py-1.5·border-b·border-emerald-100·last:border-0"⏎··············

(prettier/prettier)

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

In `@client/app/bundles/server-components/components/ServerInfo.jsx` around lines
37 - 48, Prettier flagged formatting in the ServerInfo component: the paragraph
text wrapping around the em-dash and a JSX attribute line-break in the mapped
elements (the JSX that renders grouped.map and the inner div/span attributes)
are misformatted; run your formatter (npx prettier --write
client/app/bundles/server-components/components/ServerInfo.jsx) or reflow the
paragraph so the em-dash and inline <code> elements are on the same logical line
and ensure each JSX attribute stays on the same line for the small spans/divs
(keys: grouped, labels, the mapped arrow functions rendering group.map and the
inner divs/spans) to satisfy ESLint/Prettier rules.

Comment on lines +19 to +25
try {
// Read the first ~200 bytes — 'use client' must be at the very top of the file
const fd = fs.openSync(filePath, 'r');
const buf = Buffer.alloc(200);
fs.readSync(fd, buf, 0, 200, 0);
fs.closeSync(fd);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Guard file descriptor cleanup with finally to prevent FD leaks on read errors.

If fs.readSync throws after fs.openSync, fs.closeSync is skipped.

🛠️ Suggested patch
   try {
     // Read the first ~200 bytes — 'use client' must be at the very top of the file
-    const fd = fs.openSync(filePath, 'r');
-    const buf = Buffer.alloc(200);
-    fs.readSync(fd, buf, 0, 200, 0);
-    fs.closeSync(fd);
+    const fd = fs.openSync(filePath, 'r');
+    try {
+      const buf = Buffer.alloc(200);
+      fs.readSync(fd, buf, 0, 200, 0);
+
+      const head = buf.toString('utf-8');
+      // Check for 'use client' as the first statement.
+      // Allow comments (single-line // or block /* */) before the directive.
+      result = /^(?:\s*(?:\/\/[^\n]*\n|\/\*[\s\S]*?\*\/))*\s*['"]use client['"]/.test(head);
+    } finally {
+      fs.closeSync(fd);
+    }
-
-    const head = buf.toString('utf-8');
-    // Check for 'use client' as the first statement.
-    // Allow comments (single-line // or block /* */) before the directive.
-    result = /^(?:\s*(?:\/\/[^\n]*\n|\/\*[\s\S]*?\*\/))*\s*['"]use client['"]/.test(head);
   } catch (_) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@config/webpack/rspackRscPlugin.js` around lines 19 - 25, The current try
block opens a file descriptor (fd) then calls fs.readSync and fs.closeSync, but
if fs.readSync throws the fd is not closed; refactor the logic in
rspackRscPlugin.js to declare fd in an outer scope, wrap the open/read in try
and ensure fs.closeSync(fd) is invoked in a finally block (only if fd is a valid
descriptor), so the file descriptor is always cleaned up even on errors when
reading; update the block around fd, fs.openSync, fs.readSync and fs.closeSync
accordingly.

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: ae420af2b8

ℹ️ 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".

Comment on lines +13 to +16
const useClientCache = new Map();

function hasUseClientDirective(filePath) {
if (useClientCache.has(filePath)) return useClientCache.get(filePath);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reset use-client cache on each compilation

The useClientCache map is process-global and never invalidated, so in watch mode the plugin can keep returning stale directive results for a file path after edits. If a developer adds or removes 'use client' and recompiles, the manifest can remain out of sync until the watcher is restarted, leading to missing or incorrect client component resolution. Clear this cache at the start of each compilation (or key by content/mtime) to keep manifests accurate.

Useful? React with 👍 / 👎.

Comment on lines +22 to +23
const buf = Buffer.alloc(200);
fs.readSync(fd, buf, 0, 200, 0);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Scan beyond fixed 200-byte window for use client

Directive detection reads only the first 200 bytes, so files with a longer leading comment/header before 'use client' are misclassified as server modules. In that case the module is omitted from the emitted RSC manifest and runtime resolution of that client component fails. The parser should scan the full prologue (or full file) rather than hard-capping at 200 bytes.

Useful? React with 👍 / 👎.

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 1 potential issue.

There are 3 total unresolved issues (including 2 from previous reviews).

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Server and RSC bundles overwrite same manifest file
    • Removed RspackRscPlugin({ isServer: true }) from the server webpack config so only the RSC compilation emits react-server-client-manifest.json and no cross-process overwrite occurs.

Create PR

Or push these changes by commenting:

@cursor push c304ccb508
Preview (c304ccb508)
diff --git a/config/webpack/serverWebpackConfig.js b/config/webpack/serverWebpackConfig.js
--- a/config/webpack/serverWebpackConfig.js
+++ b/config/webpack/serverWebpackConfig.js
@@ -5,7 +5,6 @@
 const { config } = require('shakapacker');
 const commonWebpackConfig = require('./commonWebpackConfig');
 const { getBundler } = require('./bundlerUtils');
-const { RspackRscPlugin } = require('./rspackRscPlugin');
 
 /**
  * Extract a specific loader from a webpack rule's use array.
@@ -169,9 +168,6 @@
   // If using the React on Rails Pro node server renderer, uncomment the next line
   // serverWebpackConfig.target = 'node'
 
-  // RSC: Generate react-server-client-manifest.json for SSR component resolution
-  serverWebpackConfig.plugins.push(new RspackRscPlugin({ isServer: true }));
-
   return serverWebpackConfig;
 };

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit ae420af. Configure here.

// serverWebpackConfig.target = 'node'

// RSC: Generate react-server-client-manifest.json for SSR component resolution
serverWebpackConfig.plugins.push(new RspackRscPlugin({ isServer: true }));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Server and RSC bundles overwrite same manifest file

High Severity

Both serverWebpackConfig.js and rscWebpackConfig.js add new RspackRscPlugin({ isServer: true }), which emits react-server-client-manifest.json to the same ssr-generated/ output directory. In watch mode, the wp-server and wp-rsc processes run independently and non-deterministically overwrite each other's manifest. The two manifests contain different module IDs for the same client components, so whichever write wins leaves the other bundle's references broken. The RSC renderer likely needs module IDs from the RSC compilation specifically, meaning a server-bundle rebuild silently corrupts the RSC manifest.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ae420af. Configure here.

@cursor
Copy link
Copy Markdown

cursor bot commented Apr 4, 2026

Bugbot Autofix prepared fixes for all 3 issues found in the latest run.

  • ✅ Fixed: Hardcoded localhost URL breaks non-local deployments
    • Replaced the hardcoded localhost fetch URL with an environment-aware resolver that supports RSC_COMMENTS_URL, PRODUCTION_HOST, and a local PORT fallback.
  • ✅ Fixed: Module-level cache causes stale results in watch mode
    • Cleared the module-level useClientCache at the start of each compilation so 'use client' directive changes are picked up in watch mode.
  • ✅ Fixed: Unused extractLoader export in server config is dead code
    • Removed the unused extractLoader from serverWebpackConfig.js and restored direct function exports/imports for the server webpack config.

Create PR

Or push these changes by commenting:

@cursor push 538b2a7fa2
Preview (538b2a7fa2)
diff --git a/client/app/bundles/server-components/components/CommentsFeed.jsx b/client/app/bundles/server-components/components/CommentsFeed.jsx
--- a/client/app/bundles/server-components/components/CommentsFeed.jsx
+++ b/client/app/bundles/server-components/components/CommentsFeed.jsx
@@ -11,12 +11,25 @@
 const marked = new Marked();
 marked.use(gfmHeadingId());
 
+function commentsApiUrl() {
+  if (process.env.RSC_COMMENTS_URL) return process.env.RSC_COMMENTS_URL;
+
+  if (process.env.PRODUCTION_HOST) {
+    const host = process.env.PRODUCTION_HOST.replace(/\/$/, '');
+    const baseUrl = host.match(/^https?:\/\//) ? host : `https://${host}`;
+    return `${baseUrl}/comments.json`;
+  }
+
+  const port = process.env.PORT || '3000';
+  return `http://127.0.0.1:${port}/comments.json`;
+}
+
 async function CommentsFeed() {
   // Simulate network latency to demonstrate streaming
   await new Promise((resolve) => setTimeout(resolve, 800));
 
   // Fetch comments directly from the Rails API — no client-side fetch needed
-  const response = await fetch('http://localhost:3000/comments.json');
+  const response = await fetch(commentsApiUrl());
   const comments = await response.json();
 
   // Use lodash to process (stays on server)

diff --git a/config/webpack/rspackRscPlugin.js b/config/webpack/rspackRscPlugin.js
--- a/config/webpack/rspackRscPlugin.js
+++ b/config/webpack/rspackRscPlugin.js
@@ -48,6 +48,7 @@
 
   apply(compiler) {
     compiler.hooks.thisCompilation.tap('RspackRscPlugin', (compilation) => {
+      useClientCache.clear();
       compilation.hooks.processAssets.tap(
         {
           name: 'RspackRscPlugin',

diff --git a/config/webpack/serverWebpackConfig.js b/config/webpack/serverWebpackConfig.js
--- a/config/webpack/serverWebpackConfig.js
+++ b/config/webpack/serverWebpackConfig.js
@@ -8,21 +8,6 @@
 const { RspackRscPlugin } = require('./rspackRscPlugin');
 
 /**
- * Extract a specific loader from a webpack rule's use array.
- *
- * @param {Object} rule - Webpack rule with a use array
- * @param {string} loaderName - Substring to match against loader names
- * @returns {Object|null} The matching loader entry, or null
- */
-function extractLoader(rule, loaderName) {
-  if (!Array.isArray(rule.use)) return null;
-  return rule.use.find((item) => {
-    const testValue = typeof item === 'string' ? item : item?.loader;
-    return testValue && testValue.includes(loaderName);
-  });
-}
-
-/**
  * Generates the server-side rendering (SSR) bundle configuration.
  *
  * This creates a separate bundle optimized for server-side rendering:
@@ -175,4 +160,4 @@
   return serverWebpackConfig;
 };
 
-module.exports = { default: configureServer, extractLoader };
+module.exports = configureServer;

diff --git a/config/webpack/webpackConfig.js b/config/webpack/webpackConfig.js
--- a/config/webpack/webpackConfig.js
+++ b/config/webpack/webpackConfig.js
@@ -2,7 +2,7 @@
 // https://github.com/shakacode/react_on_rails_tutorial_with_ssr_and_hmr_fast_refresh/blob/master/config/webpack/webpackConfig.js
 
 const clientWebpackConfig = require('./clientWebpackConfig');
-const { default: serverWebpackConfig } = require('./serverWebpackConfig');
+const serverWebpackConfig = require('./serverWebpackConfig');
 const rscWebpackConfig = require('./rscWebpackConfig');
 
 const webpackConfig = (envSpecific) => {

You can send follow-ups to the cloud agent here.

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