Skip to content

Fix SSR runtime failures for React Server Components#723

Open
justin808 wants to merge 15 commits intomasterfrom
jg/fix-rsc-ssr-timeout
Open

Fix SSR runtime failures for React Server Components#723
justin808 wants to merge 15 commits intomasterfrom
jg/fix-rsc-ssr-timeout

Conversation

@justin808
Copy link
Copy Markdown
Member

@justin808 justin808 commented Apr 6, 2026

Summary

Fixes the 37/38 rspec test failures on the RSC branch caused by SSR runtime errors after switching from ExecJS to the react-on-rails-pro NodeRenderer.

  • CI: start Node renderer service — The RSC branch requires the NodeRenderer at port 3800 for SSR, but CI never started it, causing Net::ReadTimeout on all prerendered pages. Added a startup step with TCP readiness check and RENDERER_PASSWORD env var.
  • Server bundle: use fallbacks instead of externals — The previous commit externalized Node builtins (path/fs/stream) as CommonJS require() calls, but the Node renderer's vm.createContext() sandbox has no require. Reverted to resolve.fallback: false which stubs these unused code paths at build time.
  • Server bundle: polyfill MessageChannelreact-dom/server.browser.js instantiates MessageChannel at module load. The Node renderer VM lacks this global (unlike Bun used by ExecJS on master). Added a BannerPlugin polyfill.
  • RouterApp.server.jsx: add 'use client' directive — The auto-bundling system misclassified this traditional SSR component (uses StaticRouter) as a React Server Component because it lacked 'use client'. Added the directive so it registers via ReactOnRails.register() instead of registerServerComponent().

All 38 rspec tests pass locally.

Test plan

  • bundle exec rspec — all 38 tests pass (0 failures)
  • bundle exec rubocop — no offenses
  • All 3 bundles (client, server, RSC) build successfully
  • CI passes on this branch

🤖 Generated with Claude Code


Note

Medium Risk
Medium risk due to new SSR/RSC runtime dependencies (Node renderer, bundle splitting, polyfills) and changes to the build pipeline that could affect rendering and CI stability.

Overview
Enables React on Rails Pro NodeRenderer and React Server Components. Adds react_on_rails_pro configuration (react_on_rails_pro.rb), a Node renderer launcher (react-on-rails-pro-node-renderer.js), and updates CI to start the renderer (with password) and wait for port readiness before running RSpec.

Introduces a third Shakapacker/Rspack bundle for RSC. Adds rscWebpackConfig.js, wires it into webpackConfig.js with RSC_BUNDLE_ONLY, and adds a Rspack-compatible RspackRscPlugin to emit React Flight manifests; also updates client/server bundler configs with fallbacks and a MessageChannel polyfill for the VM-based SSR environment.

Adds an RSC demo route and client/server registration changes. Creates /server-components page + navigation link, adds RSC entry packs (rsc-bundle.js, rsc-client-components.js), and updates multiple components/packs to use react-on-rails-pro and explicit 'use client' directives to avoid RSC misclassification.

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

Summary by CodeRabbit

  • New Features
    • RSC Demo page added (accessible from navigation) showing server environment, streamed comments with animated skeleton, and interactive toggle panels for selective hydration.
  • Updates
    • Improved server-rendering and React Server Components support for streaming, selective client hydration, and more responsive interactive experiences.

justin808 and others added 10 commits April 4, 2026 00:18
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>
- Move eslint-disable after 'use client' directive in SimpleCommentScreen
- Add no-promise-executor-return disable for setTimeout in CommentsFeed
- Replace array index key with semantic key in ServerInfo
- Add PropTypes to TogglePanel component
- Fix import ordering in stimulus-bundle.js

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

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
- Add react-on-rails-pro-node-renderer for SSR and RSC payload generation
- Configure Pro initializer with NodeRenderer, renderer_url, password
- Alias react-dom/server.browser to react-dom/server.node in server
  webpack config (React 19's browser build requires MessageChannel which
  isn't available in the Node renderer VM)
- Add auto_load_bundle: false to RSC view (server components use
  registerServerComponent, not ror_components auto-loading)
- Add node-renderer to Procfile.dev

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace hardcoded localhost URL with RAILS_INTERNAL_URL env variable
- Add response.ok check to prevent silent fetch failures
- Guard 800ms demo delay to non-production environments
- Restrict sanitize-html img tag to explicit allowed attributes/schemes
- Clear useClientCache on each compilation for correct watch mode
- Remove incorrect 'use client' from server-only files
- Fix import/order lint violation in rsc-client-components
- Gate trace option to development environment only
- Remove duplicate RspackRscPlugin from server config (RSC-only)
- Fix url-loader/file-loader guard to use .includes() matching
- Pass RSC config to envSpecific callback

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add .node-renderer-bundles/ to .gitignore (Node renderer cache)
- CommentsFeed: skip artificial delay in production, use configurable
  base URL, add error handling for fetch, tighten sanitize-html config
- RspackRscPlugin: clear cache on each compilation for watch mode
- View: use trace only in development
- rscWebpackConfig: safer file-loader option merge

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two issues:
1. RSC bundle (127 errors): Remove server-bundle-generated.js import from
   rsc-bundle.js. It includes RouterApp.server.jsx (traditional SSR component)
   that uses react-redux/react-router with client-only React APIs incompatible
   with the react-server condition. Client references are handled automatically
   by the RSC loader/plugin.

2. Server bundle (3 errors): Add Node.js builtin fallbacks (path, fs, stream)
   to server webpack config. react-on-rails-pro now includes RSC modules that
   import these builtins, but they aren't used in the traditional SSR path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The server bundle runs in Node.js, so use externals to resolve path, fs,
stream at runtime via require() instead of replacing them with empty modules.
This avoids potential runtime crashes when react-on-rails-pro RSC modules
are imported transitively.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ation

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

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

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

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

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

All 38 rspec tests now pass locally.

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

coderabbitai bot commented Apr 6, 2026

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds React Server Components support: RSC webpack config and Rspack plugin, a Node renderer with CI/start step, Rails initializer/routes/view/controller for an RSC demo, migration to react-on-rails-pro, new server/client RSC components, packs/webpack changes, and development tooling updates.

Changes

Cohort / File(s) Summary
CI & Dev tooling
\.github/workflows/rspec_test.yml, .gitignore, Procfile.dev
Adds RENDERER_PASSWORD env and CI step to start react-on-rails-pro-node-renderer.js and poll localhost:3800; ignores .node-renderer-bundles/; adds wp-rsc and node-renderer dev processes.
Gems & npm deps
Gemfile, package.json
Replaces react_on_railsreact_on_rails_pro in Gemfile; updates package.json (pin React, add react-on-rails-pro, react-on-rails-pro-node-renderer, react-on-rails-rsc, and node-renderer script).
Node renderer
react-on-rails-pro-node-renderer.js
New Node script to configure/start the Node renderer (bundle cache path, port, password, log level, workers, CI override).
Rails integration
config/initializers/react_on_rails_pro.rb, config/routes.rb, app/controllers/pages_controller.rb, app/views/pages/server_components.html.erb
New initializer enabling RSC support and conditional NodeRenderer config; adds rsc_payload_route, GET /server-components route, controller action and view that mount ServerComponentsPage.
RSC bundles & 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
Adds RSC bundle entry and client registration pack; registers client components for RSC; packs updated to use react-on-rails-pro.
New Server Components
client/app/bundles/server-components/ServerComponentsPage.jsx, .../components/ServerInfo.jsx, .../components/CommentsFeed.jsx, .../components/TogglePanel.jsx
Adds ServerComponentsPage plus ServerInfo (runtime info), CommentsFeed (async fetch, optional delay, markdown→sanitize→render), and TogglePanel (client interactive toggle with propTypes).
Client code & imports
client/app/bundles/.../*, client/app/bundles/comments/startup/*, client/app/bundles/comments/components/*, client/app/bundles/comments/rescript/...
Many modules now include 'use client'; multiple imports switched from react-on-railsreact-on-rails-pro; minor ESLint and JSX tweaks.
Navigation & constants
client/app/bundles/comments/components/NavigationBar/NavigationBar.jsx, client/app/bundles/comments/constants/paths.js
Adds SERVER_COMPONENTS_PATH and an "RSC Demo" nav link; active-link logic updated.
Requests & stores
client/app/libs/requestsManager.js, client/app/packs/stores-registration.js
Switched imports to react-on-rails-pro; request and store usage otherwise unchanged.
Client webpack & aliases
config/webpack/clientWebpackConfig.js, config/webpack/commonWebpackConfig.js
Client config adds RspackRscPlugin for client manifest, removes server/rsc entries from client entries, sets Node builtin fallbacks; common config aliases react-on-railsreact-on-rails-pro.
RSC webpack & plugin
config/webpack/rscWebpackConfig.js, config/webpack/rspackRscPlugin.js
New RSC webpack config producing rsc-bundle.js (single-chunk, no CSS emit, exportOnlyLocals, emitFile:false, react-server resolution); new RspackRscPlugin detects 'use client' and emits client/server manifest JSON.
Server webpack & orchestration
config/webpack/serverWebpackConfig.js, config/webpack/webpackConfig.js
Server config adds extractLoader helper, Node builtin fallbacks, BannerPlugin polyfill, and exports helper; top-level webpackConfig refactored to support RSC_BUNDLE_ONLY and include RSC config in outputs.
Docs & README
CLAUDE.md, README.md
Adds architecture docs for three-bundle system, NodeRenderer/CI notes, RSC guidance, updated README TOC and development process docs.
Miscellaneous
assorted client JSX/ESLint tweaks
Small JSX/ESLint updates and other minor changes to support RSC flow and package migration.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client Browser
    participant Rails as Rails Server
    participant NodeRend as Node Renderer (port 3800)
    participant API as Rails API (/comments.json)

    Client->>Rails: GET /server-components
    activate Rails
    Rails->>NodeRend: Request RSC render (auth + payload)
    activate NodeRend

    NodeRend->>API: Fetch /comments.json
    activate API
    API-->>NodeRend: Return comments JSON
    deactivate API

    NodeRend->>NodeRend: Render ServerInfo & CommentsFeed (RSC)
    NodeRend->>NodeRend: Serialize RSC stream
    NodeRend-->>Rails: Streamed RSC payload
    deactivate NodeRend

    Rails->>Client: HTML + RSC payload + client pack tags
    deactivate Rails

    Client->>Client: Load client packs, hydrate TogglePanel, render streamed content
Loading
sequenceDiagram
    participant Bundler as Webpack/Rspack
    participant RscPlugin as RspackRscPlugin
    participant FS as File System

    Bundler->>FS: Read entries (client, server, rsc)
    Bundler->>RscPlugin: Invoke during asset processing
    activate RscPlugin
    RscPlugin->>FS: Read module starts to detect "'use client'"
    FS-->>RscPlugin: File contents
    RscPlugin->>Bundler: Emit react-client-manifest.json / react-server-client-manifest.json
    deactivate RscPlugin
    Bundler->>FS: Output bundles/artifacts
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 I thumped my paws on files anew,

Bundles, renderers, RSC in view,
I nibble "use client" with a happy hop,
Streams and toggles—oh what a crop!
Hop, code, hop: the demo's ready to chop.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% 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 title 'Fix SSR runtime failures for React Server Components' directly describes the main purpose of the PR, which is fixing 37/38 rspec failures related to SSR runtime issues with React Server Components after switching to the NodeRenderer.
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/fix-rsc-ssr-timeout

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.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 6, 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.


@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Apr 6, 2026

Greptile Summary

This PR fixes 37/38 rspec failures on the RSC branch caused by SSR runtime errors after switching from ExecJS to the react-on-rails-pro NodeRenderer. It makes four targeted changes: starting the NodeRenderer in CI with a TCP readiness check, replacing Node-builtin externals with resolve.fallback: false in the server webpack bundle, polyfilling MessageChannel for the VM sandbox environment, and adding 'use client' to RouterApp.server.jsx so the auto-bundler no longer misclassifies it as a React Server Component.

Key changes:

  • .github/workflows/rspec_test.yml: Starts the Node renderer before rspec with a 30-second TCP readiness loop on port 3800; adds RENDERER_PASSWORD env var at job scope.
  • config/webpack/serverWebpackConfig.js: Adds resolve.fallback stubs for path, fs, fs/promises, and stream; injects a BannerPlugin MessageChannel polyfill so react-dom/server.browser can initialize inside the VM sandbox.
  • client/app/bundles/comments/startup/RouterApp/ror_components/RouterApp.server.jsx: Adds 'use client' so this traditional SSR component is registered via ReactOnRails.register() instead of registerServerComponent().
  • New RSC infrastructure: Adds rscWebpackConfig.js, rspackRscPlugin.js, rsc-bundle.js, rsc-client-components.js, and server component files (ServerComponentsPage, CommentsFeed, ServerInfo, TogglePanel).
  • A security concern exists in CommentsFeed.jsx where allowedSchemes globally includes 'data' in sanitize-html, which enables data: URI payloads on <a href> from user-generated comment content — data: is intentionally excluded from sanitize-html's own defaults for this reason.

Confidence Score: 3/5

Mostly safe to merge but contains a P1 security concern in CommentsFeed.jsx that should be fixed before the RSC demo page is publicly accessible

The CI infrastructure fix and four SSR fixes are well-reasoned and the 38/38 passing test result is reassuring. The incomplete MessageChannel polyfill is low risk since tests pass. However, the global data: URI scheme in sanitize-html's allowedSchemes creates an XSS vector in user-generated markdown content rendered by CommentsFeed — sanitize-html intentionally excludes data: from its defaults for this reason, and overriding that globally is a genuine security regression.

client/app/bundles/server-components/components/CommentsFeed.jsx (data: URI XSS risk), config/webpack/serverWebpackConfig.js (incomplete MessageChannel polyfill)

Important Files Changed

Filename Overview
client/app/bundles/server-components/components/CommentsFeed.jsx New async RSC component; allows data: URI globally in sanitize-html, enabling a potential XSS vector via on user-generated content
config/webpack/serverWebpackConfig.js Adds resolve.fallback stubs for Node builtins and a BannerPlugin MessageChannel polyfill; polyfill is incomplete (missing port1.postMessage, start/close)
.github/workflows/rspec_test.yml Adds RENDERER_PASSWORD env var and starts Node renderer with 30-second TCP readiness check before rspec; RENDERER_PASSWORD is plain text rather than a secret
client/app/bundles/comments/startup/RouterApp/ror_components/RouterApp.server.jsx Adds 'use client' directive so traditional SSR component is correctly registered via ReactOnRails.register() instead of registerServerComponent()
config/webpack/rscWebpackConfig.js New RSC bundle config: targets Node.js, enables react-server condition, adds RspackRscPlugin, removes CSS extraction
config/webpack/rspackRscPlugin.js Custom Rspack plugin that detects 'use client' directives and emits react-client-manifest.json for the React Flight protocol
react-on-rails-pro-node-renderer.js New launcher for the React on Rails Pro Node renderer service (port 3800, 3 workers, bundle cache in .node-renderer-bundles/)
config/initializers/react_on_rails_pro.rb Configures NodeRenderer at port 3800 with RSC support enabled and rsc-bundle.js as the RSC bundle file
client/app/packs/rsc-bundle.js RSC entry point that registers ServerComponentsPage; intentionally excludes server-bundle-generated.js to avoid react-server condition conflicts
Procfile.dev Adds wp-rsc watcher and node-renderer process; RENDERER_PASSWORD is hardcoded in plain text in a tracked file
client/app/bundles/server-components/ServerComponentsPage.jsx New RSC page demonstrating server-only data fetching (os module, fetch) with Suspense streaming and mixed server/client components
client/app/bundles/server-components/components/TogglePanel.jsx New 'use client' interactive toggle component used inside server components via the RSC donut pattern
Gemfile Switches from react_on_rails 16.6.0.rc.0 to react_on_rails_pro 16.5.1 (stable release with NodeRenderer support)

Sequence Diagram

sequenceDiagram
    participant CI as GitHub Actions CI
    participant NR as Node Renderer (port 3800)
    participant Rails as Rails App
    participant RSpec as RSpec Tests

    CI->>NR: node react-on-rails-pro-node-renderer.js &
    loop TCP readiness check (up to 30s)
        CI->>NR: nc -z localhost 3800
        NR-->>CI: connection accepted
    end
    CI->>RSpec: bundle exec rake ci:rspec

    RSpec->>Rails: GET /server-components (RSC page)
    Rails->>NR: HTTP POST /render (rsc-bundle.js)
    NR->>NR: VM runs RSC bundle
    NR-->>Rails: React Flight payload
    Rails-->>RSpec: Rendered HTML

    RSpec->>Rails: GET /comments (traditional SSR)
    Rails->>NR: HTTP POST /render (server-bundle.js)
    NR->>NR: VM runs server bundle
    Note over NR: MessageChannel polyfill active
    NR-->>Rails: Rendered HTML
    Rails-->>RSpec: HTML response
Loading

Reviews (1): Last reviewed commit: "Fix SSR runtime failures: Node renderer,..." | Re-trigger Greptile

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: 1022234fa8

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

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

🧹 Nitpick comments (9)
client/app/bundles/server-components/components/CommentsFeed.jsx (1)

59-59: Consider restricting data: scheme to specific MIME types.

The data scheme allows any content type. For tighter security, consider restricting to image MIME types only.

🛡️ Suggested improvement
-          allowedSchemes: ['https', 'http', 'data'],
+          allowedSchemes: ['https', 'http'],
+          allowedSchemesByTag: {
+            img: ['https', 'http', 'data'],
+          },

Alternatively, if data: URIs for images are not required, remove it entirely:

-          allowedSchemes: ['https', 'http', 'data'],
+          allowedSchemes: ['https', 'http'],
🤖 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 59,
The allowedSchemes configuration currently includes a blanket 'data' entry which
permits any data: MIME type; update the configuration in CommentsFeed.jsx where
allowedSchemes is defined to either remove 'data' entirely or restrict it to
image MIME types only (e.g., allow only data:image/* by replacing 'data' with a
constrained pattern or validated check such as permitting only data URLs that
match /^data:image\/(png|jpeg|gif|webp);base64,/), ensuring the
sanitizer/validator for allowedSchemes enforces that constraint.
app/views/pages/server_components.html.erb (1)

1-6: Consider removing the hardcoded DOM id.

The id: "ServerComponentsPage-react-component-0" appears to be hardcoded. React on Rails typically generates unique IDs automatically. Hardcoding this could cause issues if the component is rendered multiple times on a page or if the ID generation logic changes.

Unless there's a specific requirement for this exact ID (e.g., RSC payload targeting), consider omitting it to let the framework handle ID generation.

Suggested change
 <%= append_javascript_pack_tag('rsc-client-components') %>
 <%= react_component("ServerComponentsPage",
     prerender: false,
     auto_load_bundle: false,
-    trace: Rails.env.development?,
-    id: "ServerComponentsPage-react-component-0") %>
+    trace: Rails.env.development?) %>
🤖 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 1 - 6, The
react_component invocation for ServerComponentsPage includes a hardcoded id
("ServerComponentsPage-react-component-0") which can cause collisions; remove
the id option from the react_component call in the ServerComponentsPage render
so React on Rails can auto-generate unique IDs (or, if a stable ID is truly
required for RSC payload targeting, replace the hardcoded string with a
generated unique value/prop such as a SecureRandom.uuid or a passed-in
server-side identifier before rendering).
client/app/bundles/comments/startup/RouterApp/ror_components/RouterApp.server.jsx (1)

1-2: Intentional but confusing: 'use client' in a .server.jsx file.

Per the PR objectives, this directive is intentionally added to ensure the component is registered via ReactOnRails.register() (traditional SSR) rather than registerServerComponent() (RSC). This is necessary because the component uses client-only APIs like ReactOnRails.getStore().

However, the filename RouterApp.server.jsx with a 'use client' directive creates cognitive dissonance. Consider adding a comment explaining this apparent contradiction, or renaming the file to something like RouterApp.ssr.jsx to clarify it's for traditional SSR (not RSC server components).

Suggested clarifying comment
 'use client';
+// NOTE: Despite the .server.jsx filename (which indicates traditional SSR with StaticRouter),
+// this component needs 'use client' to prevent RSC bundler from treating it as a React Server
+// Component. It uses client APIs (ReactOnRails.getStore) and is for hydration-based SSR.

 // Compare to ./RouterApp.client.jsx
🤖 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`
around lines 1 - 2, The 'use client' directive in RouterApp.server.jsx is
intentionally used so the component registers with ReactOnRails via
ReactOnRails.register() and can call client-only APIs like
ReactOnRails.getStore(), but the .server.jsx filename is misleading; either add
a clear top-of-file comment explaining this contradiction (mentioning that 'use
client' is required to force traditional SSR registration with
ReactOnRails.register() and to allow ReactOnRails.getStore()), or rename the
file to RouterApp.ssr.jsx (and update any imports/registrations) to make its SSR
intent explicit.
.github/workflows/rspec_test.yml (1)

86-98: Add output logging to the Node renderer process for better debugging.

The Node renderer process started in background will persist across steps, but without capturing its output, it's difficult to diagnose failures if the renderer crashes between the health check and the rspec test execution. Consider using nohup with output redirection to preserve logs for troubleshooting:

Suggested improvement
       - name: Start Node renderer for SSR
         run: |
-          node react-on-rails-pro-node-renderer.js &
+          nohup node react-on-rails-pro-node-renderer.js > node-renderer.log 2>&1 &
           echo "Waiting for Node renderer on port 3800..."
           for i in $(seq 1 30); do
             if nc -z localhost 3800 2>/dev/null; then
               echo "Node renderer is ready"
               exit 0
             fi
             sleep 1
           done
           echo "Node renderer failed to start within 30 seconds"
+          echo "--- Node renderer logs ---"
+          cat node-renderer.log || true
           exit 1
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/rspec_test.yml around lines 86 - 98, The Node renderer
background start (react-on-rails-pro-node-renderer.js) in the "Start Node
renderer for SSR" step currently discards stdout/stderr; modify the step to run
the renderer with nohup (or equivalent) and redirect both stdout and stderr to a
log file (e.g., nohup node react-on-rails-pro-node-renderer.js >
node-renderer.log 2>&1 &) so the process output is preserved for debugging while
still performing the existing health check on port 3800; ensure the log file is
accessible in later steps (or optionally tail the log) so failures between the
readiness check and rspec run can be diagnosed.
client/app/packs/rsc-bundle.js (1)

6-7: This side-effect import does not register the stores in the RSC render path.

client/app/packs/stores-registration.js is itself a 'use client' module that only runs ReactOnRails.registerStore(...) at module evaluation time. Imported from this server entry, that side effect is not part of the server render path, so the comment here overstates what is actually available. If the RSC page needs those stores, load them from a pack that is guaranteed to execute in the browser; otherwise remove this import/comment to avoid a misleading invariant.

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

In `@client/app/packs/rsc-bundle.js` around lines 6 - 7, The import of
'./stores-registration' and its comment are misleading because that module is a
'use client' file which runs ReactOnRails.registerStore(...) only in the
browser, so it is not executed in the RSC/server render path; either remove the
import and the comment from rsc-bundle.js to avoid the incorrect invariant, or
move the store registration into a client-only pack that is guaranteed to run in
the browser (e.g., create or update a browser entry that imports
'./stores-registration' and ensure that pack is included on client pages); refer
to the './stores-registration' module and the ReactOnRails.registerStore calls
when making the change.
config/webpack/rscWebpackConfig.js (2)

16-22: Unused helper function.

extractLoader is defined but only used within configureRsc. Consider inlining it or confirming it's needed. The function itself is clean and correct.

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

In `@config/webpack/rscWebpackConfig.js` around lines 16 - 22, The helper function
extractLoader is defined but only referenced inside configureRsc; remove the
standalone extractLoader declaration and inline its logic directly into
configureRsc (or, if you prefer to keep it, ensure it's actually reused
elsewhere) by replacing calls to extractLoader(...) with the equivalent
Array.isArray(rule.use) check and find logic that inspects item or item.loader
for loaderName; reference the extractLoader name and configureRsc to locate
where to perform the change.

47-71: Rule mutation is necessary but consider defensive coding.

The no-param-reassign lint errors are expected when modifying webpack rules. The mutations are intentional and safe since commonWebpackConfig() returns a fresh config each time. However, there's a potential edge case:

  • Lines 65-70: The else if only checks rule.use?.loader but some rules may have rule.loader directly without a use wrapper.
Consider handling direct `rule.loader` case
-    } 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 };
+    } 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 };
+    } else if (rule.loader && (rule.loader.includes('url-loader') || rule.loader.includes('file-loader'))) {
+      rule.options = { ...(rule.options || {}), emitFile: 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 47 - 71, The loop in
rscConfig.module.rules.forEach mutates rule.use options for url/file loaders but
misses rules that define loader directly on the rule (rule.loader), so update
the handler to also detect and handle a direct rule.loader case: when
rule.loader (not rule.use) is a string containing 'url-loader' or 'file-loader',
set/merge rule.options = { ...(rule.options || {}), emitFile: false } with
type-safe checks (ensure rule.loader is a string and rule.options exists or is
created) so direct-loader rules receive the same emitFile:false behavior as
use-wrapped loaders.
config/webpack/rspackRscPlugin.js (2)

30-32: Silent error suppression hides potential issues.

The empty catch block silently swallows all file read errors. Consider logging in debug mode or at least differentiating between "file not found" (expected for generated files) and other errors.

Consider logging unexpected errors
   } catch (e) {
-    // file doesn't exist or can't be read
+    // File doesn't exist or can't be read - expected for generated/virtual modules
+    if (e.code !== 'ENOENT' && e.code !== 'EACCES') {
+      // Unexpected error - log for debugging
+      console.warn(`RspackRscPlugin: Unexpected error reading ${filePath}:`, e.message);
+    }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@config/webpack/rspackRscPlugin.js` around lines 30 - 32, The empty catch in
rspackRscPlugin.js silently swallows all read errors; update the catch in the
file-read block (the try/catch surrounding the read operation in the Rsc plugin)
to detect and ignore only the "file not found" case (e.g., error.code ===
'ENOENT') while logging any other errors (use processLogger.debug or
console.error depending on available logger, and include the error object) so
unexpected IO issues are surfaced; ensure the catch references the thrown error
(e) rather than using an unused placeholder (_).

12-36: Module-level cache could cause issues with parallel compilations.

The useClientCache is shared across all plugin instances. If multiple compilations run in parallel (e.g., client and server builds), they share the same cache. While the cache is cleared at the start of each compilation (line 52-54), parallel compilations could interfere with each other.

Also, synchronous file I/O in hasUseClientDirective could slow builds with many files. Consider using async I/O or webpack's file system abstraction.

Consider moving cache to instance level
 class RspackRscPlugin {
   constructor(options) {
     if (!options || typeof options.isServer !== 'boolean') {
       throw new Error('RspackRscPlugin: isServer option (boolean) is required.');
     }
     this.isServer = options.isServer;
     this.clientManifestFilename = options.isServer
       ? 'react-server-client-manifest.json'
       : 'react-client-manifest.json';
     this.ssrManifestFilename = 'react-ssr-manifest.json';
+    this._useClientCache = new Map();
   }

Then update hasUseClientDirective to be an instance method using this._useClientCache.

🤖 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 - 36, The module-level Map
useClientCache and function hasUseClientDirective should be converted to
per-plugin-instance state and methods: add this._useClientCache (a Map) on the
plugin constructor and change hasUseClientDirective to an instance method (e.g.,
this.hasUseClientDirective) that reads/writes this._useClientCache instead of
the module-scope useClientCache; also replace the synchronous fs.* reads inside
hasUseClientDirective with asynchronous I/O or the compiler/filesystem
abstraction provided by the build (e.g., use compiler.inputFileSystem/async
reads) so the check is non-blocking and isolated per compilation, and remove any
module-level useClientCache references so parallel builds do not share state.
🤖 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/CommentsFeed.jsx`:
- Around line 28-32: The code assigns the full JSON body to comments but the
Rails API returns { comments: [...] }, so _.orderBy is being passed an object;
update the assignment after response.json() to extract the array (e.g., const {
comments: commentsArray } = await response.json() or const commentsArray =
(await response.json()).comments || []) and then feed commentsArray into
_.orderBy and _.take; adjust references from comments to commentsArray (or
rename consistently) so sortedComments = _.orderBy(commentsArray,
['created_at'], ['desc']) and recentComments = _.take(sortedComments, 10),
handling a missing comments key by defaulting to an empty array.

In `@client/app/bundles/server-components/components/ServerInfo.jsx`:
- Around line 38-40: The JSX text in the ServerInfo component contains Prettier
formatting inconsistencies; run Prettier (or your project's formatter) to
reformat ServerInfo.jsx so the JSX text nodes and inline <code> elements are
properly spaced and wrapped according to the project's rules. Locate the
ServerInfo React component and reformat the block containing the sentence about
the Node.js os module and lodash usage so that spacing around inline code
elements and line breaks match the project's Prettier config, then commit the
formatted file.

In `@client/app/bundles/server-components/ServerComponentsPage.jsx`:
- Around line 10-127: ESLint flags the component definition and Prettier reports
formatting errors; replace the arrow function component "const
ServerComponentsPage = () => { ... }" with a function declaration "function
ServerComponentsPage() { ... }", keep the same JSX and component usages
(ServerInfo, TogglePanel, CommentsFeed, Suspense), then run your project's
formatter/Prettier on this file to fix the remaining formatting violations and
ensure the file exports the component (export default ServerComponentsPage) if
applicable.

In `@client/app/packs/stimulus-bundle.js`:
- Around line 24-28: The registerServerComponent call is using the wrong
signature by passing an options object; update the invocation of
registerServerComponent to pass only variadic component name strings (e.g., call
registerServerComponent with 'ServerComponentsPage' as a string) and remove the
rscPayloadGenerationUrlPath options object since that parameter is not supported
and defaults to 'rsc_payload/' internally.

In `@config/initializers/react_on_rails_pro.rb`:
- Around line 3-7: Replace the ENV["REACT_RENDERER_URL"] ||
"http://localhost:3800" pattern with ENV.fetch("REACT_RENDERER_URL",
"http://localhost:3800") inside the ReactOnRailsPro.configure block so
config.renderer_url uses ENV.fetch like config.renderer_password; update the
config.renderer_url assignment to call ENV.fetch("REACT_RENDERER_URL",
"http://localhost:3800") to ensure empty strings fall back to the default URL.

In `@config/webpack/rspackRscPlugin.js`:
- Around line 94-100: The current emit block emits an empty SSR manifest; update
the logic around this.isServer / compilation.emitAsset so that
this.ssrManifestFilename (react-ssr-manifest.json) is written with the actual
module metadata object containing moduleMap, serverModuleMap, and moduleLoading
(populated from the plugin's collected module registry or the same source that
builds the client manifest) instead of {}. Locate where the plugin builds the
client manifest and mirror that collection step for server modules (or extend
that collection), then serialize the assembled metadata { moduleMap,
serverModuleMap, moduleLoading } into JSON and pass it to compilation.emitAsset
using new sources.RawSource(JSON.stringify(..., null, 2)). Ensure the names
this.ssrManifestFilename and any registry variables used for module ids are
reused so the client and SSR manifests stay consistent.

In `@config/webpack/webpackConfig.js`:
- Around line 30-36: The envSpecific callback is invoked with three configs
(clientConfig, serverConfig, rscConfig) but the environment-specific functions
only accept two params, so the RSC config is ignored; update the signatures in
the environment modules (e.g., change productionEnvOnly and testOnly to accept a
third parameter such as _rscWebpackConfig) so they match the call site
(envSpecific(clientWebpackConfig(), serverWebpackConfig(), rscWebpackConfig()))
and can modify the RSC config when needed.

In `@react-on-rails-pro-node-renderer.js`:
- Around line 4-10: The config object uses process.env values inconsistently:
port can remain a string while workersCount is coerced with Number(), which can
produce NaN; normalize both by parsing and validating their numeric environment
variables before assigning to config—convert process.env.RENDERER_PORT and
process.env.NODE_RENDERER_CONCURRENCY to integers (e.g., parseInt or Number with
fallback), ensure sensible defaults if parsing fails, and assign the
parsed/validated numbers to config.port and config.workersCount (refer to the
config object and the port and workersCount properties) so the configuration is
always numeric and predictable.

---

Nitpick comments:
In @.github/workflows/rspec_test.yml:
- Around line 86-98: The Node renderer background start
(react-on-rails-pro-node-renderer.js) in the "Start Node renderer for SSR" step
currently discards stdout/stderr; modify the step to run the renderer with nohup
(or equivalent) and redirect both stdout and stderr to a log file (e.g., nohup
node react-on-rails-pro-node-renderer.js > node-renderer.log 2>&1 &) so the
process output is preserved for debugging while still performing the existing
health check on port 3800; ensure the log file is accessible in later steps (or
optionally tail the log) so failures between the readiness check and rspec run
can be diagnosed.

In `@app/views/pages/server_components.html.erb`:
- Around line 1-6: The react_component invocation for ServerComponentsPage
includes a hardcoded id ("ServerComponentsPage-react-component-0") which can
cause collisions; remove the id option from the react_component call in the
ServerComponentsPage render so React on Rails can auto-generate unique IDs (or,
if a stable ID is truly required for RSC payload targeting, replace the
hardcoded string with a generated unique value/prop such as a SecureRandom.uuid
or a passed-in server-side identifier before rendering).

In
`@client/app/bundles/comments/startup/RouterApp/ror_components/RouterApp.server.jsx`:
- Around line 1-2: The 'use client' directive in RouterApp.server.jsx is
intentionally used so the component registers with ReactOnRails via
ReactOnRails.register() and can call client-only APIs like
ReactOnRails.getStore(), but the .server.jsx filename is misleading; either add
a clear top-of-file comment explaining this contradiction (mentioning that 'use
client' is required to force traditional SSR registration with
ReactOnRails.register() and to allow ReactOnRails.getStore()), or rename the
file to RouterApp.ssr.jsx (and update any imports/registrations) to make its SSR
intent explicit.

In `@client/app/bundles/server-components/components/CommentsFeed.jsx`:
- Line 59: The allowedSchemes configuration currently includes a blanket 'data'
entry which permits any data: MIME type; update the configuration in
CommentsFeed.jsx where allowedSchemes is defined to either remove 'data'
entirely or restrict it to image MIME types only (e.g., allow only data:image/*
by replacing 'data' with a constrained pattern or validated check such as
permitting only data URLs that match
/^data:image\/(png|jpeg|gif|webp);base64,/), ensuring the sanitizer/validator
for allowedSchemes enforces that constraint.

In `@client/app/packs/rsc-bundle.js`:
- Around line 6-7: The import of './stores-registration' and its comment are
misleading because that module is a 'use client' file which runs
ReactOnRails.registerStore(...) only in the browser, so it is not executed in
the RSC/server render path; either remove the import and the comment from
rsc-bundle.js to avoid the incorrect invariant, or move the store registration
into a client-only pack that is guaranteed to run in the browser (e.g., create
or update a browser entry that imports './stores-registration' and ensure that
pack is included on client pages); refer to the './stores-registration' module
and the ReactOnRails.registerStore calls when making the change.

In `@config/webpack/rscWebpackConfig.js`:
- Around line 16-22: The helper function extractLoader is defined but only
referenced inside configureRsc; remove the standalone extractLoader declaration
and inline its logic directly into configureRsc (or, if you prefer to keep it,
ensure it's actually reused elsewhere) by replacing calls to extractLoader(...)
with the equivalent Array.isArray(rule.use) check and find logic that inspects
item or item.loader for loaderName; reference the extractLoader name and
configureRsc to locate where to perform the change.
- Around line 47-71: The loop in rscConfig.module.rules.forEach mutates rule.use
options for url/file loaders but misses rules that define loader directly on the
rule (rule.loader), so update the handler to also detect and handle a direct
rule.loader case: when rule.loader (not rule.use) is a string containing
'url-loader' or 'file-loader', set/merge rule.options = { ...(rule.options ||
{}), emitFile: false } with type-safe checks (ensure rule.loader is a string and
rule.options exists or is created) so direct-loader rules receive the same
emitFile:false behavior as use-wrapped loaders.

In `@config/webpack/rspackRscPlugin.js`:
- Around line 30-32: The empty catch in rspackRscPlugin.js silently swallows all
read errors; update the catch in the file-read block (the try/catch surrounding
the read operation in the Rsc plugin) to detect and ignore only the "file not
found" case (e.g., error.code === 'ENOENT') while logging any other errors (use
processLogger.debug or console.error depending on available logger, and include
the error object) so unexpected IO issues are surfaced; ensure the catch
references the thrown error (e) rather than using an unused placeholder (_).
- Around line 12-36: The module-level Map useClientCache and function
hasUseClientDirective should be converted to per-plugin-instance state and
methods: add this._useClientCache (a Map) on the plugin constructor and change
hasUseClientDirective to an instance method (e.g., this.hasUseClientDirective)
that reads/writes this._useClientCache instead of the module-scope
useClientCache; also replace the synchronous fs.* reads inside
hasUseClientDirective with asynchronous I/O or the compiler/filesystem
abstraction provided by the build (e.g., use compiler.inputFileSystem/async
reads) so the check is non-blocking and isolated per compilation, and remove any
module-level useClientCache references so parallel builds do not share state.
🪄 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: 4082de09-59c2-4c40-9a11-48ccbf0cb6ea

📥 Commits

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

⛔ 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 (36)
  • .github/workflows/rspec_test.yml
  • .gitignore
  • 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
  • react-on-rails-pro-node-renderer.js

Comment on lines +38 to +40
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.
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 issues.

Static analysis flagged formatting inconsistencies in the JSX text content. Run Prettier to auto-fix.

🔧 Suggested 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>
📝 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
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 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>
🧰 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)

🤖 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
38 - 40, The JSX text in the ServerInfo component contains Prettier formatting
inconsistencies; run Prettier (or your project's formatter) to reformat
ServerInfo.jsx so the JSX text nodes and inline <code> elements are properly
spaced and wrapped according to the project's rules. Locate the ServerInfo React
component and reformat the block containing the sentence about the Node.js os
module and lodash usage so that spacing around inline code elements and line
breaks match the project's Prettier config, then commit the formatted file.

Comment on lines +10 to +127
const ServerComponentsPage = () => {
return (
<div className="max-w-4xl mx-auto py-8 px-4">
<header className="mb-10">
<h1 className="text-3xl font-bold text-slate-800 mb-2">
React Server Components Demo
</h1>
<p className="text-slate-600 text-lg">
This page is rendered using <strong>React Server Components</strong> with React on Rails Pro.
Server components run on the server and stream their output to the client, keeping
heavy dependencies out of the browser bundle entirely.
</p>
</header>

<div className="space-y-8">
{/* Server Info - uses Node.js os module (impossible on client) */}
<section>
<h2 className="text-xl font-semibold text-slate-700 mb-4 flex items-center gap-2">
Server Environment
<span className="text-xs font-normal bg-emerald-100 text-emerald-700 px-2 py-0.5 rounded-full">
Server Only
</span>
</h2>
<ServerInfo />
</section>

{/* Interactive toggle - demonstrates mixing server + client components */}
<section>
<h2 className="text-xl font-semibold text-slate-700 mb-4 flex items-center gap-2">
Interactive Client Component
<span className="text-xs font-normal bg-blue-100 text-blue-700 px-2 py-0.5 rounded-full">
Client Hydrated
</span>
</h2>
<TogglePanel title="How does this work?">
<div className="prose prose-slate max-w-none text-sm">
<p>
This toggle is a <code>&apos;use client&apos;</code> component, meaning it ships JavaScript
to the browser for interactivity. But the content inside is rendered on the server
and passed as children — a key RSC pattern called the <strong>donut pattern</strong>.
</p>
<ul>
<li>The <code>TogglePanel</code> wrapper runs on the client (handles click events)</li>
<li>The children content is rendered on the server (no JS cost)</li>
<li>Heavy libraries used by server components never reach the browser</li>
</ul>
</div>
</TogglePanel>
</section>

{/* Async data fetching with Suspense streaming */}
<section>
<h2 className="text-xl font-semibold text-slate-700 mb-4 flex items-center gap-2">
Streamed Comments
<span className="text-xs font-normal bg-amber-100 text-amber-700 px-2 py-0.5 rounded-full">
Async + Suspense
</span>
</h2>
<p className="text-slate-500 text-sm mb-4">
Comments are fetched directly on the server using the Rails API.
The page shell renders immediately while this section streams in progressively.
</p>
<Suspense
fallback={
<div className="animate-pulse space-y-3">
{[1, 2, 3].map((i) => (
<div key={i} className="bg-slate-100 rounded-lg p-4">
<div className="h-4 bg-slate-200 rounded w-1/4 mb-2" />
<div className="h-3 bg-slate-200 rounded w-3/4" />
</div>
))}
</div>
}
>
<CommentsFeed />
</Suspense>
</section>

{/* Architecture explanation */}
<section className="bg-slate-50 border border-slate-200 rounded-xl p-6">
<h2 className="text-lg font-semibold text-slate-700 mb-3">
What makes this different?
</h2>
<div className="grid md:grid-cols-2 gap-4 text-sm text-slate-600">
<div>
<h3 className="font-medium text-slate-800 mb-1">Smaller Client Bundle</h3>
<p>
Libraries like <code>lodash</code>, <code>marked</code>, and Node.js <code>os</code> module
are used on this page but never downloaded by the browser.
</p>
</div>
<div>
<h3 className="font-medium text-slate-800 mb-1">Direct Data Access</h3>
<p>
Server components fetch data by calling your Rails API internally — no
client-side fetch waterfalls or loading spinners for initial data.
</p>
</div>
<div>
<h3 className="font-medium text-slate-800 mb-1">Progressive Streaming</h3>
<p>
The page shell renders instantly. Async components (like the comments feed)
stream in as their data resolves, with Suspense boundaries showing fallbacks.
</p>
</div>
<div>
<h3 className="font-medium text-slate-800 mb-1">Selective Hydration</h3>
<p>
Only client components (like the toggle above) receive JavaScript.
Everything else is pure HTML — zero hydration cost.
</p>
</div>
</div>
</section>
</div>
</div>
);
};
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

Resolve the current ESLint/Prettier errors before merging.

Static analysis is already flagging react/function-component-definition on Line 10, and the JSX block still has multiple prettier/prettier violations. This page is not lint-clean yet, so please rewrite the component as a function declaration and run the formatter on the file.

🧰 Tools
🪛 ESLint

[error] 10-127: Function component is not a function declaration

(react/function-component-definition)


[error] 14-16: Replace ⏎··········React·Server·Components·Demo⏎········ with React·Server·Components·Demo

(prettier/prettier)


[error] 18-20: Replace ⏎··········Server·components·run·on·the·server·and·stream·their·output·to·the·client,·keeping⏎··········heavy·dependencies·out with ·Server⏎··········components·run·on·the·server·and·stream·their·output·to·the·client,·keeping·heavy·dependencies·out⏎·········

(prettier/prettier)


[error] 47-47: Insert ·to

(prettier/prettier)


[error] 48-49: Replace ·to·the·browser·for·interactivity.·But·the·content·inside·is·rendered·on·the·server⏎················and·passed·as with ·the·browser·for·interactivity.·But·the·content·inside·is·rendered·on·the·server·and·passed·as⏎···············

(prettier/prettier)


[error] 52-52: Replace The·<code>TogglePanel</code>·wrapper·runs·on·the·client·(handles·click·events) with ⏎··················The·<code>TogglePanel</code>·wrapper·runs·on·the·client·(handles·click·events)⏎················

(prettier/prettier)


[error] 69-70: Replace ⏎············The·page·shell·renders with ·The·page·shell·renders⏎···········

(prettier/prettier)


[error] 90-92: Replace ⏎············What·makes·this·different?⏎·········· with What·makes·this·different?

(prettier/prettier)


[error] 104-105: Replace ⏎················client-side·fetch with ·client-side·fetch⏎···············

(prettier/prettier)


[error] 111-112: Replace ⏎················stream·in·as·their with ·stream·in·as·their⏎···············

(prettier/prettier)


[error] 118-119: Replace ⏎················Everything·else·is·pure with ·Everything·else·is·pure⏎···············

(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/ServerComponentsPage.jsx` around lines
10 - 127, ESLint flags the component definition and Prettier reports formatting
errors; replace the arrow function component "const ServerComponentsPage = () =>
{ ... }" with a function declaration "function ServerComponentsPage() { ... }",
keep the same JSX and component usages (ServerInfo, TogglePanel, CommentsFeed,
Suspense), then run your project's formatter/Prettier on this file to fix the
remaining formatting violations and ensure the file exports the component
(export default ServerComponentsPage) if applicable.

Comment on lines +94 to +100
// Emit SSR manifest (maps module IDs to SSR module data)
if (!this.isServer) {
compilation.emitAsset(
this.ssrManifestFilename,
new sources.RawSource(JSON.stringify({}, null, 2)),
);
}
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

🌐 Web query:

What should react-ssr-manifest.json contain for React Server Components streaming SSR?

💡 Result:

react-ssr-manifest.json is a Vite-generated file used in SSR setups to map server-side module IDs to their corresponding client-side chunk files (JS/CSS/assets). This enables the server to preload the correct client assets during streaming SSR with Suspense and React.lazy, avoiding waterfalls. It contains an object where keys are normalized source module paths (e.g., "src/pages/create/create.jsx") and values are arrays of asset URLs (absolute or relative based on publicPath): { "src/pages/create/create.jsx": [ "/assets/create-Cgw3MXE6.js", "/assets/create-CgBOejMp.css" ], "src/pages/create/create.module.scss": [ "/assets/create-Cgw3MXE6.js", "/assets/create-CgBOejMp.css" ], ... } For React Server Components (RSC) streaming SSR specifically: - Standard Vite ssr-manifest.json (via build.ssrManifest: true) supports traditional SSR but lacks full context.modules for dynamic imports (Vue has it via @vitejs/plugin-vue; React needs custom plugins like vite-preload). - RSC requires additional manifests: client-manifest.json (maps client "use client" components to chunks) and server-consumer-manifest.json (formerly SSRManifest; module loading config + moduleMap for server-to-client references). These are generated by react-server-dom-webpack plugin. - In Vite RSC setups (@vitejs/plugin-rsc), use virtual:vite-rsc/assets-manifest and buildAssetsManifest for client assets in SSR. Example server usage (Express): const ssrManifest = JSON.parse(fs.readFileSync('.vite/ssr-manifest.json', 'utf-8')); To support React.lazy preloading in RSC streaming, process vite's manifest.json (with dynamicImports) or use plugins like vite-preload. Note: react-ssr-manifest.json is distinct from RSC-specific manifests; for full RSC streaming, combine with client/server manifests passed to renderToReadableStream(, clientManifest).

Citations:


🏁 Script executed:

# First, let's look at the full context of the rspackRscPlugin.js file
fd -e js -e ts -name '*rspack*' -o -name '*RSC*' -o -name '*rsc*' | head -20

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 312


🏁 Script executed:

# Find and examine the rspackRscPlugin.js file
find . -type f -name "rspackRscPlugin.js" -o -name "*rspackRsc*"

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 117


🏁 Script executed:

# Read the rspackRscPlugin.js file
cat -n ./config/webpack/rspackRscPlugin.js

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 5899


🏁 Script executed:

# Search for usage of react-ssr-manifest.json in the codebase
rg "react-ssr-manifest" --type js --type ts --type json

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 173


🏁 Script executed:

# Search for references to ssrManifestFilename
rg "ssrManifestFilename" --type js --type ts

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 247


🏁 Script executed:

# Check for any SSR or manifest-related configuration
rg "ssr-manifest|ssrManifest" --type js --type ts --type json -i

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 247


🏁 Script executed:

# Check for any comments or documentation about the plugin
grep -r "SSR manifest\|ssr.*manifest" --include="*.md" --include="*.txt" --include="*.js"

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 591


🏁 Script executed:

# Look for any server-side build config or SSR setup
find . -type f \( -name "README*" -o -name "*SSR*" -o -name "*config*" \) | grep -E "(README|SSR|config)" | head -20

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 759


🏁 Script executed:

# Check if there are any references to building or using the manifests
rg "react-client-manifest|react-server-client-manifest" --type js --type ts

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 455


🏁 Script executed:

# Look at the react-server-dom-webpack-plugin to understand expected format
cat -n node_modules/react-on-rails-rsc/dist/react-server-dom-webpack/cjs/react-server-dom-webpack-plugin.js | head -100

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 4532


🏁 Script executed:

# Check the react-on-rails-pro file that creates SSR manifest
cat -n node_modules/react-on-rails-pro/lib/getReactServerComponent.server.js 2>/dev/null | head -50

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 2775


🏁 Script executed:

# Search for any documentation or examples about manifest structure in node_modules
rg "serverConsumerManifest|react-ssr-manifest" node_modules/react-on-rails-rsc node_modules/react-on-rails-pro --type js -A 5 | head -60

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 11492


🏁 Script executed:

# Search for what should populate the server manifest in the webpack plugin
rg "moduleMap|serverModuleMap|moduleLoading" node_modules/react-on-rails-rsc --type js -B 2 -A 2 | head -100

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 15978


🏁 Script executed:

# Check if there are any server build configs that might populate this
rg "serverConsumerManifest|react-ssr-manifest" config/ --type js -B 3 -A 3

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 535


Populate react-ssr-manifest.json with module metadata for RSC hydration.

The SSR manifest is emitted as an empty object, but React's flight protocol requires it to contain module metadata (moduleMap, serverModuleMap, moduleLoading). Without this data, buildClientRenderer() and createFromNodeStream() cannot resolve server module references during streaming SSR. The plugin's own comment indicates the intent: "maps module IDs to SSR module data", but the implementation leaves it unpopulated. Consider populating this manifest with module metadata alongside the client manifest.

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

In `@config/webpack/rspackRscPlugin.js` around lines 94 - 100, The current emit
block emits an empty SSR manifest; update the logic around this.isServer /
compilation.emitAsset so that this.ssrManifestFilename (react-ssr-manifest.json)
is written with the actual module metadata object containing moduleMap,
serverModuleMap, and moduleLoading (populated from the plugin's collected module
registry or the same source that builds the client manifest) instead of {}.
Locate where the plugin builds the client manifest and mirror that collection
step for server modules (or extend that collection), then serialize the
assembled metadata { moduleMap, serverModuleMap, moduleLoading } into JSON and
pass it to compilation.emitAsset using new sources.RawSource(JSON.stringify(...,
null, 2)). Ensure the names this.ssrManifestFilename and any registry variables
used for module ids are reused so the client and SSR manifests stay consistent.

Comment on lines +30 to +36
const clientConfig = clientWebpackConfig();
const serverConfig = serverWebpackConfig();
const rscConfig = rscWebpackConfig();
if (envSpecific) envSpecific(clientConfig, serverConfig, rscConfig);
// eslint-disable-next-line no-console
console.log('[React on Rails] Creating both client and server bundles.');
result = [clientConfig, serverConfig];
console.log('[React on Rails] Creating client, server, and RSC bundles.');
result = [clientConfig, serverConfig, rscConfig];
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find all files that call webpackConfig() with a callback to verify they handle the new signature
rg -n "webpackConfig\(" config/webpack/ --type js -A 3

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 304


🏁 Script executed:

cat -n config/webpack/production.js

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 660


🏁 Script executed:

cat -n config/webpack/test.js

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 341


🏁 Script executed:

cat -n config/webpack/webpackConfig.js | head -50

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 2323


Update environment-specific callbacks to accept the rscConfig parameter.

The envSpecific callbacks are called with different numbers of arguments depending on the build mode. In the default case (lines 30-36), they receive 3 arguments (clientConfig, serverConfig, rscConfig), but the callbacks in production.js and test.js only accept 2 parameters. JavaScript silently ignores the extra argument, preventing environment-specific RSC configuration.

Update the callback signatures in environment files to accept the third parameter:

// config/webpack/production.js
const productionEnvOnly = (_clientWebpackConfig, _serverWebpackConfig, _rscWebpackConfig) => {
  // place any code here that is for production only
};

// config/webpack/test.js  
const testOnly = (_clientWebpackConfig, _serverWebpackConfig, _rscWebpackConfig) => {
  // place any code here that is for test only
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@config/webpack/webpackConfig.js` around lines 30 - 36, The envSpecific
callback is invoked with three configs (clientConfig, serverConfig, rscConfig)
but the environment-specific functions only accept two params, so the RSC config
is ignored; update the signatures in the environment modules (e.g., change
productionEnvOnly and testOnly to accept a third parameter such as
_rscWebpackConfig) so they match the call site
(envSpecific(clientWebpackConfig(), serverWebpackConfig(), rscWebpackConfig()))
and can modify the RSC config when needed.

Comment on lines +4 to +10
const config = {
serverBundleCachePath: path.resolve(__dirname, '.node-renderer-bundles'),
logLevel: process.env.RENDERER_LOG_LEVEL || 'debug',
password: process.env.RENDERER_PASSWORD || 'devPassword',
port: process.env.RENDERER_PORT || 3800,
supportModules: true,
workersCount: Number(process.env.NODE_RENDERER_CONCURRENCY || 3),
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

🧩 Analysis chain

🏁 Script executed:

find . -name "*react-on-rails-pro-node-renderer*" -type f

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 119


🏁 Script executed:

cat -n ./react-on-rails-pro-node-renderer.js | head -50

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 752


🏁 Script executed:

# Check if there are any other usages of config.port or config.workersCount in the codebase
rg "config\.port|config\.workersCount" --type js

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 144


🏁 Script executed:

# Test the actual behavior of Number coercion with process.env
python3 << 'EOF'
# Simulate process.env behavior - all values are strings or undefined

# Current code behavior
def test_current():
    print("=== Current Code Behavior ===")
    
    # Case 1: env var undefined
    result = int(3)  # Number('undefined' || 3) -> Number(3)
    print(f"Number(undefined || 3) = {result}")
    
    # Case 2: env var is invalid string
    try:
        result = int('abc')  # Number('abc' || 3) -> Number('abc')
    except:
        result = "NaN"
    print(f"Number('abc' || 3) = {result}")
    
    # Case 3: env var is valid number string
    result = int('8080')  # Number('8080') 
    print(f"Number('8080') = {result}")

test_current()

print("\n=== Suggested Fix Behavior ===")

# Test Number.parseInt with ?? operator
import math

def test_suggested():
    # Case 1: env var undefined
    val = '' if None is None else None  # simulating ?? with empty string
    try:
        result = int(val, 10) if val else float('nan')
    except:
        result = float('nan')
    print(f"Number.parseInt(undefined ?? '', 10) = {result}")
    
    # Case 2: env var is invalid string
    val = 'abc'
    try:
        result = int(val, 10)
    except:
        result = float('nan')
    print(f"Number.parseInt('abc', 10) = {result}")
    
    # Case 3: env var is valid number string
    val = '8080'
    result = int(val, 10)
    print(f"Number.parseInt('8080', 10) = {result}")

test_suggested()

print("\n=== Key Issue ===")
print("Number(undefined || 3) evaluates the || operator FIRST")
print("So if RENDERER_PORT is undefined, it uses 3 as the numeric default")
print("But if RENDERER_PORT='8080' (string), Number('8080') = 8080 works fine")
print("The real issue: Number('invalid') = NaN, and that could happen if env var is malformed")

EOF

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 627


🏁 Script executed:

# Check how reactOnRailsProNodeRenderer is called and if there are any type hints or documentation
rg "reactOnRailsProNodeRenderer" -A 3 -B 3 --type js

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 684


Parse numeric environment variables before passing to config.

process.env values are strings. The port setting stays stringly-typed when RENDERER_PORT is set, while workersCount is coerced with Number(). This inconsistency means invalid NODE_RENDERER_CONCURRENCY becomes NaN. Normalize both values upfront so configuration is predictable across deployments.

Suggested hardening
+const rendererPort = Number.parseInt(process.env.RENDERER_PORT ?? '', 10);
+const workersCount = Number.parseInt(process.env.NODE_RENDERER_CONCURRENCY ?? '', 10);
+
 const config = {
   serverBundleCachePath: path.resolve(__dirname, '.node-renderer-bundles'),
   logLevel: process.env.RENDERER_LOG_LEVEL || 'debug',
   password: process.env.RENDERER_PASSWORD || 'devPassword',
-  port: process.env.RENDERER_PORT || 3800,
+  port: Number.isFinite(rendererPort) ? rendererPort : 3800,
   supportModules: true,
-  workersCount: Number(process.env.NODE_RENDERER_CONCURRENCY || 3),
+  workersCount: Number.isFinite(workersCount) ? workersCount : 3,
 };
📝 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 config = {
serverBundleCachePath: path.resolve(__dirname, '.node-renderer-bundles'),
logLevel: process.env.RENDERER_LOG_LEVEL || 'debug',
password: process.env.RENDERER_PASSWORD || 'devPassword',
port: process.env.RENDERER_PORT || 3800,
supportModules: true,
workersCount: Number(process.env.NODE_RENDERER_CONCURRENCY || 3),
const rendererPort = Number.parseInt(process.env.RENDERER_PORT ?? '', 10);
const workersCount = Number.parseInt(process.env.NODE_RENDERER_CONCURRENCY ?? '', 10);
const config = {
serverBundleCachePath: path.resolve(__dirname, '.node-renderer-bundles'),
logLevel: process.env.RENDERER_LOG_LEVEL || 'debug',
password: process.env.RENDERER_PASSWORD || 'devPassword',
port: Number.isFinite(rendererPort) ? rendererPort : 3800,
supportModules: true,
workersCount: Number.isFinite(workersCount) ? workersCount : 3,
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@react-on-rails-pro-node-renderer.js` around lines 4 - 10, The config object
uses process.env values inconsistently: port can remain a string while
workersCount is coerced with Number(), which can produce NaN; normalize both by
parsing and validating their numeric environment variables before assigning to
config—convert process.env.RENDERER_PORT and
process.env.NODE_RENDERER_CONCURRENCY to integers (e.g., parseInt or Number with
fallback), ensure sensible defaults if parsing fails, and assign the
parsed/validated numbers to config.port and config.workersCount (refer to the
config object and the port and workersCount properties) so the configuration is
always numeric and predictable.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
prerender: false,
auto_load_bundle: false,
trace: Rails.env.development?,
id: "ServerComponentsPage-react-component-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.

Hardcoded DOM id can cause collisions and is fragile

"ServerComponentsPage-react-component-0" mirrors React on Rails' auto-generated ID pattern. If the framework's ID generation logic changes, or if this component is ever rendered more than once on a page, this will silently conflict with the framework-assigned ID — potentially causing hydration failures or duplicate-ID accessibility violations.

Suggested change
id: "ServerComponentsPage-react-component-0") %>
trace: Rails.env.development?) %>

Unless there is a specific reason the RSC payload route needs to target this exact id (in which case that dependency should be documented), let the framework generate it.

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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@config/webpack/rscWebpackConfig.js`:
- Around line 24-27: configureRsc currently builds its RSC webpack config from
commonWebpackConfig(), which duplicates the server Node renderer and can drift;
change configureRsc to import/derive the server-side base config (the server
webpack config export, e.g. serverWebpackConfig) instead of calling
commonWebpackConfig(), then apply only RSC-specific overrides (entry, RSC loader
rules, manifest plugin adjustments) on that base; keep getBundler usage and
ensure you only mutate a shallow clone of the server config to avoid affecting
other bundles.
- Around line 45-69: The rule-pruning loop mutates each rule parameter (causing
no-param-reassign lint errors); rewrite the pass to replace
rscConfig.module.rules with rscConfig.module.rules.map(...) that returns new
rule objects—do not reassign properties on the incoming rule. For array-form
uses, produce a new use array filtered of
mini-css-extract-plugin/CssExtractRspackPlugin/cssExtractLoader/style-loader,
locate the css-loader entry in the new array and if its options.modules exists
return a cloned css-loader object with options.modules merged to include
exportOnlyLocals: true; for single-object uses (rule.use.loader matching
url-loader or file-loader) return a new rule with use.options cloned and
emitFile: false set. Ensure all objects/arrays (rule, rule.use, cssLoader,
rule.use.options) are shallow-copied rather than mutated so
rscConfig.module.rules receives a brand-new array of updated rule objects.
🪄 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: b892ca98-3eae-4264-bc23-3333f60fbb05

📥 Commits

Reviewing files that changed from the base of the PR and between e16612c and c0a66b1.

📒 Files selected for processing (8)
  • Procfile.dev
  • client/app/bundles/server-components/components/CommentsFeed.jsx
  • config/initializers/react_on_rails_pro.rb
  • config/webpack/rscWebpackConfig.js
  • config/webpack/rspackRscPlugin.js
  • config/webpack/serverWebpackConfig.js
  • config/webpack/webpackConfig.js
  • react-on-rails-pro-node-renderer.js
✅ Files skipped from review due to trivial changes (1)
  • Procfile.dev
🚧 Files skipped from review as they are similar to previous changes (3)
  • config/webpack/serverWebpackConfig.js
  • react-on-rails-pro-node-renderer.js
  • config/initializers/react_on_rails_pro.rb

Comment on lines +24 to +27
const configureRsc = () => {
const bundler = getBundler();
const rscConfig = commonWebpackConfig();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Build the RSC config from the server-side base, not commonWebpackConfig().

This file is re-creating a second Node-renderer config by hand, so it can drift from config/webpack/serverWebpackConfig.js and miss future server-only runtime fixes. Extract a shared Node-side base or derive this config from the server bundle config, then layer only the RSC-specific entry / loader / manifest changes here.

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

In `@config/webpack/rscWebpackConfig.js` around lines 24 - 27, configureRsc
currently builds its RSC webpack config from commonWebpackConfig(), which
duplicates the server Node renderer and can drift; change configureRsc to
import/derive the server-side base config (the server webpack config export,
e.g. serverWebpackConfig) instead of calling commonWebpackConfig(), then apply
only RSC-specific overrides (entry, RSC loader rules, manifest plugin
adjustments) on that base; keep getBundler usage and ensure you only mutate a
shallow clone of the server config to avoid affecting other bundles.

- Fix JSON response structure mismatch in CommentsFeed (unwrap data.comments)
- Fix registerServerComponent API call (remove unsupported options object)
- Enable NodeRenderer in test env by using Rails.env.local? instead of development?
- Add warning for non-ENOENT errors in rspackRscPlugin _hasUseClientDirective
- Avoid no-param-reassign ESLint violations in rscWebpackConfig loader pruning

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

Review comments addressed (248ba65)

Triaged 40 review comments from 5 bots across 2 review rounds. Summary:

Fixed (6 items)

  1. CommentsFeed JSON mismatch — Rails returns { comments: [...] } but code treated the whole object as an array. Fixed by unwrapping data.comments.
  2. registerServerComponent wrong API — Function only accepts ...string[] but was passed an options object. Removed the unsupported options arg.
  3. NodeRenderer not enabled in CI — Guard used Rails.env.development? which excluded test env. Changed to Rails.env.local? (covers dev + test).
  4. Silent catch in rspackRscPlugin — Empty catch swallowed all errors. Now warns on non-ENOENT errors.
  5. ExecJS fallback compat — Acknowledged; commonjs2 is intentional for NodeRenderer. ExecJS fallback is not a target use case.
  6. ESLint no-param-reassign — Rewrote loader-pruning loops to use .map() with spreads instead of mutating rule objects.

Skipped with rationale (32 items)

  • 6 comments about data: URI / hardcoded passwords — already fixed in prior commits
  • 2 duplicate NodeRenderer comments
  • 2 extractLoader duplication nits — out of scope refactor
  • 4 speculative concerns (MessageChannel, nc, parallel cache, chunks format)
  • 5 style/lint nits
  • 3 intentional demo behavior (ServerInfo, duplicate content, debug log level)
  • 10 not applicable (tutorial app secrets, already-handled URL resolution, refactor suggestions)

All 40 threads replied to and resolved.

Comment on lines +180 to +197
serverWebpackConfig.plugins.push(
new bundler.BannerPlugin({
banner: [
'if(typeof MessageChannel==="undefined"){',
' globalThis.MessageChannel=class MessageChannel{',
' constructor(){',
' this.port1={onmessage:null};',
' this.port2={postMessage:function(msg){',
' var p=this._port1;if(p.onmessage)p.onmessage({data:msg});',
' }};',
' this.port2._port1=this.port1;',
' }',
' };',
'}',
].join('\n'),
raw: 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.

The hand-rolled MessageChannel polyfill is asymmetric — only port2.postMessage → port1.onmessage is wired up; port1.postMessage → port2.onmessage is never connected.

React's concurrent scheduler uses both ports for bi-directional task scheduling. If the VM ever calls port1.postMessage(...), port2.onmessage won't fire and scheduling will silently stall.

Suggested change
serverWebpackConfig.plugins.push(
new bundler.BannerPlugin({
banner: [
'if(typeof MessageChannel==="undefined"){',
' globalThis.MessageChannel=class MessageChannel{',
' constructor(){',
' this.port1={onmessage:null};',
' this.port2={postMessage:function(msg){',
' var p=this._port1;if(p.onmessage)p.onmessage({data:msg});',
' }};',
' this.port2._port1=this.port1;',
' }',
' };',
'}',
].join('\n'),
raw: true,
}),
);
serverWebpackConfig.plugins.push(
new bundler.BannerPlugin({
banner: [
'if(typeof MessageChannel==="undefined"){',
' globalThis.MessageChannel=class MessageChannel{',
' constructor(){',
' var p1={onmessage:null};',
' var p2={onmessage:null};',
' p1.postMessage=function(msg){if(p2.onmessage)p2.onmessage({data:msg});};',
' p2.postMessage=function(msg){if(p1.onmessage)p1.onmessage({data:msg});};',
' this.port1=p1;',
' this.port2=p2;',
' }',
' };',
'}',
].join('\n'),
raw: true,
}),
);

Comment on lines +16 to +22
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);
});
}
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 an exact copy of the same function defined (and exported) in serverWebpackConfig.js. The server config already exports it via module.exports.extractLoader = extractLoader.

Suggested change
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);
});
}
const { extractLoader } = require('./serverWebpackConfig');

This eliminates the duplication and ensures both configs stay in sync if the logic ever changes.

password: rendererPassword,
port: process.env.RENDERER_PORT || 3800,
supportModules: true,
workersCount: Number(process.env.NODE_RENDERER_CONCURRENCY || 3),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Number('abc') silently returns NaN, which the renderer will then accept as a valid workersCount. This can cause cryptic failures at startup. Add a guard:

Suggested change
workersCount: Number(process.env.NODE_RENDERER_CONCURRENCY || 3),
workersCount: (() => {
const n = Number(process.env.NODE_RENDERER_CONCURRENCY || 3);
if (!Number.isFinite(n) || n < 1) throw new Error(`Invalid NODE_RENDERER_CONCURRENCY: "${process.env.NODE_RENDERER_CONCURRENCY}"`);
return n;
})(),

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

Choose a reason for hiding this comment

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

Information disclosure: os.hostname() returns the actual server/container hostname, which can leak infrastructure details (e.g. pod names, EC2 instance IDs, internal DNS). This is a tutorial app so the risk is limited, but it's worth either omitting the field or replacing it with a privacy-safe placeholder in non-development environments:

Suggested change
hostname: os.hostname(),
hostname: process.env.NODE_ENV === 'development' ? os.hostname() : '[redacted]',

Comment on lines +80 to +87
const safeHtml = sanitizeHtml(rawHtml, {
allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img']),
allowedAttributes: {
...sanitizeHtml.defaults.allowedAttributes,
img: ['src', 'alt', 'title', 'width', 'height'],
},
allowedSchemes: ['https', 'http'],
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Allowing img tags with src pointing to arbitrary http/https URLs means any user-authored comment can embed external images, including tracking pixels that reveal visitor IPs to third-party servers.

Consider either:

  1. Dropping img from allowedTags (safest for a comments feed), or
  2. Keeping img but restricting src to relative paths only via allowedSchemesByTag:
Suggested change
const safeHtml = sanitizeHtml(rawHtml, {
allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img']),
allowedAttributes: {
...sanitizeHtml.defaults.allowedAttributes,
img: ['src', 'alt', 'title', 'width', 'height'],
},
allowedSchemes: ['https', 'http'],
});
const safeHtml = sanitizeHtml(rawHtml, {
allowedTags: sanitizeHtml.defaults.allowedTags,
allowedAttributes: sanitizeHtml.defaults.allowedAttributes,
});

@claude
Copy link
Copy Markdown

claude bot commented Apr 7, 2026

Code Review — Fix SSR runtime failures for React Server Components

Overall this is solid work: the root-cause analysis in the PR description is accurate, and the fixes (fallbacks over externals, BannerPlugin polyfill, 'use client' directive) are the right approach. The RSC plugin and webpack config are well-structured for Rspack compatibility. A few issues worth addressing before merge:

Bug — asymmetric MessageChannel polyfill (serverWebpackConfig.js)

The injected polyfill only wires port2.postMessage → port1.onmessage. React's concurrent scheduler uses both ports for bi-directional scheduling; if it ever calls port1.postMessage, port2.onmessage won't fire and rendering can stall silently. See inline comment for a symmetric replacement.

Security — img tags in user-generated content (CommentsFeed.jsx)

sanitizeHtml is configured to allow img tags with arbitrary http/https src attributes. Any user comment can embed external images, including tracking pixels. For a public comments feed the simplest fix is to drop img from allowedTags entirely.

Security — hostname exposure (ServerInfo.jsx)

os.hostname() reveals the actual container/server hostname to every page visitor. Fine for local dev demos, but if this page is ever reachable in a deployed environment it leaks infrastructure details. Suggest redacting in non-development environments.

Code quality — duplicated extractLoader (rscWebpackConfig.js)

extractLoader in the RSC config is an exact copy of the function already defined and exported by serverWebpackConfig.js. Import it instead of re-defining.

Robustness — silent NaN for NODE_RENDERER_CONCURRENCY (react-on-rails-pro-node-renderer.js)

Number(process.env.NODE_RENDERER_CONCURRENCY || 3) silently produces NaN if the env var is set to a non-numeric string. Add a Number.isFinite guard and throw early.

CI — outdated action versions

actions/checkout@v3 and actions/setup-node@v3 are end-of-life; v4 uses Node 20 internally and includes security fixes. These weren't changed in this PR but are worth updating opportunistically.

Minor — dead code in serverWebpackConfig.js (pre-existing)

Lines 76–82 do an initial mini-css-extract-plugin strip that is entirely superseded by the more complete pass at lines 117–158. No correctness impact, but worth cleaning up to reduce confusion.

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.

🧹 Nitpick comments (3)
client/app/bundles/server-components/components/CommentsFeed.jsx (1)

43-48: Use destructuring and add defensive fallback for missing comments array.

ESLint flags the assignment on line 44. Additionally, if data.comments is undefined, _.orderBy will return an empty array but _.take on it is fine. However, using destructuring with a default is cleaner.

♻️ Proposed fix
-  const data = await response.json();
-  const comments = data.comments;
+  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 43 - 48, Replace the direct property access assignment to data.comments
with destructuring that provides a default empty array and avoid reassigning
variables: extract comments via const { comments = [] } = data (from the
response.json() result) and then pass that comments array into _.orderBy and
_.take to compute sortedComments and recentComments (referencing _.orderBy,
_.take, sortedComments, recentComments) so the code is defensive when
data.comments is missing and satisfies ESLint.
config/webpack/rspackRscPlugin.js (2)

40-47: Only the first .js file per chunk is collected.

The break on Line 45 stops after finding the first .js file. If a chunk emits multiple relevant .js files (e.g., async chunks or split bundles), subsequent files are ignored. This may be intentional given LimitChunkCountPlugin({ maxChunks: 1 }) is used in the RSC config, but if this plugin is reused elsewhere, it could miss module-to-chunk mappings.

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

In `@config/webpack/rspackRscPlugin.js` around lines 40 - 47, The current loop
over compilation.chunks collects only the first matching .js file because of the
break in the inner loop; remove the break and instead push all files that
endWith('.js') and not endWith('.hot-update.js') into chunkFiles (the block
iterating compilation.chunks and chunk.files that builds chunkFiles), so all
emitted JS outputs for a chunk are captured; if the single-file assumption is
intentional due to LimitChunkCountPlugin({ maxChunks: 1 }), add a comment or a
feature flag to preserve the existing behavior when required.

92-94: Regex doesn't handle hashbang or BOM before directive.

The regex pattern correctly handles comments but doesn't account for:

  1. Hashbang (#!/usr/bin/env node\n) which is valid at the start of ESM files
  2. UTF-8 BOM (\uFEFF) which some editors prepend

This is likely a minor edge case since RSC source files rarely have hashbangs, but worth noting.

♻️ Optional: Extended regex to handle edge cases
-      result = /^(?:\s*(?:\/\/[^\n]*\n|\/\*[\s\S]*?\*\/))*\s*['"]use client['"]/.test(head);
+      // Handle BOM, hashbang, whitespace, and comments before directive
+      result = /^(?:\uFEFF)?(?:#![^\n]*\n)?(?:\s*(?:\/\/[^\n]*\n|\/\*[\s\S]*?\*\/))*\s*['"]use client['"]/.test(head);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@config/webpack/rspackRscPlugin.js` around lines 92 - 94, The regex that
assigns result from head (the line with result = /.../.test(head)) currently
allows only comments before "use client" and should be updated to also accept an
optional UTF‑8 BOM (\uFEFF) and an optional hashbang (#!... newline) before
comments/directive; modify the pattern used in the assignment to result (the
regex literal that tests head) to permit an optional BOM and an optional leading
hashbang line prior to the existing comment-handling and the ['"]use client['"]
check so files starting with BOM or #! are correctly detected.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@client/app/bundles/server-components/components/CommentsFeed.jsx`:
- Around line 43-48: Replace the direct property access assignment to
data.comments with destructuring that provides a default empty array and avoid
reassigning variables: extract comments via const { comments = [] } = data (from
the response.json() result) and then pass that comments array into _.orderBy and
_.take to compute sortedComments and recentComments (referencing _.orderBy,
_.take, sortedComments, recentComments) so the code is defensive when
data.comments is missing and satisfies ESLint.

In `@config/webpack/rspackRscPlugin.js`:
- Around line 40-47: The current loop over compilation.chunks collects only the
first matching .js file because of the break in the inner loop; remove the break
and instead push all files that endWith('.js') and not endWith('.hot-update.js')
into chunkFiles (the block iterating compilation.chunks and chunk.files that
builds chunkFiles), so all emitted JS outputs for a chunk are captured; if the
single-file assumption is intentional due to LimitChunkCountPlugin({ maxChunks:
1 }), add a comment or a feature flag to preserve the existing behavior when
required.
- Around line 92-94: The regex that assigns result from head (the line with
result = /.../.test(head)) currently allows only comments before "use client"
and should be updated to also accept an optional UTF‑8 BOM (\uFEFF) and an
optional hashbang (#!... newline) before comments/directive; modify the pattern
used in the assignment to result (the regex literal that tests head) to permit
an optional BOM and an optional leading hashbang line prior to the existing
comment-handling and the ['"]use client['"] check so files starting with BOM or
#! are correctly detected.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 18d17adb-65a2-4ba5-85c4-1325b3c8dc71

📥 Commits

Reviewing files that changed from the base of the PR and between c0a66b1 and 248ba65.

📒 Files selected for processing (5)
  • client/app/bundles/server-components/components/CommentsFeed.jsx
  • client/app/packs/stimulus-bundle.js
  • config/initializers/react_on_rails_pro.rb
  • config/webpack/rscWebpackConfig.js
  • config/webpack/rspackRscPlugin.js
✅ Files skipped from review due to trivial changes (1)
  • config/initializers/react_on_rails_pro.rb
🚧 Files skipped from review as they are similar to previous changes (1)
  • client/app/packs/stimulus-bundle.js

justin808 added a commit to shakacode/react_on_rails that referenced this pull request Apr 8, 2026
Automated tests can pass while bin/dev is completely broken (as seen in
shakacode/react-webpack-rails-tutorial#723). This adds a structured
checklist that agents must follow when changes affect how the app starts,
builds, or serves.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Document the three-bundle architecture (client, server SSR, RSC), Node
Renderer setup requirements, VM sandbox constraints, RSC component
classification rules, and common troubleshooting patterns. Update README
with current version targets, RSC section, expanded config file list,
and all six Procfile.dev processes.

Motivated by the debugging challenges in PR #723 where undocumented
constraints (VM sandbox lacks require/MessageChannel, 'use client'
classification, Node renderer must run for tests) caused significant
debugging time. Filed shakacode/react_on_rails#3076 with upstream
doc suggestions.

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

claude bot commented Apr 8, 2026

Code Review: Fix SSR runtime failures for React Server Components

Overall this PR is well-structured and solves a real problem cleanly. The PR description is thorough and the CLAUDE.md additions are genuinely helpful for future contributors. A few issues worth addressing before merge:

Security: Hardcoded password in CI workflow

.github/workflows/rspec_test.yml sets RENDERER_PASSWORD: devPassword directly in the env block. Even for a demo/tutorial repo this sets a bad precedent. Prefer a GitHub Actions secret:

RENDERER_PASSWORD: ${{ secrets.RENDERER_PASSWORD }}

Add RENDERER_PASSWORD=devPassword to the repo Actions secrets. This avoids encoding credentials in source and matches the pattern already used for RENDERER_PASSWORD in production (which correctly requires an env var with no default).

Bug: chunks array format in React flight manifest

In rspackRscPlugin.js (_processModule), the chunks array is built as:

chunks.push(chunk.id, file); // produces [chunkId, filename]

The React flight client manifest format expects chunks to be a flat array of chunk filenames (or URLs), not interleaved [id, filename] pairs. The standard ReactFlightWebpackPlugin emits [filename]. If the React runtime consumes this manifest and tries to load chunk.id as a URL it will silently fail to load client components. Worth verifying against the format react-on-rails-rsc expects.

Code duplication: extractLoader defined twice

The function is defined identically in serverWebpackConfig.js (lines 9-15) and rscWebpackConfig.js (lines 17-23). It is even exported from the server config (module.exports.extractLoader = extractLoader) but rscWebpackConfig.js does not import it — it just redeclares it. Move it to bundlerUtils.js or import it from serverWebpackConfig.

Stale comment in rscWebpackConfig.js

Line 7: "Unlike the server bundle (which uses ExecJS)" — ExecJS was removed in this PR. The server bundle now runs in the Node renderer VM sandbox. Update to:

// Unlike the server bundle (which runs in the Node renderer VM sandbox),
// the RSC bundle targets full Node.js and can use built-in modules directly.

Minor: http in allowedSchemes for sanitize-html

In CommentsFeed.jsx, allowedSchemes: ['https', 'http'] allows mixed-content image rendering. For a tutorial ['https'] only is the safer default.

Nit: react-on-rails-rsc pinned to a prerelease

package.json pins "react-on-rails-rsc": "19.0.5-rc.1". RC packages can shift behavior between publishes; using a stable release or tilde range would be safer for a tutorial repo that others clone and run.

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

Choose a reason for hiding this comment

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

Security: this hardcodes a credential in source. Even as a dev-only password in a tutorial, it teaches poor practice. Use a GitHub Actions secret instead:

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

Add RENDERER_PASSWORD=devPassword to this repo's Actions secrets (Settings → Secrets and variables → Actions). That way the value is injected at runtime and not committed to the repo.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
README.md (1)

76-93: ⚠️ Potential issue | 🟡 Minor

Fix broken Table of Contents anchors.

The TOC links on Line 91 (#about-shakacode) and Line 93 (#open-code-of-conduct) do not resolve to headings in this README. Please remove or update them to valid anchors.

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

In `@README.md` around lines 76 - 93, The table of contents contains broken
anchors '#about-shakacode' and '#open-code-of-conduct' — locate the TOC entries
referencing these anchors and either remove them or update them to the exact
heading slugs used in the document (match the heading text/casing and spacing
converted to the correct GitHub-style anchor) so the links resolve; specifically
edit the TOC lines containing "[About ShakaCode](`#about-shakacode`)" and "[Open
Code of Conduct](`#open-code-of-conduct`)" to the correct anchors or delete them.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@CLAUDE.md`:
- Line 19: Update the heading "Three Bundle System" to use a hyphenated compound
adjective by changing the text to "Three-Bundle System" in the CLAUDE.md heading
(locate the heading string "Three Bundle System" and replace it with
"Three-Bundle System").

---

Outside diff comments:
In `@README.md`:
- Around line 76-93: The table of contents contains broken anchors
'#about-shakacode' and '#open-code-of-conduct' — locate the TOC entries
referencing these anchors and either remove them or update them to the exact
heading slugs used in the document (match the heading text/casing and spacing
converted to the correct GitHub-style anchor) so the links resolve; specifically
edit the TOC lines containing "[About ShakaCode](`#about-shakacode`)" and "[Open
Code of Conduct](`#open-code-of-conduct`)" to the correct anchors or delete them.
🪄 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: 3d31334d-28a5-4bb6-af9a-61e284dabef4

📥 Commits

Reviewing files that changed from the base of the PR and between 248ba65 and 6429ce0.

📒 Files selected for processing (2)
  • CLAUDE.md
  • README.md

bundle exec rubocop -a
```

## Architecture: Three Bundle System
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

Use hyphenated compound modifier in heading.

Line 19 should read “Three-Bundle System” for correct compound adjective style.

Suggested edit
-## Architecture: Three Bundle System
+## Architecture: Three-Bundle System
📝 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
## Architecture: Three Bundle System
## Architecture: Three-Bundle System
🧰 Tools
🪛 LanguageTool

[grammar] ~19-~19: Use a hyphen to join words.
Context: ...c rubocop -a ``` ## Architecture: Three Bundle System This project builds **thr...

(QB_NEW_EN_HYPHEN)

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

In `@CLAUDE.md` at line 19, Update the heading "Three Bundle System" to use a
hyphenated compound adjective by changing the text to "Three-Bundle System" in
the CLAUDE.md heading (locate the heading string "Three Bundle System" and
replace it with "Three-Bundle System").

Comment on lines +128 to +131
const chunks = [];
for (const file of chunkFiles) {
chunks.push(chunk.id, file);
}
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 bug: chunks.push(chunk.id, file) interleaves chunk ID and filename into the same flat array, producing [chunkId, filename]. The React flight client manifest format expects chunks to be an array of chunk filenames (or URLs) only — not pairs. The standard ReactFlightWebpackPlugin outputs ["filename.js"].

If the React runtime tries to resolve chunk.id (a number) as a URL, client components will silently fail to hydrate. Double-check against the manifest format that react-on-rails-rsc consumes — it may expect just the filename:

Suggested change
const chunks = [];
for (const file of chunkFiles) {
chunks.push(chunk.id, file);
}
const chunks = [...chunkFiles];

Comment on lines +16 to +23
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);
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code duplication: extractLoader is defined identically in serverWebpackConfig.js (lines 9–15), which even exports it at the bottom (module.exports.extractLoader = extractLoader). Import from there instead of redeclaring:

Suggested change
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);
});
}
const { extractLoader } = require('./serverWebpackConfig');

Or better yet, move the utility to bundlerUtils.js so neither config file exports implementation details of the other.

// The RSC bundle runs server components in the Node renderer and produces
// the Flight payload that React uses to hydrate on the client.
//
// Unlike the server bundle (which uses ExecJS), the RSC bundle targets Node.js
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 comment — ExecJS was removed in this PR. The server bundle now runs in the Node renderer's VM sandbox, not ExecJS.

Suggested change
// Unlike the server bundle (which uses ExecJS), the RSC bundle targets Node.js
// Unlike the server bundle (which runs in the Node renderer's VM sandbox),
// the RSC bundle targets full Node.js and can use built-in modules (os, fs, path, etc.) directly.

...sanitizeHtml.defaults.allowedAttributes,
img: ['src', 'alt', 'title', 'width', 'height'],
},
allowedSchemes: ['https', 'http'],
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Allowing http enables mixed-content rendering (HTTP images on an HTTPS page) and is generally discouraged. For a tutorial that others learn from, ['https'] only is a safer default:

Suggested change
allowedSchemes: ['https', 'http'],
allowedSchemes: ['https'],

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: 6429ce0cae

ℹ️ 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 +187 to +188
' this.port2={postMessage:function(msg){',
' var p=this._port1;if(p.onmessage)p.onmessage({data:msg});',
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 Make MessageChannel polyfill asynchronous

The injected MessageChannel shim invokes port1.onmessage synchronously inside postMessage, but React 19's server scheduler relies on MessageChannel to schedule work on a later tick. In larger SSR/RSC streams this synchronous re-entry can recurse (postMessage immediately runs the handler, which schedules again) and lead to stack overflows or renderer stalls/timeouts. Queue the handler asynchronously (for example via setImmediate/setTimeout(0)/microtask) so the polyfill preserves actual MessageChannel behavior.

Useful? React with 👍 / 👎.

- Import react-on-rails-pro in rsc-bundle.js to initialize RSC support
  (resolves to ReactOnRailsRSC.js with react-server condition)
- Move @rspack/dev-server to devDependencies where it belongs
- Add QA_PLAYBOOK.md with manual testing checklist for all rendering modes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
return rule.use.find((item) => {
const testValue = typeof item === 'string' ? item : item?.loader;
return testValue && testValue.includes(loaderName);
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Duplicate function: extractLoader is already defined and exported from serverWebpackConfig.js. Import it from there instead of redefining it here.

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

Then remove the local definition (lines 15–21).

allowedSchemes: ['https', 'http'],
});

return (
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 / mixed content: allowedSchemes includes 'http', which permits images loaded over insecure connections. When the app is served over HTTPS this will trigger mixed-content warnings and browsers will block the image. Restrict to 'https' only.

Suggested change
return (
allowedSchemes: ['https'],

logLevel: process.env.RENDERER_LOG_LEVEL || 'debug',
password: rendererPassword,
port: process.env.RENDERER_PORT || 3800,
supportModules: 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.

Type bug: process.env.RENDERER_PORT is always a string when set. The || fallback only applies when it's unset/empty, so a configured port is passed as a string (e.g. "3800") while the default is the number 3800. Depending on how the renderer internally binds the port this may work, but it's fragile.

Suggested change
supportModules: true,
port: Number(process.env.RENDERER_PORT) || 3800,

@@ -82,6 +83,20 @@ jobs:
- name: Build shakapacker chunks
run: NODE_ENV=development bundle exec bin/shakapacker

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Observability: The renderer's stdout/stderr goes to the runner's default output mixed with the CI step log, making it hard to diagnose start-up failures. Redirect to a log file so it can be shown only on failure:

Suggested change
node react-on-rails-pro-node-renderer.js > /tmp/node-renderer.log 2>&1 &

Then add a step after the readiness check (or in an if: failure() step) to cat /tmp/node-renderer.log so failures are captured.

arch: os.arch(),
nodeVersion: process.version,
uptime: Math.floor(os.uptime() / 3600),
totalMemory: (os.totalmem() / (1024 * 1024 * 1024)).toFixed(1),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Info disclosure (low, demo context): hostname leaks the server's machine name to any visitor. Fine for a tutorial/demo, but worth keeping out of any production deployment. Consider omitting it or replacing it with a sanitised label before this demo is referenced as a production template.

@claude
Copy link
Copy Markdown

claude bot commented Apr 8, 2026

PR Review: Fix SSR runtime failures for React Server Components

Overall this is a well-structured PR. The root-cause analysis (VM sandbox has no require/MessageChannel, CI never started the Node renderer, RouterApp.server.jsx misclassified as RSC) is solid, and the fixes are targeted. The three-bundle architecture and its constraints are now clearly documented in CLAUDE.md.

Issues found

Severity Location Summary
Bug config/webpack/rscWebpackConfig.js:15–21 extractLoader is duplicated — already defined and exported from serverWebpackConfig.js
Security CommentsFeed.jsx:89 allowedSchemes includes 'http', causing mixed-content blocks when served over HTTPS
Bug (minor) react-on-rails-pro-node-renderer.js:16 RENDERER_PORT from env is a string; use Number() to avoid type mismatch
Observability .github/workflows/rspec_test.yml:85 Node renderer stdout is not redirected — failures are hard to diagnose
Info disclosure ServerInfo.jsx:14 hostname is exposed to visitors (low severity, fine for a tutorial demo)

Inline comments posted for each item above.

Other notes

  • The MessageChannel BannerPlugin polyfill correctly matches React's scheduler usage pattern (React posts on port2, listens on port1).
  • The resolve.fallback: false revert (vs externals) is the right call given the VM sandbox has no require — the comment in serverWebpackConfig.js explains this well.
  • The 30-second TCP readiness check in CI is a robust approach.
  • libraryTarget: 'commonjs2' is correctly absent from the RSC bundle since it targets full Node.js, not the VM sandbox.

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