Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
913e8f0
feat: add React on Rails Pro with RSC support
ihabadham Oct 5, 2025
d23eba9
feat: configure webpack and Rails for RSC bundle generation
ihabadham Oct 5, 2025
a3ecc1b
feat: implement RSC markdown page with shared component pattern
ihabadham Oct 5, 2025
a88a903
feat: configure Node renderer with GitHub packages
ihabadham Oct 5, 2025
db4e465
chore: add gitignore entries for RSC bundles and audit file
ihabadham Oct 6, 2025
0c54276
feat: configure Rails for RSC bundle paths
ihabadham Oct 6, 2025
ba16eec
feat: configure node renderer to use ssr-generated bundles
ihabadham Oct 6, 2025
75a416f
feat: configure server webpack bundle for node renderer
ihabadham Oct 6, 2025
b5ee8a5
feat: add react-dom/server alias to RSC webpack config
ihabadham Oct 6, 2025
f141a1f
fix: add "use client" directives to client components
ihabadham Oct 6, 2025
6ca187b
chore: add foreman to development dependencies
ihabadham Oct 6, 2025
562b8a9
chore: switch RORP from local path to GitHub Packages
ihabadham Oct 6, 2025
37e2df5
chore: remove foreman gem and cleanup gitignore
ihabadham Oct 6, 2025
4a7fa09
feat: add bottom padding to layout
ihabadham Oct 6, 2025
4026991
feat: add complete navigation between all three pages
ihabadham Oct 6, 2025
3464829
refactor: convert RSC component to inline styles
ihabadham Oct 6, 2025
5136a8b
feat: implement shared MarkdownViewer pattern
ihabadham Oct 6, 2025
da296d3
docs: add GitHub Packages authentication setup instructions
ihabadham Oct 6, 2025
151cc2c
security: add XSS protection and fix task list selectors
ihabadham Oct 7, 2025
fb7a0d8
security: require RENDERER_PASSWORD in production
ihabadham Oct 7, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@
/public/packs
/public/packs-test
/node_modules

# Server-side bundles generated by webpack
/ssr-generated
/yarn-error.log
yarn-debug.log*
.yarn-integrity
Expand All @@ -46,3 +49,9 @@ yarn-debug.log*

# Generated React on Rails packs
**/generated/**

# Claude Code configuration
.claude/

# npm credentials
.npmrc
5 changes: 5 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,8 @@ group :test do
end
gem "shakapacker", "~> 8.3"
gem "react_on_rails", "~> 16.1.1"

# React on Rails Pro from GitHub Packages
source "https://rubygems.pkg.github.com/shakacode-tools" do
gem "react_on_rails_pro"
end
15 changes: 15 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,9 @@ GEM
raabro (~> 1.4)
globalid (1.3.0)
activesupport (>= 6.1)
http-2 (1.1.1)
httpx (1.6.2)
http-2 (>= 1.0.0)
i18n (1.14.7)
concurrent-ruby (~> 1.0)
io-console (0.8.1)
Expand Down Expand Up @@ -368,6 +371,17 @@ GEM
nokogiri (~> 1.8)
zeitwerk (2.7.3)

GEM
remote: https://rubygems.pkg.github.com/shakacode-tools/
specs:
react_on_rails_pro (4.0.0)
addressable
connection_pool
execjs (~> 2.9)
httpx (~> 1.5)
rainbow
react_on_rails (>= 14.1.0)

PLATFORMS
aarch64-linux
aarch64-linux-gnu
Expand All @@ -391,6 +405,7 @@ DEPENDENCIES
puma (>= 5.0)
rails (~> 8.0.2, >= 8.0.2.1)
react_on_rails (~> 16.1.1)
react_on_rails_pro!
rubocop-rails-omakase
selenium-webdriver
shakapacker (~> 8.3)
Expand Down
2 changes: 2 additions & 0 deletions Procfile.dev
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@
rails: bundle exec rails s -p 3000
wp-client: WEBPACK_SERVE=true bin/shakapacker-dev-server
wp-server: SERVER_BUNDLE_ONLY=yes bin/shakapacker --watch
wp-rsc: HMR=true RSC_BUNDLE_ONLY=yes bin/shakapacker --watch
node-renderer: RENDERER_LOG_LEVEL=debug RENDERER_PORT=3800 node client/node-renderer.js
21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,12 +78,29 @@ This demo includes comprehensive documentation for both developers and AI coding

## 🚀 Quick Start

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

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
```

2. **Configure Bundler for GitHub Packages:**
```bash
bundle config rubygems.pkg.github.com YOUR_PRO_TOKEN
```

### Installation

```bash
# Clone the demo repository
git clone https://github.com/shakacode/react_on_rails-demo-v15-ssr-auto-registration-bundle-splitting.git
cd react_on_rails-demo-v15-ssr-auto-registration-bundle-splitting

# Install dependencies
# Install dependencies (after configuring GitHub Packages above)
bundle install && npm install

# Start development server
Expand All @@ -93,6 +110,8 @@ bundle install && npm install
open http://localhost:3000
```

**Note:** Without GitHub Packages authentication, `bundle install` and `npm install` will fail when trying to download `react_on_rails_pro` and `@shakacode-tools/react-on-rails-pro-node-renderer`.

### 🌐 Demo Components

- **[HelloWorld](http://localhost:3000)** - Lightweight component (12.5KB bundle)
Expand Down
15 changes: 15 additions & 0 deletions app/controllers/rsc_markdown_page_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# frozen_string_literal: true

class RscMarkdownPageController < ApplicationController
include ReactOnRailsPro::Stream

def index
@rsc_markdown_page_props = {
title: "React Server Components Demo",
author: "Demo System",
lastModified: Time.current
}

stream_view_containing_react_components(template: "rsc_markdown_page/index")
end
end
Original file line number Diff line number Diff line change
@@ -1,29 +1,29 @@
"use client";

import React, { useState, useEffect } from 'react';
import MarkdownViewer from '../../MarkdownViewer/ror_components/MarkdownViewer';
import * as style from './HeavyMarkdownEditor.module.css';

// Import markdown components for client-side only
let ReactMarkdown, remarkGfm;

const HeavyMarkdownEditor = (props) => {
const [markdown, setMarkdown] = useState(props.initialText || '# Start editing markdown here...');
const [isLoaded, setIsLoaded] = useState(false);
const [MarkdownComponent, setMarkdownComponent] = useState(null);
const [processedHtml, setProcessedHtml] = useState('');

useEffect(() => {
const loadMarkdown = async () => {
try {
const [{ default: ReactMarkdownComp }, { default: remarkGfmComp }] = await Promise.all([
const [{ default: ReactMarkdown }, { default: remarkGfm }, { renderToString }] = await Promise.all([
import('react-markdown'),
import('remark-gfm')
import('remark-gfm'),
import('react-dom/server')
]);

const Component = ({ children }) => (
<ReactMarkdownComp remarkPlugins={[remarkGfmComp]}>
{children}
</ReactMarkdownComp>

// Convert markdown to HTML using react-markdown, then use shared MarkdownViewer
const htmlString = renderToString(
React.createElement(ReactMarkdown, { remarkPlugins: [remarkGfm] }, markdown)
);
setMarkdownComponent(() => Component);

setProcessedHtml(htmlString);
setIsLoaded(true);
} catch (error) {
console.warn('Failed to load markdown components:', error);
Expand All @@ -32,7 +32,7 @@ const HeavyMarkdownEditor = (props) => {
};

loadMarkdown();
}, []);
}, [markdown]);

// Skeleton loader component that fills the preview space properly
const SkeletonLoader = () => (
Expand Down Expand Up @@ -129,9 +129,9 @@ const HeavyMarkdownEditor = (props) => {
boxSizing: 'border-box'
}}
>
{isLoaded && MarkdownComponent ? (
{processedHtml ? (
<div className={`${style.contentTransition} ${style.fadeIn}`}>
<MarkdownComponent>{markdown}</MarkdownComponent>
<MarkdownViewer processedHtml={processedHtml} />
</div>
) : isLoaded ? (
<div className={`${style.contentTransition} ${style.fadeIn}`}>
Expand All @@ -151,12 +151,15 @@ const HeavyMarkdownEditor = (props) => {
<a href="/hello_world" className={style.link}>
← Back to Lightweight HelloWorld
</a>
<a href="/rsc_markdown_page" className={style.link}>
→ Try RSC Markdown Page
</a>
<div className={style.bundleInfo}>
<strong>Bundle Impact:</strong> Heavy component with markdown libraries (~120KB transferred, 385KB resources in production)
</div>
{props.title && (
<div className={style.bundleInfo} style={{marginTop: '0.5rem', fontSize: '0.85rem'}}>
<strong>Content:</strong> {props.title}
<strong>Content:</strong> {props.title}
{props.author && <> by {props.author}</>}
{props.lastModified && <> (updated {new Date(props.lastModified).toLocaleDateString()})</>}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@
font-weight: bold;
}

.preview ul li[data-task] {
.preview ul li.task-list-item {
list-style: none;
margin-left: -1rem;
}
Expand Down Expand Up @@ -222,6 +222,10 @@
text-align: center;
padding-top: 1rem;
border-top: 2px solid #ecf0f1;
display: flex;
flex-direction: column;
gap: 0.8rem;
align-items: center;
}

.link {
Expand All @@ -233,7 +237,6 @@
border-radius: 4px;
font-weight: bold;
transition: background-color 0.3s ease, transform 0.2s ease;
margin-bottom: 1rem;
}

.link:hover {
Expand Down
5 changes: 5 additions & 0 deletions app/javascript/src/HelloWorld/ror_components/HelloWorld.jsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"use client";

import React, { useState } from 'react';
import * as style from './HelloWorld.module.css';

Expand Down Expand Up @@ -31,6 +33,9 @@ const HelloWorld = (props) => {
<a href="/heavy_markdown_editor" className={style.link}>
→ Try Heavy Markdown Editor
</a>
<a href="/rsc_markdown_page" className={style.link}>
→ Try RSC Markdown Page
</a>
<div className={style.bundleInfo}>
<strong>Bundle Size:</strong> Minimal - just React basics (~10.0KB total in production)
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@

.navigation {
text-align: center;
display: flex;
flex-direction: column;
gap: 0.8rem;
align-items: center;
}

.link {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React from 'react';
import DOMPurify from 'isomorphic-dompurify';
import * as style from './MarkdownViewer.module.css';

/**
* Lightweight shared markdown viewer component
*
* This component is deliberately lightweight - it only displays pre-processed HTML.
* No markdown libraries are imported here, keeping the bundle size minimal.
*
* Processing happens in the consuming components:
* - Server component: processes markdown server-side (heavy libs stay on server)
* - Client component: processes markdown client-side (heavy libs go to client)
*
* All HTML is sanitized using DOMPurify to prevent XSS attacks.
*
* This pattern ensures the viewer itself has minimal bundle impact.
*/
const MarkdownViewer = ({ processedHtml, className }) => {
const sanitizedHtml = DOMPurify.sanitize(processedHtml);

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

export default MarkdownViewer;
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/* Lightweight markdown content styling for the shared viewer component */
.markdownContent {
line-height: 1.6;
}

/* Headings */
.markdownContent h1,
.markdownContent h2,
.markdownContent h3,
.markdownContent h4,
.markdownContent h5,
.markdownContent h6 {
color: #2c3e50;
margin-top: 1.5rem;
margin-bottom: 0.5rem;
}

.markdownContent h1 {
border-bottom: 2px solid #e67e22;
padding-bottom: 0.3rem;
}

/* Code and pre */
.markdownContent code {
background-color: #f1c40f;
padding: 2px 4px;
border-radius: 3px;
font-size: 0.9em;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
}

.markdownContent pre {
background-color: #2c3e50;
color: #ecf0f1;
padding: 1rem;
border-radius: 4px;
overflow-x: auto;
}

.markdownContent pre code {
background: none;
color: inherit;
padding: 0;
}

/* Tables */
.markdownContent table {
border-collapse: collapse;
width: 100%;
margin: 1rem 0;
}

.markdownContent th,
.markdownContent td {
border: 1px solid #bdc3c7;
padding: 0.5rem;
text-align: left;
}

.markdownContent th {
background-color: #ecf0f1;
font-weight: bold;
}

/* Lists */
.markdownContent ul li.task-list-item {
list-style: none;
margin-left: -1rem;
}

/* Blockquotes */
.markdownContent blockquote {
border-left: 4px solid #e67e22;
margin: 1rem 0;
padding-left: 1rem;
color: #7f8c8d;
}

/* Links */
.markdownContent a {
color: #3498db;
text-decoration: none;
}

.markdownContent a:hover {
text-decoration: underline;
}

/* Paragraphs */
.markdownContent p {
margin-bottom: 1rem;
}
Loading
Loading