Skip to content

Fix SSR runtime failures for React Server Components#723

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

Fix SSR runtime failures for React Server Components#723
justin808 wants to merge 11 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 because it changes SSR/RSC rendering infrastructure, build configs, and CI/dev runtime by introducing a Node renderer service and an additional bundling pipeline, which can impact production rendering and asset compilation if misconfigured.

Overview
Fixes SSR/RSC runtime failures by migrating from react_on_rails to react_on_rails_pro with the NodeRenderer and wiring the renderer into runtime/CI.

Adds React Server Components support end-to-end: new rsc_payload_route, a new server-components demo page, and new client/RSC pack entrypoints that register server components and required client components.

Updates the build pipeline to produce three bundles (client, server SSR, and RSC) via new rscWebpackConfig.js plus a custom RspackRscPlugin to emit RSC manifests, and adjusts client/server configs with Node-builtin fallbacks and a MessageChannel polyfill for the Node-renderer VM. CI and Procfile.dev now start the Node renderer, and .gitignore excludes its bundle cache.

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

Summary by CodeRabbit

  • New Features
    • Added an RSC Demo page (linked from navigation) showcasing server-rendered info, streamed comments, and interactive client-hydrated panels.
  • Updates
    • Integrated server-side rendering/RSC infrastructure and local renderer support to enable streaming and selective hydration.
    • Switched to the pro server-rendering integration for improved RSC/authentication handling and runtime behavior.

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

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4d48fd2d-5d7f-49ff-a476-7b3e6556463d

📥 Commits

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

⛔ 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 (2)
  • Gemfile
  • package.json
🚧 Files skipped from review as they are similar to previous changes (2)
  • Gemfile
  • package.json

Walkthrough

Adds React Server Components support with a Node.js renderer and RSC bundling: new RSC webpack config and plugin, renderer startup script and CI step, Rails initializer/routes/view/controller for an RSC demo page, migrations to react-on-rails-pro, new server/client RSC components, and pack/pack registration changes.

Changes

Cohort / File(s) Summary
CI & Dev tooling
\.github/workflows/rspec_test.yml, .gitignore, Procfile.dev
Workflow: adds RENDERER_PASSWORD env and a step to start react-on-rails-pro-node-renderer.js and wait for localhost:3800; gitignore: ignores .node-renderer-bundles/; Procfile.dev: adds wp-rsc and node-renderer processes.
Gems & npm deps
Gemfile, package.json
Swapped react_on_railsreact_on_rails_pro in Gemfile; package.json: adds react-on-rails-pro, react-on-rails-pro-node-renderer, react-on-rails-rsc, pins React to 19.0.4, and adds node-renderer npm script.
Renderer startup
react-on-rails-pro-node-renderer.js
New Node script that configures and launches the react-on-rails-pro node renderer (bundle cache dir, port, password, worker counts, logging).
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 configures ReactOnRailsPro (NodeRenderer, renderer_url, renderer_password, enable_rsc_support, rsc bundle filename); adds rsc_payload_route and GET /server-components route; adds server_components action and view rendering 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 RSC client components; updates packs to register/consume RSC payloads and 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 layout plus ServerInfo (node runtime info), CommentsFeed (async fetch, markdown→HTML, sanitize, streaming-friendly), and TogglePanel (client interactive toggle with propTypes).
Client code and directives
client/app/bundles/.../*, client/app/bundles/comments/startup/*, client/app/bundles/comments/components/*/Footer.jsx, client/app/bundles/comments/rescript/.../RescriptShow.jsx
Many client modules now include 'use client'; several startup and component files replace imports from react-on-railsreact-on-rails-pro.
Navigation & constants
client/app/bundles/comments/components/NavigationBar/NavigationBar.jsx, client/app/bundles/comments/constants/paths.js, client/app/bundles/comments/components/SimpleCommentScreen/...
Adds SERVER_COMPONENTS_PATH constant and an "RSC Demo" nav link; updates components to use pro integration for auth headers.
Requests & store registration
client/app/libs/requestsManager.js, client/app/packs/stores-registration.js
Switched react-on-railsreact-on-rails-pro in request/store registration code; request headers still use ReactOnRails.authenticityHeaders().
Client webpack changes
config/webpack/clientWebpackConfig.js, config/webpack/commonWebpackConfig.js
Client config: add RspackRscPlugin client manifest generation, remove server/rsc entries from client entry, set fallbacks for Node builtins; common config: alias react-on-railsreact-on-rails-pro.
RSC webpack & plugin
config/webpack/rscWebpackConfig.js, config/webpack/rspackRscPlugin.js
New RSC webpack config to build rsc-bundle.js (no CSS emit, exportOnlyLocals, emitFile:false, react-server resolution, single-chunk); new RspackRscPlugin to emit react-client/server manifest JSON by scanning modules for 'use client'.
Server webpack & orchestration
config/webpack/serverWebpackConfig.js, config/webpack/webpackConfig.js
Server config: add extractLoader helper, Node builtins fallback, BannerPlugin polyfill, and change exports to { default: configureServer, extractLoader }; top-level webpackConfig refactored to handle RSC-only mode via RSC_BUNDLE_ONLY and return three-config output when appropriate.
Miscellaneous
client/app/bundles/comments/components/NavigationBar/..., other small files
Various small JSX/ESLint adjustments and registration updates to support RSC flow and pro package usage.

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 request)
    activate NodeRend

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

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

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

    Client->>Client: Load client packs, hydrate TogglePanel (useState)
    Client->>Client: Stream comments via Suspense (show skeleton)
Loading
sequenceDiagram
    participant Bundler as Webpack/Rspack
    participant RscPlugin as RspackRscPlugin
    participant FS as File System

    Bundler->>Bundler: Start build
    Bundler->>FS: Read entries (client/server/rsc)
    activate RscPlugin
    RscPlugin->>FS: Scan modules for "'use client'" directive
    FS-->>RscPlugin: File contents
    RscPlugin->>Bundler: Emit react-client-manifest.json / react-server-client-manifest.json
    deactivate RscPlugin
    Bundler->>FS: Output bundles (client, server, rsc)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 Hop-scotch through code, I drum and hum,

New bundles bloom and node renderers come,
Server whispers stream, client toggles play,
I nibble 'use client' and watch features sway,
A crunchy carrot of RSC—hip, hop, hooray!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically addresses the main change: fixing SSR runtime failures for React Server Components through multiple architectural updates including Node renderer integration, webpack configuration, and component directives.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch jg/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.


Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

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

Reviewed by Cursor Bugbot for commit 1022234. Configure here.

@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

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

Choose a reason for hiding this comment

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

P1 data: URI scheme enables XSS on anchor elements in user content

allowedSchemes: ['https', 'http', 'data'] is a global setting in sanitize-html — it applies to every allowed tag, not just <img>. The default allowedTags include <a>, so a comment containing <a href="data:text/html,<script>alert(1)</script>">click</a> passes sanitization and executes JavaScript when clicked. The data: scheme is intentionally absent from sanitize-html's own defaults for this exact reason.

If data URIs are needed for embedded images, scope them to img.src only via allowedSchemesByTag rather than the global allowedSchemes:

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', 'data'],
});
const safeHtml = sanitizeHtml(rawHtml, {
allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img']),
allowedAttributes: {
...sanitizeHtml.defaults.allowedAttributes,
img: ['src', 'alt', 'title', 'width', 'height'],
},
allowedSchemes: ['https', 'http'],
allowedSchemesByTag: {
img: ['https', 'http', 'data'],
},

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.

P2 Incomplete MessageChannel polyfill may throw on future React scheduler changes

The polyfill only wires port2.postMessage → port1.onmessage, which covers React's current scheduler pattern (port1.onmessage = handler; port2.postMessage(null)). However it leaves port1.postMessage, and both ports' start() / close() methods completely undefined. Any code calling these will throw TypeError: port1.postMessage is not a function — this won't surface in today's tests but could silently break with a React update.

A more complete stub:

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"){',
' var _noop=function(){};',
' var _port=function(){return{onmessage:null,postMessage:_noop,start:_noop,close:_noop};};',
' globalThis.MessageChannel=class MessageChannel{',
' constructor(){',
' this.port1=_port();',
' this.port2=_port();',
' var _p1=this.port1;',
' this.port2.postMessage=function(msg){',
' if(_p1.onmessage)_p1.onmessage({data:msg});',
' };',
' }',
' };',
'}',
].join('\n'),
raw: true,
}),
);

# RSC Rspack watcher for React Server Components bundle
wp-rsc: RSC_BUNDLE_ONLY=true bin/shakapacker --watch
# React on Rails Pro Node renderer for SSR and RSC payload generation
node-renderer: RENDERER_PASSWORD=devPassword node react-on-rails-pro-node-renderer.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.

P2 RENDERER_PASSWORD hardcoded in a tracked file

RENDERER_PASSWORD=devPassword is committed in plain text. Anyone who clones the repo immediately knows the renderer password. For a tutorial project the risk is low, but it establishes a pattern of secrets living in tracked files. Since react-on-rails-pro-node-renderer.js already falls back to 'devPassword' when the env var is unset, removing the explicit assignment from Procfile.dev costs nothing and encourages developers to set a real password in an untracked .env file for non-default deployments:

Suggested change
node-renderer: RENDERER_PASSWORD=devPassword node react-on-rails-pro-node-renderer.js
node-renderer: node react-on-rails-pro-node-renderer.js

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

Comment on lines +5 to +6
config.server_renderer = "NodeRenderer"
config.renderer_url = ENV["REACT_RENDERER_URL"] || "http://localhost:3800"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Gate NodeRenderer usage to environments that start it

This change hard-switches React on Rails to NodeRenderer at http://localhost:3800, which makes every prerendered render depend on an external renderer service, but the commit only starts that service in CI and Procfile.dev. Run profiles that still launch only Rails (for example the default Procfile) will now hit renderer timeouts on prerendered pages because nothing is listening on port 3800. Consider conditionally enabling NodeRenderer only when configured, or updating all supported process profiles to run the renderer sidecar.

Useful? React with 👍 / 👎.

# Node renderer for server-side rendering and RSC payload generation
config.server_renderer = "NodeRenderer"
config.renderer_url = ENV["REACT_RENDERER_URL"] || "http://localhost:3800"
config.renderer_password = ENV.fetch("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: Hardcoded fallback password

ENV.fetch with a second argument silently falls back to "devPassword" if the env var is unset. If this initializer ever loads in a staging or production environment without RENDERER_PASSWORD configured, the renderer accepts a well-known credential.

Consider raising in non-development environments:

Suggested change
config.renderer_password = ENV.fetch("RENDERER_PASSWORD", "devPassword")
config.renderer_password = if Rails.env.development? || Rails.env.test?
ENV.fetch("RENDERER_PASSWORD", "devPassword")
else
ENV.fetch("RENDERER_PASSWORD")
end

const config = {
serverBundleCachePath: path.resolve(__dirname, '.node-renderer-bundles'),
logLevel: process.env.RENDERER_LOG_LEVEL || 'debug',
password: process.env.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: Hardcoded fallback password

Same issue as the Rails initializer — process.env.RENDERER_PASSWORD || 'devPassword' silently falls back to a well-known credential. If NODE_ENV is available, guard the fallback:

Suggested change
password: process.env.RENDERER_PASSWORD || 'devPassword',
password: process.env.RENDERER_PASSWORD || (process.env.NODE_ENV === 'production' ? (() => { throw new Error('RENDERER_PASSWORD env var is required in production'); })() : 'devPassword'),

Or more readably, add an explicit check before the config object:

if (process.env.NODE_ENV === 'production' && !process.env.RENDERER_PASSWORD) {
  throw new Error('RENDERER_PASSWORD env var is required in production');
}

allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img']),
allowedAttributes: {
...sanitizeHtml.defaults.allowedAttributes,
img: ['src', 'alt', 'title', 'width', 'height'],
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: data: URI scheme

Adding 'data' to allowedSchemes is risky. While the current allowedTags set limits exposure, sanitize-html configs tend to expand over time. If <a> ever ends up in allowedTags with href allowed, data:text/html,<script>...</script> becomes a viable XSS vector.

data: URIs in <img src> are the only realistic use case, and browsers support blob: as a safer alternative for that. Unless there is a specific need, drop 'data' here:

Suggested change
img: ['src', 'alt', 'title', 'width', 'height'],
allowedSchemes: ['https', 'http'],

import _ from 'lodash';

async function ServerInfo() {
const serverInfo = {
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: Sensitive system info exposed without environment guard

This component renders os.hostname(), total/free RAM, CPU count, and Node version — information that is useful for attackers performing reconnaissance. There is no guard preventing it from rendering in production.

Even in a demo repo, add an explicit development check:

Suggested change
const serverInfo = {
if (process.env.NODE_ENV === 'production') {
return <p className="text-red-600">ServerInfo is disabled in production.</p>;
}
const serverInfo = {

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.

Code duplication: extractLoader is already defined in serverWebpackConfig.js

This function is identical to the one exported from serverWebpackConfig.js. Rather than duplicating it, extract it to a shared utility — bundlerUtils.js or a new webpackUtils.js — and import from both configs:

// webpackUtils.js
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);
  });
}
module.exports = { extractLoader };

Then in both configs: const { extractLoader } = require('./webpackUtils');

- name: Start Node renderer for SSR
run: |
node react-on-rails-pro-node-renderer.js &
echo "Waiting for Node renderer on port 3800..."
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

CI reliability: nc may not be installed

nc (netcat) is not present on all GitHub-hosted Ubuntu runners by default. If the runner image doesn't include it, the check silently fails (nc not found → 2>/dev/null swallows the error → loop runs to timeout).

Use Bash's built-in /dev/tcp instead, which is always available:

Suggested change
echo "Waiting for Node renderer on port 3800..."
if bash -c '</dev/tcp/localhost/3800' 2>/dev/null; then

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 +28 to +32
const comments = await response.json();

// Use lodash to process (stays on server)
const sortedComments = _.orderBy(comments, ['created_at'], ['desc']);
const recentComments = _.take(sortedComments, 10);
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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the JSON structure returned by the comments endpoint
cat app/views/comments/index.json.jbuilder
cat app/views/comments/_comment.json.jbuilder

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 297


Bug: JSON response structure mismatch.

The Rails API returns { comments: [...] } (via jbuilder at app/views/comments/index.json.jbuilder), but line 28 assigns the entire response object to comments. The subsequent _.orderBy on line 31 operates on an object instead of the array, causing incorrect sorting behavior.

🐛 Proposed fix
-  const comments = await response.json();
+  const data = await response.json();
+  const comments = data.comments;
 
   // Use lodash to process (stays on server)
   const sortedComments = _.orderBy(comments, ['created_at'], ['desc']);
📝 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 comments = await response.json();
// Use lodash to process (stays on server)
const sortedComments = _.orderBy(comments, ['created_at'], ['desc']);
const recentComments = _.take(sortedComments, 10);
const data = await response.json();
const comments = data.comments;
// Use lodash to process (stays on server)
const sortedComments = _.orderBy(comments, ['created_at'], ['desc']);
const recentComments = _.take(sortedComments, 10);
🤖 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 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.

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 +24 to +28
// React Server Components registration (client-side)
registerServerComponent(
{ rscPayloadGenerationUrlPath: 'rsc_payload/' },
'ServerComponentsPage',
);
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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for registerServerComponent usage patterns and type definitions
rg -n "registerServerComponent" --type=js --type=ts -C3

# Check if there's documentation or type definitions
fd -i "registerServerComponent" --exec cat {}

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 1828


🏁 Script executed:

#!/bin/bash
# Search for type definitions or JSDoc for registerServerComponent
rg -n "registerServerComponent" --type=ts --type=tsx -B5 -A10 | head -100

# Look for .d.ts files related to react-on-rails-pro
fd -i "\.d\.ts$" | xargs grep -l "registerServerComponent" 2>/dev/null || echo "No .d.ts files found"

# Check for any test files that show usage
fd -i "test\|spec" --type=f | xargs grep -l "registerServerComponent" 2>/dev/null || echo "No test files found"

# Look for any documentation files
fd -i "readme\|docs" --type=f -exec grep -l "registerServerComponent" {} \; 2>/dev/null || echo "No docs found"

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 181


🏁 Script executed:

#!/bin/bash
# Check package.json for react-on-rails-pro
cat package.json | grep -A2 -B2 "react-on-rails-pro"

# Look for node_modules
if [ -d "node_modules/react-on-rails-pro" ]; then
  echo "=== Found react-on-rails-pro in node_modules ==="
  find node_modules/react-on-rails-pro -name "*.ts" -o -name "*.js" -o -name "*.d.ts" | head -20
  
  # Check for index or type files
  ls -la node_modules/react-on-rails-pro/ 2>/dev/null | head -20
fi

# Look for any comments in the bundle files
echo "=== Checking stimulus-bundle.js for comments ==="
cat -n client/app/packs/stimulus-bundle.js

echo ""
echo "=== Checking rsc-bundle.js for comments ==="
cat -n client/app/packs/rsc-bundle.js

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 4579


🏁 Script executed:

#!/bin/bash
# Check the client type definition
echo "=== client.d.ts ===" 
cat node_modules/react-on-rails-pro/lib/registerServerComponent/client.d.ts

echo ""
echo "=== client.js ===" 
cat node_modules/react-on-rails-pro/lib/registerServerComponent/client.js

echo ""
echo "=== server.d.ts ===" 
cat node_modules/react-on-rails-pro/lib/registerServerComponent/server.d.ts

echo ""
echo "=== server.js ===" 
cat node_modules/react-on-rails-pro/lib/registerServerComponent/server.js

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 5544


Fix the registerServerComponent client API call signature.

The client API accepts variadic string arguments for component names only: registerServerComponent(...componentNames: string[]). The current code incorrectly passes an options object as the first argument. Use:

registerServerComponent('ServerComponentsPage');

The rscPayloadGenerationUrlPath is not a configuration parameter for this function; it defaults to 'rsc_payload/' internally.

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

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

Comment on lines +3 to +7
ReactOnRailsPro.configure do |config|
# Node renderer for server-side rendering and RSC payload generation
config.server_renderer = "NodeRenderer"
config.renderer_url = ENV["REACT_RENDERER_URL"] || "http://localhost:3800"
config.renderer_password = ENV.fetch("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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, find the initializer file and the JavaScript renderer
find . -name "react_on_rails_pro.rb" -o -name "*node-renderer*" | head -20

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 608


🏁 Script executed:

# Also check if there are any configuration files or setup
git ls-files | grep -E "(react|renderer|config)" | head -30

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 948


🏁 Script executed:

# Look for the ReactOnRailsPro configuration class
rg "ReactOnRailsPro" --type rb -l

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 108


🏁 Script executed:

# Read the Ruby initializer file
cat -n ./config/initializers/react_on_rails_pro.rb

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 699


🏁 Script executed:

# Read the JavaScript renderer to check for || fallbacks
cat -n ./react-on-rails-pro-node-renderer.js

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 752


🏁 Script executed:

# Check the node_modules version of the renderer too
cat -n ./node_modules/react-on-rails-pro-node-renderer/src/default-node-renderer.ts | head -100

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 416


🏁 Script executed:

# Check how renderer_url and renderer_password are actually used in the codebase
rg "renderer_url|renderer_password" --type rb -B 2 -A 2

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 108


🏁 Script executed:

# Look for the actual communication with the renderer
rg "REACT_RENDERER_URL|RENDERER_PASSWORD" --type rb

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 108


🏁 Script executed:

# Check if there's a gem or library that uses these config values
rg "ReactOnRailsPro" --type rb -A 5 -B 5

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 108


🏁 Script executed:

# Search for renderer_url and renderer_password usage
rg "renderer_url|renderer_password" -A 2 -B 2

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 642


🏁 Script executed:

# Search for environment variable references
rg "REACT_RENDERER_URL|RENDERER_PASSWORD"

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 512


🏁 Script executed:

# Look for ReactOnRailsPro usage
rg "ReactOnRailsPro" -A 5 -B 5

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 752


🏁 Script executed:

# Test Ruby ENV behavior with blank strings
python3 << 'EOF'
# Verify the actual Ruby ENV behavior with empty strings
# In Ruby, ENV["key"] returns nil if not set, and a string (even empty) if set
# ENV.fetch("key", default) returns the value if set, or the default if not set

test_cases = [
    ("ENV['VAR'] when VAR not set", "nil"),
    ("ENV['VAR'] when VAR=''", "empty string ''"),
    ("ENV.fetch('VAR', 'default') when VAR not set", "'default'"),
    ("ENV.fetch('VAR', 'default') when VAR=''", "empty string ''"),
    ("ENV['VAR'] || 'default' when VAR not set", "'default'"),
    ("ENV['VAR'] || 'default' when VAR=''", "empty string '' (because '' is falsy in Ruby? NO - empty string is truthy!)"),
]

print("Ruby ENV behavior:")
for test, result in test_cases:
    print(f"  {test}")
    print(f"    → {result}")
    print()
EOF

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 499


🏁 Script executed:

# Verify JavaScript empty string behavior (different from Ruby)
node << 'EOF'
// In JavaScript, empty string IS falsy
console.log("JavaScript truthiness of empty string:");
console.log("'' || 'default' =", '' || 'default');
console.log("'value' || 'default' =", 'value' || 'default');
console.log("undefined || 'default' =", undefined || 'default');
EOF

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 208


🏁 Script executed:

# Check if there's any middleware or code that actually uses these config values
rg "config\.renderer_url|config\.renderer_password" -A 3

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 522


🏁 Script executed:

# Check if react-on-rails-pro gem is installed and see how it uses these config values
cat -n Gemfile | grep -A 5 -B 5 "react.*on.*rails"

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 425


🏁 Script executed:

# Look for any code that calls the renderer with these config values
rg "reactor|renderer" -i --max-count 20

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 2155


🏁 Script executed:

# Check the Procfile.dev to understand the setup
cat -n Procfile.dev

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 1176


🏁 Script executed:

# Check if there's documentation or source for react_on_rails_pro gem
find . -name "*.md" -o -name "*.txt" | xargs grep -l "REACT_RENDERER_URL\|renderer_password" 2>/dev/null

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 139


🏁 Script executed:

# Look for how the gem actually communicates with the renderer
rg "renderer_url|renderer_password" -i --type js --type ts

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 177


🏁 Script executed:

# Search for API calls or HTTP requests that use renderer_url/password
rg "fetch|http|request" -i | grep -i render | head -20

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 1131


🏁 Script executed:

# Let's verify Ruby's truthiness with empty strings more explicitly
ruby << 'EOF'
# Test Ruby truthiness with empty strings
puts "Testing Ruby empty string truthiness:"
puts "'' is truthy: #{"" ? "yes" : "no"}"
puts
puts "ENV behavior with empty string:"
ENV["TEST_VAR"] = ""
puts "ENV['TEST_VAR'] = ''"
puts "ENV['TEST_VAR'] || 'default' = #{ENV["TEST_VAR"] || "default"}"
puts
ENV["TEST_VAR"] = nil
puts "ENV['TEST_VAR'] = nil (not set)"
puts "ENV['TEST_VAR'] || 'default' = #{ENV["TEST_VAR"] || "default"}"
puts
puts "ENV.fetch behavior:"
ENV["TEST_VAR"] = ""
puts "ENV.fetch('TEST_VAR', 'default') when TEST_VAR='' = #{ENV.fetch("TEST_VAR", "default")}"
ENV.delete("TEST_VAR")
puts "ENV.fetch('TEST_VAR', 'default') when TEST_VAR not set = #{ENV.fetch("TEST_VAR", "default")}"
EOF

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 493


🏁 Script executed:

# Check the README for react-on-rails-pro-node-renderer to understand expected configuration
cat ./node_modules/react-on-rails-pro-node-renderer/README.md | head -80

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 2470


🏁 Script executed:

# Look for tests or examples showing how the gem expects these to be configured
find . -name "*.md" -o -name "*.rb" | xargs grep -l "ReactOnRailsPro.configure" 2>/dev/null

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 183


🏁 Script executed:

# Check if there are any environment-specific configuration files or documentation
find . -name "*REACT_RENDERER*" -o -name "*renderer*config*" 2>/dev/null | head -20

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 64


🏁 Script executed:

# Look for any GitHub issues or comments about this configuration
rg "REACT_RENDERER_URL|renderer_url" -i --max-count 10

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 200


🏁 Script executed:

# Double-check what the library docs actually recommend - look at the exact configuration example
grep -A 10 "ReactOnRailsPro.configure" ./node_modules/react-on-rails-pro-node-renderer/README.md

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 419


🏁 Script executed:

# Verify if there are any production deployments or CI configuration that show expected setup
find . -name ".env*" -o -name "*.yml" | xargs grep -l "REACT_RENDERER_URL\|RENDERER_PASSWORD" 2>/dev/null

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 116


🏁 Script executed:

# Check CI configuration for environment setup
cat -n config/ci.rb

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 791


🏁 Script executed:

# Final verification: check if there are any .presence calls or environment-aware configuration patterns in the codebase
rg "\.presence|Rails\.env\." config/initializers/ | head -20

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 471


🏁 Script executed:

# Check if there are any production deployment guides or .env.production examples
find . -name ".env*" -o -name "*.example" | head -20

Repository: shakacode/react-webpack-rails-tutorial

Length of output: 64


Use ENV.fetch() consistently to match library documentation and prevent empty string handling mismatches.

Line 6 uses ENV["REACT_RENDERER_URL"] || "..." while the react-on-rails-pro-node-renderer documentation recommends ENV.fetch() for both. This matters because Ruby treats empty strings as truthy (so "" || default returns ""), while JavaScript treats them as falsy (so "" || default returns default). If REACT_RENDERER_URL="", the Rails side keeps an empty URL while the Node renderer falls back to localhost, causing silent connection failures.

Suggested change
 ReactOnRailsPro.configure do |config|
   # Node renderer for server-side rendering and RSC payload generation
   config.server_renderer = "NodeRenderer"
-  config.renderer_url = ENV["REACT_RENDERER_URL"] || "http://localhost:3800"
+  config.renderer_url = ENV.fetch("REACT_RENDERER_URL", "http://localhost:3800")
   config.renderer_password = ENV.fetch("RENDERER_PASSWORD", "devPassword")
📝 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
ReactOnRailsPro.configure do |config|
# Node renderer for server-side rendering and RSC payload generation
config.server_renderer = "NodeRenderer"
config.renderer_url = ENV["REACT_RENDERER_URL"] || "http://localhost:3800"
config.renderer_password = ENV.fetch("RENDERER_PASSWORD", "devPassword")
ReactOnRailsPro.configure do |config|
# Node renderer for server-side rendering and RSC payload generation
config.server_renderer = "NodeRenderer"
config.renderer_url = ENV.fetch("REACT_RENDERER_URL", "http://localhost:3800")
config.renderer_password = ENV.fetch("RENDERER_PASSWORD", "devPassword")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@config/initializers/react_on_rails_pro.rb` around lines 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.

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>
...sanitizeHtml.defaults.allowedAttributes,
img: ['src', 'alt', 'title', 'width', 'height'],
},
allowedSchemes: ['https', 'http', 'data'],
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: overly-permissive data: scheme in sanitize-html

allowedSchemes: ['https', 'http', 'data'] permits any data: URI, including data:text/html,<script>alert(1)</script>. An attacker who injects a malicious markdown comment could exploit this since sanitize-html does not validate the MIME type of data: URIs — it only checks the scheme prefix.

Suggested change
allowedSchemes: ['https', 'http', 'data'],
allowedSchemes: ['https', 'http'],
allowedSchemesByTag: {
img: ['https', 'http', 'data'],
},

This restricts data: URIs to <img> tags only, which is the only plausible legitimate use case here. If data: images aren't required at all, removing it from allowedSchemes entirely is the safer choice.

# Node renderer for server-side rendering and RSC payload generation
config.server_renderer = "NodeRenderer"
config.renderer_url = ENV["REACT_RENDERER_URL"] || "http://localhost:3800"
config.renderer_password = ENV.fetch("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: hardcoded devPassword fallback reachable in production

ENV.fetch("RENDERER_PASSWORD", "devPassword") silently uses "devPassword" if the env var is not set — including in production. Unlike ENV.fetch with no default (which raises KeyError if missing), this gives no indication that authentication is effectively disabled.

Suggested change
config.renderer_password = ENV.fetch("RENDERER_PASSWORD", "devPassword")
config.renderer_password = ENV.fetch("RENDERER_PASSWORD")

Removing the fallback will raise KeyError at boot if RENDERER_PASSWORD is absent, making misconfiguration loud and obvious rather than silently insecure. For local development, set it explicitly in .env or Procfile.dev.

// Check for 'use client' as the first statement.
// Allow comments (single-line // or block /* */) before the directive.
result = /^(?:\s*(?:\/\/[^\n]*\n|\/\*[\s\S]*?\*\/))*\s*['"]use client['"]/.test(head);
} catch (_) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: silent catch masks real I/O errors, causing false negatives

The empty catch swallows all exceptions — not just ENOENT. A permissions error (EACCES), a read error mid-file, or an out-of-memory condition will silently leave result = false, so a valid 'use client' file is treated as a server component and excluded from the manifest. This is a hard-to-diagnose runtime failure.

Suggested change
} catch (_) {
} catch (e) {
if (e.code !== 'ENOENT') {
// Log unexpected errors (permissions, I/O failures) without crashing the build
console.warn(`[RspackRscPlugin] Could not read ${filePath}: ${e.message}`);
}

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

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

Choose a reason for hiding this comment

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

Potential issue: module-level cache is shared across parallel compilations

useClientCache is a module-level singleton. While thisCompilation clears it at the start of each build, if two compilations run concurrently (e.g., the client and RSC bundles built in parallel via webpack --config with --parallel, or a watch-mode rebuild overlapping with an initial build), one compilation's clear() will wipe entries mid-flight for the other.

Moving the cache onto the plugin instance eliminates the race:

Suggested change
const useClientCache = new Map();
// (module-level useClientCache removed — see constructor)

Then in the constructor:

this._useClientCache = new Map();

And update hasUseClientDirective to be an instance method reading this._useClientCache, and change the thisCompilation clear to this._useClientCache.clear(). This gives each plugin instance its own isolated cache.


- name: Start Node renderer for SSR
run: |
node react-on-rails-pro-node-renderer.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.

CI: Node renderer output is discarded, making failures undiagnosable

The background process started with & sends both stdout and stderr to /dev/null (GitHub Actions default for backgrounded steps). If the renderer crashes between the TCP readiness check and the rspec run — or logs a startup warning that points to a misconfiguration — there's no way to see it.

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

Then add a log dump on failure:

          echo "Node renderer failed to start within 30 seconds"
          echo "--- node-renderer.log ---"
          cat node-renderer.log || true
          exit 1

This preserves renderer output for the failure case without adding noise to successful runs.

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.

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

Choose a reason for hiding this comment

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

'use client' in a .server.jsx file is intentional but needs explanation

The PR description explains the reasoning (the RSC bundler would otherwise treat this as a React Server Component, but it uses ReactOnRails.getStore() which is client-only). However, any future reader will be confused by a .server.jsx file with a 'use client' directive.

A short comment at the top would prevent this from looking like a mistake:

Suggested change
'use client';
'use client';
// NOTE: Despite the .server.jsx filename (traditional SSR using StaticRouter),
// this directive is required to prevent the RSC bundler from treating this as a
// React Server Component. It calls ReactOnRails.getStore() (client-only API)
// and must be registered via ReactOnRails.register(), not registerServerComponent().

if (cssLoader?.options?.modules) {
cssLoader.options.modules = { ...cssLoader.options.modules, exportOnlyLocals: true };
}
} else if (
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Incomplete handling: rule.loader (top-level) case is missed for url/file-loader

The else if branch checks rule.use.loader — the case where rule.use is an object with a loader key. But webpack/Rspack also allows rule.loader directly on the rule itself (shorthand for rule.use: { loader: '...' }). Rules configured that way will silently skip the emitFile: false opt-out, causing url-loader/file-loader to emit asset files from the RSC bundle into the output directory unexpectedly.

Suggested change
} else if (
} else if (
typeof rule.loader === 'string'
&& (rule.loader.includes('url-loader') || rule.loader.includes('file-loader'))
) {
rule.options = { ...(rule.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 };
}

@claude
Copy link
Copy Markdown

claude bot commented Apr 6, 2026

Review: Fix SSR runtime failures for React Server Components

This is a substantial and well-structured PR. The root causes identified in the description are correct, and the approach — NodeRenderer + server bundle with resolve.fallback, MessageChannel polyfill, and 'use client' on RouterApp.server.jsx — is sound. The new RSC demo page and plugin are useful additions.

I've left inline comments on the issues below.

Must Fix

File Issue
🔴 CommentsFeed.jsx:59 data: scheme in sanitize-html allows data:text/html XSS — restrict to allowedSchemesByTag for img only
🔴 react_on_rails_pro.rb:7 ENV.fetch("RENDERER_PASSWORD", "devPassword") silently uses a hardcoded password in production if the env var is absent — remove the fallback
🔴 rspackRscPlugin.js:30 Empty catch swallows all I/O errors, not just ENOENT — a permissions or read error silently produces result = false, excluding valid 'use client' modules from the manifest

Should Fix

File Issue
🟡 rspackRscPlugin.js:13 Module-level useClientCache is shared across all plugin instances; concurrent client + RSC compilations can corrupt each other's cache — move to instance state
🟡 rscWebpackConfig.js:65 The emitFile: false loop misses the rule.loader shorthand form — url-loader/file-loader rules defined without rule.use will silently emit asset files from the RSC bundle
🟡 .github/workflows/rspec_test.yml:88 Background Node renderer output is discarded — use nohup ... > node-renderer.log 2>&1 and dump the log on startup timeout

Consider

File Issue
🔵 server_components.html.erb:6 Hardcoded id: "ServerComponentsPage-react-component-0" mirrors RoR's auto-generated pattern and will conflict if the ID scheme changes — omit it unless RSC payload routing requires it
🔵 RouterApp.server.jsx:1 'use client' in a .server.jsx file will confuse future readers — add a comment explaining the intentional contradiction

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

ℹ️ 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 +23 to +24
const baseUrl = process.env.RAILS_INTERNAL_URL || 'http://localhost:3000';
const response = await fetch(`${baseUrl}/comments.json`);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Remove localhost default for internal Rails API calls

The new RSC data fetch hardcodes http://localhost:3000 as the fallback origin, so any environment where Rails is not listening on that exact address (common in production, containers, and custom ports) will fail with connection errors and break the /server-components page when RAILS_INTERNAL_URL is unset. I also checked the repo and this variable is not configured anywhere else, so this path is currently the default behavior outside explicitly tuned deployments.

Useful? React with 👍 / 👎.

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