Fix SSR runtime failures for React Server Components#723
Fix SSR runtime failures for React Server Components#723
Conversation
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>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (2)
📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
WalkthroughAdds 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
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)
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)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
🚀 Quick Review App CommandsWelcome! Here are the commands you can use in this PR:
|
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
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 SummaryThis 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 Key changes:
Confidence Score: 3/5Mostly 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
Sequence DiagramsequenceDiagram
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
Reviews (1): Last reviewed commit: "Fix SSR runtime failures: Node renderer,..." | Re-trigger Greptile |
| const safeHtml = sanitizeHtml(rawHtml, { | ||
| allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img']), | ||
| allowedAttributes: { | ||
| ...sanitizeHtml.defaults.allowedAttributes, | ||
| img: ['src', 'alt', 'title', 'width', 'height'], | ||
| }, | ||
| allowedSchemes: ['https', 'http', 'data'], | ||
| }); |
There was a problem hiding this comment.
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:
| 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'], | |
| }, |
| 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, | ||
| }), | ||
| ); |
There was a problem hiding this comment.
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:
| 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 |
There was a problem hiding this comment.
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:
| node-renderer: RENDERER_PASSWORD=devPassword node react-on-rails-pro-node-renderer.js | |
| node-renderer: node react-on-rails-pro-node-renderer.js |
There was a problem hiding this comment.
💡 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".
| config.server_renderer = "NodeRenderer" | ||
| config.renderer_url = ENV["REACT_RENDERER_URL"] || "http://localhost:3800" |
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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:
| 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', |
There was a problem hiding this comment.
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:
| 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'], |
There was a problem hiding this comment.
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:
| img: ['src', 'alt', 'title', 'width', 'height'], | |
| allowedSchemes: ['https', 'http'], |
| import _ from 'lodash'; | ||
|
|
||
| async function ServerInfo() { | ||
| const serverInfo = { |
There was a problem hiding this comment.
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:
| const serverInfo = { | |
| if (process.env.NODE_ENV === 'production') { | |
| return <p className="text-red-600">ServerInfo is disabled in production.</p>; | |
| } | |
| const serverInfo = { |
| 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); | ||
| }); | ||
| } |
There was a problem hiding this comment.
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..." |
There was a problem hiding this comment.
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:
| echo "Waiting for Node renderer on port 3800..." | |
| if bash -c '</dev/tcp/localhost/3800' 2>/dev/null; then |
There was a problem hiding this comment.
Actionable comments posted: 8
🧹 Nitpick comments (9)
client/app/bundles/server-components/components/CommentsFeed.jsx (1)
59-59: Consider restrictingdata:scheme to specific MIME types.The
datascheme 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 DOMid.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.jsxfile.Per the PR objectives, this directive is intentionally added to ensure the component is registered via
ReactOnRails.register()(traditional SSR) rather thanregisterServerComponent()(RSC). This is necessary because the component uses client-only APIs likeReactOnRails.getStore().However, the filename
RouterApp.server.jsxwith a'use client'directive creates cognitive dissonance. Consider adding a comment explaining this apparent contradiction, or renaming the file to something likeRouterApp.ssr.jsxto 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
nohupwith 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.jsis itself a'use client'module that only runsReactOnRails.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.
extractLoaderis defined but only used withinconfigureRsc. 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-reassignlint errors are expected when modifying webpack rules. The mutations are intentional and safe sincecommonWebpackConfig()returns a fresh config each time. However, there's a potential edge case:
- Lines 65-70: The
else ifonly checksrule.use?.loaderbut some rules may haverule.loaderdirectly without ausewrapper.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
useClientCacheis 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
hasUseClientDirectivecould 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
hasUseClientDirectiveto be an instance method usingthis._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
⛔ Files ignored due to path filters (2)
Gemfile.lockis excluded by!**/*.lockyarn.lockis excluded by!**/yarn.lock,!**/*.lock
📒 Files selected for processing (36)
.github/workflows/rspec_test.yml.gitignoreGemfileProcfile.devapp/controllers/pages_controller.rbapp/views/pages/server_components.html.erbclient/app/bundles/comments/components/Footer/ror_components/Footer.jsxclient/app/bundles/comments/components/NavigationBar/NavigationBar.jsxclient/app/bundles/comments/components/SimpleCommentScreen/ror_components/SimpleCommentScreen.jsxclient/app/bundles/comments/constants/paths.jsclient/app/bundles/comments/rescript/ReScriptShow/ror_components/RescriptShow.jsxclient/app/bundles/comments/startup/App/ror_components/App.jsxclient/app/bundles/comments/startup/ClientRouterAppExpress.jsxclient/app/bundles/comments/startup/NavigationBarApp/ror_components/NavigationBarApp.jsxclient/app/bundles/comments/startup/RouterApp/ror_components/RouterApp.client.jsxclient/app/bundles/comments/startup/RouterApp/ror_components/RouterApp.server.jsxclient/app/bundles/comments/startup/serverRegistration.jsxclient/app/bundles/server-components/ServerComponentsPage.jsxclient/app/bundles/server-components/components/CommentsFeed.jsxclient/app/bundles/server-components/components/ServerInfo.jsxclient/app/bundles/server-components/components/TogglePanel.jsxclient/app/libs/requestsManager.jsclient/app/packs/rsc-bundle.jsclient/app/packs/rsc-client-components.jsclient/app/packs/stimulus-bundle.jsclient/app/packs/stores-registration.jsconfig/initializers/react_on_rails_pro.rbconfig/routes.rbconfig/webpack/clientWebpackConfig.jsconfig/webpack/commonWebpackConfig.jsconfig/webpack/rscWebpackConfig.jsconfig/webpack/rspackRscPlugin.jsconfig/webpack/serverWebpackConfig.jsconfig/webpack/webpackConfig.jspackage.jsonreact-on-rails-pro-node-renderer.js
| const comments = await response.json(); | ||
|
|
||
| // Use lodash to process (stays on server) | ||
| const sortedComments = _.orderBy(comments, ['created_at'], ['desc']); | ||
| const recentComments = _.take(sortedComments, 10); |
There was a problem hiding this comment.
🧩 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.jbuilderRepository: 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.
| 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.
| 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. |
There was a problem hiding this comment.
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.
| 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.
| 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>'use client'</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> | ||
| ); | ||
| }; |
There was a problem hiding this comment.
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.
| // React Server Components registration (client-side) | ||
| registerServerComponent( | ||
| { rscPayloadGenerationUrlPath: 'rsc_payload/' }, | ||
| 'ServerComponentsPage', | ||
| ); |
There was a problem hiding this comment.
🧩 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.jsRepository: 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.jsRepository: 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.
| 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") |
There was a problem hiding this comment.
🧩 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 -20Repository: 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 -30Repository: shakacode/react-webpack-rails-tutorial
Length of output: 948
🏁 Script executed:
# Look for the ReactOnRailsPro configuration class
rg "ReactOnRailsPro" --type rb -lRepository: shakacode/react-webpack-rails-tutorial
Length of output: 108
🏁 Script executed:
# Read the Ruby initializer file
cat -n ./config/initializers/react_on_rails_pro.rbRepository: 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.jsRepository: 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 -100Repository: 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 2Repository: 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 rbRepository: 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 5Repository: 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 2Repository: 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 5Repository: 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()
EOFRepository: 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');
EOFRepository: 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 3Repository: 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 20Repository: shakacode/react-webpack-rails-tutorial
Length of output: 2155
🏁 Script executed:
# Check the Procfile.dev to understand the setup
cat -n Procfile.devRepository: 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/nullRepository: 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 tsRepository: 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 -20Repository: 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")}"
EOFRepository: 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 -80Repository: 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/nullRepository: 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 -20Repository: 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 10Repository: 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.mdRepository: 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/nullRepository: shakacode/react-webpack-rails-tutorial
Length of output: 116
🏁 Script executed:
# Check CI configuration for environment setup
cat -n config/ci.rbRepository: 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 -20Repository: 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 -20Repository: 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.
| 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.
| // Emit SSR manifest (maps module IDs to SSR module data) | ||
| if (!this.isServer) { | ||
| compilation.emitAsset( | ||
| this.ssrManifestFilename, | ||
| new sources.RawSource(JSON.stringify({}, null, 2)), | ||
| ); | ||
| } |
There was a problem hiding this comment.
🧩 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:
- 1: https://stackoverflow.com/questions/78841536/how-to-get-context-modules-for-vite-react-ssr-manifest-json
- 2: How to get context.modules for vite react ssr-manifest.json vitejs/vite-plugin-react#356
- 3: https://tessl.io/registry/tessl/npm-react-server-dom-webpack/19.1.0/files/docs/webpack-plugin.md
- 4: https://tessl.io/registry/tessl/npm-react-server-dom-webpack/19.1.0/files/docs/server-apis.md
- 5: https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-rsc/docs/architecture.md
- 6: https://registry.npmjs.org/%40vitejs%2Fplugin-rsc
- 7: https://github.com/Daydreamer-riri/vite-react-ssg/blob/main/src/node/assets.ts
- 8: https://github.com/vitejs/vite/blob/130ef31b/packages/vite/src/node/ssr/ssrManifestPlugin.ts
🏁 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 -20Repository: 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.jsRepository: 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 jsonRepository: shakacode/react-webpack-rails-tutorial
Length of output: 173
🏁 Script executed:
# Search for references to ssrManifestFilename
rg "ssrManifestFilename" --type js --type tsRepository: 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 -iRepository: 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 -20Repository: 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 tsRepository: 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 -100Repository: 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 -50Repository: 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 -60Repository: 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 -100Repository: 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 3Repository: 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.
| 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]; |
There was a problem hiding this comment.
🧩 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 3Repository: shakacode/react-webpack-rails-tutorial
Length of output: 304
🏁 Script executed:
cat -n config/webpack/production.jsRepository: shakacode/react-webpack-rails-tutorial
Length of output: 660
🏁 Script executed:
cat -n config/webpack/test.jsRepository: shakacode/react-webpack-rails-tutorial
Length of output: 341
🏁 Script executed:
cat -n config/webpack/webpackConfig.js | head -50Repository: 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.
| 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), |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -name "*react-on-rails-pro-node-renderer*" -type fRepository: shakacode/react-webpack-rails-tutorial
Length of output: 119
🏁 Script executed:
cat -n ./react-on-rails-pro-node-renderer.js | head -50Repository: 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 jsRepository: 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")
EOFRepository: 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 jsRepository: 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.
| 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'], |
There was a problem hiding this comment.
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.
| 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") |
There was a problem hiding this comment.
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.
| 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 (_) { |
There was a problem hiding this comment.
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.
| } 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(); |
There was a problem hiding this comment.
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:
| 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 & |
There was a problem hiding this comment.
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.
| 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 1This 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") %> |
There was a problem hiding this comment.
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.
| 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'; | |||
There was a problem hiding this comment.
'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:
| '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 ( |
There was a problem hiding this comment.
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.
| } 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 }; | |
| } |
Review: Fix SSR runtime failures for React Server ComponentsThis is a substantial and well-structured PR. The root causes identified in the description are correct, and the approach — NodeRenderer + server bundle with I've left inline comments on the issues below. Must Fix
Should Fix
Consider
|
There was a problem hiding this comment.
💡 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".
| const baseUrl = process.env.RAILS_INTERNAL_URL || 'http://localhost:3000'; | ||
| const response = await fetch(`${baseUrl}/comments.json`); |
There was a problem hiding this comment.
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 👍 / 👎.

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.
Net::ReadTimeouton all prerendered pages. Added a startup step with TCP readiness check andRENDERER_PASSWORDenv var.path/fs/stream) as CommonJSrequire()calls, but the Node renderer'svm.createContext()sandbox has norequire. Reverted toresolve.fallback: falsewhich stubs these unused code paths at build time.react-dom/server.browser.jsinstantiatesMessageChannelat module load. The Node renderer VM lacks this global (unlike Bun used by ExecJS on master). Added aBannerPluginpolyfill.'use client'directive — The auto-bundling system misclassified this traditional SSR component (usesStaticRouter) as a React Server Component because it lacked'use client'. Added the directive so it registers viaReactOnRails.register()instead ofregisterServerComponent().All 38 rspec tests pass locally.
Test plan
bundle exec rspec— all 38 tests pass (0 failures)bundle exec rubocop— no offenses🤖 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_railstoreact_on_rails_prowith the NodeRenderer and wiring the renderer into runtime/CI.Adds React Server Components support end-to-end: new
rsc_payload_route, a newserver-componentsdemo 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.jsplus a customRspackRscPluginto emit RSC manifests, and adjusts client/server configs with Node-builtin fallbacks and aMessageChannelpolyfill for the Node-renderer VM. CI andProcfile.devnow start the Node renderer, and.gitignoreexcludes 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