-
Notifications
You must be signed in to change notification settings - Fork 374
Pro RSC migration 3/3: React Server Components demo on webpack #729
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
ihabadham
wants to merge
21
commits into
ihabadham/feature/pro-rsc/base
Choose a base branch
from
ihabadham/feature/pro-rsc/rsc-demo
base: ihabadham/feature/pro-rsc/base
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
2424b1c
Enable RSC support in Pro initializer
ihabadham 325f3e3
Add RSCWebpackPlugin to client webpack config
ihabadham 2f11ffe
Create rscWebpackConfig.js for the RSC bundle
ihabadham 1c4bef2
Wire RSC bundle into webpackConfig.js
ihabadham cf394ba
Add 'use client' to all registered component entry points
ihabadham 8e8c518
Add /server-components route, controller action, and view
ihabadham 02b8b1c
Add RSC demo components for /server-components page
ihabadham b770daa
Add RSC watcher to Procfile.dev and nav link to /server-components
ihabadham 138befb
Include RSC bundle in default webpack build
ihabadham bc716e0
Move ServerComponentsPage to ror_components for auto-discovery
ihabadham 649e0bd
Wire RSC loader for both SWC and Babel transpilers
ihabadham 1ac1b27
Disable stubTimers in Pro Node renderer for RSC streaming
ihabadham fd9faf1
Enable replayServerAsyncOperationLogs on Pro Node renderer
ihabadham 9e17d03
Refactor RSC demo to canonical RoR Pro data flow
ihabadham df039e2
Add RSCRoute + ErrorBoundary demo to RSC page
ihabadham f2d0d3d
Use refetchComponent explicitly in retry path
ihabadham 0b626bc
Address PR review feedback
ihabadham 09b113e
Add request + system specs for the RSC demo
ihabadham f23beba
Gate LiveActivity 300ms delay on RSC_SUSPENSE_DEMO_DELAY
ihabadham e2c76a5
Wrap RSCRoute in local Suspense to prevent whole-page collapse
ihabadham 86eed7a
Add request spec variant exercising populated CommentsFeed
ihabadham File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| <%= stream_react_component("ServerComponentsPage", | ||
| props: { comments: @server_components_comments }, | ||
| prerender: true, | ||
| auto_load_bundle: true, | ||
| trace: Rails.env.development?) %> |
2 changes: 2 additions & 0 deletions
2
client/app/bundles/comments/components/Footer/ror_components/Footer.jsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
2 changes: 2 additions & 0 deletions
2
...pp/bundles/comments/components/SimpleCommentScreen/ror_components/SimpleCommentScreen.jsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
2 changes: 2 additions & 0 deletions
2
client/app/bundles/comments/rescript/ReScriptShow/ror_components/RescriptShow.jsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
2 changes: 2 additions & 0 deletions
2
client/app/bundles/comments/startup/App/ror_components/App.jsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
2 changes: 2 additions & 0 deletions
2
client/app/bundles/comments/startup/NavigationBarApp/ror_components/NavigationBarApp.jsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
2 changes: 2 additions & 0 deletions
2
client/app/bundles/comments/startup/RouterApp/ror_components/RouterApp.client.jsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
2 changes: 2 additions & 0 deletions
2
client/app/bundles/comments/startup/RouterApp/ror_components/RouterApp.server.jsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
84 changes: 84 additions & 0 deletions
84
client/app/bundles/server-components/components/CommentsFeed.jsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| import React from 'react'; | ||
| import { Marked } from 'marked'; | ||
| import { gfmHeadingId } from 'marked-gfm-heading-id'; | ||
| import sanitizeHtml from 'sanitize-html'; | ||
| import TogglePanel from './TogglePanel'; | ||
|
|
||
| const marked = new Marked(); | ||
| marked.use(gfmHeadingId()); | ||
|
|
||
| // Opt-in delay so the surrounding <Suspense> fallback is visible in the demo. | ||
| // Set RSC_SUSPENSE_DEMO_DELAY=true to enable; defaults off in production. | ||
| async function CommentsFeed({ comments = [] }) { | ||
| if (process.env.RSC_SUSPENSE_DEMO_DELAY === 'true') { | ||
| await new Promise((resolve) => { | ||
| setTimeout(resolve, 800); | ||
| }); | ||
| } | ||
|
|
||
| if (comments.length === 0) { | ||
| return ( | ||
| <div className="bg-amber-50 border border-amber-200 rounded-lg p-6 text-center"> | ||
| <p className="text-amber-700"> | ||
| No comments yet. Add some comments from the{' '} | ||
| <a href="/" className="underline font-medium"> | ||
| home page | ||
| </a>{' '} | ||
| to see them rendered here by server components. | ||
| </p> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| return ( | ||
| <div className="space-y-3"> | ||
| {comments.map((comment) => { | ||
| // marked + sanitize-html (~200KB combined) stay server-side. | ||
| const rawHtml = marked.parse(comment.text || ''); | ||
|
ihabadham marked this conversation as resolved.
ihabadham marked this conversation as resolved.
|
||
| const safeHtml = sanitizeHtml(rawHtml, { | ||
| allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img']), | ||
| allowedAttributes: { | ||
| ...sanitizeHtml.defaults.allowedAttributes, | ||
| img: ['src', 'alt', 'title', 'width', 'height'], | ||
| }, | ||
| allowedSchemes: ['https', 'http'], | ||
|
ihabadham marked this conversation as resolved.
ihabadham marked this conversation as resolved.
ihabadham marked this conversation as resolved.
ihabadham marked this conversation as resolved.
ihabadham marked this conversation as resolved.
ihabadham marked this conversation as resolved.
|
||
| }); | ||
|
ihabadham marked this conversation as resolved.
ihabadham marked this conversation as resolved.
|
||
|
|
||
| return ( | ||
| <div | ||
| key={comment.id} | ||
| className="bg-white border border-slate-200 rounded-lg p-4 shadow-sm hover:shadow-md transition-shadow" | ||
| > | ||
| <div className="flex items-center justify-between mb-2"> | ||
| <span className="font-semibold text-slate-800">{comment.author}</span> | ||
| <span className="text-xs text-slate-400"> | ||
| {new Date(comment.created_at).toLocaleDateString('en-US', { | ||
| month: 'short', | ||
| day: 'numeric', | ||
| year: 'numeric', | ||
| hour: '2-digit', | ||
| minute: '2-digit', | ||
| })} | ||
| </span> | ||
| </div> | ||
| <TogglePanel title="Show rendered markdown"> | ||
| {/* Content is sanitized via sanitize-html before rendering */} | ||
| {/* eslint-disable-next-line react/no-danger */} | ||
| <div | ||
| className="prose prose-sm prose-slate max-w-none" | ||
| dangerouslySetInnerHTML={{ __html: safeHtml }} | ||
| /> | ||
| </TogglePanel> | ||
| <p className="text-slate-600 text-sm mt-1">{comment.text}</p> | ||
| </div> | ||
| ); | ||
| })} | ||
| <p className="text-xs text-slate-400 text-center pt-2"> | ||
| {comments.length} comment{comments.length !== 1 ? 's' : ''} rendered on the server using{' '} | ||
| <code>marked</code> + <code>sanitize-html</code> (never sent to browser) | ||
| </p> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| export default CommentsFeed; | ||
96 changes: 96 additions & 0 deletions
96
client/app/bundles/server-components/components/LiveActivityRefresher.jsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| 'use client'; | ||
|
|
||
| import React, { useState, Suspense } from 'react'; | ||
| import { ErrorBoundary } from 'react-error-boundary'; | ||
| import RSCRoute from 'react-on-rails-pro/RSCRoute'; | ||
| import { useRSC } from 'react-on-rails-pro/RSCProvider'; | ||
|
|
||
| // Same shape and dimensions as the rendered LiveActivity card. Local Suspense | ||
| // fallback prevents the RSCRoute suspension from bubbling to an outer | ||
| // boundary, which would collapse the whole page during in-flight fetches. | ||
| const ActivityCardSkeleton = () => ( | ||
| <div className="bg-gradient-to-br from-indigo-50 to-purple-50 border border-indigo-200 rounded-xl p-5"> | ||
| <div className="grid grid-cols-3 gap-4 text-sm"> | ||
| {['Server Time', 'Free RAM', 'Uptime (hrs)'].map((label) => ( | ||
| <div key={label}> | ||
| <div className="text-xs text-indigo-600 font-medium uppercase tracking-wide mb-1"> | ||
| {label} | ||
| </div> | ||
| <div className="font-mono text-indigo-300 animate-pulse">—</div> | ||
| </div> | ||
| ))} | ||
| </div> | ||
| </div> | ||
| ); | ||
|
|
||
| const LiveActivityRefresher = () => { | ||
| const [refreshKey, setRefreshKey] = useState(0); | ||
| const [simulateError, setSimulateError] = useState(false); | ||
| const { refetchComponent } = useRSC(); | ||
|
|
||
| const handleRefresh = () => { | ||
| setSimulateError(false); | ||
| setRefreshKey((k) => k + 1); | ||
| }; | ||
|
|
||
| const handleSimulateError = () => { | ||
| setSimulateError(true); | ||
| setRefreshKey((k) => k + 1); | ||
| }; | ||
|
|
||
| // refetchComponent primes the cache with corrected props before resetting | ||
| // the boundary, so the post-reset render hits cache instead of re-fetching. | ||
| const buildRetry = (resetErrorBoundary) => () => { | ||
| const newKey = refreshKey + 1; | ||
|
ihabadham marked this conversation as resolved.
|
||
| setSimulateError(false); | ||
| setRefreshKey(newKey); | ||
| refetchComponent('LiveActivity', { simulateError: false, refreshKey: newKey }) | ||
| // eslint-disable-next-line no-console | ||
|
ihabadham marked this conversation as resolved.
|
||
| .catch((err) => console.error('Retry refetch failed:', err)) | ||
| .finally(() => resetErrorBoundary()); | ||
|
ihabadham marked this conversation as resolved.
ihabadham marked this conversation as resolved.
|
||
| }; | ||
|
|
||
| return ( | ||
| <div className="space-y-3"> | ||
| <div className="flex items-center gap-2"> | ||
| <button | ||
| type="button" | ||
| onClick={handleRefresh} | ||
| className="px-3 py-1.5 text-sm bg-indigo-600 text-white rounded hover:bg-indigo-700" | ||
| > | ||
| Refresh | ||
| </button> | ||
| <button | ||
| type="button" | ||
| onClick={handleSimulateError} | ||
| className="px-3 py-1.5 text-sm bg-amber-100 text-amber-800 border border-amber-300 rounded hover:bg-amber-200" | ||
| > | ||
| Simulate Error | ||
| </button> | ||
| <span className="text-xs text-slate-500 ml-2">Refresh count: {refreshKey}</span> | ||
| </div> | ||
| <ErrorBoundary | ||
| fallbackRender={({ error, resetErrorBoundary }) => ( | ||
| <div className="bg-rose-50 border border-rose-200 rounded-lg p-4"> | ||
| <p className="text-rose-700 font-semibold mb-1">Server component fetch failed</p> | ||
| <p className="text-rose-600 text-sm font-mono mb-3">{error.message}</p> | ||
| <button | ||
| type="button" | ||
| onClick={buildRetry(resetErrorBoundary)} | ||
| className="px-3 py-1.5 text-sm bg-rose-600 text-white rounded hover:bg-rose-700" | ||
| > | ||
| Retry | ||
| </button> | ||
| </div> | ||
| )} | ||
| resetKeys={[refreshKey]} | ||
| > | ||
| <Suspense fallback={<ActivityCardSkeleton />}> | ||
| <RSCRoute componentName="LiveActivity" componentProps={{ simulateError, refreshKey }} /> | ||
|
ihabadham marked this conversation as resolved.
|
||
| </Suspense> | ||
| </ErrorBoundary> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default LiveActivityRefresher; | ||
56 changes: 56 additions & 0 deletions
56
client/app/bundles/server-components/components/ServerInfo.jsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| // Server Component - uses Node.js os module, which only exists on the server. | ||
| // This component and its dependencies are never sent to the browser. | ||
|
|
||
| import React from 'react'; | ||
| import os from 'os'; | ||
| import _ from 'lodash'; | ||
|
|
||
| function ServerInfo() { | ||
| const serverInfo = { | ||
| platform: os.platform(), | ||
| arch: os.arch(), | ||
| nodeVersion: process.version, | ||
| uptime: Math.floor(os.uptime() / 3600), | ||
| totalMemory: (os.totalmem() / (1024 * 1024 * 1024)).toFixed(1), | ||
| freeMemory: (os.freemem() / (1024 * 1024 * 1024)).toFixed(1), | ||
| cpus: os.cpus().length, | ||
| }; | ||
|
|
||
| // Using lodash on the server — this 70KB+ library stays server-side | ||
| const infoEntries = _.toPairs(serverInfo); | ||
| const grouped = _.chunk(infoEntries, 4); | ||
|
|
||
| const labels = { | ||
| platform: 'Platform', | ||
| arch: 'Architecture', | ||
| nodeVersion: 'Node.js', | ||
| uptime: 'Uptime (hrs)', | ||
| totalMemory: 'Total RAM (GB)', | ||
| freeMemory: 'Free RAM (GB)', | ||
| cpus: 'CPU Cores', | ||
| }; | ||
|
|
||
| return ( | ||
| <div className="bg-gradient-to-br from-emerald-50 to-teal-50 border border-emerald-200 rounded-xl p-6"> | ||
| <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> | ||
| <div className="grid md:grid-cols-2 gap-x-8 gap-y-1"> | ||
| {grouped.map((group) => ( | ||
| <div key={group.map(([k]) => k).join('-')} className="space-y-1"> | ||
| {group.map(([key, value]) => ( | ||
| <div key={key} className="flex justify-between py-1.5 border-b border-emerald-100 last:border-0"> | ||
| <span className="text-sm text-emerald-700 font-medium">{labels[key] || key}</span> | ||
| <span className="text-sm text-emerald-900 font-mono">{value}</span> | ||
| </div> | ||
| ))} | ||
| </div> | ||
| ))} | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| export default ServerInfo; |
40 changes: 40 additions & 0 deletions
40
client/app/bundles/server-components/components/TogglePanel.jsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| 'use client'; | ||
|
|
||
| import React, { useState } from 'react'; | ||
| import PropTypes from 'prop-types'; | ||
|
|
||
| const TogglePanel = ({ title, children }) => { | ||
| const [isOpen, setIsOpen] = useState(false); | ||
|
|
||
| return ( | ||
| <div className="border border-slate-200 rounded-lg overflow-hidden"> | ||
| <button | ||
|
ihabadham marked this conversation as resolved.
|
||
| type="button" | ||
| onClick={() => setIsOpen((prev) => !prev)} | ||
| className="w-full flex items-center justify-between px-4 py-2.5 bg-slate-50 hover:bg-slate-100 transition-colors text-left" | ||
| > | ||
| <span className="text-sm font-medium text-slate-700">{title}</span> | ||
| <svg | ||
| className={`w-4 h-4 text-slate-400 transition-transform ${isOpen ? 'rotate-180' : ''}`} | ||
| fill="none" | ||
| viewBox="0 0 24 24" | ||
| stroke="currentColor" | ||
| > | ||
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" /> | ||
| </svg> | ||
| </button> | ||
| {isOpen && ( | ||
| <div className="px-4 py-3 bg-white"> | ||
| {children} | ||
| </div> | ||
| )} | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| TogglePanel.propTypes = { | ||
| title: PropTypes.string.isRequired, | ||
| children: PropTypes.node.isRequired, | ||
| }; | ||
|
|
||
| export default TogglePanel; | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.