Skip to content

Conversation

ihabadham
Copy link

@ihabadham ihabadham commented Oct 7, 2025

Summary

  • Add React on Rails Pro with RSC support
  • Implement RSC markdown page demonstrating server-side rendering
  • Create shared MarkdownViewer component pattern used by both client and RSC components
  • Add bidirectional navigation between all demo pages
  • Configure webpack bundles for RSC generation
  • Document GitHub Packages authentication setup

Key Changes

  • RSC Implementation: New RSCMarkdownPage component with server-side markdown processing using marked
  • Shared Component Pattern: MarkdownViewer used by both HeavyMarkdownEditor (client) and RSCMarkdownPage (server)
    • HeavyMarkdownEditor converts react-markdown output to HTML via renderToString
    • Maintains heavy bundle demonstration (~1.53MB uncompressed)
  • Navigation: Complete navigation flow between HelloWorld, HeavyMarkdownEditor, and RSCMarkdownPage
  • Styling: RSC component uses inline styles (CSS modules not supported with RSC)
  • Configuration:
    • Node renderer with RSC support
    • Server webpack config for RSC bundle generation
    • Rails config for RSC bundle paths
    • GitHub Packages setup for react_on_rails_pro

Bundle Sizes

  • HeavyMarkdownEditor: 315KB transferred, ~1.53MB uncompressed (includes react-markdown + react-dom/server)
  • RSCMarkdownPage: Minimal client bundle, markdown processed on server

Test Plan

  • Verify all three pages render correctly
  • Test navigation between pages works bidirectionally
  • Confirm HeavyMarkdownEditor still demonstrates heavy client bundle
  • Verify RSC page processes markdown on server (check network tab - no large JS bundles)
  • Test markdown rendering (tables, lists, code blocks) on both pages
  • Verify GitHub Packages authentication instructions work for new users

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added a server-rendered Markdown page (RSC) with navigation links from HelloWorld and HeavyMarkdownEditor.
    • Added a lightweight MarkdownViewer component for sanitized HTML display.
  • Style
    • Updated navigation layout to vertical flex and increased bottom page padding; adjusted preview list styling.
  • Documentation
    • Expanded Quick Start with GitHub Packages authentication and updated install/start guidance.
  • Chores
    • Added dev processes for HMR and a Node renderer, new dependencies, ignore rules, routes, and server-rendering configuration.

ihabadham and others added 18 commits October 5, 2025 21:03
- Add react_on_rails_pro gem (local path for development)
- Add react-on-rails-rsc npm package (v19.0.2)
- Link @shakacode-tools/react-on-rails-pro-node-renderer locally
- Add .claude/ to .gitignore

This enables React Server Components support for the demo app.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Configure complete RSC infrastructure following React on Rails Pro documentation:

**Webpack Configuration:**
- Create rscWebpackConfig.js with RSC loader before babel-loader
- Add react-server condition to resolve config
- Add RSCWebpackPlugin to client and server webpack configs
- Update generateWebpackConfigs to include RSC bundle in default build
- Support RSC_BUNDLE_ONLY environment variable

**Rails Configuration:**
- Add rsc_bundle_js_file = "rsc-bundle.js" to react_on_rails initializer
- Create react_on_rails_pro initializer with enable_rsc_support = true
- Add rsc_payload_route to routes for RSC payload handling

**Development Workflow:**
- Add wp-rsc process to Procfile.dev for RSC bundle watch mode

This sets up the foundation for implementing RSC components with proper
bundle splitting and server-side component execution.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Demonstrate React Server Components with a shared viewer pattern:

**Shared MarkdownViewer Component:**
- Lightweight display component with no markdown processing dependencies
- Accepts pre-processed HTML as prop
- Reusable by both server and client components
- Minimal bundle impact (~2KB)

**RSCMarkdownPage Server Component:**
- Processes markdown server-side using 'marked' library
- Heavy markdown library stays on server, never ships to client
- Uses async/await for server-side data fetching
- Renders using shared MarkdownViewer component

**Rails Integration:**
- Created RscMarkdownPageController with ReactOnRailsPro::Stream
- Uses stream_view_containing_react_components for streaming SSR
- View uses stream_react_component helper for RSC rendering
- Added route at /rsc_markdown_page

**Dependencies:**
- Added 'marked' npm package for server-side markdown processing

This demonstrates the key RSC benefit: markdown processing happens entirely
on the server, keeping client bundles minimal while providing full markdown
rendering capabilities.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
- Set up .gitignore to exclude .npmrc credentials
- Install react-on-rails-pro-node-renderer from GitHub packages
- Add client/node-renderer.js configuration file
- Update Procfile.dev to run node-renderer process
- Configure Node renderer in react_on_rails_pro.rb initializer

This switches from local npm link to published packages,
resolving dependency issues with fastify and other packages.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
- Ignore /ssr-generated directory containing server-side webpack bundles
- Ignore RSC_CONFIG_AUDIT.md analysis file

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
- Set server_bundle_output_path to "ssr-generated"
- Enable enforce_private_server_bundles to prevent fallback to public/packs
- Required for React on Rails Pro node renderer to locate bundles

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
- Update bundlePath from public/packs to ssr-generated
- Aligns with Rails configuration for private server bundles

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
- Enable target: 'node' for Node.js runtime compatibility
- Enable libraryTarget: 'commonjs2' for module export format
- Add babel-loader SSR caller flag for proper React transforms
- Output to ssr-generated directory instead of public/packs
- Remove publicPath as server bundles are not web-served

Required for React on Rails Pro node renderer to execute server bundles.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
- Alias react-dom/server to false in RSC bundle
- Prevents runtime errors from unused server rendering imports
- RSC payload generation uses react-on-rails-rsc instead

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
- Add directive to HelloWorld for SSR registration
- Add directive to HeavyMarkdownEditor for client-side rendering

Auto-bundler uses "use client" to determine component registration:
- WITH directive → ReactOnRails.register() (SSR/client components)
- WITHOUT directive → registerServerComponent() (RSC components)

Fixes component mis-registration that caused node renderer crashes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Add foreman gem for managing dev processes via bin/dev.
Temporary solution until system-level foreman is installed.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Replace local development path with GitHub Packages source for
react_on_rails_pro gem. Makes the setup portable and production-ready.

Configured via bundle config (credentials not in source code).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
- Remove foreman from Gemfile (use system-installed foreman instead)
- Remove RSC_CONFIG_AUDIT.md from gitignore (file untracked for reference)

Keeps the branch cleaner without temporary dependencies.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Add padding-bottom to body to prevent content from touching viewport edge,
matching the existing top padding.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
- Add RSC Markdown Page links to HelloWorld and HeavyMarkdownEditor
- Add flexbox with gap for proper spacing between navigation buttons
- Remove redundant margin-bottom in favor of gap
- All three pages now have bidirectional navigation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
CSS modules are not currently supported for React Server Components.
Convert RSCMarkdownPage to use inline styles and embedded <style> tag
for markdown content styling.

- Replace CSS module imports with inline style objects
- Add embedded <style> tag for markdown content (tables, code, etc.)
- Remove unused RSCMarkdownPage.module.css
- Maintain visual consistency with previous CSS module styles

Note: MarkdownViewer.module.css is kept as it's still used by client
components that import MarkdownViewer.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Refactor HeavyMarkdownEditor to use the shared MarkdownViewer component
while maintaining the heavy bundle demonstration.

Changes:
- Import shared MarkdownViewer component
- Add react-dom/server to convert ReactMarkdown output to HTML string
- Store processedHtml instead of MarkdownComponent in state
- Re-process markdown on every change for live preview
- Pass HTML to shared MarkdownViewer for display

Bundle impact (still heavy):
- Transferred: ~315KB (react-markdown + remark-gfm + react-dom/server)
- Uncompressed: ~1.53MB
- This demonstrates client-side bundle weight vs RSC's server-side processing

The shared component pattern is now complete:
- HeavyMarkdownEditor: Client-side processing → MarkdownViewer
- RSCMarkdownPage: Server-side processing → MarkdownViewer

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Add comprehensive setup instructions for GitHub Packages authentication
required to install React on Rails Pro dependencies.

Changes:
- Add Prerequisites section with step-by-step token creation
- Document npm configuration for @shakacode-tools packages
- Document Bundler configuration for rubygems.pkg.github.com
- Add note about installation failure without authentication
- Clarify that each developer needs their own GitHub token

This addresses best practices for token management in demo repositories
where each developer should use their own credentials rather than
sharing tokens.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Copy link

coderabbitai bot commented Oct 7, 2025

Walkthrough

Adds React Server Components support and server-side markdown rendering: new gem/source, node renderer, initializers, webpack RSC configs and bundle, routes, controller, view, RSC component and MarkdownViewer, editor and navigation updates, package/gem dependencies, Procfile and .gitignore updates, README and layout padding.

Changes

Cohort / File(s) Summary
Ignore and process management
/.gitignore, Procfile.dev
Adds ignores for /ssr-generated, .claude/, .npmrc. Adds wp-rsc (RSC dev server with RSC_BUNDLE_ONLY) and node-renderer process entries.
Gems and npm dependencies
Gemfile, package.json
Adds GitHub Packages source block and react_on_rails_pro gem; adds NPM deps @shakacode-tools/react-on-rails-pro-node-renderer, isomorphic-dompurify, marked, react-on-rails-rsc.
ReactOnRails Pro configuration
config/initializers/react_on_rails.rb, config/initializers/react_on_rails_pro.rb
Adds RSC/server bundle options and a Pro configure block enabling RSC, selecting NodeRenderer, and configuring renderer URL/password.
Routes, controller, view
config/routes.rb, app/controllers/rsc_markdown_page_controller.rb, app/views/rsc_markdown_page/index.html.erb
Adds rsc_payload_route and get "rsc_markdown_page" route; new streaming controller RscMarkdownPageController#index; view streams RSCMarkdownPage with props.
Webpack: RSC integration
config/webpack/generateWebpackConfigs.js, config/webpack/rscWebpackConfig.js, config/webpack/serverWebpackConfig.js, config/webpack/clientWebpackConfig.js
Introduces RSC webpack config and plugin; creates rsc-bundle, supports RSC_BUNDLE_ONLY, adjusts server output to ssr-generated, sets server target node, and injects RSC loader/plugin.
Node renderer
client/node-renderer.js
Adds Node renderer bootstrap/config: port, log level, password, supportModules, additionalContext, stubTimers and async log replay; initializes renderer.
RSC page component and viewer
app/javascript/src/RSCMarkdownPage/.../RSCMarkdownPage.jsx, app/javascript/src/MarkdownViewer/.../MarkdownViewer.jsx, .../MarkdownViewer.module.css
Adds server-side RSC RSCMarkdownPage (uses marked to render HTML server-side) and new MarkdownViewer component plus styles to sanitize and render processed HTML.
Editor and HelloWorld updates
app/javascript/src/HeavyMarkdownEditor/.../HeavyMarkdownEditor.jsx, .../HeavyMarkdownEditor.module.css, app/javascript/src/HelloWorld/.../HelloWorld.jsx, .../HelloWorld.module.css
HeavyMarkdownEditor switched to pre-rendered HTML via react-dom/server, uses MarkdownViewer, adds link to RSC page; CSS updates convert navigation to vertical flex and adjust task list selector. HelloWorld adds "use client" and link to RSC page.
Layout and docs
app/views/layouts/application.html.erb, README.md
Adds bottom padding to body and documents GitHub Packages authentication, Quick Start changes, demo notes, and installation guidance for Pro packages.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant B as Browser
  participant R as Rails (RscMarkdownPageController)
  participant S as ReactOnRailsPro::Stream
  participant NR as Node Renderer
  participant RB as rsc-bundle (server RSC bundle)

  B->>R: GET /rsc_markdown_page
  R->>S: stream_view_containing_react_components(...)
  S->>NR: request render RSC component with props
  NR->>RB: execute rsc-bundle.js (server-only code)
  RB-->>NR: streamed HTML chunks
  NR-->>S: rendered stream
  S-->>B: incremental HTML stream to client
  note right of NR: renderer auth (password/URL) per initializer/env
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

I thump my paws — bundles split and hum,
Markdown baked where server rivers run.
Webpack stitches RSC and node,
Streams arrive down my burrowed road.
A carrot token, secret and sweet — I hop, compile, and eat. 🥕

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title clearly summarizes the primary feature addition: React Server Components support together with the shared MarkdownViewer pattern, which is the main focus of the changeset. It is concise, specific, and directly related to the pull request objectives.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch rsc-markdown-shared-component

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (5)
Gemfile (1)

61-64: Consider pinning the gem version.

The react_on_rails_pro gem has no version constraint, which may lead to unexpected updates. Consider specifying a version or range:

 # React on Rails Pro from GitHub Packages
 source "https://rubygems.pkg.github.com/shakacode-tools" do
-  gem "react_on_rails_pro"
+  gem "react_on_rails_pro", "~> 4.0"
 end
README.md (2)

81-95: Consider adding token security guidance.

The authentication setup is well-documented. Consider adding a note about token security:

 ### Prerequisites: GitHub Packages Authentication
 
 This demo uses **React on Rails Pro** from GitHub Packages. While RORP will be open source soon, it currently requires authentication with your Pro subscription token.
 
+**Security Note:** Never commit your authentication token. The `.npmrc` file is already ignored by `.gitignore`.
+
 1. **Configure npm for GitHub Packages:**

86-89: Clarify token environment variable option.

The npm config commands store the token in the user's global npm config. Consider documenting the .npmrc file option for project-specific auth:

 1. **Configure npm for GitHub Packages:**
    ```bash
    npm config set @shakacode-tools:registry https://npm.pkg.github.com
    npm config set //npm.pkg.github.com/:_authToken YOUR_PRO_TOKEN
    ```
+   
+   **Alternative:** Create a local `.npmrc` file (already ignored):
+   ```
+   @shakacode-tools:registry=https://npm.pkg.github.com
+   //npm.pkg.github.com/:_authToken=YOUR_PRO_TOKEN
+   ```
config/webpack/generateWebpackConfigs.js (1)

13-15: Include RSC config in envSpecific if needed
Current env-specific callbacks (productionEnvOnly, testOnly, developmentEnvOnly) are defined as (clientConfig, serverConfig) and do not receive rscConfig. If you need to apply environment-specific tweaks to the RSC bundle, update generateWebpackConfigs to call envSpecific(clientConfig, serverConfig, rscConfig) and revise each env config file’s callback signature accordingly.

app/javascript/src/RSCMarkdownPage/ror_components/RSCMarkdownPage.jsx (1)

22-90: Consider extracting the default markdown content.

The default markdown content (69 lines) is embedded directly in the component, which makes it harder to maintain and test.

Consider moving it to a separate file:

// defaultMarkdownContent.js
export const defaultMarkdownContent = `# React Server Components Demo
...
`;
 import React from 'react';
 import { marked } from 'marked';
 import MarkdownViewer from '../../MarkdownViewer/ror_components/MarkdownViewer';
+import { defaultMarkdownContent } from './defaultMarkdownContent';

 async function RSCMarkdownPage({ initialText, title, author, lastModified }) {
-  const defaultMarkdown = `# React Server Components Demo
-  ...
-  `;
-
-  const markdown = initialText || defaultMarkdown;
+  const markdown = initialText || defaultMarkdownContent;

This separation improves readability and makes it easier to update the demo content independently.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a964857 and da296d3.

⛔ Files ignored due to path filters (2)
  • Gemfile.lock is excluded by !**/*.lock
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (23)
  • .gitignore (2 hunks)
  • Gemfile (1 hunks)
  • Procfile.dev (1 hunks)
  • README.md (2 hunks)
  • app/controllers/rsc_markdown_page_controller.rb (1 hunks)
  • app/javascript/src/HeavyMarkdownEditor/ror_components/HeavyMarkdownEditor.jsx (4 hunks)
  • app/javascript/src/HeavyMarkdownEditor/ror_components/HeavyMarkdownEditor.module.css (1 hunks)
  • app/javascript/src/HelloWorld/ror_components/HelloWorld.jsx (2 hunks)
  • app/javascript/src/HelloWorld/ror_components/HelloWorld.module.css (1 hunks)
  • app/javascript/src/MarkdownViewer/ror_components/MarkdownViewer.jsx (1 hunks)
  • app/javascript/src/MarkdownViewer/ror_components/MarkdownViewer.module.css (1 hunks)
  • app/javascript/src/RSCMarkdownPage/ror_components/RSCMarkdownPage.jsx (1 hunks)
  • app/views/layouts/application.html.erb (1 hunks)
  • app/views/rsc_markdown_page/index.html.erb (1 hunks)
  • client/node-renderer.js (1 hunks)
  • config/initializers/react_on_rails.rb (1 hunks)
  • config/initializers/react_on_rails_pro.rb (1 hunks)
  • config/routes.rb (1 hunks)
  • config/webpack/clientWebpackConfig.js (2 hunks)
  • config/webpack/generateWebpackConfigs.js (2 hunks)
  • config/webpack/rscWebpackConfig.js (1 hunks)
  • config/webpack/serverWebpackConfig.js (4 hunks)
  • package.json (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (8)
config/webpack/generateWebpackConfigs.js (6)
config/webpack/serverWebpackConfig.js (3)
  • require (4-4)
  • require (9-9)
  • serverWebpackConfig (16-16)
config/webpack/clientWebpackConfig.js (2)
  • require (5-5)
  • clientConfig (8-8)
config/webpack/development.js (2)
  • require (4-4)
  • generateWebpackConfigs (6-6)
config/webpack/test.js (1)
  • generateWebpackConfigs (4-4)
config/webpack/production.js (1)
  • generateWebpackConfigs (4-4)
config/webpack/rscWebpackConfig.js (2)
  • serverWebpackConfig (2-2)
  • rscConfig (20-20)
config/initializers/react_on_rails_pro.rb (1)
client/node-renderer.js (1)
  • config (7-33)
config/webpack/serverWebpackConfig.js (1)
config/webpack/clientWebpackConfig.js (1)
  • require (5-5)
app/javascript/src/HeavyMarkdownEditor/ror_components/HeavyMarkdownEditor.jsx (1)
app/javascript/src/MarkdownViewer/ror_components/MarkdownViewer.jsx (1)
  • MarkdownViewer (16-23)
app/javascript/src/RSCMarkdownPage/ror_components/RSCMarkdownPage.jsx (1)
app/javascript/src/MarkdownViewer/ror_components/MarkdownViewer.jsx (1)
  • MarkdownViewer (16-23)
config/webpack/clientWebpackConfig.js (2)
config/webpack/serverWebpackConfig.js (3)
  • commonWebpackConfig (5-5)
  • require (4-4)
  • require (9-9)
config/webpack/generateWebpackConfigs.js (1)
  • clientConfig (9-9)
app/javascript/src/MarkdownViewer/ror_components/MarkdownViewer.jsx (1)
app/javascript/src/RSCMarkdownPage/ror_components/RSCMarkdownPage.jsx (1)
  • processedHtml (96-99)
config/webpack/rscWebpackConfig.js (3)
config/webpack/serverWebpackConfig.js (4)
  • serverWebpackConfig (16-16)
  • require (4-4)
  • require (9-9)
  • rules (73-73)
config/webpack/generateWebpackConfigs.js (2)
  • serverWebpackConfig (5-5)
  • rscConfig (11-11)
config/webpack/clientWebpackConfig.js (1)
  • require (5-5)
🪛 Biome (2.1.2)
app/javascript/src/RSCMarkdownPage/ror_components/RSCMarkdownPage.jsx

[error] 103-103: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

app/javascript/src/MarkdownViewer/ror_components/MarkdownViewer.jsx

[error] 20-20: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

🔇 Additional comments (25)
.gitignore (1)

56-57: LGTM! Critical security pattern added.

Ignoring .npmrc is essential to prevent accidental commits of GitHub Packages authentication tokens. This aligns with the README's GitHub Packages authentication setup.

app/javascript/src/HelloWorld/ror_components/HelloWorld.module.css (1)

72-78: LGTM! Consistent navigation pattern.

The flexbox layout provides better vertical spacing for navigation links. This pattern is consistent with similar changes in HeavyMarkdownEditor.module.css.

package.json (3)

20-20: LGTM! Node renderer dependency added.

The @shakacode-tools/react-on-rails-pro-node-renderer@^4.0.0 aligns with the React on Rails Pro integration documented in the README.


29-29: LGTM! Markdown processing dependency added.

The marked@^16.3.0 package provides fast server-side markdown-to-HTML conversion for the RSCMarkdownPage component.

Based on learnings


36-36: LGTM! RSC support dependency added.

The react-on-rails-rsc@^19.0.2 version aligns correctly with React 19.x (react@^19.1.1 and react-dom@^19.1.1).

Based on learnings

app/views/layouts/application.html.erb (1)

34-34: LGTM! Symmetric vertical padding added.

The padding-bottom: 2rem matches the existing padding-top: 2rem, providing consistent spacing for the new RSC markdown pages.

app/views/rsc_markdown_page/index.html.erb (1)

1-3: Props Initialization Verified RscMarkdownPageController#index sets @rsc_markdown_page_props as expected.

app/javascript/src/MarkdownViewer/ror_components/MarkdownViewer.module.css (1)

24-30: No contrast issues detected
Inline code background (#f1c40f) with black text achieves a 12.64:1 contrast ratio, exceeding WCAG AAA requirements.

Procfile.dev (1)

6-7: LGTM!

The new processes correctly set up the RSC development workflow:

  • wp-rsc enables HMR for RSC-only bundle rebuilds during development.
  • node-renderer starts the renderer service on port 3800 with debug logging, matching the configuration in client/node-renderer.js.
app/javascript/src/HelloWorld/ror_components/HelloWorld.jsx (2)

1-1: LGTM!

The "use client" directive is correctly placed, as this component uses client-side state (useState) and cannot be a server component.


36-38: LGTM!

The navigation link to the new RSC Markdown Page is correctly implemented and follows the existing pattern.

config/webpack/clientWebpackConfig.js (2)

5-5: LGTM!

The import correctly references the RSCWebpackPlugin from react-on-rails-rsc/WebpackPlugin, consistent with the server webpack configuration.


16-17: LGTM!

The RSCWebpackPlugin is correctly configured with isServer: false for the client bundle, enabling proper manifest generation for client-side RSC boundaries.

app/javascript/src/HeavyMarkdownEditor/ror_components/HeavyMarkdownEditor.module.css (1)

225-228: LGTM!

The navigation layout change to a vertical flex container improves the presentation when multiple navigation links are present and maintains consistency with similar changes in other components.

config/routes.rb (2)

12-13: LGTM!

The rsc_payload_route declaration is correctly placed to enable RSC payload routing. This is a helper method provided by ReactOnRailsPro.


19-19: LGTM!

The new route correctly maps to the RscMarkdownPageController#index action, enabling navigation to the RSC markdown demo page.

config/webpack/generateWebpackConfigs.js (4)

6-6: LGTM!

The import of rscWebpackConfig is correctly placed alongside the existing client and server webpack configurations.


11-11: LGTM!

Creating the rscConfig before the conditional logic ensures it's available for all bundling scenarios.


27-30: LGTM!

The RSC_BUNDLE_ONLY conditional correctly mirrors the existing patterns for CLIENT_BUNDLE_ONLY and SERVER_BUNDLE_ONLY, enabling isolated RSC bundle builds during development.


34-35: LGTM!

The default bundling behavior correctly includes all three bundles (client, server, and RSC), with accurate console logging reflecting the change.

app/controllers/rsc_markdown_page_controller.rb (2)

3-4: LGTM!

The controller correctly includes ReactOnRailsPro::Stream to enable streaming behavior for React Server Components.


6-14: LGTM!

The index action properly initializes props and invokes streaming with the correct template path. The use of Time.current for lastModified is appropriate for server-side rendering.

client/node-renderer.js (1)

7-33: bundlePath alignment verified. Both client/node-renderer.js and config/webpack/serverWebpackConfig.js resolve to ssr-generated, so no changes are needed.

app/javascript/src/RSCMarkdownPage/ror_components/RSCMarkdownPage.jsx (2)

1-19: LGTM! Clear documentation of the RSC pattern.

The JSDoc comment effectively explains the shared component architecture and the benefits of server-side markdown processing. The distinction between heavy server-side processing and lightweight client rendering is well articulated.


103-340: Inline styles are extensive but align with RSC constraints.

The component uses inline styles throughout due to CSS modules not being supported with React Server Components (as noted in the PR objectives). While this makes the component verbose, it's a necessary trade-off for RSC compatibility.

The styling is consistent and the visual hierarchy is clear. If inline styles become unmanageable as the component grows, consider extracting style objects to constants at the top of the file to improve maintainability.

Comment on lines +96 to +99
const processedHtml = await marked(markdown, {
gfm: true, // GitHub Flavored Markdown
breaks: true,
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Sanitize markdown output to prevent XSS attacks.

The marked library processes markdown without sanitization before passing it to MarkdownViewer, which renders the HTML using dangerouslySetInnerHTML. If initialText originates from user input or any untrusted source, this creates an XSS vulnerability where malicious markdown could inject arbitrary HTML/JavaScript.

According to the marked documentation, you should pair its output with an HTML sanitizer when processing untrusted content. Based on learnings:

Apply this fix to sanitize the HTML output:

 import React from 'react';
 import { marked } from 'marked';
+import DOMPurify from 'isomorphic-dompurify';
 import MarkdownViewer from '../../MarkdownViewer/ror_components/MarkdownViewer';
   const processedHtml = await marked(markdown, {
     gfm: true, // GitHub Flavored Markdown
     breaks: true,
   });
+  const sanitizedHtml = DOMPurify.sanitize(processedHtml);

   return (
     <>
       {/* ... */}
-      <MarkdownViewer processedHtml={processedHtml} className="rsc-markdown-content" />
+      <MarkdownViewer processedHtml={sanitizedHtml} className="rsc-markdown-content" />
     </>
   );

If this is purely a demo with hardcoded content and initialText never comes from users, document this assumption clearly in the component's JSDoc to prevent future misuse.

Also applies to: 290-290


🏁 Script executed:

#!/bin/bash
set -e

echo "1. Locating RSCMarkdownPage.jsx..."
fd -e jsx RSCMarkdownPage.jsx

echo "2. Inspecting RSCMarkdownPage.jsx (lines 1-200)..."
sed -n '1,200p' app/javascript/src/RSCMarkdownPage/ror_components/RSCMarkdownPage.jsx

echo "3. Locating MarkdownViewer component..."
fd -e jsx -e tsx MarkdownViewer.jsx

echo "4. Searching for dangerouslySetInnerHTML across codebase..."
rg -n 'dangerouslySetInnerHTML' -C3

echo "5. Searching for DOMPurify usage..."
rg -n 'DOMPurify' -C3 || true

echo "6. Searching for sanitize calls..."
rg -n 'sanitize' -C3 || true

Length of output: 11069


Sanitize marked output before rendering.

Wrap processedHtml in a sanitizer before passing it to MarkdownViewer (which uses dangerouslySetInnerHTML):

 import { marked } from 'marked';
+import DOMPurify from 'isomorphic-dompurify';
 
   const processedHtml = await marked(markdown, {
     gfm: true,
     breaks: true,
   });
+  const sanitizedHtml = DOMPurify.sanitize(processedHtml);
 
   return (
     <>
       {/* … */}
-      <MarkdownViewer processedHtml={processedHtml} className="rsc-markdown-content" />
+      <MarkdownViewer processedHtml={sanitizedHtml} className="rsc-markdown-content" />
     </>
   );

If initialText is always demo content and never comes from users, add a JSDoc note to that effect.

🤖 Prompt for AI Agents
In app/javascript/src/RSCMarkdownPage/ror_components/RSCMarkdownPage.jsx around
lines 96 to 99, the HTML produced by marked is passed straight to MarkdownViewer
which uses dangerouslySetInnerHTML; sanitize processedHtml first (e.g., import
and use DOMPurify to call DOMPurify.sanitize(processedHtml) or a configured
sanitize-html equivalent) and pass the sanitized string into MarkdownViewer
instead, and if initialText is truly static demo content add a JSDoc comment
above the component or the prop explaining it never contains user input so it's
safe.

Comment on lines +103 to +159
<style dangerouslySetInnerHTML={{ __html: `
.rsc-markdown-content h1,
.rsc-markdown-content h2,
.rsc-markdown-content h3 {
color: #2c3e50;
margin-top: 1.5rem;
margin-bottom: 0.5rem;
}
.rsc-markdown-content h1 {
border-bottom: 2px solid #e67e22;
padding-bottom: 0.3rem;
}
.rsc-markdown-content code {
background-color: #f1c40f;
padding: 2px 4px;
border-radius: 3px;
font-size: 0.9em;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
}
.rsc-markdown-content pre {
background-color: #2c3e50;
color: #ecf0f1;
padding: 1rem;
border-radius: 4px;
overflow-x: auto;
}
.rsc-markdown-content pre code {
background: none;
color: inherit;
padding: 0;
}
.rsc-markdown-content table {
border-collapse: collapse;
width: 100%;
margin: 1rem 0;
}
.rsc-markdown-content th,
.rsc-markdown-content td {
border: 1px solid #bdc3c7;
padding: 0.5rem;
text-align: left;
}
.rsc-markdown-content th {
background-color: #ecf0f1;
font-weight: bold;
}
.rsc-markdown-content blockquote {
border-left: 4px solid #e67e22;
margin: 1rem 0;
padding-left: 1rem;
color: #7f8c8d;
}
.rsc-markdown-content p {
margin-bottom: 1rem;
line-height: 1.6;
}
` }} />
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

CSS injection via inline styles (low risk).

Static analysis flagged dangerouslySetInnerHTML for the inline CSS. While CSS injection is less dangerous than HTML injection, it can still be a vector for certain attacks (e.g., data exfiltration via CSS selectors, UI redressing).

Since the CSS is static and hardcoded in this component, the risk is minimal. However, if you ever need to make these styles dynamic or configurable, ensure they are validated and escaped appropriately. For truly dynamic styles, consider using a CSS-in-JS solution that provides automatic escaping.

🧰 Tools
🪛 Biome (2.1.2)

[error] 103-103: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

🤖 Prompt for AI Agents
In app/javascript/src/RSCMarkdownPage/ror_components/RSCMarkdownPage.jsx around
lines 103 to 159 the component injects static CSS via dangerouslySetInnerHTML
which flags CSS injection; since the styles are static, remove the inline
dangerouslySetInnerHTML and move the CSS into a safer place — e.g. a dedicated
.css/.module.css file imported into the component or a
styled-components/CSS-in-JS definition — and if any styles ever become dynamic,
validate/sanitize values or switch to a CSS-in-JS approach that scopes and
escapes style values instead of concatenating raw strings.

ihabadham and others added 2 commits October 7, 2025 12:41
- Add isomorphic-dompurify for HTML sanitization
- Sanitize all HTML in MarkdownViewer to prevent XSS attacks
- Fix task list CSS selector from li[data-task] to li.task-list-item
  (matches actual HTML output from marked/react-markdown)

Addresses CodeRabbit security review comments

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
- Remove default password fallback for production environments
- Keep "devPassword" fallback for development/test convenience
- Production deployments will now fail fast if RENDERER_PASSWORD is unset

Addresses CodeRabbit security review comment

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (4)
app/javascript/src/MarkdownViewer/ror_components/MarkdownViewer.module.css (2)

1-21: Consider using CSS custom properties for colors.

The stylesheet uses hard-coded color values throughout (e.g., #2c3e50, #e67e22). For better maintainability and theme consistency, consider defining these as CSS custom properties at the :root or .markdownContent level.

Example refactor:

+/* Color palette */
+.markdownContent {
+  --color-heading: #2c3e50;
+  --color-accent: #e67e22;
+  --color-text-muted: #7f8c8d;
+  --color-link: #3498db;
+  --color-code-bg: #f1c40f;
+  --color-pre-bg: #2c3e50;
+  --color-pre-text: #ecf0f1;
+  line-height: 1.6;
+}

 .markdownContent h1,
 .markdownContent h2,
 .markdownContent h3,
 .markdownContent h4,
 .markdownContent h5,
 .markdownContent h6 {
-  color: #2c3e50;
+  color: var(--color-heading);
   margin-top: 1.5rem;
   margin-bottom: 0.5rem;
 }

 .markdownContent h1 {
-  border-bottom: 2px solid #e67e22;
+  border-bottom: 2px solid var(--color-accent);
   padding-bottom: 0.3rem;
 }

24-26: Consider a more subtle background for inline code.

The bright yellow background (#f1c40f) for inline code may be visually distracting. Consider using a more subtle color like a light gray (#f4f4f4 or #e8e8e8) for better readability and less visual noise.

Example:

 .markdownContent code {
-  background-color: #f1c40f;
+  background-color: #f4f4f4;
   padding: 2px 4px;
   border-radius: 3px;
   font-size: 0.9em;
   font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
 }
app/javascript/src/MarkdownViewer/ror_components/MarkdownViewer.jsx (2)

19-19: Consider adding prop validation.

Adding PropTypes or TypeScript types would improve type safety and provide better developer experience with IDE autocomplete and inline documentation.

Example with PropTypes:

 import React from 'react';
+import PropTypes from 'prop-types';
 import DOMPurify from 'isomorphic-dompurify';
 import * as style from './MarkdownViewer.module.css';

 const MarkdownViewer = ({ processedHtml, className }) => {
   const sanitizedHtml = DOMPurify.sanitize(processedHtml);

   return (
     <div
       className={`${style.markdownContent} ${className || ''}`}
       dangerouslySetInnerHTML={{ __html: sanitizedHtml }}
     />
   );
 };

+MarkdownViewer.propTypes = {
+  processedHtml: PropTypes.string.isRequired,
+  className: PropTypes.string,
+};
+
 export default MarkdownViewer;

24-24: Simplify className concatenation.

The className || '' fallback is not strictly necessary since undefined will be coerced to an empty string in string concatenation, and trailing whitespace is typically trimmed by browsers.

Apply this diff for cleaner code:

-      className={`${style.markdownContent} ${className || ''}`}
+      className={`${style.markdownContent} ${className ?? ''}`.trim()}

Or use a dedicated library like classnames for more robust className handling if the project already has it as a dependency.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between da296d3 and fb7a0d8.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (5)
  • app/javascript/src/HeavyMarkdownEditor/ror_components/HeavyMarkdownEditor.module.css (2 hunks)
  • app/javascript/src/MarkdownViewer/ror_components/MarkdownViewer.jsx (1 hunks)
  • app/javascript/src/MarkdownViewer/ror_components/MarkdownViewer.module.css (1 hunks)
  • config/initializers/react_on_rails_pro.rb (1 hunks)
  • package.json (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • package.json
  • config/initializers/react_on_rails_pro.rb
  • app/javascript/src/HeavyMarkdownEditor/ror_components/HeavyMarkdownEditor.module.css
🧰 Additional context used
🧬 Code graph analysis (1)
app/javascript/src/MarkdownViewer/ror_components/MarkdownViewer.jsx (1)
app/javascript/src/RSCMarkdownPage/ror_components/RSCMarkdownPage.jsx (1)
  • processedHtml (96-99)
🪛 Biome (2.1.2)
app/javascript/src/MarkdownViewer/ror_components/MarkdownViewer.jsx

[error] 25-25: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

🔇 Additional comments (2)
app/javascript/src/MarkdownViewer/ror_components/MarkdownViewer.module.css (1)

66-69: Task list selector correctly updated!

The selector now uses .task-list-item, which correctly matches the HTML structure generated by remark-gfm. This addresses the previous concern about the incompatible li[data-task] selector.

app/javascript/src/MarkdownViewer/ror_components/MarkdownViewer.jsx (1)

19-27: XSS concern addressed with DOMPurify sanitization!

The component now sanitizes processedHtml using DOMPurify.sanitize() on line 20 before rendering it with dangerouslySetInnerHTML. This addresses the previous critical XSS concern and implements the recommended pattern.

Note: The Biome static analysis warning about dangerouslySetInnerHTML is a false positive in this case, as the HTML is properly sanitized before injection. The use of isomorphic-dompurify ensures safe sanitization on both client and server.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant