From 4d09e130580381b1c78a444feb3ebc104bf1f8e8 Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Sat, 4 Apr 2026 00:18:02 -1000 Subject: [PATCH 01/16] Add React Server Components with React on Rails Pro 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) --- Gemfile | 2 +- Gemfile.lock | 36 ++++- Procfile.dev | 2 + app/controllers/pages_controller.rb | 2 + app/views/pages/server_components.html.erb | 5 + .../Footer/ror_components/Footer.jsx | 2 + .../NavigationBar/NavigationBar.jsx | 8 + .../ror_components/SimpleCommentScreen.jsx | 4 +- .../app/bundles/comments/constants/paths.js | 1 + .../ror_components/RescriptShow.jsx | 2 + .../startup/App/ror_components/App.jsx | 4 +- .../startup/ClientRouterAppExpress.jsx | 2 +- .../ror_components/NavigationBarApp.jsx | 4 +- .../ror_components/RouterApp.client.jsx | 4 +- .../ror_components/RouterApp.server.jsx | 4 +- .../comments/startup/serverRegistration.jsx | 4 +- .../ServerComponentsPage.jsx | 129 ++++++++++++++++ .../components/CommentsFeed.jsx | 85 +++++++++++ .../components/ServerInfo.jsx | 58 +++++++ .../components/TogglePanel.jsx | 34 +++++ client/app/libs/requestsManager.js | 2 +- client/app/packs/rsc-bundle.js | 16 ++ client/app/packs/rsc-client-components.js | 11 ++ client/app/packs/stimulus-bundle.js | 12 +- client/app/packs/stores-registration.js | 4 +- config/initializers/react_on_rails_pro.rb | 9 ++ config/routes.rb | 4 + config/webpack/clientWebpackConfig.js | 22 ++- config/webpack/commonWebpackConfig.js | 7 + config/webpack/rscWebpackConfig.js | 111 ++++++++++++++ config/webpack/rspackRscPlugin.js | 144 ++++++++++++++++++ config/webpack/serverWebpackConfig.js | 21 ++- config/webpack/webpackConfig.js | 28 ++-- package.json | 11 +- yarn.lock | 61 +++++--- 35 files changed, 804 insertions(+), 51 deletions(-) create mode 100644 app/views/pages/server_components.html.erb create mode 100644 client/app/bundles/server-components/ServerComponentsPage.jsx create mode 100644 client/app/bundles/server-components/components/CommentsFeed.jsx create mode 100644 client/app/bundles/server-components/components/ServerInfo.jsx create mode 100644 client/app/bundles/server-components/components/TogglePanel.jsx create mode 100644 client/app/packs/rsc-bundle.js create mode 100644 client/app/packs/rsc-client-components.js create mode 100644 config/initializers/react_on_rails_pro.rb create mode 100644 config/webpack/rscWebpackConfig.js create mode 100644 config/webpack/rspackRscPlugin.js diff --git a/Gemfile b/Gemfile index e9eb85053..7acd9bfc0 100644 --- a/Gemfile +++ b/Gemfile @@ -5,7 +5,7 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" } ruby "3.4.6" -gem "react_on_rails", "16.6.0.rc.0" +gem "react_on_rails_pro", "16.5.1" gem "shakapacker", "10.0.0.rc.0" # Bundle edge Rails instead: gem "rails", github: "rails/rails" diff --git a/Gemfile.lock b/Gemfile.lock index b290c3986..3e0ee2285 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -78,6 +78,12 @@ GEM addressable (2.8.7) public_suffix (>= 2.0.2, < 7.0) ast (2.4.3) + async (2.38.1) + console (~> 1.29) + fiber-annotation + io-event (~> 1.11) + metrics (~> 0.12) + traces (~> 0.18) autoprefixer-rails (10.4.16.0) execjs (~> 2) awesome_print (1.9.2) @@ -115,6 +121,10 @@ GEM coffee-script-source (1.12.2) concurrent-ruby (1.3.6) connection_pool (3.0.2) + console (1.34.3) + fiber-annotation + fiber-local (~> 1.1) + json coveralls_reborn (0.25.0) simplecov (>= 0.18.1, < 0.22.0) term-ansicolor (~> 1.6) @@ -146,16 +156,24 @@ GEM railties (>= 5.0.0) ffi (1.17.2-arm64-darwin) ffi (1.17.2-x86_64-linux-gnu) + fiber-annotation (0.2.0) + fiber-local (1.1.0) + fiber-storage + fiber-storage (1.0.1) foreman (0.88.1) generator_spec (0.10.0) activesupport (>= 3.0.0) railties (>= 3.0.0) globalid (1.3.0) activesupport (>= 6.1) + http-2 (1.1.3) + httpx (1.7.5) + http-2 (>= 1.1.3) i18n (1.14.8) concurrent-ruby (~> 1.0) interception (0.5) io-console (0.8.2) + io-event (1.14.5) irb (1.17.0) pp (>= 0.6.0) prism (>= 1.3.0) @@ -165,6 +183,8 @@ GEM actionview (>= 5.0.0) activesupport (>= 5.0.0) json (2.19.1) + jwt (2.10.2) + base64 language_server-protocol (3.17.0.5) launchy (3.0.1) addressable (~> 2.8) @@ -182,6 +202,7 @@ GEM marcel (1.1.0) matrix (0.4.2) method_source (1.1.0) + metrics (0.15.0) mini_mime (1.1.5) minitest (6.0.2) drb (~> 2.0) @@ -296,13 +317,23 @@ GEM erb psych (>= 4.0.0) tsort - react_on_rails (16.6.0.rc.0) + react_on_rails (16.5.1) addressable connection_pool execjs (~> 2.5) rails (>= 5.2) rainbow (~> 3.0) shakapacker (>= 6.0) + react_on_rails_pro (16.5.1) + addressable + async (>= 2.29) + connection_pool + execjs (~> 2.9) + http-2 (>= 1.1.1) + httpx (~> 1.5) + jwt (~> 2.7) + rainbow + react_on_rails (= 16.5.1) redcarpet (3.6.0) redis (5.3.0) redis-client (>= 0.22.0) @@ -425,6 +456,7 @@ GEM tins (1.33.0) bigdecimal sync + traces (0.18.2) tsort (0.2.0) turbo-rails (2.0.11) actionpack (>= 6.0.0) @@ -486,7 +518,7 @@ DEPENDENCIES rails-html-sanitizer rails_best_practices rainbow - react_on_rails (= 16.6.0.rc.0) + react_on_rails_pro (= 16.5.1) redcarpet redis (~> 5.0) rspec-rails (~> 6.0.0) diff --git a/Procfile.dev b/Procfile.dev index 102c0a8df..cae685eca 100644 --- a/Procfile.dev +++ b/Procfile.dev @@ -12,3 +12,5 @@ rails: bundle exec thrust bin/rails server -p 3000 wp-client: RAILS_ENV=development NODE_ENV=development bin/shakapacker-dev-server # Server Rspack watcher for SSR bundle wp-server: SERVER_BUNDLE_ONLY=yes bin/shakapacker --watch +# RSC Rspack watcher for React Server Components bundle +wp-rsc: RSC_BUNDLE_ONLY=true bin/shakapacker --watch diff --git a/app/controllers/pages_controller.rb b/app/controllers/pages_controller.rb index 507cc6cf7..f435f04eb 100644 --- a/app/controllers/pages_controller.rb +++ b/app/controllers/pages_controller.rb @@ -38,6 +38,8 @@ def simple; end def rescript; end + def server_components; end + private def set_comments diff --git a/app/views/pages/server_components.html.erb b/app/views/pages/server_components.html.erb new file mode 100644 index 000000000..edff32059 --- /dev/null +++ b/app/views/pages/server_components.html.erb @@ -0,0 +1,5 @@ +<%= append_javascript_pack_tag('rsc-client-components') %> +<%= react_component("ServerComponentsPage", + prerender: false, + trace: true, + id: "ServerComponentsPage-react-component-0") %> diff --git a/client/app/bundles/comments/components/Footer/ror_components/Footer.jsx b/client/app/bundles/comments/components/Footer/ror_components/Footer.jsx index 5e7f42104..d153dfb22 100644 --- a/client/app/bundles/comments/components/Footer/ror_components/Footer.jsx +++ b/client/app/bundles/comments/components/Footer/ror_components/Footer.jsx @@ -1,3 +1,5 @@ +'use client'; + import React from 'react'; import PropTypes from 'prop-types'; import BaseComponent from 'libs/components/BaseComponent'; diff --git a/client/app/bundles/comments/components/NavigationBar/NavigationBar.jsx b/client/app/bundles/comments/components/NavigationBar/NavigationBar.jsx index db2b4e53c..30b99f371 100644 --- a/client/app/bundles/comments/components/NavigationBar/NavigationBar.jsx +++ b/client/app/bundles/comments/components/NavigationBar/NavigationBar.jsx @@ -102,6 +102,14 @@ function NavigationBar(props) { Rescript +
  • + + RSC Demo + +
  • { + return ( +
    +
    +

    + React Server Components Demo +

    +

    + This page is rendered using React Server Components 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. +

    +
    + +
    + {/* Server Info - uses Node.js os module (impossible on client) */} +
    +

    + Server Environment + + Server Only + +

    + +
    + + {/* Interactive toggle - demonstrates mixing server + client components */} +
    +

    + Interactive Client Component + + Client Hydrated + +

    + +
    +

    + This toggle is a 'use client' 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 donut pattern. +

    +
      +
    • The TogglePanel wrapper runs on the client (handles click events)
    • +
    • The children content is rendered on the server (no JS cost)
    • +
    • Heavy libraries used by server components never reach the browser
    • +
    +
    +
    +
    + + {/* Async data fetching with Suspense streaming */} +
    +

    + Streamed Comments + + Async + Suspense + +

    +

    + Comments are fetched directly on the server using the Rails API. + The page shell renders immediately while this section streams in progressively. +

    + + {[1, 2, 3].map((i) => ( +
    +
    +
    +
    + ))} +
    + } + > + + +
    + + {/* Architecture explanation */} +
    +

    + What makes this different? +

    +
    +
    +

    Smaller Client Bundle

    +

    + Libraries like lodash, marked, and Node.js os module + are used on this page but never downloaded by the browser. +

    +
    +
    +

    Direct Data Access

    +

    + Server components fetch data by calling your Rails API internally — no + client-side fetch waterfalls or loading spinners for initial data. +

    +
    +
    +

    Progressive Streaming

    +

    + The page shell renders instantly. Async components (like the comments feed) + stream in as their data resolves, with Suspense boundaries showing fallbacks. +

    +
    +
    +

    Selective Hydration

    +

    + Only client components (like the toggle above) receive JavaScript. + Everything else is pure HTML — zero hydration cost. +

    +
    +
    +
    +
    +
    + ); +}; + +export default ServerComponentsPage; diff --git a/client/app/bundles/server-components/components/CommentsFeed.jsx b/client/app/bundles/server-components/components/CommentsFeed.jsx new file mode 100644 index 000000000..3d023cad4 --- /dev/null +++ b/client/app/bundles/server-components/components/CommentsFeed.jsx @@ -0,0 +1,85 @@ +// Server Component - fetches comments directly from the Rails API on the server. +// Uses marked for markdown rendering. Both fetch and marked stay server-side. + +import React from 'react'; +import { Marked } from 'marked'; +import { gfmHeadingId } from 'marked-gfm-heading-id'; +import sanitizeHtml from 'sanitize-html'; +import _ from 'lodash'; +import TogglePanel from './TogglePanel'; + +const marked = new Marked(); +marked.use(gfmHeadingId()); + +async function CommentsFeed() { + // Simulate network latency to demonstrate streaming + await new Promise((resolve) => setTimeout(resolve, 800)); + + // Fetch comments directly from the Rails API — no client-side fetch needed + const response = await fetch('http://localhost:3000/comments.json'); + const comments = await response.json(); + + // Use lodash to process (stays on server) + const sortedComments = _.orderBy(comments, ['created_at'], ['desc']); + const recentComments = _.take(sortedComments, 10); + + if (recentComments.length === 0) { + return ( +
    +

    + No comments yet. Add some comments from the{' '} + home page to see them rendered here + by server components. +

    +
    + ); + } + + return ( +
    + {recentComments.map((comment) => { + // Render markdown on the server using marked + sanitize-html. + // sanitize-html strips any dangerous HTML before rendering. + // These libraries (combined ~200KB) never reach the client. + const rawHtml = marked.parse(comment.text || ''); + const safeHtml = sanitizeHtml(rawHtml, { + allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img']), + }); + + return ( +
    +
    + {comment.author} + + {new Date(comment.created_at).toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + })} + +
    + + {/* Content is sanitized via sanitize-html before rendering */} +
    + +

    {comment.text}

    +
    + ); + })} +

    + {recentComments.length} comment{recentComments.length !== 1 ? 's' : ''} rendered on the server using + {' '}marked + sanitize-html (never sent to browser) +

    +
    + ); +} + +export default CommentsFeed; diff --git a/client/app/bundles/server-components/components/ServerInfo.jsx b/client/app/bundles/server-components/components/ServerInfo.jsx new file mode 100644 index 000000000..7c059e3c7 --- /dev/null +++ b/client/app/bundles/server-components/components/ServerInfo.jsx @@ -0,0 +1,58 @@ +// 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'; + +async 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, + hostname: os.hostname(), + }; + + // 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', + hostname: 'Hostname', + }; + + return ( +
    +

    + This data comes from the Node.js os module + — it runs only on the server. The lodash library + used to format it never reaches the browser. +

    +
    + {grouped.map((group, gi) => ( +
    + {group.map(([key, value]) => ( +
    + {labels[key] || key} + {value} +
    + ))} +
    + ))} +
    +
    + ); +} + +export default ServerInfo; diff --git a/client/app/bundles/server-components/components/TogglePanel.jsx b/client/app/bundles/server-components/components/TogglePanel.jsx new file mode 100644 index 000000000..f5a38a9eb --- /dev/null +++ b/client/app/bundles/server-components/components/TogglePanel.jsx @@ -0,0 +1,34 @@ +'use client'; + +import React, { useState } from 'react'; + +const TogglePanel = ({ title, children }) => { + const [isOpen, setIsOpen] = useState(false); + + return ( +
    + + {isOpen && ( +
    + {children} +
    + )} +
    + ); +}; + +export default TogglePanel; diff --git a/client/app/libs/requestsManager.js b/client/app/libs/requestsManager.js index 6b5fad453..c9209c7b4 100644 --- a/client/app/libs/requestsManager.js +++ b/client/app/libs/requestsManager.js @@ -1,5 +1,5 @@ import request from 'axios'; -import ReactOnRails from 'react-on-rails'; +import ReactOnRails from 'react-on-rails-pro'; const API_URL = 'comments.json'; diff --git a/client/app/packs/rsc-bundle.js b/client/app/packs/rsc-bundle.js new file mode 100644 index 000000000..83d8936a1 --- /dev/null +++ b/client/app/packs/rsc-bundle.js @@ -0,0 +1,16 @@ +// RSC (React Server Components) bundle entry point. +// This file is only used by the RSC bundle configuration. +// It imports the same client component registrations as server-bundle.js, +// plus the server component registrations. + +// Import existing client component registrations +import './stores-registration'; +import './../generated/server-bundle-generated.js'; + +// React Server Components registration (server-side) +import registerServerComponent from 'react-on-rails-pro/registerServerComponent/server'; +import ServerComponentsPage from '../bundles/server-components/ServerComponentsPage'; + +registerServerComponent({ + ServerComponentsPage, +}); diff --git a/client/app/packs/rsc-client-components.js b/client/app/packs/rsc-client-components.js new file mode 100644 index 000000000..b33ff5a20 --- /dev/null +++ b/client/app/packs/rsc-client-components.js @@ -0,0 +1,11 @@ +'use client'; + +// RSC client components registration. +// Components with 'use client' that are used in server components must be +// available in a client bundle chunk so the React flight client can load them. + +import TogglePanel from '../bundles/server-components/components/TogglePanel'; +import ReactOnRails from 'react-on-rails-pro'; + +// Register as a standard component so it's bundled in a client-accessible chunk +ReactOnRails.register({ TogglePanel }); diff --git a/client/app/packs/stimulus-bundle.js b/client/app/packs/stimulus-bundle.js index a11ccf149..97b9586b5 100644 --- a/client/app/packs/stimulus-bundle.js +++ b/client/app/packs/stimulus-bundle.js @@ -1,4 +1,6 @@ -import ReactOnRails from 'react-on-rails'; +'use client'; + +import ReactOnRails from 'react-on-rails-pro'; import 'jquery-ujs'; import { Turbo } from '@hotwired/turbo-rails'; @@ -17,3 +19,11 @@ ReactOnRails.setOptions({ // Components are now auto-registered via ror_components directories // No need for manual registration + +// React Server Components registration (client-side) +import registerServerComponent from 'react-on-rails-pro/registerServerComponent/client'; + +registerServerComponent( + { rscPayloadGenerationUrlPath: 'rsc_payload/' }, + 'ServerComponentsPage', +); diff --git a/client/app/packs/stores-registration.js b/client/app/packs/stores-registration.js index 435653379..cecf0a958 100644 --- a/client/app/packs/stores-registration.js +++ b/client/app/packs/stores-registration.js @@ -1,4 +1,6 @@ -import ReactOnRails from 'react-on-rails'; +'use client'; + +import ReactOnRails from 'react-on-rails-pro'; import routerCommentsStore from '../bundles/comments/store/routerCommentsStore'; import commentsStore from '../bundles/comments/store/commentsStore'; diff --git a/config/initializers/react_on_rails_pro.rb b/config/initializers/react_on_rails_pro.rb new file mode 100644 index 000000000..a3a8c1312 --- /dev/null +++ b/config/initializers/react_on_rails_pro.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +ReactOnRailsPro.configure do |config| + # Enable React Server Components support + config.enable_rsc_support = true + + # RSC bundle file name (built by rscWebpackConfig.js) + config.rsc_bundle_js_file = "rsc-bundle.js" +end diff --git a/config/routes.rb b/config/routes.rb index 1d8c7b7a5..50a6b1e7e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -3,6 +3,9 @@ Rails.application.routes.draw do # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html + # React Server Components payload route + rsc_payload_route + # Serve websocket cable requests in-process # mount ActionCable.server => '/cable' @@ -11,6 +14,7 @@ get "simple", to: "pages#simple" get "rescript", to: "pages#rescript" get "no-router", to: "pages#no_router" + get "server-components", to: "pages#server_components" # React Router needs a wildcard get "react-router(/*all)", to: "pages#index" diff --git a/config/webpack/clientWebpackConfig.js b/config/webpack/clientWebpackConfig.js index 6352208fb..84cd35230 100644 --- a/config/webpack/clientWebpackConfig.js +++ b/config/webpack/clientWebpackConfig.js @@ -3,6 +3,7 @@ const commonWebpackConfig = require('./commonWebpackConfig'); const { getBundler } = require('./bundlerUtils'); +const { RspackRscPlugin } = require('./rspackRscPlugin'); const configureClient = () => { const bundler = getBundler(); @@ -16,11 +17,28 @@ const configureClient = () => { }), ); - // server-bundle is special and should ONLY be built by the serverConfig - // In case this entry is not deleted, a very strange "window" not found + // RSC: Generate react-client-manifest.json for client component resolution + clientConfig.plugins.push(new RspackRscPlugin({ isServer: false })); + + // server-bundle and rsc-bundle should ONLY be built by their respective configs. + // In case these entries are not deleted, a very strange "window" not found // error shows referring to window["webpackJsonp"]. That is because the // client config is going to try to load chunks. delete clientConfig.entry['server-bundle']; + delete clientConfig.entry['rsc-bundle']; + + // react-on-rails-pro includes server-side code that imports Node.js modules. + // Provide empty fallbacks for the client bundle so it doesn't fail to resolve them. + clientConfig.resolve = { + ...clientConfig.resolve, + fallback: { + ...clientConfig.resolve?.fallback, + path: false, + fs: false, + 'fs/promises': false, + stream: false, + }, + }; return clientConfig; }; diff --git a/config/webpack/commonWebpackConfig.js b/config/webpack/commonWebpackConfig.js index 1a99ddbc5..6aad08238 100644 --- a/config/webpack/commonWebpackConfig.js +++ b/config/webpack/commonWebpackConfig.js @@ -8,6 +8,13 @@ const commonOptions = { resolve: { // Add .res.js extension for ReScript-compiled modules (modern ReScript convention) extensions: ['.css', '.ts', '.tsx', '.res.js'], + // Alias react-on-rails to react-on-rails-pro so third-party packages + // (like rescript-react-on-rails) that import 'react-on-rails' get the Pro version. + // This avoids "Cannot mix react-on-rails (core) with react-on-rails-pro" errors. + alias: { + 'react-on-rails$': 'react-on-rails-pro', + 'react-on-rails/node_package': 'react-on-rails-pro/node_package', + }, }, }; diff --git a/config/webpack/rscWebpackConfig.js b/config/webpack/rscWebpackConfig.js new file mode 100644 index 000000000..3d76a7148 --- /dev/null +++ b/config/webpack/rscWebpackConfig.js @@ -0,0 +1,111 @@ +// RSC (React Server Components) bundle configuration. +// +// This creates a third bundle alongside client and server bundles. +// The RSC bundle runs server components in the Node renderer and produces +// the Flight payload that React uses to hydrate on the client. +// +// Unlike the server bundle (which uses ExecJS), the RSC bundle targets Node.js +// and can use Node.js built-in modules like os, fs, path, etc. + +const path = require('path'); +const { config } = require('shakapacker'); +const commonWebpackConfig = require('./commonWebpackConfig'); +const { getBundler } = require('./bundlerUtils'); +const { RspackRscPlugin } = require('./rspackRscPlugin'); + +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); + }); +} + +const configureRsc = () => { + const bundler = getBundler(); + const rscConfig = commonWebpackConfig(); + + // Use the dedicated rsc-bundle entry point + const rscEntry = rscConfig.entry['rsc-bundle']; + if (!rscEntry) { + throw new Error( + 'RSC bundle entry not found. Ensure client/app/packs/rsc-bundle.js exists.', + ); + } + rscConfig.entry = { 'rsc-bundle': rscEntry }; + + // Remove CSS extraction plugins (same as server config — CSS handled by client) + rscConfig.plugins = rscConfig.plugins.filter( + (plugin) => + plugin.constructor.name !== 'WebpackAssetsManifest' && + plugin.constructor.name !== 'MiniCssExtractPlugin' && + plugin.constructor.name !== 'CssExtractRspackPlugin' && + plugin.constructor.name !== 'ForkTsCheckerWebpackPlugin', + ); + + // Remove CSS extraction loaders from style rules + rscConfig.module.rules.forEach((rule) => { + if (Array.isArray(rule.use)) { + rule.use = rule.use.filter((item) => { + const testValue = typeof item === 'string' ? item : item?.loader; + return !( + testValue?.match(/mini-css-extract-plugin/) || + testValue?.match(/CssExtractRspackPlugin/) || + testValue?.includes('cssExtractLoader') || + testValue === 'style-loader' + ); + }); + const cssLoader = rule.use.find((item) => { + const testValue = typeof item === 'string' ? item : item?.loader; + return testValue?.includes('css-loader'); + }); + if (cssLoader?.options?.modules) { + cssLoader.options.modules = { ...cssLoader.options.modules, exportOnlyLocals: true }; + } + } else if (rule.use && (rule.use.loader === 'url-loader' || rule.use.loader === 'file-loader')) { + rule.use.options.emitFile = false; + } + }); + + // Add the RSC WebpackLoader to transpiler rules. + // This loader handles 'use client' directive detection and server/client component separation. + rscConfig.module.rules.forEach((rule) => { + if (Array.isArray(rule.use)) { + const transpilerLoader = extractLoader(rule, 'swc-loader') || extractLoader(rule, 'babel-loader'); + if (transpilerLoader) { + rule.use.push({ + loader: 'react-on-rails-rsc/WebpackLoader', + }); + } + } + }); + + // Enable react-server condition for server component resolution + rscConfig.resolve = { + ...rscConfig.resolve, + conditionNames: ['react-server', '...'], + }; + + // No code splitting for the RSC bundle + rscConfig.optimization = { minimize: false }; + rscConfig.plugins.unshift(new bundler.optimize.LimitChunkCountPlugin({ maxChunks: 1 })); + + // Output to the same SSR directory as the server bundle + rscConfig.output = { + filename: 'rsc-bundle.js', + globalObject: 'this', + path: path.resolve(__dirname, '../../ssr-generated'), + publicPath: config.publicPath, + }; + + // Target Node.js so server-only modules (os, fs, stream, etc.) resolve correctly + rscConfig.target = 'node'; + rscConfig.devtool = 'eval'; + + // RSC manifest plugin + rscConfig.plugins.push(new RspackRscPlugin({ isServer: true })); + + return rscConfig; +}; + +module.exports = configureRsc; diff --git a/config/webpack/rspackRscPlugin.js b/config/webpack/rspackRscPlugin.js new file mode 100644 index 000000000..d3cbf9a22 --- /dev/null +++ b/config/webpack/rspackRscPlugin.js @@ -0,0 +1,144 @@ +// Rspack-compatible RSC manifest plugin. +// +// The standard RSCWebpackPlugin from react-on-rails-rsc uses webpack internals +// (ModuleDependency, NullDependency, contextModuleFactory.resolveDependencies) +// that are not available in Rspack. This lightweight plugin generates the +// react-client-manifest.json and react-server-client-manifest.json files +// that the React flight protocol needs to resolve client component references. + +const fs = require('fs'); +const { sources } = require('@rspack/core'); + +// Cache for file 'use client' checks +const useClientCache = new Map(); + +function hasUseClientDirective(filePath) { + if (useClientCache.has(filePath)) return useClientCache.get(filePath); + + let result = false; + try { + // Read the first ~200 bytes — 'use client' must be at the very top of the file + const fd = fs.openSync(filePath, 'r'); + const buf = Buffer.alloc(200); + fs.readSync(fd, buf, 0, 200, 0); + fs.closeSync(fd); + + const head = buf.toString('utf-8'); + // Check for 'use client' as the first statement (with or without semicolons/quotes) + result = /^(?:\s*\/\/[^\n]*\n)*\s*['"]use client['"]/.test(head); + } catch (_) { + // file doesn't exist or can't be read + } + + useClientCache.set(filePath, result); + return result; +} + +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'; + } + + apply(compiler) { + compiler.hooks.thisCompilation.tap('RspackRscPlugin', (compilation) => { + compilation.hooks.processAssets.tap( + { + name: 'RspackRscPlugin', + stage: compilation.constructor.PROCESS_ASSETS_STAGE_REPORT || 5000, + }, + () => { + const manifest = {}; + + for (const chunk of compilation.chunks) { + const chunkFiles = []; + for (const file of chunk.files) { + if (file.endsWith('.js') && !file.endsWith('.hot-update.js')) { + chunkFiles.push(file); + break; + } + } + + const modules = compilation.chunkGraph + ? compilation.chunkGraph.getChunkModulesIterable(chunk) + : []; + + for (const mod of modules) { + this._processModule(mod, chunk, chunkFiles, manifest, compilation); + // Handle concatenated modules + if (mod.modules) { + for (const innerMod of mod.modules) { + this._processModule(innerMod, chunk, chunkFiles, manifest, compilation); + } + } + } + } + + compilation.emitAsset( + this.clientManifestFilename, + new sources.RawSource(JSON.stringify(manifest, null, 2)), + ); + + // Emit SSR manifest (maps module IDs to SSR module data) + if (!this.isServer) { + compilation.emitAsset( + this.ssrManifestFilename, + new sources.RawSource(JSON.stringify({}, null, 2)), + ); + } + }, + ); + }); + } + + _processModule(mod, chunk, chunkFiles, manifest, compilation) { + const resource = mod.resource || mod.userRequest; + if (!resource || !resource.match(/\.(js|jsx|ts|tsx)$/)) return; + // Skip node_modules + if (resource.includes('node_modules')) return; + + // Check original file for 'use client' directive + if (!hasUseClientDirective(resource)) return; + + const moduleId = compilation.chunkGraph + ? compilation.chunkGraph.getModuleId(mod) + : mod.id; + + if (moduleId == null) return; + + const chunks = []; + for (const file of chunkFiles) { + chunks.push(chunk.id, file); + } + + // Build the module entry with all exported names + const ssrEntry = { + id: moduleId, + chunks: chunks, + name: '*', + async: false, + }; + + // Use resource path as the key (React flight protocol convention) + const key = resource; + if (!manifest[key]) { + manifest[key] = {}; + } + manifest[key]['*'] = ssrEntry; + manifest[key][''] = ssrEntry; + + // Also add default export entry + manifest[key]['default'] = { + ...ssrEntry, + name: 'default', + }; + } +} + +module.exports = { RspackRscPlugin }; diff --git a/config/webpack/serverWebpackConfig.js b/config/webpack/serverWebpackConfig.js index 8dada6496..70d29cc6e 100644 --- a/config/webpack/serverWebpackConfig.js +++ b/config/webpack/serverWebpackConfig.js @@ -5,6 +5,22 @@ const path = require('path'); const { config } = require('shakapacker'); const commonWebpackConfig = require('./commonWebpackConfig'); const { getBundler } = require('./bundlerUtils'); +const { RspackRscPlugin } = require('./rspackRscPlugin'); + +/** + * Extract a specific loader from a webpack rule's use array. + * + * @param {Object} rule - Webpack rule with a use array + * @param {string} loaderName - Substring to match against loader names + * @returns {Object|null} The matching loader entry, or null + */ +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); + }); +} /** * Generates the server-side rendering (SSR) bundle configuration. @@ -153,7 +169,10 @@ const configureServer = () => { // If using the React on Rails Pro node server renderer, uncomment the next line // serverWebpackConfig.target = 'node' + // RSC: Generate react-server-client-manifest.json for SSR component resolution + serverWebpackConfig.plugins.push(new RspackRscPlugin({ isServer: true })); + return serverWebpackConfig; }; -module.exports = configureServer; +module.exports = { default: configureServer, extractLoader }; diff --git a/config/webpack/webpackConfig.js b/config/webpack/webpackConfig.js index 4f68574e2..007135387 100644 --- a/config/webpack/webpackConfig.js +++ b/config/webpack/webpackConfig.js @@ -2,31 +2,37 @@ // https://github.com/shakacode/react_on_rails_tutorial_with_ssr_and_hmr_fast_refresh/blob/master/config/webpack/webpackConfig.js const clientWebpackConfig = require('./clientWebpackConfig'); -const serverWebpackConfig = require('./serverWebpackConfig'); +const { default: serverWebpackConfig } = require('./serverWebpackConfig'); +const rscWebpackConfig = require('./rscWebpackConfig'); const webpackConfig = (envSpecific) => { - const clientConfig = clientWebpackConfig(); - const serverConfig = serverWebpackConfig(); - - if (envSpecific) { - envSpecific(clientConfig, serverConfig); - } - let result; // For HMR, need to separate the the client and server webpack configurations if (process.env.WEBPACK_SERVE || process.env.CLIENT_BUNDLE_ONLY) { + const clientConfig = clientWebpackConfig(); + if (envSpecific) envSpecific(clientConfig, null); // eslint-disable-next-line no-console console.log('[React on Rails] Creating only the client bundles.'); result = clientConfig; } else if (process.env.SERVER_BUNDLE_ONLY) { + const serverConfig = serverWebpackConfig(); + if (envSpecific) envSpecific(null, serverConfig); // eslint-disable-next-line no-console console.log('[React on Rails] Creating only the server bundle.'); result = serverConfig; + } else if (process.env.RSC_BUNDLE_ONLY) { + const rscConfig = rscWebpackConfig(); + // eslint-disable-next-line no-console + console.log('[React on Rails] Creating only the RSC bundle.'); + result = rscConfig; } else { - // default is the standard client and server build + const clientConfig = clientWebpackConfig(); + const serverConfig = serverWebpackConfig(); + const rscConfig = rscWebpackConfig(); + if (envSpecific) envSpecific(clientConfig, serverConfig); // 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]; } // To debug, uncomment next line and inspect "result" diff --git a/package.json b/package.json index d52d2980c..31da36d60 100644 --- a/package.json +++ b/package.json @@ -44,10 +44,10 @@ "@hotwired/stimulus-webpack-helpers": "^1.0.1", "@hotwired/turbo-rails": "^7.3.0", "@rails/actioncable": "7.0.5", - "@rspack/cli": "2.0.0-beta.7", - "@rspack/core": "2.0.0-beta.7", "@rescript/core": "^0.5.0", "@rescript/react": "^0.11.0", + "@rspack/cli": "2.0.0-beta.7", + "@rspack/core": "2.0.0-beta.7", "@swc/core": "^1.13.5", "ajv": "^8.17.1", "autoprefixer": "^10.4.14", @@ -77,10 +77,11 @@ "postcss-loader": "7.3.3", "postcss-preset-env": "^8.5.0", "prop-types": "^15.8.1", - "react": "^19.0.0", - "react-dom": "^19.0.0", + "react": "19.0.4", + "react-dom": "19.0.4", "react-intl": "^6.4.4", - "react-on-rails": "16.6.0-rc.0", + "react-on-rails-pro": "16.5.1", + "react-on-rails-rsc": "19.0.5-rc.1", "react-redux": "^8.1.0", "react-router": "^6.13.0", "react-router-dom": "^6.13.0", diff --git a/yarn.lock b/yarn.lock index 14b990d67..f4fb21a9b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2840,6 +2840,13 @@ acorn-jsx@^5.3.2: resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== +acorn-loose@^8.3.0: + version "8.5.2" + resolved "https://registry.npmjs.org/acorn-loose/-/acorn-loose-8.5.2.tgz#a7cc7dfbb7c8f3c2e55b055db640dc657e278d26" + integrity sha512-PPvV6g8UGMGgjrMu+n/f9E/tCSkNQ2Y97eFvuVdJfG11+xdIeDcLyNdC8SHcrHbRqkfwLASdplyR6B6sKM1U4A== + dependencies: + acorn "^8.15.0" + acorn@^8.15.0, acorn@^8.9.0: version "8.15.0" resolved "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz" @@ -7190,7 +7197,7 @@ negotiator@~0.6.4: resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz#777948e2452651c570b712dd01c23e262713fff7" integrity sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w== -neo-async@^2.6.2: +neo-async@^2.6.1, neo-async@^2.6.2: version "2.6.2" resolved "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== @@ -8365,12 +8372,12 @@ react-deep-force-update@^1.0.0: resolved "https://registry.npmjs.org/react-deep-force-update/-/react-deep-force-update-1.1.2.tgz" integrity sha512-WUSQJ4P/wWcusaH+zZmbECOk7H5N2pOIl0vzheeornkIMhu+qrNdGFm0bDZLCb0hSF0jf/kH1SgkNGfBdTc4wA== -react-dom@^19.0.0: - version "19.2.0" - resolved "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz" - integrity sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ== +react-dom@19.0.4: + version "19.0.4" + resolved "https://registry.npmjs.org/react-dom/-/react-dom-19.0.4.tgz#792d2868dc672c6f8abfce62bdb250e913dcfe2b" + integrity sha512-JiVlwQwuINIQf2+XUjtRFtLxhTE6hcyX7ZyCmY0HM7I/Kgi7qyXThkzwzg6uCfu3rTg9Ofe1x8qWYrfqthIrzg== dependencies: - scheduler "^0.27.0" + scheduler "^0.25.0" react-intl@^6.4.4: version "6.8.9" @@ -8403,10 +8410,26 @@ react-is@^18.0.0, react-is@^18.3.1: resolved "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz" integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== -react-on-rails@16.6.0-rc.0: - version "16.6.0-rc.0" - resolved "https://registry.npmjs.org/react-on-rails/-/react-on-rails-16.6.0-rc.0.tgz#ed0ed7085133905ad1e243cc97233e97d10a1c99" - integrity sha512-fSEomzwojgWob6uTWSfkbpP+XE++8kQBjNFpTT7V419QOON1daIHypQwA9gc8L8uX1If5r8hmAs57iWyGWmJuQ== +react-on-rails-pro@16.5.1: + version "16.5.1" + resolved "https://registry.npmjs.org/react-on-rails-pro/-/react-on-rails-pro-16.5.1.tgz#6b2503e7db55a3ff088d51675ccc345b8adb95a5" + integrity sha512-IhE1QklbvWRq4CEZayDUoAKzYbqICdUsQuhC4+NUF1/K0Z6e+a8VJtDK/8nMUZBIDjR/bomE+dez+rnF9F+sHw== + dependencies: + react-on-rails "16.5.1" + +react-on-rails-rsc@19.0.5-rc.1: + version "19.0.5-rc.1" + resolved "https://registry.npmjs.org/react-on-rails-rsc/-/react-on-rails-rsc-19.0.5-rc.1.tgz#a08818b9995bdaf93cdb2b04cb2e7cd5251be08b" + integrity sha512-Qf+pT82eKsicLW29/2Mfz6H3Cq+2rn5tDGSTy2tdvMGsQhq5gVFwnhLKE66FasH/gvFjoTIZJmUFy7PLmHoMMw== + dependencies: + acorn-loose "^8.3.0" + neo-async "^2.6.1" + webpack-sources "^3.2.0" + +react-on-rails@16.5.1: + version "16.5.1" + resolved "https://registry.npmjs.org/react-on-rails/-/react-on-rails-16.5.1.tgz#7fc4eb502e48445ab4f01ae039a25e2aa72447d4" + integrity sha512-IrfmuY5z0GN596nyE27teLbBTvxcaYR+MVjMGbkmoDV79/Tm2MsWt3hdPeYqD7gXu0AQagd+oHvYuyxfSJ4RGw== react-proxy@^1.1.7: version "1.1.8" @@ -8471,10 +8494,10 @@ react-transition-group@4.4.5: loose-envify "^1.4.0" prop-types "^15.6.2" -react@^19.0.0: - version "19.2.0" - resolved "https://registry.npmjs.org/react/-/react-19.2.0.tgz" - integrity sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ== +react@19.0.4: + version "19.0.4" + resolved "https://registry.npmjs.org/react/-/react-19.0.4.tgz#8031673e73cbb8ecba2c35c8c461396aa38dc69d" + integrity sha512-6RpEg9/n0sThnO+2CaMLWuvL1iyctm9/lcSTwvmyCoJYD5eiIrwxevXtrMqrtUr96HCdQB8/Yf+oK1QGy8kXEQ== read-cache@^1.0.0: version "1.0.0" @@ -8878,10 +8901,10 @@ saxes@^6.0.0: dependencies: xmlchars "^2.2.0" -scheduler@^0.27.0: - version "0.27.0" - resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz" - integrity sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q== +scheduler@^0.25.0: + version "0.25.0" + resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz#336cd9768e8cceebf52d3c80e3dcf5de23e7e015" + integrity sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA== schema-utils@^3.0.0, schema-utils@^3.3.0: version "3.3.0" @@ -10101,7 +10124,7 @@ webpack-merge@5, webpack-merge@^5.7.3, webpack-merge@^5.8.0: flat "^5.0.2" wildcard "^2.0.0" -webpack-sources@^3.3.4: +webpack-sources@^3.2.0, webpack-sources@^3.3.4: version "3.3.4" resolved "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.4.tgz#a338b95eb484ecc75fbb196cbe8a2890618b4891" integrity sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q== From 4c0df6e159e9eb413f2acff4c04633cfbd955714 Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Sat, 4 Apr 2026 00:24:16 -1000 Subject: [PATCH 02/16] Fix ESLint violations in RSC components and updated files - 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) --- .../ror_components/SimpleCommentScreen.jsx | 2 +- .../bundles/server-components/components/CommentsFeed.jsx | 1 + .../app/bundles/server-components/components/ServerInfo.jsx | 4 ++-- .../bundles/server-components/components/TogglePanel.jsx | 6 ++++++ client/app/packs/stimulus-bundle.js | 3 +-- 5 files changed, 11 insertions(+), 5 deletions(-) diff --git a/client/app/bundles/comments/components/SimpleCommentScreen/ror_components/SimpleCommentScreen.jsx b/client/app/bundles/comments/components/SimpleCommentScreen/ror_components/SimpleCommentScreen.jsx index 157943dab..158cbbfb3 100644 --- a/client/app/bundles/comments/components/SimpleCommentScreen/ror_components/SimpleCommentScreen.jsx +++ b/client/app/bundles/comments/components/SimpleCommentScreen/ror_components/SimpleCommentScreen.jsx @@ -1,6 +1,6 @@ 'use client'; +/* eslint-disable max-classes-per-file */ -// eslint-disable-next-line max-classes-per-file import React from 'react'; import request from 'axios'; import Immutable from 'immutable'; diff --git a/client/app/bundles/server-components/components/CommentsFeed.jsx b/client/app/bundles/server-components/components/CommentsFeed.jsx index 3d023cad4..04cd1549f 100644 --- a/client/app/bundles/server-components/components/CommentsFeed.jsx +++ b/client/app/bundles/server-components/components/CommentsFeed.jsx @@ -13,6 +13,7 @@ marked.use(gfmHeadingId()); async function CommentsFeed() { // Simulate network latency to demonstrate streaming + // eslint-disable-next-line no-promise-executor-return await new Promise((resolve) => setTimeout(resolve, 800)); // Fetch comments directly from the Rails API — no client-side fetch needed diff --git a/client/app/bundles/server-components/components/ServerInfo.jsx b/client/app/bundles/server-components/components/ServerInfo.jsx index 7c059e3c7..4475eb826 100644 --- a/client/app/bundles/server-components/components/ServerInfo.jsx +++ b/client/app/bundles/server-components/components/ServerInfo.jsx @@ -40,8 +40,8 @@ async function ServerInfo() { used to format it never reaches the browser.

    - {grouped.map((group, gi) => ( -
    + {grouped.map((group) => ( +
    k).join('-')} className="space-y-1"> {group.map(([key, value]) => (
    {labels[key] || key} diff --git a/client/app/bundles/server-components/components/TogglePanel.jsx b/client/app/bundles/server-components/components/TogglePanel.jsx index f5a38a9eb..1336b56b3 100644 --- a/client/app/bundles/server-components/components/TogglePanel.jsx +++ b/client/app/bundles/server-components/components/TogglePanel.jsx @@ -1,6 +1,7 @@ 'use client'; import React, { useState } from 'react'; +import PropTypes from 'prop-types'; const TogglePanel = ({ title, children }) => { const [isOpen, setIsOpen] = useState(false); @@ -31,4 +32,9 @@ const TogglePanel = ({ title, children }) => { ); }; +TogglePanel.propTypes = { + title: PropTypes.string.isRequired, + children: PropTypes.node.isRequired, +}; + export default TogglePanel; diff --git a/client/app/packs/stimulus-bundle.js b/client/app/packs/stimulus-bundle.js index 97b9586b5..920396bab 100644 --- a/client/app/packs/stimulus-bundle.js +++ b/client/app/packs/stimulus-bundle.js @@ -1,6 +1,7 @@ 'use client'; import ReactOnRails from 'react-on-rails-pro'; +import registerServerComponent from 'react-on-rails-pro/registerServerComponent/client'; import 'jquery-ujs'; import { Turbo } from '@hotwired/turbo-rails'; @@ -21,8 +22,6 @@ ReactOnRails.setOptions({ // No need for manual registration // React Server Components registration (client-side) -import registerServerComponent from 'react-on-rails-pro/registerServerComponent/client'; - registerServerComponent( { rscPayloadGenerationUrlPath: 'rsc_payload/' }, 'ServerComponentsPage', From 2f3c42c017fca1ce633caa50cf83ac14c5e4d9a1 Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Sat, 4 Apr 2026 00:27:42 -1000 Subject: [PATCH 03/16] Fix remaining lint issues: eslint-disable placement and no-danger - 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) --- .../SimpleCommentScreen/ror_components/SimpleCommentScreen.jsx | 2 +- .../app/bundles/server-components/components/CommentsFeed.jsx | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/client/app/bundles/comments/components/SimpleCommentScreen/ror_components/SimpleCommentScreen.jsx b/client/app/bundles/comments/components/SimpleCommentScreen/ror_components/SimpleCommentScreen.jsx index 158cbbfb3..a10b09daa 100644 --- a/client/app/bundles/comments/components/SimpleCommentScreen/ror_components/SimpleCommentScreen.jsx +++ b/client/app/bundles/comments/components/SimpleCommentScreen/ror_components/SimpleCommentScreen.jsx @@ -1,5 +1,5 @@ +// eslint-disable max-classes-per-file 'use client'; -/* eslint-disable max-classes-per-file */ import React from 'react'; import request from 'axios'; diff --git a/client/app/bundles/server-components/components/CommentsFeed.jsx b/client/app/bundles/server-components/components/CommentsFeed.jsx index 04cd1549f..bdab06114 100644 --- a/client/app/bundles/server-components/components/CommentsFeed.jsx +++ b/client/app/bundles/server-components/components/CommentsFeed.jsx @@ -66,6 +66,7 @@ async function CommentsFeed() {
    {/* Content is sanitized via sanitize-html before rendering */} + {/* eslint-disable-next-line react/no-danger */}
    Date: Sat, 4 Apr 2026 00:31:10 -1000 Subject: [PATCH 04/16] Fix eslint-disable syntax for file-level rules before 'use client' 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) --- .../ror_components/SimpleCommentScreen.jsx | 2 +- config/webpack/rspackRscPlugin.js | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/client/app/bundles/comments/components/SimpleCommentScreen/ror_components/SimpleCommentScreen.jsx b/client/app/bundles/comments/components/SimpleCommentScreen/ror_components/SimpleCommentScreen.jsx index a10b09daa..cac19b8a2 100644 --- a/client/app/bundles/comments/components/SimpleCommentScreen/ror_components/SimpleCommentScreen.jsx +++ b/client/app/bundles/comments/components/SimpleCommentScreen/ror_components/SimpleCommentScreen.jsx @@ -1,4 +1,4 @@ -// eslint-disable max-classes-per-file +/* eslint-disable max-classes-per-file */ 'use client'; import React from 'react'; diff --git a/config/webpack/rspackRscPlugin.js b/config/webpack/rspackRscPlugin.js index d3cbf9a22..af821e042 100644 --- a/config/webpack/rspackRscPlugin.js +++ b/config/webpack/rspackRscPlugin.js @@ -24,8 +24,9 @@ function hasUseClientDirective(filePath) { fs.closeSync(fd); const head = buf.toString('utf-8'); - // Check for 'use client' as the first statement (with or without semicolons/quotes) - result = /^(?:\s*\/\/[^\n]*\n)*\s*['"]use client['"]/.test(head); + // 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 (_) { // file doesn't exist or can't be read } From 05cb2bc6d3eed7a184214936a842978cfd7a10f2 Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Sat, 4 Apr 2026 22:00:22 -1000 Subject: [PATCH 05/16] Add Node renderer and fix RSC page rendering - 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) --- Procfile.dev | 2 + app/views/pages/server_components.html.erb | 1 + config/initializers/react_on_rails_pro.rb | 5 + config/webpack/serverWebpackConfig.js | 13 +- package.json | 4 +- react-on-rails-pro-node-renderer.js | 17 + yarn.lock | 451 ++++++++++++++++++++- 7 files changed, 484 insertions(+), 9 deletions(-) create mode 100644 react-on-rails-pro-node-renderer.js diff --git a/Procfile.dev b/Procfile.dev index cae685eca..94a640088 100644 --- a/Procfile.dev +++ b/Procfile.dev @@ -14,3 +14,5 @@ wp-client: RAILS_ENV=development NODE_ENV=development bin/shakapacker-dev-server wp-server: SERVER_BUNDLE_ONLY=yes bin/shakapacker --watch # 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 diff --git a/app/views/pages/server_components.html.erb b/app/views/pages/server_components.html.erb index edff32059..f5cada566 100644 --- a/app/views/pages/server_components.html.erb +++ b/app/views/pages/server_components.html.erb @@ -1,5 +1,6 @@ <%= append_javascript_pack_tag('rsc-client-components') %> <%= react_component("ServerComponentsPage", prerender: false, + auto_load_bundle: false, trace: true, id: "ServerComponentsPage-react-component-0") %> diff --git a/config/initializers/react_on_rails_pro.rb b/config/initializers/react_on_rails_pro.rb index a3a8c1312..e5ab7965a 100644 --- a/config/initializers/react_on_rails_pro.rb +++ b/config/initializers/react_on_rails_pro.rb @@ -1,6 +1,11 @@ # frozen_string_literal: true 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") + # Enable React Server Components support config.enable_rsc_support = true diff --git a/config/webpack/serverWebpackConfig.js b/config/webpack/serverWebpackConfig.js index 70d29cc6e..d61bb6868 100644 --- a/config/webpack/serverWebpackConfig.js +++ b/config/webpack/serverWebpackConfig.js @@ -95,7 +95,6 @@ const configureServer = () => { serverWebpackConfig.output = { filename: 'server-bundle.js', globalObject: 'this', - // If using the React on Rails Pro node server renderer, uncomment the next line // libraryTarget: 'commonjs2', path: path.resolve(__dirname, '../../ssr-generated'), publicPath: config.publicPath, @@ -164,10 +163,14 @@ const configureServer = () => { // The default of cheap-module-source-map is slow and provides poor info. serverWebpackConfig.devtool = 'eval'; - // If using the default 'web', then libraries like Emotion and loadable-components - // break with SSR. The fix is to use a node renderer and change the target. - // If using the React on Rails Pro node server renderer, uncomment the next line - // serverWebpackConfig.target = 'node' + // Alias react-dom/server to the Node.js version for the Pro Node renderer. + // The default browser version uses MessageChannel which isn't available in the Node VM. + serverWebpackConfig.resolve = serverWebpackConfig.resolve || {}; + serverWebpackConfig.resolve.alias = { + ...serverWebpackConfig.resolve.alias, + 'react-dom/server.browser$': 'react-dom/server.node', + 'react-dom/server.browser.js$': 'react-dom/server.node.js', + }; // RSC: Generate react-server-client-manifest.json for SSR component resolution serverWebpackConfig.plugins.push(new RspackRscPlugin({ isServer: true })); diff --git a/package.json b/package.json index 31da36d60..697827ebe 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,8 @@ "test:client": "yarn jest", "build:test": "rm -rf public/packs-test && RAILS_ENV=test NODE_ENV=test bin/shakapacker", "build:dev": "rm -rf public/packs && RAILS_ENV=development NODE_ENV=development bin/shakapacker", - "build:clean": "rm -rf public/packs || true" + "build:clean": "rm -rf public/packs || true", + "node-renderer": "node ./react-on-rails-pro-node-renderer.js" }, "dependencies": { "@babel/cli": "^7.21.0", @@ -81,6 +82,7 @@ "react-dom": "19.0.4", "react-intl": "^6.4.4", "react-on-rails-pro": "16.5.1", + "react-on-rails-pro-node-renderer": "16.5.1", "react-on-rails-rsc": "19.0.5-rc.1", "react-redux": "^8.1.0", "react-router": "^6.13.0", diff --git a/react-on-rails-pro-node-renderer.js b/react-on-rails-pro-node-renderer.js new file mode 100644 index 000000000..0f728691b --- /dev/null +++ b/react-on-rails-pro-node-renderer.js @@ -0,0 +1,17 @@ +const path = require('path'); +const { reactOnRailsProNodeRenderer } = require('react-on-rails-pro-node-renderer'); + +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), +}; + +if (process.env.CI) { + config.workersCount = 2; +} + +reactOnRailsProNodeRenderer(config); diff --git a/yarn.lock b/yarn.lock index f4fb21a9b..88c9651b4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1336,6 +1336,76 @@ resolved "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz" integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q== +"@fastify/ajv-compiler@^4.0.5": + version "4.0.5" + resolved "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-4.0.5.tgz#fdb0887a7af51abaae8c1829e8099d34f8ddd302" + integrity sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A== + dependencies: + ajv "^8.12.0" + ajv-formats "^3.0.1" + fast-uri "^3.0.0" + +"@fastify/busboy@^3.0.0": + version "3.2.0" + resolved "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.2.0.tgz#13ed8212f3b9ba697611529d15347f8528058cea" + integrity sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA== + +"@fastify/deepmerge@^3.0.0": + version "3.2.1" + resolved "https://registry.npmjs.org/@fastify/deepmerge/-/deepmerge-3.2.1.tgz#0fe56a4ee3eec874556006439f7bc7d616f10dc1" + integrity sha512-N5Oqvltoa2r9z1tbx4xjky0oRR60v+T47Ic4J1ukoVQcptLOrIdRnCSdTGmOmajZuHVKlTnfcmrjyqsGEW1ztA== + +"@fastify/error@^4.0.0": + version "4.2.0" + resolved "https://registry.npmjs.org/@fastify/error/-/error-4.2.0.tgz#d40f46ba75f541fdcc4dc276b7308bbc8e8e6d7a" + integrity sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ== + +"@fastify/fast-json-stringify-compiler@^5.0.0": + version "5.0.3" + resolved "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-5.0.3.tgz#fae495bf30dbbd029139839ec5c2ea111bde7d3f" + integrity sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ== + dependencies: + fast-json-stringify "^6.0.0" + +"@fastify/formbody@^7.4.0 || ^8.0.2": + version "8.0.2" + resolved "https://registry.npmjs.org/@fastify/formbody/-/formbody-8.0.2.tgz#7f97c8ab25933db77760bbeaacd2ff5355a54682" + integrity sha512-84v5J2KrkXzjgBpYnaNRPqwgMsmY7ZDjuj0YVuMR3NXCJRCgKEZy/taSP1wUYGn0onfxJpLyRGDLa+NMaDJtnA== + dependencies: + fast-querystring "^1.1.2" + fastify-plugin "^5.0.0" + +"@fastify/forwarded@^3.0.0": + version "3.0.1" + resolved "https://registry.npmjs.org/@fastify/forwarded/-/forwarded-3.0.1.tgz#9662b7bd4a59f6d123cc3487494f75f635c32d23" + integrity sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw== + +"@fastify/merge-json-schemas@^0.2.0": + version "0.2.1" + resolved "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.2.1.tgz#3aa30d2f0c81a8ac5995b6d94ed4eaa2c3055824" + integrity sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A== + dependencies: + dequal "^2.0.3" + +"@fastify/multipart@^8.3.1 || ^9.0.3": + version "9.4.0" + resolved "https://registry.npmjs.org/@fastify/multipart/-/multipart-9.4.0.tgz#be50e7d12d989cb42b835a5e46e08b40ab5b0728" + integrity sha512-Z404bzZeLSXTBmp/trCBuoVFX28pM7rhv849Q5TsbTFZHuk1lc4QjQITTPK92DKVpXmNtJXeHSSc7GYvqFpxAQ== + dependencies: + "@fastify/busboy" "^3.0.0" + "@fastify/deepmerge" "^3.0.0" + "@fastify/error" "^4.0.0" + fastify-plugin "^5.0.0" + secure-json-parse "^4.0.0" + +"@fastify/proxy-addr@^5.0.0": + version "5.1.0" + resolved "https://registry.npmjs.org/@fastify/proxy-addr/-/proxy-addr-5.1.0.tgz#f5360b5dd83c7de3d41b415be4aab84ae44aa106" + integrity sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw== + dependencies: + "@fastify/forwarded" "^3.0.0" + ipaddr.js "^2.1.0" + "@formatjs/ecma402-abstract@2.2.4": version "2.2.4" resolved "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.2.4.tgz" @@ -1956,6 +2026,11 @@ "@parcel/watcher-win32-ia32" "2.5.1" "@parcel/watcher-win32-x64" "2.5.1" +"@pinojs/redact@^0.4.0": + version "0.4.0" + resolved "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz#c3de060dd12640dcc838516aa2a6803cc7b2e9d6" + integrity sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg== + "@pkgjs/parseargs@^0.11.0": version "0.11.0" resolved "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz" @@ -2822,6 +2897,11 @@ resolved "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31" integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ== +abstract-logging@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz#6b0c371df212db7129b57d2e7fcf282b8bf1c839" + integrity sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA== + accepts@~1.3.8: version "1.3.8" resolved "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz" @@ -2882,6 +2962,13 @@ ajv-formats@^2.1.1: dependencies: ajv "^8.0.0" +ajv-formats@^3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz#3d5dc762bca17679c3c2ea7e90ad6b7532309578" + integrity sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ== + dependencies: + ajv "^8.0.0" + ajv-keywords@^3.5.2: version "3.5.2" resolved "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz" @@ -2914,6 +3001,16 @@ ajv@^8.0.0, ajv@^8.17.1, ajv@^8.9.0: json-schema-traverse "^1.0.0" require-from-string "^2.0.2" +ajv@^8.12.0: + version "8.18.0" + resolved "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz#8864186b6738d003eb3a933172bb3833e10cefbc" + integrity sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A== + dependencies: + fast-deep-equal "^3.1.3" + fast-uri "^3.0.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + ansi-escapes@^4.2.1: version "4.3.2" resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz" @@ -3156,6 +3253,11 @@ atob@^2.1.2: resolved "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz" integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== +atomic-sleep@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b" + integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ== + autoprefixer@^10.4.14: version "10.4.21" resolved "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz" @@ -3175,6 +3277,14 @@ available-typed-arrays@^1.0.7: dependencies: possible-typed-array-names "^1.0.0" +avvio@^9.0.0: + version "9.2.0" + resolved "https://registry.npmjs.org/avvio/-/avvio-9.2.0.tgz#16bb653c022237d1aeb984b00d3cbe2d96b77c20" + integrity sha512-2t/sy01ArdHHE0vRH5Hsay+RtCZt3dLPji7W7/MMOCEgze5b7SNDC4j5H6FnVgPkI1MTNFGzHdHrVXDDl7QSSQ== + dependencies: + "@fastify/error" "^4.0.0" + fastq "^1.17.1" + axe-core@^4.10.0: version "4.10.3" resolved "https://registry.npmjs.org/axe-core/-/axe-core-4.10.3.tgz" @@ -3414,6 +3524,11 @@ bser@2.1.1: dependencies: node-int64 "^0.4.0" +buffer-equal-constant-time@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== + buffer-from@^1.0.0: version "1.1.2" resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz" @@ -3787,6 +3902,11 @@ cookie@0.7.1: resolved "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz" integrity sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w== +cookie@^1.0.1: + version "1.1.1" + resolved "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz#3bb9bdfc82369db9c2f69c93c9c3ceb310c88b3c" + integrity sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ== + cookie@~0.7.1: version "0.7.2" resolved "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7" @@ -4336,6 +4456,13 @@ eastasianwidth@^0.2.0: resolved "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz" integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== +ecdsa-sig-formatter@1.0.11: + version "1.0.11" + resolved "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" + integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== + dependencies: + safe-buffer "^5.0.1" + ee-first@1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz" @@ -5000,6 +5127,11 @@ express@^4.18.2: utils-merge "1.0.1" vary "~1.1.2" +fast-decode-uri-component@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz#46f8b6c22b30ff7a81357d4f59abfae938202543" + integrity sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg== + fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" @@ -5026,12 +5158,31 @@ fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0: resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== +fast-json-stringify@^6.0.0: + version "6.3.0" + resolved "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-6.3.0.tgz#e59f2fbd558842d7ec085276444d15e6500c16d4" + integrity sha512-oRCntNDY/329HJPlmdNLIdogNtt6Vyjb1WuT01Soss3slIdyUp8kAcDU3saQTOquEK8KFVfwIIF7FebxUAu+yA== + dependencies: + "@fastify/merge-json-schemas" "^0.2.0" + ajv "^8.12.0" + ajv-formats "^3.0.1" + fast-uri "^3.0.0" + json-schema-ref-resolver "^3.0.0" + rfdc "^1.2.0" + fast-levenshtein@^2.0.6: version "2.0.6" resolved "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== -fast-uri@^3.0.1: +fast-querystring@^1.0.0, fast-querystring@^1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz#a6d24937b4fc6f791b4ee31dcb6f53aeafb89f53" + integrity sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg== + dependencies: + fast-decode-uri-component "^1.0.1" + +fast-uri@^3.0.0, fast-uri@^3.0.1: version "3.1.0" resolved "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz" integrity sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA== @@ -5041,6 +5192,39 @@ fastest-levenshtein@^1.0.12: resolved "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg== +fastify-plugin@^5.0.0: + version "5.1.0" + resolved "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-5.1.0.tgz#7083e039d6418415f9a669f8c25e72fc5bf2d3e7" + integrity sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw== + +fastify@^5.8.1: + version "5.8.4" + resolved "https://registry.npmjs.org/fastify/-/fastify-5.8.4.tgz#9ad9ebeea57980cf8722b5c44ca27ea9255cf2d5" + integrity sha512-sa42J1xylbBAYUWALSBoyXKPDUvM3OoNOibIefA+Oha57FryXKKCZarA1iDntOCWp3O35voZLuDg2mdODXtPzQ== + dependencies: + "@fastify/ajv-compiler" "^4.0.5" + "@fastify/error" "^4.0.0" + "@fastify/fast-json-stringify-compiler" "^5.0.0" + "@fastify/proxy-addr" "^5.0.0" + abstract-logging "^2.0.1" + avvio "^9.0.0" + fast-json-stringify "^6.0.0" + find-my-way "^9.0.0" + light-my-request "^6.0.0" + pino "^9.14.0 || ^10.1.0" + process-warning "^5.0.0" + rfdc "^1.3.1" + secure-json-parse "^4.0.0" + semver "^7.6.0" + toad-cache "^3.7.0" + +fastq@^1.17.1: + version "1.20.1" + resolved "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz#ca750a10dc925bc8b18839fd203e3ef4b3ced675" + integrity sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw== + dependencies: + reusify "^1.0.4" + fastq@^1.6.0: version "1.19.1" resolved "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz" @@ -5110,6 +5294,15 @@ finalhandler@~1.3.1: statuses "~2.0.2" unpipe "~1.0.0" +find-my-way@^9.0.0: + version "9.5.0" + resolved "https://registry.npmjs.org/find-my-way/-/find-my-way-9.5.0.tgz#3e6819bf4310b5293f490c032e70be0b506d0dc8" + integrity sha512-VW2RfnmscZO5KgBY5XVyKREMW5nMZcxDy+buTOsL+zIPnBlbKm+00sgzoQzq1EVh4aALZLfKdwv6atBGcjvjrQ== + dependencies: + fast-deep-equal "^3.1.3" + fast-querystring "^1.0.0" + safe-regex2 "^5.0.0" + find-root@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz" @@ -5208,6 +5401,15 @@ fs-extra@^10.0.0: jsonfile "^6.0.1" universalify "^2.0.0" +fs-extra@^11.2.0: + version "11.3.4" + resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz#ab6934eca8bcf6f7f6b82742e33591f86301d6fc" + integrity sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + fs-monkey@^1.0.4: version "1.1.0" resolved "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.1.0.tgz#632aa15a20e71828ed56b24303363fb1414e5997" @@ -5765,7 +5967,7 @@ ipaddr.js@1.9.1: resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz" integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== -ipaddr.js@^2.0.1: +ipaddr.js@^2.0.1, ipaddr.js@^2.1.0: version "2.3.0" resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz#71dce70e1398122208996d1c22f2ba46a24b1abc" integrity sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg== @@ -6612,6 +6814,13 @@ json-parse-even-better-errors@^2.3.0, json-parse-even-better-errors@^2.3.1: resolved "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz" integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== +json-schema-ref-resolver@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz#28f6a410122cde9238762a5e9296faa38be28708" + integrity sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A== + dependencies: + dequal "^2.0.3" + json-schema-traverse@^0.4.1: version "0.4.1" resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz" @@ -6664,6 +6873,22 @@ jsonify@^0.0.1: resolved "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz#2aa3111dae3d34a0f151c63f3a45d995d9420978" integrity sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg== +jsonwebtoken@^9.0.3: + version "9.0.3" + resolved "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz#6cd57ab01e9b0ac07cb847d53d3c9b6ee31f7ae2" + integrity sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g== + dependencies: + jws "^4.0.1" + lodash.includes "^4.3.0" + lodash.isboolean "^3.0.3" + lodash.isinteger "^4.0.4" + lodash.isnumber "^3.0.3" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.once "^4.0.0" + ms "^2.1.1" + semver "^7.5.4" + "jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.5: version "3.3.5" resolved "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz" @@ -6674,6 +6899,23 @@ jsonify@^0.0.1: object.assign "^4.1.4" object.values "^1.1.6" +jwa@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz#bf8176d1ad0cd72e0f3f58338595a13e110bc804" + integrity sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg== + dependencies: + buffer-equal-constant-time "^1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + +jws@^4.0.1: + version "4.0.1" + resolved "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz#07edc1be8fac20e677b283ece261498bd38f0690" + integrity sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA== + dependencies: + jwa "^2.0.1" + safe-buffer "^5.0.1" + keyv@^4.5.3: version "4.5.4" resolved "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz" @@ -6731,6 +6973,15 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" +light-my-request@^6.0.0: + version "6.6.0" + resolved "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz#c9448772323f65f33720fb5979c7841f14060add" + integrity sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A== + dependencies: + cookie "^1.0.1" + process-warning "^4.0.0" + set-cookie-parser "^2.6.0" + lilconfig@^3.1.1, lilconfig@^3.1.3: version "3.1.3" resolved "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz" @@ -6872,6 +7123,11 @@ lodash.has@^4.5.2: resolved "https://registry.npmjs.org/lodash.has/-/lodash.has-4.5.2.tgz#d19f4dc1095058cccbe2b0cdf4ee0fe4aa37c862" integrity sha512-rnYUdIo6xRCJnQmbVFEwcxF144erlD+M3YcJUVesflU9paQaE8p+fJDcIQrlMYbxoANFL+AB9hZrzSBBk5PL+g== +lodash.includes@^4.3.0: + version "4.3.0" + resolved "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" + integrity sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w== + lodash.isarguments@^3.0.0: version "3.1.0" resolved "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz" @@ -6882,6 +7138,31 @@ lodash.isarray@^3.0.0: resolved "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz" integrity sha512-JwObCrNJuT0Nnbuecmqr5DgtuBppuCvGD9lxjFpAzwnVtdGoDQ1zig+5W8k5/6Gcn0gZ3936HDAlGd28i7sOGQ== +lodash.isboolean@^3.0.3: + version "3.0.3" + resolved "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" + integrity sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg== + +lodash.isinteger@^4.0.4: + version "4.0.4" + resolved "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" + integrity sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA== + +lodash.isnumber@^3.0.3: + version "3.0.3" + resolved "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" + integrity sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw== + +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA== + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw== + lodash.keys@^3.0.0: version "3.1.2" resolved "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz" @@ -6901,6 +7182,11 @@ lodash.merge@^4.6.0, lodash.merge@^4.6.2: resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== +lodash.once@^4.0.0: + version "4.1.1" + resolved "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== + lodash.restparam@^3.0.0: version "3.6.1" resolved "https://registry.npmjs.org/lodash.restparam/-/lodash.restparam-3.6.1.tgz" @@ -7350,6 +7636,11 @@ obuf@^1.0.0, obuf@^1.1.2: resolved "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== +on-exit-leak-free@^2.1.0: + version "2.1.2" + resolved "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz#fed195c9ebddb7d9e4c3842f93f281ac8dadd3b8" + integrity sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA== + on-finished@2.4.1, on-finished@~2.4.1: version "2.4.1" resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz" @@ -7610,6 +7901,59 @@ pify@^4.0.1: resolved "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz" integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== +pino-abstract-transport@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz#de241578406ac7b8a33ce0d77ae6e8a0b3b68a60" + integrity sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw== + dependencies: + split2 "^4.0.0" + +pino-abstract-transport@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz#b21e5f33a297e8c4c915c62b3ce5dd4a87a52c23" + integrity sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg== + dependencies: + split2 "^4.0.0" + +pino-std-serializers@^7.0.0: + version "7.1.0" + resolved "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz#a7b0cd65225f29e92540e7853bd73b07479893fc" + integrity sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw== + +pino@^9.0.0: + version "9.14.0" + resolved "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz#673d9711c2d1e64d18670c1ec05ef7ba14562556" + integrity sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w== + dependencies: + "@pinojs/redact" "^0.4.0" + atomic-sleep "^1.0.0" + on-exit-leak-free "^2.1.0" + pino-abstract-transport "^2.0.0" + pino-std-serializers "^7.0.0" + process-warning "^5.0.0" + quick-format-unescaped "^4.0.3" + real-require "^0.2.0" + safe-stable-stringify "^2.3.1" + sonic-boom "^4.0.1" + thread-stream "^3.0.0" + +"pino@^9.14.0 || ^10.1.0": + version "10.3.1" + resolved "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz#6552c8f8d8481844c9e452e7bf0be90bff1939ce" + integrity sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg== + dependencies: + "@pinojs/redact" "^0.4.0" + atomic-sleep "^1.0.0" + on-exit-leak-free "^2.1.0" + pino-abstract-transport "^3.0.0" + pino-std-serializers "^7.0.0" + process-warning "^5.0.0" + quick-format-unescaped "^4.0.3" + real-require "^0.2.0" + safe-stable-stringify "^2.3.1" + sonic-boom "^4.0.1" + thread-stream "^4.0.0" + pirates@^4.0.1, pirates@^4.0.4: version "4.0.7" resolved "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz" @@ -8271,6 +8615,16 @@ process-nextick-args@~2.0.0: resolved "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== +process-warning@^4.0.0: + version "4.0.1" + resolved "https://registry.npmjs.org/process-warning/-/process-warning-4.0.1.tgz#5c1db66007c67c756e4e09eb170cdece15da32fb" + integrity sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q== + +process-warning@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz#566e0bf79d1dff30a72d8bbbe9e8ecefe8d378d7" + integrity sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA== + process@^0.11.10: version "0.11.10" resolved "https://registry.npmjs.org/process/-/process-0.11.10.tgz" @@ -8330,6 +8684,11 @@ queue-microtask@^1.2.2: resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== +quick-format-unescaped@^4.0.3: + version "4.0.4" + resolved "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz#93ef6dd8d3453cbc7970dd614fad4c5954d6b5a7" + integrity sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg== + quick-lru@^5.1.1: version "5.1.1" resolved "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz" @@ -8410,6 +8769,19 @@ react-is@^18.0.0, react-is@^18.3.1: resolved "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz" integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== +react-on-rails-pro-node-renderer@16.5.1: + version "16.5.1" + resolved "https://registry.npmjs.org/react-on-rails-pro-node-renderer/-/react-on-rails-pro-node-renderer-16.5.1.tgz#34d08578981593d567838cc82c77b3fd4d03df47" + integrity sha512-QkW+BUhczPbgJ/kLJzEuMoL08wQiZUox7HG1ZBi1cZdjNxGPTcZC7qrD8+NEPgjAoUGZazCcv7DR0kTpYfPURw== + dependencies: + "@fastify/formbody" "^7.4.0 || ^8.0.2" + "@fastify/multipart" "^8.3.1 || ^9.0.3" + fastify "^5.8.1" + fs-extra "^11.2.0" + jsonwebtoken "^9.0.3" + lockfile "^1.0.4" + pino "^9.0.0" + react-on-rails-pro@16.5.1: version "16.5.1" resolved "https://registry.npmjs.org/react-on-rails-pro/-/react-on-rails-pro-16.5.1.tgz#6b2503e7db55a3ff088d51675ccc345b8adb95a5" @@ -8540,6 +8912,11 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" +real-require@^0.2.0: + version "0.2.0" + resolved "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz#209632dea1810be2ae063a6ac084fee7e33fba78" + integrity sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg== + rechoir@^0.8.0: version "0.8.0" resolved "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz#49f866e0d32146142da3ad8f0eff352b3215ff22" @@ -8750,6 +9127,11 @@ resolve@^2.0.0-next.5: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" +ret@~0.5.0: + version "0.5.0" + resolved "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz#30a4d38a7e704bd96dc5ffcbe7ce2a9274c41c95" + integrity sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw== + retry@^0.13.1: version "0.13.1" resolved "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" @@ -8773,6 +9155,11 @@ rework@^1.0.1: convert-source-map "^0.3.3" css "^2.0.0" +rfdc@^1.2.0, rfdc@^1.3.1: + version "1.4.1" + resolved "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz#778f76c4fb731d93414e8f925fbecf64cce7f6ca" + integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA== + rimraf@^3.0.2: version "3.0.2" resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz" @@ -8817,7 +9204,7 @@ safe-array-concat@^1.1.3: has-symbols "^1.1.0" isarray "^2.0.5" -safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.1.0, safe-buffer@~5.2.0: +safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -8849,6 +9236,18 @@ safe-regex-test@^1.0.3, safe-regex-test@^1.1.0: es-errors "^1.3.0" is-regex "^1.2.1" +safe-regex2@^5.0.0: + version "5.1.0" + resolved "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.1.0.tgz#758fd224d066f5abe24f67bd574a01c9dd447f51" + integrity sha512-pNHAuBW7TrcleFHsxBr5QMi/Iyp0ENjUKz7GCcX1UO7cMh+NmVK6HxQckNL1tJp1XAJVjG6B8OKIPqodqj9rtw== + dependencies: + ret "~0.5.0" + +safe-stable-stringify@^2.3.1: + version "2.5.0" + resolved "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz#4ca2f8e385f2831c432a719b108a3bf7af42a1dd" + integrity sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA== + "safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": version "2.1.2" resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" @@ -8935,6 +9334,11 @@ schema-utils@^4.3.3: ajv-formats "^2.1.1" ajv-keywords "^5.1.0" +secure-json-parse@^4.0.0: + version "4.1.0" + resolved "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz#4f1ab41c67a13497ea1b9131bb4183a22865477c" + integrity sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA== + select-hose@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" @@ -8963,6 +9367,11 @@ semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.3, semver@^7.5.4: resolved "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz" integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA== +semver@^7.6.0: + version "7.7.4" + resolved "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz#28464e36060e991fa7a11d0279d2d3f3b57a7e8a" + integrity sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA== + send@0.19.0: version "0.19.0" resolved "https://registry.npmjs.org/send/-/send-0.19.0.tgz" @@ -9046,6 +9455,11 @@ set-blocking@^2.0.0: resolved "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz" integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== +set-cookie-parser@^2.6.0: + version "2.7.2" + resolved "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz#ccd08673a9ae5d2e44ea2a2de25089e67c7edf68" + integrity sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw== + set-function-length@^1.2.2: version "1.2.2" resolved "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz" @@ -9190,6 +9604,13 @@ sockjs@^0.3.24: uuid "^8.3.2" websocket-driver "^0.7.4" +sonic-boom@^4.0.1: + version "4.2.1" + resolved "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz#28598250df4899c0ac572d7e2f0460690ba6a030" + integrity sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q== + dependencies: + atomic-sleep "^1.0.0" + "source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.1, source-map-js@^1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz" @@ -9260,6 +9681,11 @@ spdy@^4.0.2: select-hose "^2.0.0" spdy-transport "^3.0.0" +split2@^4.0.0: + version "4.2.0" + resolved "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4" + integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg== + sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz" @@ -9678,6 +10104,20 @@ thenify-all@^1.0.0: dependencies: any-promise "^1.0.0" +thread-stream@^3.0.0: + version "3.1.0" + resolved "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz#4b2ef252a7c215064507d4ef70c05a5e2d34c4f1" + integrity sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A== + dependencies: + real-require "^0.2.0" + +thread-stream@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/thread-stream/-/thread-stream-4.0.0.tgz#732f007c24da7084f729d6e3a7e3f5934a7380b7" + integrity sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA== + dependencies: + real-require "^0.2.0" + thunky@^1.0.2: version "1.1.0" resolved "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" @@ -9712,6 +10152,11 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" +toad-cache@^3.7.0: + version "3.7.0" + resolved "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz#b9b63304ea7c45ec34d91f1d2fa513517025c441" + integrity sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw== + toidentifier@1.0.1, toidentifier@~1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz" From 0d8d75ac39219a50522b513a8cddd570a51443d5 Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Sat, 4 Apr 2026 22:05:10 -1000 Subject: [PATCH 06/16] Address PR review feedback: fix bugs, security, and config issues - 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) --- app/views/pages/server_components.html.erb | 2 +- .../ror_components/RouterApp.server.jsx | 2 -- .../comments/startup/serverRegistration.jsx | 2 -- .../components/CommentsFeed.jsx | 20 +++++++++++++++---- client/app/packs/rsc-client-components.js | 2 +- config/webpack/rscWebpackConfig.js | 7 +++++-- config/webpack/rspackRscPlugin.js | 5 +++++ config/webpack/serverWebpackConfig.js | 4 ---- config/webpack/webpackConfig.js | 3 ++- 9 files changed, 30 insertions(+), 17 deletions(-) diff --git a/app/views/pages/server_components.html.erb b/app/views/pages/server_components.html.erb index f5cada566..d9643a213 100644 --- a/app/views/pages/server_components.html.erb +++ b/app/views/pages/server_components.html.erb @@ -2,5 +2,5 @@ <%= react_component("ServerComponentsPage", prerender: false, auto_load_bundle: false, - trace: true, + trace: Rails.env.development?, id: "ServerComponentsPage-react-component-0") %> diff --git a/client/app/bundles/comments/startup/RouterApp/ror_components/RouterApp.server.jsx b/client/app/bundles/comments/startup/RouterApp/ror_components/RouterApp.server.jsx index 7dd9d0a81..dd3578ba8 100644 --- a/client/app/bundles/comments/startup/RouterApp/ror_components/RouterApp.server.jsx +++ b/client/app/bundles/comments/startup/RouterApp/ror_components/RouterApp.server.jsx @@ -1,5 +1,3 @@ -'use client'; - // Compare to ./RouterApp.client.jsx import { Provider } from 'react-redux'; import React from 'react'; diff --git a/client/app/bundles/comments/startup/serverRegistration.jsx b/client/app/bundles/comments/startup/serverRegistration.jsx index da44c5abd..c7db967ab 100644 --- a/client/app/bundles/comments/startup/serverRegistration.jsx +++ b/client/app/bundles/comments/startup/serverRegistration.jsx @@ -1,5 +1,3 @@ -'use client'; - // Example of React + Redux import ReactOnRails from 'react-on-rails-pro'; diff --git a/client/app/bundles/server-components/components/CommentsFeed.jsx b/client/app/bundles/server-components/components/CommentsFeed.jsx index bdab06114..0230c1d19 100644 --- a/client/app/bundles/server-components/components/CommentsFeed.jsx +++ b/client/app/bundles/server-components/components/CommentsFeed.jsx @@ -12,12 +12,19 @@ const marked = new Marked(); marked.use(gfmHeadingId()); async function CommentsFeed() { - // Simulate network latency to demonstrate streaming - // eslint-disable-next-line no-promise-executor-return - await new Promise((resolve) => setTimeout(resolve, 800)); + // Simulate network latency to demonstrate Suspense streaming (development only) + if (process.env.NODE_ENV !== 'production') { + await new Promise((resolve) => { + setTimeout(resolve, 800); + }); + } // Fetch comments directly from the Rails API — no client-side fetch needed - const response = await fetch('http://localhost:3000/comments.json'); + const baseUrl = process.env.RAILS_INTERNAL_URL || 'http://localhost:3000'; + const response = await fetch(`${baseUrl}/comments.json`); + if (!response.ok) { + throw new Error(`Failed to fetch comments: ${response.status} ${response.statusText}`); + } const comments = await response.json(); // Use lodash to process (stays on server) @@ -45,6 +52,11 @@ async function CommentsFeed() { const rawHtml = marked.parse(comment.text || ''); const safeHtml = sanitizeHtml(rawHtml, { allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img']), + allowedAttributes: { + ...sanitizeHtml.defaults.allowedAttributes, + img: ['src', 'alt', 'title', 'width', 'height'], + }, + allowedSchemes: ['https', 'http', 'data'], }); return ( diff --git a/client/app/packs/rsc-client-components.js b/client/app/packs/rsc-client-components.js index b33ff5a20..55773819c 100644 --- a/client/app/packs/rsc-client-components.js +++ b/client/app/packs/rsc-client-components.js @@ -4,8 +4,8 @@ // Components with 'use client' that are used in server components must be // available in a client bundle chunk so the React flight client can load them. -import TogglePanel from '../bundles/server-components/components/TogglePanel'; import ReactOnRails from 'react-on-rails-pro'; +import TogglePanel from '../bundles/server-components/components/TogglePanel'; // Register as a standard component so it's bundled in a client-accessible chunk ReactOnRails.register({ TogglePanel }); diff --git a/config/webpack/rscWebpackConfig.js b/config/webpack/rscWebpackConfig.js index 3d76a7148..ebd137cda 100644 --- a/config/webpack/rscWebpackConfig.js +++ b/config/webpack/rscWebpackConfig.js @@ -62,8 +62,11 @@ const configureRsc = () => { if (cssLoader?.options?.modules) { cssLoader.options.modules = { ...cssLoader.options.modules, exportOnlyLocals: true }; } - } else if (rule.use && (rule.use.loader === 'url-loader' || rule.use.loader === 'file-loader')) { - 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 }; } }); diff --git a/config/webpack/rspackRscPlugin.js b/config/webpack/rspackRscPlugin.js index af821e042..1c3c2f659 100644 --- a/config/webpack/rspackRscPlugin.js +++ b/config/webpack/rspackRscPlugin.js @@ -48,6 +48,11 @@ class RspackRscPlugin { } apply(compiler) { + // Clear cache on each compilation so watch-mode picks up 'use client' changes + compiler.hooks.thisCompilation.tap('RspackRscPlugin-ClearCache', () => { + useClientCache.clear(); + }); + compiler.hooks.thisCompilation.tap('RspackRscPlugin', (compilation) => { compilation.hooks.processAssets.tap( { diff --git a/config/webpack/serverWebpackConfig.js b/config/webpack/serverWebpackConfig.js index d61bb6868..a97ae19b2 100644 --- a/config/webpack/serverWebpackConfig.js +++ b/config/webpack/serverWebpackConfig.js @@ -5,7 +5,6 @@ const path = require('path'); const { config } = require('shakapacker'); const commonWebpackConfig = require('./commonWebpackConfig'); const { getBundler } = require('./bundlerUtils'); -const { RspackRscPlugin } = require('./rspackRscPlugin'); /** * Extract a specific loader from a webpack rule's use array. @@ -172,9 +171,6 @@ const configureServer = () => { 'react-dom/server.browser.js$': 'react-dom/server.node.js', }; - // RSC: Generate react-server-client-manifest.json for SSR component resolution - serverWebpackConfig.plugins.push(new RspackRscPlugin({ isServer: true })); - return serverWebpackConfig; }; diff --git a/config/webpack/webpackConfig.js b/config/webpack/webpackConfig.js index 007135387..595fc63c3 100644 --- a/config/webpack/webpackConfig.js +++ b/config/webpack/webpackConfig.js @@ -22,6 +22,7 @@ const webpackConfig = (envSpecific) => { result = serverConfig; } else if (process.env.RSC_BUNDLE_ONLY) { const rscConfig = rscWebpackConfig(); + if (envSpecific) envSpecific(null, null, rscConfig); // eslint-disable-next-line no-console console.log('[React on Rails] Creating only the RSC bundle.'); result = rscConfig; @@ -29,7 +30,7 @@ const webpackConfig = (envSpecific) => { const clientConfig = clientWebpackConfig(); const serverConfig = serverWebpackConfig(); const rscConfig = rscWebpackConfig(); - if (envSpecific) envSpecific(clientConfig, serverConfig); + if (envSpecific) envSpecific(clientConfig, serverConfig, rscConfig); // eslint-disable-next-line no-console console.log('[React on Rails] Creating client, server, and RSC bundles.'); result = [clientConfig, serverConfig, rscConfig]; From aaeba86e4d2728bcd1a6d771bd56b990b4d0373f Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Sat, 4 Apr 2026 22:47:51 -1000 Subject: [PATCH 07/16] Address PR review feedback: fix bugs, security, and config issues - 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) --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 366e6a9b7..5c7b739bb 100644 --- a/.gitignore +++ b/.gitignore @@ -57,6 +57,9 @@ client/app/bundles/comments/rescript/**/*.bs.js # Using React on Rails default directory /ssr-generated/ +# Node renderer bundle cache +.node-renderer-bundles/ + # Generated React on Rails packs **/generated/** From 92306ff01022d265bd7f4d3b45d9e63e067140dd Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Sat, 4 Apr 2026 22:54:46 -1000 Subject: [PATCH 08/16] Fix CI: resolve RSC bundle and server bundle build errors 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) --- client/app/packs/rsc-bundle.js | 7 +++++-- config/webpack/serverWebpackConfig.js | 11 +++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/client/app/packs/rsc-bundle.js b/client/app/packs/rsc-bundle.js index 83d8936a1..b6fcae1bb 100644 --- a/client/app/packs/rsc-bundle.js +++ b/client/app/packs/rsc-bundle.js @@ -3,11 +3,14 @@ // It imports the same client component registrations as server-bundle.js, // plus the server component registrations. -// Import existing client component registrations +// Import stores registration (has 'use client' — RSC loader replaces with client reference) import './stores-registration'; -import './../generated/server-bundle-generated.js'; // React Server Components registration (server-side) +// Note: server-bundle-generated.js is NOT imported here because it contains +// traditional SSR components (e.g., RouterApp.server.jsx) that use client-only +// React APIs (useState, Component, etc.) incompatible with the react-server condition. +// Client component references are handled automatically by the RSC loader/plugin. import registerServerComponent from 'react-on-rails-pro/registerServerComponent/server'; import ServerComponentsPage from '../bundles/server-components/ServerComponentsPage'; diff --git a/config/webpack/serverWebpackConfig.js b/config/webpack/serverWebpackConfig.js index a97ae19b2..e79fabe6c 100644 --- a/config/webpack/serverWebpackConfig.js +++ b/config/webpack/serverWebpackConfig.js @@ -171,6 +171,17 @@ const configureServer = () => { 'react-dom/server.browser.js$': 'react-dom/server.node.js', }; + // react-on-rails-pro includes RSC-related modules that import Node.js builtins + // (path, fs, stream). These code paths aren't used in the traditional SSR bundle, + // so provide empty fallbacks to avoid resolution errors. + serverWebpackConfig.resolve.fallback = { + ...serverWebpackConfig.resolve.fallback, + path: false, + fs: false, + 'fs/promises': false, + stream: false, + }; + return serverWebpackConfig; }; From 3f7e452b77b2fa0137aa29e92dd31ff5da1db9b8 Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Sun, 5 Apr 2026 00:47:56 -1000 Subject: [PATCH 09/16] Use externals instead of fallbacks for Node builtins in server bundle 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) --- config/webpack/serverWebpackConfig.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/config/webpack/serverWebpackConfig.js b/config/webpack/serverWebpackConfig.js index e79fabe6c..6967ed4f2 100644 --- a/config/webpack/serverWebpackConfig.js +++ b/config/webpack/serverWebpackConfig.js @@ -172,14 +172,15 @@ const configureServer = () => { }; // react-on-rails-pro includes RSC-related modules that import Node.js builtins - // (path, fs, stream). These code paths aren't used in the traditional SSR bundle, - // so provide empty fallbacks to avoid resolution errors. - serverWebpackConfig.resolve.fallback = { - ...serverWebpackConfig.resolve.fallback, - path: false, - fs: false, - 'fs/promises': false, - stream: false, + // (path, fs, stream). Externalize them so they resolve at runtime via require() + // in the Node.js environment where the SSR bundle executes. + const existingExternals = serverWebpackConfig.externals || {}; + serverWebpackConfig.externals = { + ...(typeof existingExternals === 'object' && !Array.isArray(existingExternals) ? existingExternals : {}), + path: 'commonjs path', + fs: 'commonjs fs', + 'fs/promises': 'commonjs fs/promises', + stream: 'commonjs stream', }; return serverWebpackConfig; From 1022234fa883e2a60bf9640bbd32312b8962dd93 Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Sun, 5 Apr 2026 15:24:23 -1000 Subject: [PATCH 10/16] Fix SSR runtime failures: Node renderer, polyfills, and RSC classification 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) --- .github/workflows/rspec_test.yml | 15 ++++++ .../ror_components/RouterApp.server.jsx | 2 + config/webpack/serverWebpackConfig.js | 47 ++++++++++++------- 3 files changed, 47 insertions(+), 17 deletions(-) diff --git a/.github/workflows/rspec_test.yml b/.github/workflows/rspec_test.yml index d117458a6..5ebcb89da 100644 --- a/.github/workflows/rspec_test.yml +++ b/.github/workflows/rspec_test.yml @@ -33,6 +33,7 @@ jobs: DRIVER: selenium_chrome CHROME_BIN: /usr/bin/google-chrome USE_COVERALLS: true + RENDERER_PASSWORD: devPassword steps: - name: Install Chrome @@ -82,6 +83,20 @@ jobs: - name: Build shakapacker chunks run: NODE_ENV=development bundle exec bin/shakapacker + - name: Start Node renderer for SSR + run: | + node react-on-rails-pro-node-renderer.js & + 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" + exit 1 + - name: Run rspec with xvfb uses: coactions/setup-xvfb@v1 with: diff --git a/client/app/bundles/comments/startup/RouterApp/ror_components/RouterApp.server.jsx b/client/app/bundles/comments/startup/RouterApp/ror_components/RouterApp.server.jsx index dd3578ba8..7dd9d0a81 100644 --- a/client/app/bundles/comments/startup/RouterApp/ror_components/RouterApp.server.jsx +++ b/client/app/bundles/comments/startup/RouterApp/ror_components/RouterApp.server.jsx @@ -1,3 +1,5 @@ +'use client'; + // Compare to ./RouterApp.client.jsx import { Provider } from 'react-redux'; import React from 'react'; diff --git a/config/webpack/serverWebpackConfig.js b/config/webpack/serverWebpackConfig.js index 6967ed4f2..48e98424a 100644 --- a/config/webpack/serverWebpackConfig.js +++ b/config/webpack/serverWebpackConfig.js @@ -162,26 +162,39 @@ const configureServer = () => { // The default of cheap-module-source-map is slow and provides poor info. serverWebpackConfig.devtool = 'eval'; - // Alias react-dom/server to the Node.js version for the Pro Node renderer. - // The default browser version uses MessageChannel which isn't available in the Node VM. + // react-on-rails-pro includes RSC-related modules that import Node.js builtins + // (path, fs, stream). These code paths aren't exercised in the SSR bundle, + // so provide empty fallbacks to satisfy the resolver without bundling them. serverWebpackConfig.resolve = serverWebpackConfig.resolve || {}; - serverWebpackConfig.resolve.alias = { - ...serverWebpackConfig.resolve.alias, - 'react-dom/server.browser$': 'react-dom/server.node', - 'react-dom/server.browser.js$': 'react-dom/server.node.js', + serverWebpackConfig.resolve.fallback = { + ...serverWebpackConfig.resolve.fallback, + path: false, + fs: false, + 'fs/promises': false, + stream: false, }; - // react-on-rails-pro includes RSC-related modules that import Node.js builtins - // (path, fs, stream). Externalize them so they resolve at runtime via require() - // in the Node.js environment where the SSR bundle executes. - const existingExternals = serverWebpackConfig.externals || {}; - serverWebpackConfig.externals = { - ...(typeof existingExternals === 'object' && !Array.isArray(existingExternals) ? existingExternals : {}), - path: 'commonjs path', - fs: 'commonjs fs', - 'fs/promises': 'commonjs fs/promises', - stream: 'commonjs stream', - }; + // The Node renderer runs bundles in a VM sandbox that lacks browser globals + // like MessageChannel and TextEncoder. Inject polyfills at the top of the + // bundle so react-dom/server.browser can initialize. + 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, + }), + ); return serverWebpackConfig; }; From e16612c1aeb021707ec5490f0dd163493cb642b6 Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Sun, 5 Apr 2026 20:41:34 -1000 Subject: [PATCH 11/16] Update react_on_rails and react_on_rails_pro to 16.6.0.rc.0 Co-Authored-By: Claude Opus 4.6 (1M context) --- Gemfile | 2 +- Gemfile.lock | 142 +++++++++++++++++++++++++-------------------------- package.json | 4 +- yarn.lock | 30 +++++------ 4 files changed, 89 insertions(+), 89 deletions(-) diff --git a/Gemfile b/Gemfile index 7acd9bfc0..2ebde371a 100644 --- a/Gemfile +++ b/Gemfile @@ -5,7 +5,7 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" } ruby "3.4.6" -gem "react_on_rails_pro", "16.5.1" +gem "react_on_rails_pro", "16.6.0.rc.0" gem "shakapacker", "10.0.0.rc.0" # Bundle edge Rails instead: gem "rails", github: "rails/rails" diff --git a/Gemfile.lock b/Gemfile.lock index 3e0ee2285..ab6c0e1f4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,31 +1,31 @@ GEM remote: https://rubygems.org/ specs: - action_text-trix (2.1.17) + action_text-trix (2.1.18) railties - actioncable (8.1.2) - actionpack (= 8.1.2) - activesupport (= 8.1.2) + actioncable (8.1.3) + actionpack (= 8.1.3) + activesupport (= 8.1.3) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (8.1.2) - actionpack (= 8.1.2) - activejob (= 8.1.2) - activerecord (= 8.1.2) - activestorage (= 8.1.2) - activesupport (= 8.1.2) + actionmailbox (8.1.3) + actionpack (= 8.1.3) + activejob (= 8.1.3) + activerecord (= 8.1.3) + activestorage (= 8.1.3) + activesupport (= 8.1.3) mail (>= 2.8.0) - actionmailer (8.1.2) - actionpack (= 8.1.2) - actionview (= 8.1.2) - activejob (= 8.1.2) - activesupport (= 8.1.2) + actionmailer (8.1.3) + actionpack (= 8.1.3) + actionview (= 8.1.3) + activejob (= 8.1.3) + activesupport (= 8.1.3) mail (>= 2.8.0) rails-dom-testing (~> 2.2) - actionpack (8.1.2) - actionview (= 8.1.2) - activesupport (= 8.1.2) + actionpack (8.1.3) + actionview (= 8.1.3) + activesupport (= 8.1.3) nokogiri (>= 1.8.5) rack (>= 2.2.4) rack-session (>= 1.0.1) @@ -33,36 +33,36 @@ GEM rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) useragent (~> 0.16) - actiontext (8.1.2) + actiontext (8.1.3) action_text-trix (~> 2.1.15) - actionpack (= 8.1.2) - activerecord (= 8.1.2) - activestorage (= 8.1.2) - activesupport (= 8.1.2) + actionpack (= 8.1.3) + activerecord (= 8.1.3) + activestorage (= 8.1.3) + activesupport (= 8.1.3) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (8.1.2) - activesupport (= 8.1.2) + actionview (8.1.3) + activesupport (= 8.1.3) builder (~> 3.1) erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - activejob (8.1.2) - activesupport (= 8.1.2) + activejob (8.1.3) + activesupport (= 8.1.3) globalid (>= 0.3.6) - activemodel (8.1.2) - activesupport (= 8.1.2) - activerecord (8.1.2) - activemodel (= 8.1.2) - activesupport (= 8.1.2) + activemodel (8.1.3) + activesupport (= 8.1.3) + activerecord (8.1.3) + activemodel (= 8.1.3) + activesupport (= 8.1.3) timeout (>= 0.4.0) - activestorage (8.1.2) - actionpack (= 8.1.2) - activejob (= 8.1.2) - activerecord (= 8.1.2) - activesupport (= 8.1.2) + activestorage (8.1.3) + actionpack (= 8.1.3) + activejob (= 8.1.3) + activerecord (= 8.1.3) + activesupport (= 8.1.3) marcel (~> 1.0) - activesupport (8.1.2) + activesupport (8.1.3) base64 bigdecimal concurrent-ruby (~> 1.0, >= 1.3.1) @@ -75,10 +75,10 @@ GEM securerandom (>= 0.3) tzinfo (~> 2.0, >= 2.0.5) uri (>= 0.13.1) - addressable (2.8.7) - public_suffix (>= 2.0.2, < 7.0) + addressable (2.9.0) + public_suffix (>= 2.0.2, < 8.0) ast (2.4.3) - async (2.38.1) + async (2.39.0) console (~> 1.29) fiber-annotation io-event (~> 1.11) @@ -88,7 +88,7 @@ GEM execjs (~> 2) awesome_print (1.9.2) base64 (0.3.0) - bigdecimal (4.0.1) + bigdecimal (4.1.1) bindex (0.8.1) binding_of_caller (1.0.1) debug_inspector (>= 1.2.0) @@ -148,7 +148,7 @@ GEM erb (6.0.2) erubi (1.13.1) erubis (2.7.0) - execjs (2.10.0) + execjs (2.10.1) factory_bot (6.4.6) activesupport (>= 5.0.0) factory_bot_rails (6.4.3) @@ -173,7 +173,7 @@ GEM concurrent-ruby (~> 1.0) interception (0.5) io-console (0.8.2) - io-event (1.14.5) + io-event (1.15.1) irb (1.17.0) pp (>= 0.6.0) prism (>= 1.3.0) @@ -182,7 +182,7 @@ GEM jbuilder (2.12.0) actionview (>= 5.0.0) activesupport (>= 5.0.0) - json (2.19.1) + json (2.19.3) jwt (2.10.2) base64 language_server-protocol (3.17.0.5) @@ -204,7 +204,7 @@ GEM method_source (1.1.0) metrics (0.15.0) mini_mime (1.1.5) - minitest (6.0.2) + minitest (6.0.3) drb (~> 2.0) prism (~> 1.5) mize (0.4.1) @@ -220,9 +220,9 @@ GEM net-smtp (0.5.1) net-protocol nio4r (2.7.5) - nokogiri (1.19.1-arm64-darwin) + nokogiri (1.19.2-arm64-darwin) racc (~> 1.4) - nokogiri (1.19.1-x86_64-linux-gnu) + nokogiri (1.19.2-x86_64-linux-gnu) racc (~> 1.4) package_json (0.2.0) parallel (1.27.0) @@ -233,7 +233,7 @@ GEM pp (0.6.3) prettyprint prettyprint (0.2.0) - prism (1.5.1) + prism (1.9.0) protocol (2.0.0) ruby_parser (~> 3.0) pry (0.14.2) @@ -256,11 +256,11 @@ GEM psych (5.3.1) date stringio - public_suffix (6.0.2) + public_suffix (7.0.5) puma (6.4.2) nio4r (~> 2.0) racc (1.8.1) - rack (3.2.5) + rack (3.2.6) rack-proxy (0.7.7) rack rack-session (2.1.1) @@ -270,20 +270,20 @@ GEM rack (>= 1.3) rackup (2.3.1) rack (>= 3) - rails (8.1.2) - actioncable (= 8.1.2) - actionmailbox (= 8.1.2) - actionmailer (= 8.1.2) - actionpack (= 8.1.2) - actiontext (= 8.1.2) - actionview (= 8.1.2) - activejob (= 8.1.2) - activemodel (= 8.1.2) - activerecord (= 8.1.2) - activestorage (= 8.1.2) - activesupport (= 8.1.2) + rails (8.1.3) + actioncable (= 8.1.3) + actionmailbox (= 8.1.3) + actionmailer (= 8.1.3) + actionpack (= 8.1.3) + actiontext (= 8.1.3) + actionview (= 8.1.3) + activejob (= 8.1.3) + activemodel (= 8.1.3) + activerecord (= 8.1.3) + activestorage (= 8.1.3) + activesupport (= 8.1.3) bundler (>= 1.15.0) - railties (= 8.1.2) + railties (= 8.1.3) rails-dom-testing (2.3.0) activesupport (>= 5.0.0) minitest @@ -299,9 +299,9 @@ GEM json require_all (~> 3.0) ruby-progressbar - railties (8.1.2) - actionpack (= 8.1.2) - activesupport (= 8.1.2) + railties (8.1.3) + actionpack (= 8.1.3) + activesupport (= 8.1.3) irb (~> 1.13) rackup (>= 1.0.0) rake (>= 12.2) @@ -317,14 +317,14 @@ GEM erb psych (>= 4.0.0) tsort - react_on_rails (16.5.1) + react_on_rails (16.6.0.rc.0) addressable connection_pool execjs (~> 2.5) rails (>= 5.2) rainbow (~> 3.0) shakapacker (>= 6.0) - react_on_rails_pro (16.5.1) + react_on_rails_pro (16.6.0.rc.0) addressable async (>= 2.29) connection_pool @@ -333,7 +333,7 @@ GEM httpx (~> 1.5) jwt (~> 2.7) rainbow - react_on_rails (= 16.5.1) + react_on_rails (= 16.6.0.rc.0) redcarpet (3.6.0) redis (5.3.0) redis-client (>= 0.22.0) @@ -518,7 +518,7 @@ DEPENDENCIES rails-html-sanitizer rails_best_practices rainbow - react_on_rails_pro (= 16.5.1) + react_on_rails_pro (= 16.6.0.rc.0) redcarpet redis (~> 5.0) rspec-rails (~> 6.0.0) diff --git a/package.json b/package.json index 697827ebe..a5dbe00c3 100644 --- a/package.json +++ b/package.json @@ -81,8 +81,8 @@ "react": "19.0.4", "react-dom": "19.0.4", "react-intl": "^6.4.4", - "react-on-rails-pro": "16.5.1", - "react-on-rails-pro-node-renderer": "16.5.1", + "react-on-rails-pro": "16.6.0-rc.0", + "react-on-rails-pro-node-renderer": "16.6.0-rc.0", "react-on-rails-rsc": "19.0.5-rc.1", "react-redux": "^8.1.0", "react-router": "^6.13.0", diff --git a/yarn.lock b/yarn.lock index 88c9651b4..f6979b086 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5197,7 +5197,7 @@ fastify-plugin@^5.0.0: resolved "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-5.1.0.tgz#7083e039d6418415f9a669f8c25e72fc5bf2d3e7" integrity sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw== -fastify@^5.8.1: +fastify@^5.8.3: version "5.8.4" resolved "https://registry.npmjs.org/fastify/-/fastify-5.8.4.tgz#9ad9ebeea57980cf8722b5c44ca27ea9255cf2d5" integrity sha512-sa42J1xylbBAYUWALSBoyXKPDUvM3OoNOibIefA+Oha57FryXKKCZarA1iDntOCWp3O35voZLuDg2mdODXtPzQ== @@ -8769,25 +8769,25 @@ react-is@^18.0.0, react-is@^18.3.1: resolved "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz" integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== -react-on-rails-pro-node-renderer@16.5.1: - version "16.5.1" - resolved "https://registry.npmjs.org/react-on-rails-pro-node-renderer/-/react-on-rails-pro-node-renderer-16.5.1.tgz#34d08578981593d567838cc82c77b3fd4d03df47" - integrity sha512-QkW+BUhczPbgJ/kLJzEuMoL08wQiZUox7HG1ZBi1cZdjNxGPTcZC7qrD8+NEPgjAoUGZazCcv7DR0kTpYfPURw== +react-on-rails-pro-node-renderer@16.6.0-rc.0: + version "16.6.0-rc.0" + resolved "https://registry.npmjs.org/react-on-rails-pro-node-renderer/-/react-on-rails-pro-node-renderer-16.6.0-rc.0.tgz#1c69dc73ce9cac421415883b5ec3e8a56646f107" + integrity sha512-CGvJ+GFtVuVRK5gTpnaF0EMPA5RN7250CE3PxGVvb2qVYQiFXIzGaP0dTD0DiJiIxm8Z9/KSD49HX9MxQBN1Cg== dependencies: "@fastify/formbody" "^7.4.0 || ^8.0.2" "@fastify/multipart" "^8.3.1 || ^9.0.3" - fastify "^5.8.1" + fastify "^5.8.3" fs-extra "^11.2.0" jsonwebtoken "^9.0.3" lockfile "^1.0.4" pino "^9.0.0" -react-on-rails-pro@16.5.1: - version "16.5.1" - resolved "https://registry.npmjs.org/react-on-rails-pro/-/react-on-rails-pro-16.5.1.tgz#6b2503e7db55a3ff088d51675ccc345b8adb95a5" - integrity sha512-IhE1QklbvWRq4CEZayDUoAKzYbqICdUsQuhC4+NUF1/K0Z6e+a8VJtDK/8nMUZBIDjR/bomE+dez+rnF9F+sHw== +react-on-rails-pro@16.6.0-rc.0: + version "16.6.0-rc.0" + resolved "https://registry.npmjs.org/react-on-rails-pro/-/react-on-rails-pro-16.6.0-rc.0.tgz#dadbcaf432f9345edf06583b372599da396f5646" + integrity sha512-6obniSmZeJJvPtVWKpPlDThT5R0Cq9Ny7pUzTuYYqoH6wtS44A1bpcXKyK4MNLFxcSNkwmKKSUovMptdXFswWg== dependencies: - react-on-rails "16.5.1" + react-on-rails "16.6.0-rc.0" react-on-rails-rsc@19.0.5-rc.1: version "19.0.5-rc.1" @@ -8798,10 +8798,10 @@ react-on-rails-rsc@19.0.5-rc.1: neo-async "^2.6.1" webpack-sources "^3.2.0" -react-on-rails@16.5.1: - version "16.5.1" - resolved "https://registry.npmjs.org/react-on-rails/-/react-on-rails-16.5.1.tgz#7fc4eb502e48445ab4f01ae039a25e2aa72447d4" - integrity sha512-IrfmuY5z0GN596nyE27teLbBTvxcaYR+MVjMGbkmoDV79/Tm2MsWt3hdPeYqD7gXu0AQagd+oHvYuyxfSJ4RGw== +react-on-rails@16.6.0-rc.0: + version "16.6.0-rc.0" + resolved "https://registry.npmjs.org/react-on-rails/-/react-on-rails-16.6.0-rc.0.tgz#ed0ed7085133905ad1e243cc97233e97d10a1c99" + integrity sha512-fSEomzwojgWob6uTWSfkbpP+XE++8kQBjNFpTT7V419QOON1daIHypQwA9gc8L8uX1If5r8hmAs57iWyGWmJuQ== react-proxy@^1.1.7: version "1.1.8" From c0a66b1c54e0fe6537b5fb49ac04b2da077aef25 Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Sun, 5 Apr 2026 19:57:42 -1000 Subject: [PATCH 12/16] Address PR review fixes 1-12 and resolve review threads --- Procfile.dev | 2 +- .../components/CommentsFeed.jsx | 60 ++++++++++++----- config/initializers/react_on_rails_pro.rb | 17 ++++- config/webpack/rscWebpackConfig.js | 10 ++- config/webpack/rspackRscPlugin.js | 65 ++++++++++--------- config/webpack/serverWebpackConfig.js | 19 +++--- config/webpack/webpackConfig.js | 2 +- react-on-rails-pro-node-renderer.js | 9 ++- 8 files changed, 115 insertions(+), 69 deletions(-) diff --git a/Procfile.dev b/Procfile.dev index 94a640088..d37228687 100644 --- a/Procfile.dev +++ b/Procfile.dev @@ -15,4 +15,4 @@ wp-server: SERVER_BUNDLE_ONLY=yes bin/shakapacker --watch # 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 +node-renderer: node react-on-rails-pro-node-renderer.js diff --git a/client/app/bundles/server-components/components/CommentsFeed.jsx b/client/app/bundles/server-components/components/CommentsFeed.jsx index 0230c1d19..1260ffdf8 100644 --- a/client/app/bundles/server-components/components/CommentsFeed.jsx +++ b/client/app/bundles/server-components/components/CommentsFeed.jsx @@ -11,33 +11,59 @@ import TogglePanel from './TogglePanel'; const marked = new Marked(); marked.use(gfmHeadingId()); +function resolveRailsBaseUrl() { + if (process.env.RAILS_INTERNAL_URL) { + return process.env.RAILS_INTERNAL_URL; + } + + // Local defaults are okay in development/test, but production should be explicit. + if (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test') { + return 'http://localhost:3000'; + } + + throw new Error('RAILS_INTERNAL_URL must be set outside development/test'); +} + async function CommentsFeed() { - // Simulate network latency to demonstrate Suspense streaming (development only) - if (process.env.NODE_ENV !== 'production') { + // Simulate network latency only when explicitly enabled for demos. + if (process.env.RSC_SUSPENSE_DEMO_DELAY === 'true') { await new Promise((resolve) => { setTimeout(resolve, 800); }); } - // Fetch comments directly from the Rails API — no client-side fetch needed - const baseUrl = process.env.RAILS_INTERNAL_URL || 'http://localhost:3000'; - const response = await fetch(`${baseUrl}/comments.json`); - if (!response.ok) { - throw new Error(`Failed to fetch comments: ${response.status} ${response.statusText}`); - } - const comments = await response.json(); + let recentComments = []; + try { + // Fetch comments directly from the Rails API — no client-side fetch needed + const baseUrl = resolveRailsBaseUrl(); + const response = await fetch(`${baseUrl}/comments.json`); + if (!response.ok) { + throw new Error(`Failed to fetch comments: ${response.status} ${response.statusText}`); + } + const comments = await response.json(); - // Use lodash to process (stays on server) - const sortedComments = _.orderBy(comments, ['created_at'], ['desc']); - const recentComments = _.take(sortedComments, 10); + // Use lodash to process (stays on server) + const sortedComments = _.orderBy(comments, ['created_at'], ['desc']); + recentComments = _.take(sortedComments, 10); + } catch (error) { + // eslint-disable-next-line no-console + console.error('CommentsFeed failed to load comments', error); + return ( +
    +

    Could not load comments right now. Please try again later.

    +
    + ); + } if (recentComments.length === 0) { return (

    No comments yet. Add some comments from the{' '} - home page to see them rendered here - by server components. + + home page + {' '} + to see them rendered here by server components.

    ); @@ -56,7 +82,7 @@ async function CommentsFeed() { ...sanitizeHtml.defaults.allowedAttributes, img: ['src', 'alt', 'title', 'width', 'height'], }, - allowedSchemes: ['https', 'http', 'data'], + allowedSchemes: ['https', 'http'], }); return ( @@ -89,8 +115,8 @@ async function CommentsFeed() { ); })}

    - {recentComments.length} comment{recentComments.length !== 1 ? 's' : ''} rendered on the server using - {' '}marked + sanitize-html (never sent to browser) + {recentComments.length} comment{recentComments.length !== 1 ? 's' : ''} rendered on the server using{' '} + marked + sanitize-html (never sent to browser)

    ); diff --git a/config/initializers/react_on_rails_pro.rb b/config/initializers/react_on_rails_pro.rb index e5ab7965a..c31b87ab1 100644 --- a/config/initializers/react_on_rails_pro.rb +++ b/config/initializers/react_on_rails_pro.rb @@ -2,9 +2,20 @@ 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") + use_node_renderer = Rails.env.development? || ENV["REACT_USE_NODE_RENDERER"] == "true" + + if use_node_renderer + renderer_host = ENV.fetch("RENDERER_HOST", "localhost") + renderer_port = ENV.fetch("RENDERER_PORT", "3800") + + config.server_renderer = "NodeRenderer" + config.renderer_url = ENV.fetch("REACT_RENDERER_URL", "http://#{renderer_host}:#{renderer_port}") + config.renderer_password = if Rails.env.local? + ENV.fetch("RENDERER_PASSWORD", "local-dev-renderer-password") + else + ENV.fetch("RENDERER_PASSWORD") + end + end # Enable React Server Components support config.enable_rsc_support = true diff --git a/config/webpack/rscWebpackConfig.js b/config/webpack/rscWebpackConfig.js index ebd137cda..83ffb504a 100644 --- a/config/webpack/rscWebpackConfig.js +++ b/config/webpack/rscWebpackConfig.js @@ -28,9 +28,7 @@ const configureRsc = () => { // Use the dedicated rsc-bundle entry point const rscEntry = rscConfig.entry['rsc-bundle']; if (!rscEntry) { - throw new Error( - 'RSC bundle entry not found. Ensure client/app/packs/rsc-bundle.js exists.', - ); + throw new Error('RSC bundle entry not found. Ensure client/app/packs/rsc-bundle.js exists.'); } rscConfig.entry = { 'rsc-bundle': rscEntry }; @@ -63,8 +61,8 @@ const configureRsc = () => { cssLoader.options.modules = { ...cssLoader.options.modules, exportOnlyLocals: true }; } } else if ( - rule.use?.loader - && (rule.use.loader.includes('url-loader') || rule.use.loader.includes('file-loader')) + rule.use?.loader && + (rule.use.loader.includes('url-loader') || rule.use.loader.includes('file-loader')) ) { rule.use.options = { ...(rule.use.options || {}), emitFile: false }; } @@ -103,7 +101,7 @@ const configureRsc = () => { // Target Node.js so server-only modules (os, fs, stream, etc.) resolve correctly rscConfig.target = 'node'; - rscConfig.devtool = 'eval'; + rscConfig.devtool = process.env.NODE_ENV === 'production' ? 'source-map' : 'eval'; // RSC manifest plugin rscConfig.plugins.push(new RspackRscPlugin({ isServer: true })); diff --git a/config/webpack/rspackRscPlugin.js b/config/webpack/rspackRscPlugin.js index 1c3c2f659..331cc1170 100644 --- a/config/webpack/rspackRscPlugin.js +++ b/config/webpack/rspackRscPlugin.js @@ -9,32 +9,6 @@ const fs = require('fs'); const { sources } = require('@rspack/core'); -// Cache for file 'use client' checks -const useClientCache = new Map(); - -function hasUseClientDirective(filePath) { - if (useClientCache.has(filePath)) return useClientCache.get(filePath); - - let result = false; - try { - // Read the first ~200 bytes — 'use client' must be at the very top of the file - const fd = fs.openSync(filePath, 'r'); - const buf = Buffer.alloc(200); - fs.readSync(fd, buf, 0, 200, 0); - fs.closeSync(fd); - - const head = buf.toString('utf-8'); - // 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 (_) { - // file doesn't exist or can't be read - } - - useClientCache.set(filePath, result); - return result; -} - class RspackRscPlugin { constructor(options) { if (!options || typeof options.isServer !== 'boolean') { @@ -45,12 +19,13 @@ class RspackRscPlugin { ? 'react-server-client-manifest.json' : 'react-client-manifest.json'; this.ssrManifestFilename = 'react-ssr-manifest.json'; + this.useClientCache = new Map(); } apply(compiler) { // Clear cache on each compilation so watch-mode picks up 'use client' changes compiler.hooks.thisCompilation.tap('RspackRscPlugin-ClearCache', () => { - useClientCache.clear(); + this.useClientCache.clear(); }); compiler.hooks.thisCompilation.tap('RspackRscPlugin', (compilation) => { @@ -103,6 +78,36 @@ class RspackRscPlugin { }); } + _hasUseClientDirective(filePath) { + if (this.useClientCache.has(filePath)) return this.useClientCache.get(filePath); + + let result = false; + let fd; + try { + // Read the first ~200 bytes — 'use client' must be at the very top of the file. + fd = fs.openSync(filePath, 'r'); + const buf = Buffer.alloc(200); + fs.readSync(fd, buf, 0, 200, 0); + + const head = buf.toString('utf-8'); + // Allow comments before the directive. + result = /^(?:\s*(?:\/\/[^\n]*\n|\/\*[\s\S]*?\*\/))*\s*['"]use client['"]/.test(head); + } catch (_) { + // file doesn't exist or can't be read + } finally { + if (fd !== undefined) { + try { + fs.closeSync(fd); + } catch (_) { + // no-op + } + } + } + + this.useClientCache.set(filePath, result); + return result; + } + _processModule(mod, chunk, chunkFiles, manifest, compilation) { const resource = mod.resource || mod.userRequest; if (!resource || !resource.match(/\.(js|jsx|ts|tsx)$/)) return; @@ -110,11 +115,9 @@ class RspackRscPlugin { if (resource.includes('node_modules')) return; // Check original file for 'use client' directive - if (!hasUseClientDirective(resource)) return; + if (!this._hasUseClientDirective(resource)) return; - const moduleId = compilation.chunkGraph - ? compilation.chunkGraph.getModuleId(mod) - : mod.id; + const moduleId = compilation.chunkGraph ? compilation.chunkGraph.getModuleId(mod) : mod.id; if (moduleId == null) return; diff --git a/config/webpack/serverWebpackConfig.js b/config/webpack/serverWebpackConfig.js index 48e98424a..dd20c06d8 100644 --- a/config/webpack/serverWebpackConfig.js +++ b/config/webpack/serverWebpackConfig.js @@ -58,13 +58,13 @@ const configureServer = () => { throw new Error( `Server bundle entry 'server-bundle' not found.\n` + - `Expected file: ${fullPath}\n` + - `Current source_path: ${config.source_path}\n` + - `Current source_entry_path: ${config.source_entry_path}\n` + - `Verify:\n` + - `1. The server-bundle.js file exists at the expected location\n` + - `2. nested_entries is configured correctly in shakapacker.yml\n` + - `3. The file is properly exported from your entry point`, + `Expected file: ${fullPath}\n` + + `Current source_path: ${config.source_path}\n` + + `Current source_entry_path: ${config.source_entry_path}\n` + + `Verify:\n` + + `1. The server-bundle.js file exists at the expected location\n` + + `2. nested_entries is configured correctly in shakapacker.yml\n` + + `3. The file is properly exported from your entry point`, ); } @@ -94,7 +94,7 @@ const configureServer = () => { serverWebpackConfig.output = { filename: 'server-bundle.js', globalObject: 'this', - // libraryTarget: 'commonjs2', + libraryTarget: 'commonjs2', path: path.resolve(__dirname, '../../ssr-generated'), publicPath: config.publicPath, // https://webpack.js.org/configuration/output/#outputglobalobject @@ -199,4 +199,5 @@ const configureServer = () => { return serverWebpackConfig; }; -module.exports = { default: configureServer, extractLoader }; +module.exports = configureServer; +module.exports.extractLoader = extractLoader; diff --git a/config/webpack/webpackConfig.js b/config/webpack/webpackConfig.js index 595fc63c3..df036a145 100644 --- a/config/webpack/webpackConfig.js +++ b/config/webpack/webpackConfig.js @@ -2,7 +2,7 @@ // https://github.com/shakacode/react_on_rails_tutorial_with_ssr_and_hmr_fast_refresh/blob/master/config/webpack/webpackConfig.js const clientWebpackConfig = require('./clientWebpackConfig'); -const { default: serverWebpackConfig } = require('./serverWebpackConfig'); +const serverWebpackConfig = require('./serverWebpackConfig'); const rscWebpackConfig = require('./rscWebpackConfig'); const webpackConfig = (envSpecific) => { diff --git a/react-on-rails-pro-node-renderer.js b/react-on-rails-pro-node-renderer.js index 0f728691b..d80045899 100644 --- a/react-on-rails-pro-node-renderer.js +++ b/react-on-rails-pro-node-renderer.js @@ -1,10 +1,17 @@ const path = require('path'); const { reactOnRailsProNodeRenderer } = require('react-on-rails-pro-node-renderer'); +const isProduction = process.env.NODE_ENV === 'production'; +const rendererPassword = process.env.RENDERER_PASSWORD || (!isProduction && 'local-dev-renderer-password'); + +if (!rendererPassword) { + throw new Error('RENDERER_PASSWORD must be set in production'); +} + const config = { serverBundleCachePath: path.resolve(__dirname, '.node-renderer-bundles'), logLevel: process.env.RENDERER_LOG_LEVEL || 'debug', - password: process.env.RENDERER_PASSWORD || 'devPassword', + password: rendererPassword, port: process.env.RENDERER_PORT || 3800, supportModules: true, workersCount: Number(process.env.NODE_RENDERER_CONCURRENCY || 3), From 248ba65e40e050267a940efbec9107b507448891 Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Tue, 7 Apr 2026 13:49:17 -1000 Subject: [PATCH 13/16] Address PR review comments: fix bugs and improve code quality - Fix JSON response structure mismatch in CommentsFeed (unwrap data.comments) - Fix registerServerComponent API call (remove unsupported options object) - Enable NodeRenderer in test env by using Rails.env.local? instead of development? - Add warning for non-ENOENT errors in rspackRscPlugin _hasUseClientDirective - Avoid no-param-reassign ESLint violations in rscWebpackConfig loader pruning Co-Authored-By: Claude Opus 4.6 (1M context) --- .../components/CommentsFeed.jsx | 3 +- client/app/packs/stimulus-bundle.js | 5 +--- config/initializers/react_on_rails_pro.rb | 2 +- config/webpack/rscWebpackConfig.js | 29 +++++++++++-------- config/webpack/rspackRscPlugin.js | 8 +++-- 5 files changed, 27 insertions(+), 20 deletions(-) diff --git a/client/app/bundles/server-components/components/CommentsFeed.jsx b/client/app/bundles/server-components/components/CommentsFeed.jsx index 1260ffdf8..3914f38c2 100644 --- a/client/app/bundles/server-components/components/CommentsFeed.jsx +++ b/client/app/bundles/server-components/components/CommentsFeed.jsx @@ -40,7 +40,8 @@ async function CommentsFeed() { if (!response.ok) { throw new Error(`Failed to fetch comments: ${response.status} ${response.statusText}`); } - 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']); diff --git a/client/app/packs/stimulus-bundle.js b/client/app/packs/stimulus-bundle.js index 920396bab..5517529e4 100644 --- a/client/app/packs/stimulus-bundle.js +++ b/client/app/packs/stimulus-bundle.js @@ -22,7 +22,4 @@ ReactOnRails.setOptions({ // No need for manual registration // React Server Components registration (client-side) -registerServerComponent( - { rscPayloadGenerationUrlPath: 'rsc_payload/' }, - 'ServerComponentsPage', -); +registerServerComponent('ServerComponentsPage'); diff --git a/config/initializers/react_on_rails_pro.rb b/config/initializers/react_on_rails_pro.rb index c31b87ab1..2bc8b3506 100644 --- a/config/initializers/react_on_rails_pro.rb +++ b/config/initializers/react_on_rails_pro.rb @@ -2,7 +2,7 @@ ReactOnRailsPro.configure do |config| # Node renderer for server-side rendering and RSC payload generation - use_node_renderer = Rails.env.development? || ENV["REACT_USE_NODE_RENDERER"] == "true" + use_node_renderer = Rails.env.local? || ENV["REACT_USE_NODE_RENDERER"] == "true" if use_node_renderer renderer_host = ENV.fetch("RENDERER_HOST", "localhost") diff --git a/config/webpack/rscWebpackConfig.js b/config/webpack/rscWebpackConfig.js index 83ffb504a..a838a01e5 100644 --- a/config/webpack/rscWebpackConfig.js +++ b/config/webpack/rscWebpackConfig.js @@ -42,9 +42,9 @@ const configureRsc = () => { ); // Remove CSS extraction loaders from style rules - rscConfig.module.rules.forEach((rule) => { + rscConfig.module.rules = rscConfig.module.rules.map((rule) => { if (Array.isArray(rule.use)) { - rule.use = rule.use.filter((item) => { + const filteredUse = rule.use.filter((item) => { const testValue = typeof item === 'string' ? item : item?.loader; return !( testValue?.match(/mini-css-extract-plugin/) || @@ -53,32 +53,37 @@ const configureRsc = () => { testValue === 'style-loader' ); }); - const cssLoader = rule.use.find((item) => { + const cssLoader = filteredUse.find((item) => { const testValue = typeof item === 'string' ? item : item?.loader; return testValue?.includes('css-loader'); }); - if (cssLoader?.options?.modules) { - cssLoader.options.modules = { ...cssLoader.options.modules, exportOnlyLocals: true }; - } - } else if ( + const updatedCssOptions = cssLoader?.options?.modules + ? { ...cssLoader.options, modules: { ...cssLoader.options.modules, exportOnlyLocals: true } } + : cssLoader?.options; + const updatedUse = updatedCssOptions + ? filteredUse.map((item) => (item === cssLoader ? { ...item, options: updatedCssOptions } : item)) + : filteredUse; + return { ...rule, use: updatedUse }; + } + if ( rule.use?.loader && (rule.use.loader.includes('url-loader') || rule.use.loader.includes('file-loader')) ) { - rule.use.options = { ...(rule.use.options || {}), emitFile: false }; + return { ...rule, use: { ...rule.use, options: { ...(rule.use.options || {}), emitFile: false } } }; } + return rule; }); // Add the RSC WebpackLoader to transpiler rules. // This loader handles 'use client' directive detection and server/client component separation. - rscConfig.module.rules.forEach((rule) => { + rscConfig.module.rules = rscConfig.module.rules.map((rule) => { if (Array.isArray(rule.use)) { const transpilerLoader = extractLoader(rule, 'swc-loader') || extractLoader(rule, 'babel-loader'); if (transpilerLoader) { - rule.use.push({ - loader: 'react-on-rails-rsc/WebpackLoader', - }); + return { ...rule, use: [...rule.use, { loader: 'react-on-rails-rsc/WebpackLoader' }] }; } } + return rule; }); // Enable react-server condition for server component resolution diff --git a/config/webpack/rspackRscPlugin.js b/config/webpack/rspackRscPlugin.js index 331cc1170..b78992492 100644 --- a/config/webpack/rspackRscPlugin.js +++ b/config/webpack/rspackRscPlugin.js @@ -92,8 +92,12 @@ class RspackRscPlugin { const head = buf.toString('utf-8'); // Allow comments before the directive. result = /^(?:\s*(?:\/\/[^\n]*\n|\/\*[\s\S]*?\*\/))*\s*['"]use client['"]/.test(head); - } catch (_) { - // file doesn't exist or can't be read + } catch (err) { + // ENOENT is expected for virtual/generated modules; anything else is a real problem. + if (err.code !== 'ENOENT') { + // eslint-disable-next-line no-console + console.warn(`[RscPlugin] Failed to read ${filePath}:`, err.message); + } } finally { if (fd !== undefined) { try { From 6429ce0cae03efa3e7fa20b5760eb97447bf477c Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Tue, 7 Apr 2026 17:42:19 -1000 Subject: [PATCH 14/16] Add RSC and Node Renderer documentation to CLAUDE.md and README.md Document the three-bundle architecture (client, server SSR, RSC), Node Renderer setup requirements, VM sandbox constraints, RSC component classification rules, and common troubleshooting patterns. Update README with current version targets, RSC section, expanded config file list, and all six Procfile.dev processes. Motivated by the debugging challenges in PR #723 where undocumented constraints (VM sandbox lacks require/MessageChannel, 'use client' classification, Node renderer must run for tests) caused significant debugging time. Filed shakacode/react_on_rails#3076 with upstream doc suggestions. Co-Authored-By: Claude Opus 4.6 (1M context) --- CLAUDE.md | 70 +++++++++++++++++++++++++++++++++++++++++++++++ README.md | 81 ++++++++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 135 insertions(+), 16 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index fd56bddd1..470848189 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,6 +16,76 @@ bundle exec rubocop bundle exec rubocop -a ``` +## Architecture: Three Bundle System + +This project builds **three separate Rspack bundles** via Shakapacker: + +| Bundle | Config | Output | Runtime | +|--------|--------|--------|---------| +| **Client** | `clientWebpackConfig.js` | `public/packs/` | Browser | +| **Server (SSR)** | `serverWebpackConfig.js` | `ssr-generated/` | Node renderer VM sandbox | +| **RSC** | `rscWebpackConfig.js` | RSC output dir | Node renderer (full Node.js) | + +### Key constraints + +- The **server bundle runs in a VM sandbox** (`vm.createContext()`), NOT full Node.js. It lacks `require`, `MessageChannel`, `TextEncoder`, and other Node/browser globals. +- Use `resolve.fallback: false` (NOT `externals`) for Node builtins in the server bundle — the VM has no `require()`, so externalized `require('path')` calls will crash. +- The **RSC bundle** targets Node.js directly with the `react-server` condition, so Node builtins work normally there. +- A `BannerPlugin` injects a `MessageChannel` polyfill into the server bundle because `react-dom/server.browser` needs it at module load time. + +### Build commands + +```bash +# Build only the server bundle +SERVER_BUNDLE_ONLY=yes bin/shakapacker + +# Build only the RSC bundle +RSC_BUNDLE_ONLY=true bin/shakapacker + +# Watch modes (used by Procfile.dev) +SERVER_BUNDLE_ONLY=yes bin/shakapacker --watch +RSC_BUNDLE_ONLY=true bin/shakapacker --watch +``` + +## Node Renderer + +React on Rails Pro's NodeRenderer must be running for SSR and RSC payload generation. + +- **Port**: 3800 (configured in `config/initializers/react_on_rails_pro.rb`) +- **Launcher**: `node react-on-rails-pro-node-renderer.js` +- **Auth**: Requires `RENDERER_PASSWORD` env var (defaults to `local-dev-renderer-password` in dev/test, required with no fallback in production) +- **Started automatically** by `bin/dev` / `Procfile.dev` + +### Testing requirement + +**The Node renderer must be running before `bundle exec rspec`.** Tests will fail with `Net::ReadTimeout` if it is not running. CI starts it as a background process and waits up to 30 seconds for TCP readiness on port 3800. + +### Enabled environments + +The renderer is enabled when `Rails.env.local?` (development + test) or `REACT_USE_NODE_RENDERER=true`. Production requires explicit env var configuration. + +## RSC Component Classification + +React on Rails Pro auto-classifies components based on the `'use client'` directive: + +- **With `'use client'`** → registered via `ReactOnRails.register()` (traditional SSR/client component) +- **Without `'use client'`** → registered via `registerServerComponent()` (React Server Component) + +### Common gotcha: `.server.jsx` files + +In this codebase, `.server.jsx` does NOT mean "React Server Component" — it means traditional **server-side rendering** (e.g., `StaticRouter`). If a `.server.jsx` file uses client APIs like `ReactOnRails.getStore()` or React hooks, it **needs** the `'use client'` directive to prevent RSC misclassification. The naming predates RSC and refers to the Rails SSR render path. + +### Key files + +| File | Purpose | +|------|---------| +| `config/webpack/rscWebpackConfig.js` | RSC bundle configuration | +| `config/webpack/rspackRscPlugin.js` | Detects `'use client'` directives, emits React Flight manifests | +| `client/app/packs/rsc-bundle.js` | RSC entry point | +| `client/app/packs/rsc-client-components.js` | Client component registration for RSC | +| `config/initializers/react_on_rails_pro.rb` | Node renderer + RSC configuration | +| `react-on-rails-pro-node-renderer.js` | Node renderer launcher | + ## Conductor Compatibility (Version Managers) ### Problem diff --git a/README.md b/README.md index 395f35569..e10cd0d8e 100644 --- a/README.md +++ b/README.md @@ -83,11 +83,10 @@ You can see this tutorial live here: [http://reactrails.com/](http://reactrails. + [Configuration Files](#configuration-files) + [Additional Resources](#additional-resources) + [Thruster HTTP/2 Proxy](#thruster-http2-proxy) ++ [React Server Components (RSC)](#react-server-components-rsc) + [Sass, CSS Modules, and Tailwind CSS integration](#sass-css-modules-and-tailwind-css-integration) + [Fonts with SASS](#fonts-with-sass) + [Process Management during Development](#process-management-during-development) -+ [Rendering with Express Server](#rendering-with-express-server) - + [Setup](#setup) + [Contributors](#contributors) + [About ShakaCode](#about-shakacode) + [RubyMine and WebStorm](#rubymine-and-webstorm) @@ -95,7 +94,8 @@ You can see this tutorial live here: [http://reactrails.com/](http://reactrails. ## Demoed Functionality -- Example of using the [react_on_rails gem](https://github.com/shakacode/react_on_rails) for easy React + Rspack integration with Rails. +- Example of using [React on Rails Pro](https://www.shakacode.com/react-on-rails-pro/) with the NodeRenderer for server-side rendering. +- Example of [React Server Components (RSC)](#react-server-components-rsc) with streaming and selective hydration. - Example of React with [CSS Modules](http://glenmaddern.com/articles/css-modules) inside Rails using modern Shakapacker/Rspack builds. - Example of enabling hot reloading of both JS and CSS (modules) from your Rails app in development mode. Change your code. Save. Browser updates without a refresh! - Example of React/Redux with Rails Action Cable. @@ -110,18 +110,17 @@ You can see this tutorial live here: [http://reactrails.com/](http://reactrails. See package.json and Gemfile for versions -1. [react_on_rails gem](https://github.com/shakacode/react_on_rails/) -1. [React](http://facebook.github.io/react/) +1. [React on Rails Pro](https://www.shakacode.com/react-on-rails-pro/) with NodeRenderer for SSR and React Server Components +1. [React 19](http://facebook.github.io/react/) with React Server Components support 1. [Redux](https://github.com/reactjs/redux) 1. [react-router](https://github.com/reactjs/react-router) 1. [react-router-redux](https://github.com/reactjs/react-router-redux) 1. [Rspack with hot-reload](https://rspack.dev/guide/features/dev-server) (for local dev) -1. [Babel transpiler](https://github.com/babel/babel) +1. [SWC transpiler](https://swc.rs/) for fast JavaScript/TypeScript compilation 1. [Ruby on Rails 8](http://rubyonrails.org/) for backend app and comparison with plain HTML 1. [Thruster](https://github.com/basecamp/thruster) - Zero-config HTTP/2 proxy for optimized asset delivery 1. [Heroku deployment guide](https://devcenter.heroku.com/articles/getting-started-with-rails8) 1. [Deployment to the ControlPlane](.controlplane/readme.md) -1. [Turbolinks 5](https://github.com/turbolinks/turbolinks) 1. [Tailwind CSS](https://github.com/tailwindlabs/tailwindcss) ## Basic Demo Setup @@ -183,10 +182,10 @@ assets_bundler: rspack ### Version Targets -- `react_on_rails` gem: `16.4.0` -- `react-on-rails` npm package: `16.4.0` -- `shakapacker` gem/npm package: `9.7.0` -- `@rspack/core` and `@rspack/cli`: `2.0.0-beta.7` (latest published v2 prerelease at the time of this update) +- `react_on_rails_pro` gem: `16.6.0.rc.0` +- `react-on-rails-pro` npm package: `16.6.0-rc.0` +- `shakapacker` gem/npm package: `10.0.0-rc.0` +- `@rspack/core` and `@rspack/cli`: `2.0.0-beta.7` ### Why Rspack @@ -197,10 +196,13 @@ assets_bundler: rspack ### Configuration Files All bundler configuration is in `config/webpack/`: -- `webpackConfig.js` - Main Shakapacker entry point +- `webpackConfig.js` - Main Shakapacker entry point (routes to client, server, or RSC config) - `commonWebpackConfig.js` - Shared configuration -- `clientWebpackConfig.js` - Client bundle settings -- `serverWebpackConfig.js` - Server-side rendering bundle +- `clientWebpackConfig.js` - Client bundle settings (browser, with HMR) +- `serverWebpackConfig.js` - Server-side rendering bundle (runs in Node renderer VM sandbox) +- `rscWebpackConfig.js` - React Server Components bundle (runs in Node renderer, full Node.js) +- `rspackRscPlugin.js` - Custom plugin that detects `'use client'` directives and emits React Flight manifests +- `bundlerUtils.js` - Shared bundler detection utilities - `development.js`, `production.js`, `test.js` - Environment-specific settings ### Additional Resources @@ -243,6 +245,47 @@ The server automatically benefits from HTTP/2, caching, and compression without For detailed information, troubleshooting, and advanced configuration options, see [docs/thruster.md](docs/thruster.md). +## React Server Components (RSC) + +This project demonstrates React Server Components with React on Rails Pro. Visit `/server-components` to see the demo page. + +### How It Works + +The app builds **three separate bundles**: + +1. **Client bundle** — Browser JavaScript with HMR, built by `clientWebpackConfig.js` +2. **Server bundle** — Traditional SSR, runs in the Node renderer's VM sandbox, built by `serverWebpackConfig.js` +3. **RSC bundle** — React Server Components with the `react-server` condition, runs in full Node.js, built by `rscWebpackConfig.js` + +The [React on Rails Pro NodeRenderer](https://www.shakacode.com/react-on-rails-pro/) runs on port 3800 and handles both SSR rendering and RSC payload generation. A custom `RspackRscPlugin` detects `'use client'` directives in source files and emits the React Flight manifests (`react-client-manifest.json`, `react-server-client-manifest.json`) that React uses to resolve client component references during streaming. + +### Key Files + +| File | Purpose | +|------|---------| +| `config/webpack/rscWebpackConfig.js` | RSC bundle configuration | +| `config/webpack/rspackRscPlugin.js` | `'use client'` detection and manifest generation | +| `client/app/packs/rsc-bundle.js` | RSC entry point — registers server components | +| `client/app/packs/rsc-client-components.js` | Client component registration for RSC hydration | +| `config/initializers/react_on_rails_pro.rb` | NodeRenderer and RSC configuration | +| `react-on-rails-pro-node-renderer.js` | Node renderer launcher (port 3800, 3 workers) | +| `client/app/bundles/server-components/` | RSC demo components | + +### Troubleshooting + +| Symptom | Cause | Fix | +|---------|-------|-----| +| `Net::ReadTimeout` in tests | Node renderer not running | Start it with `bin/dev` or `node react-on-rails-pro-node-renderer.js` | +| Component renders blank/empty div | Missing `'use client'` directive | Add `'use client'` to components using client APIs (hooks, stores, event handlers) | +| `MessageChannel is not defined` | Server bundle VM missing polyfill | Check `BannerPlugin` in `serverWebpackConfig.js` | +| `require is not defined` | Server bundle using `externals` | Use `resolve.fallback: false` instead — the VM sandbox has no `require()` | +| RSC manifests missing | RSC bundle not built | Run `RSC_BUNDLE_ONLY=true bin/shakapacker` | + +### Further Reading + +- [React on Rails Pro RSC Documentation](https://www.shakacode.com/react-on-rails-pro/) +- [React Server Components RFC](https://github.com/reactjs/rfcs/blob/main/text/0188-server-components.md) + ## Sass, CSS Modules, and Tailwind CSS Integration This example project uses mainly Tailwind CSS for styling. Besides this, it also demonstrates Sass and CSS modules, particularly for some CSS transitions. @@ -270,12 +313,18 @@ export default class CommentBox extends React.Component { ### Fonts with SASS The tutorial makes use of a custom font OpenSans-Light. We're doing this to show how to add assets for the CSS processing. The font files are located under [client/app/assets/fonts](client/app/assets/fonts) and are loaded by both the Rails asset pipeline and the Rspack HMR server. -## Process management during development +## Process Management during Development ```bash bundle exec foreman start -f ``` -1. [`Procfile.dev`](Procfile.dev): Starts the Rspack Dev Server and Rails with Hot Reloading. +1. [`Procfile.dev`](Procfile.dev): Starts all development processes with Hot Reloading: + - `rescript` — ReScript watch mode + - `rails` — Rails server via Thruster on port 3000 + - `wp-client` — Client Rspack dev server with HMR + - `wp-server` — Server Rspack watcher for SSR bundle + - `wp-rsc` — RSC Rspack watcher for React Server Components bundle + - `node-renderer` — React on Rails Pro Node renderer on port 3800 1. [`Procfile.dev-static`](Procfile.dev-static): Starts the Rails server and generates static assets that are used for tests. ## Contributors From f09b789d3dc8a28fcdda4c66cf6002218c99b19f Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Tue, 7 Apr 2026 18:09:24 -1000 Subject: [PATCH 15/16] Add RSC bundle initialization, fix dev dependency, and add QA playbook - Import react-on-rails-pro in rsc-bundle.js to initialize RSC support (resolves to ReactOnRailsRSC.js with react-server condition) - Move @rspack/dev-server to devDependencies where it belongs - Add QA_PLAYBOOK.md with manual testing checklist for all rendering modes Co-Authored-By: Claude Opus 4.6 (1M context) --- QA_PLAYBOOK.md | 149 +++++++++++++++++++++++++++++++++ client/app/packs/rsc-bundle.js | 5 ++ package.json | 1 + yarn.lock | 145 ++++++++++++++++++++++++++------ 4 files changed, 274 insertions(+), 26 deletions(-) create mode 100644 QA_PLAYBOOK.md diff --git a/QA_PLAYBOOK.md b/QA_PLAYBOOK.md new file mode 100644 index 000000000..abd109092 --- /dev/null +++ b/QA_PLAYBOOK.md @@ -0,0 +1,149 @@ +# Manual QA Playbook + +Run this checklist before merging PRs to catch regressions across rendering modes. + +## Prerequisites + +```bash +bin/dev # Start all processes (Rails, webpack client/server/RSC, Node renderer, ReScript) +``` + +Verify all 6 processes start without crashes in the terminal output: +- `rails` - Rails server via Thruster on port 3000 +- `wp-client` - Rspack dev server (HMR) +- `wp-server` - SSR bundle watcher +- `wp-rsc` - RSC bundle watcher +- `node-renderer` - Node.js renderer on port 3800 +- `rescript` - ReScript compiler watch mode + +--- + +## 1. Homepage `/` (SSR + Redux + React Router) + +**Rendering:** Server-side rendered with Redux store hydration. + +- [ ] Page loads with comments visible (not a blank flash before JS loads) +- [ ] View page source shows pre-rendered HTML content (SSR working) +- [ ] No console errors +- [ ] Navigation bar renders with correct links +- [ ] Comment list displays existing comments + +**Comment CRUD:** +- [ ] Submit a comment via the form (fill name + text, click Post) +- [ ] New comment appears immediately in the list +- [ ] Submit with empty fields shows validation errors +- [ ] Switch between Inline/Horizontal/Stacked form layouts + +**React Router:** +- [ ] Click "React Router Demo" link - navigates without full page reload +- [ ] Visit `/react-router` directly - shows React Router content +- [ ] Visit `/react-router/redirect` - redirects back to `/` + +**ActionCable (real-time):** +- [ ] Open page in two tabs +- [ ] Submit a comment in tab 1 +- [ ] Comment appears in tab 2 without refresh + +--- + +## 2. Simple React `/simple` (CSR only) + +**Rendering:** Client-side only, no SSR. + +- [ ] Page loads (content appears after JS loads) +- [ ] View page source shows empty mount point (no pre-rendered HTML) +- [ ] No console errors +- [ ] Comment form works (submit, validation) + +--- + +## 3. No Router `/no-router` (SSR + Redux, no React Router) + +- [ ] Page loads with pre-rendered comments +- [ ] Comment CRUD works +- [ ] Form layout switching works +- [ ] No console errors + +--- + +## 4. ReScript `/rescript` (SSR) + +- [ ] Page loads with pre-rendered ReScript component +- [ ] No console errors +- [ ] Content displays correctly + +--- + +## 5. Stimulus `/stimulus` (Rails + Turbo + Stimulus) + +**No React on this page - pure Rails.** + +- [ ] Page loads with comment form +- [ ] Tab switching works (Horizontal/Inline/Stacked forms via Turbo) +- [ ] Submit comment - appears in list via Turbo (no full page reload) +- [ ] Validation errors display on empty submit +- [ ] "Force Refresh" reloads comment list via Turbo +- [ ] No console errors + +--- + +## 6. Classic Rails `/comments` (scaffold) + +- [ ] List page shows all comments +- [ ] Create: `/comments/new` - fill form, save, redirects to show page +- [ ] Edit: click Edit on any comment, change fields, save +- [ ] Delete: click Destroy, confirm, comment removed +- [ ] Validation: submit empty form shows errors + +--- + +## 7. Server Components `/server-components` (RSC) + +**Rendering:** React Server Components via Node renderer streaming. + +- [ ] Page loads without errors +- [ ] Server-only content renders (e.g., ServerInfo with Node.js `os` data) +- [ ] Client components hydrate and are interactive (e.g., TogglePanel) +- [ ] No console errors +- [ ] `/rsc_payload/ServerComponentsPage` returns RSC Flight payload (not HTML error page) + +--- + +## Cross-cutting checks + +### Console errors +- [ ] No red errors on any page +- [ ] No hydration mismatch warnings + +### SSR verification +For SSR pages (`/`, `/no-router`, `/rescript`): +- [ ] View Source shows pre-rendered HTML inside React mount points + +### Network +- [ ] No 404s for assets in Network tab +- [ ] WebSocket `/cable` connection established (for ActionCable pages) +- [ ] All JS/CSS bundles load successfully + +### Responsive +- [ ] Pages render correctly at mobile width (375px) +- [ ] Navigation collapses/adapts on small screens + +--- + +## RSpec smoke test + +```bash +bundle exec rspec spec/system/ spec/requests/ +``` + +All system and request specs should pass. + +--- + +## Quick regression check (minimal) + +If time is limited, test these 3 paths that cover all rendering modes: + +1. **`/`** - SSR + Redux + React Router (submit a comment) +2. **`/stimulus`** - Stimulus + Turbo (submit a comment) +3. **`/server-components`** - RSC (page loads without errors) diff --git a/client/app/packs/rsc-bundle.js b/client/app/packs/rsc-bundle.js index b6fcae1bb..caab73b45 100644 --- a/client/app/packs/rsc-bundle.js +++ b/client/app/packs/rsc-bundle.js @@ -3,6 +3,11 @@ // It imports the same client component registrations as server-bundle.js, // plus the server component registrations. +// Initialize ReactOnRails RSC support. With the 'react-server' resolve condition, +// this resolves to ReactOnRailsRSC.js which sets isRSCBundle = true and registers +// serverRenderRSCReactComponent. Must be imported before other registrations. +import 'react-on-rails-pro'; + // Import stores registration (has 'use client' — RSC loader replaces with client reference) import './stores-registration'; diff --git a/package.json b/package.json index a5dbe00c3..d1db8de5b 100644 --- a/package.json +++ b/package.json @@ -114,6 +114,7 @@ "devDependencies": { "@babel/eslint-parser": "^7.16.5", "@babel/preset-react": "^7.18.6", + "@rspack/dev-server": "2.0.0-beta.7", "@rspack/plugin-react-refresh": "1.6.1", "@tailwindcss/typography": "^0.5.10", "@testing-library/dom": "^10.4.1", diff --git a/yarn.lock b/yarn.lock index f6979b086..69fedc573 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2159,6 +2159,26 @@ dependencies: "@rspack/binding" "2.0.0-beta.7" +"@rspack/dev-middleware@2.0.0-beta.2": + version "2.0.0-beta.2" + resolved "https://registry.npmjs.org/@rspack/dev-middleware/-/dev-middleware-2.0.0-beta.2.tgz#a5bb784c9f85e6dff893950306df54a124fb0eeb" + integrity sha512-cbCcloAUYrb65LPd0HLhMKOWKasEM1rGXb4P9aonQcoODC9ThTdzUuZhXEwlGrinBDcMmOzEk9TYfTg9PDUevw== + +"@rspack/dev-server@2.0.0-beta.7": + version "2.0.0-beta.7" + resolved "https://registry.npmjs.org/@rspack/dev-server/-/dev-server-2.0.0-beta.7.tgz#97ebc8f35c2d1b5a77c808d652397182b06791df" + integrity sha512-/keLmG2PITbCCeUqDspj2DtDyxOXqlhKG8IiG7FKUky9jgiiRWu6F3S1t7hD72XeSnrxYEyFdeJ7uVuMQ73j0A== + dependencies: + "@rspack/dev-middleware" "2.0.0-beta.2" + "@types/ws" "^8.18.1" + chokidar "^5.0.0" + connect-history-api-fallback "^2.0.0" + connect-next "^4.0.0" + http-proxy-middleware "^3.0.5" + ipaddr.js "^2.3.0" + serve-static "^2.2.1" + ws "^8.19.0" + "@rspack/lite-tapable@^1.0.1": version "1.0.1" resolved "https://registry.npmjs.org/@rspack/lite-tapable/-/lite-tapable-1.0.1.tgz#d4540a5d28bd6177164bc0ba0bee4bdec0458591" @@ -2501,7 +2521,7 @@ resolved "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz#5b749ab2b16ba113423feb1a64a95dcd30398472" integrity sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg== -"@types/http-proxy@^1.17.8": +"@types/http-proxy@^1.17.15", "@types/http-proxy@^1.17.8": version "1.17.17" resolved "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.17.tgz#d9e2c4571fe3507343cb210cd41790375e59a533" integrity sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw== @@ -2664,7 +2684,7 @@ resolved "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz" integrity sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA== -"@types/ws@^8.5.5": +"@types/ws@^8.18.1", "@types/ws@^8.5.5": version "8.18.1" resolved "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz#48464e4bf2ddfd17db13d845467f6070ffea4aa9" integrity sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg== @@ -3679,6 +3699,13 @@ chokidar@^4.0.0: dependencies: readdirp "^4.0.1" +chokidar@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz#949c126a9238a80792be9a0265934f098af369a5" + integrity sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw== + dependencies: + readdirp "^5.0.0" + chrome-trace-event@^1.0.2: version "1.0.4" resolved "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz#05bffd7ff928465093314708c93bdfa9bd1f0f5b" @@ -3860,6 +3887,11 @@ connect-history-api-fallback@^2.0.0: resolved "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz#647264845251a0daf25b97ce87834cace0f5f1c8" integrity sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA== +connect-next@^4.0.0: + version "4.0.1" + resolved "https://registry.npmjs.org/connect-next/-/connect-next-4.0.1.tgz#4063d92c11cdc58bca371c074ff24539468ae3a4" + integrity sha512-nkHJWto78sXAooScrgvRt9E9omtwHBUTjCWRgxe3NbwNVC9cW4Lu0il0m2O867pgzJV66YXi2tXtSPtZy3Zmeg== + content-disposition@0.5.4, content-disposition@~0.5.4: version "0.5.4" resolved "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz" @@ -4197,7 +4229,7 @@ debug@2.6.9: dependencies: ms "2.0.0" -debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.4.1: +debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.6, debug@^4.4.1, debug@^4.4.3: version "4.4.3" resolved "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz" integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== @@ -4503,16 +4535,16 @@ emojis-list@^3.0.0: resolved "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz" integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== +encodeurl@^2.0.0, encodeurl@~2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz" + integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== + encodeurl@~1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz" integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== -encodeurl@~2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz" - integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== - enhanced-resolve@^0.9.1: version "0.9.1" resolved "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-0.9.1.tgz" @@ -4704,7 +4736,7 @@ escalade@^3.1.1, escalade@^3.2.0: resolved "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz" integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== -escape-html@~1.0.3: +escape-html@^1.0.3, escape-html@~1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz" integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== @@ -4997,7 +5029,7 @@ esutils@^2.0.2: resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== -etag@~1.8.1: +etag@^1.8.1, etag@~1.8.1: version "1.8.1" resolved "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz" integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== @@ -5392,6 +5424,11 @@ fresh@0.5.2, fresh@~0.5.2: resolved "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz" integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== +fresh@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz#8dd7df6a1b3a1b3a5cf186c05a5dd267622635a4" + integrity sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A== + fs-extra@^10.0.0: version "10.1.0" resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf" @@ -5767,6 +5804,17 @@ http-errors@2.0.0: statuses "2.0.1" toidentifier "1.0.1" +http-errors@^2.0.1, http-errors@~2.0.0, http-errors@~2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz#36d2f65bc909c8790018dd36fb4d93da6caae06b" + integrity sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ== + dependencies: + depd "~2.0.0" + inherits "~2.0.4" + setprototypeof "~1.2.0" + statuses "~2.0.2" + toidentifier "~1.0.1" + http-errors@~1.8.0: version "1.8.1" resolved "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz#7c3f28577cbc8a207388455dbd62295ed07bd68c" @@ -5778,17 +5826,6 @@ http-errors@~1.8.0: statuses ">= 1.5.0 < 2" toidentifier "1.0.1" -http-errors@~2.0.0, http-errors@~2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz#36d2f65bc909c8790018dd36fb4d93da6caae06b" - integrity sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ== - dependencies: - depd "~2.0.0" - inherits "~2.0.4" - setprototypeof "~1.2.0" - statuses "~2.0.2" - toidentifier "~1.0.1" - http-parser-js@>=0.5.1: version "0.5.10" resolved "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz#b3277bd6d7ed5588e20ea73bf724fcbe44609075" @@ -5813,6 +5850,18 @@ http-proxy-middleware@^2.0.3: is-plain-obj "^3.0.0" micromatch "^4.0.2" +http-proxy-middleware@^3.0.5: + version "3.0.5" + resolved "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-3.0.5.tgz#9dcde663edc44079bc5a9c63e03fe5e5d6037fab" + integrity sha512-GLZZm1X38BPY4lkXA01jhwxvDoOkkXqjgVyUzVxiEK4iuRu03PZoYHhHRwxnfhQMDuaxi3vVri0YgSro/1oWqg== + dependencies: + "@types/http-proxy" "^1.17.15" + debug "^4.3.6" + http-proxy "^1.18.1" + is-glob "^4.0.3" + is-plain-object "^5.0.0" + micromatch "^4.0.8" + http-proxy@^1.18.1: version "1.18.1" resolved "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" @@ -5967,7 +6016,7 @@ ipaddr.js@1.9.1: resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz" integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== -ipaddr.js@^2.0.1, ipaddr.js@^2.1.0: +ipaddr.js@^2.0.1, ipaddr.js@^2.1.0, ipaddr.js@^2.3.0: version "2.3.0" resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz#71dce70e1398122208996d1c22f2ba46a24b1abc" integrity sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg== @@ -7360,7 +7409,7 @@ mime-db@1.52.0: resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== -"mime-db@>= 1.43.0 < 2": +"mime-db@>= 1.43.0 < 2", mime-db@^1.54.0: version "1.54.0" resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz#cddb3ee4f9c64530dff640236661d42cb6a314f5" integrity sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ== @@ -7372,6 +7421,13 @@ mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.24, mime-types@~2.1.34, dependencies: mime-db "1.52.0" +mime-types@^3.0.2: + version "3.0.2" + resolved "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz#39002d4182575d5af036ffa118100f2524b2e2ab" + integrity sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A== + dependencies: + mime-db "^1.54.0" + mime@1.6.0: version "1.6.0" resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz" @@ -7641,7 +7697,7 @@ on-exit-leak-free@^2.1.0: resolved "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz#fed195c9ebddb7d9e4c3842f93f281ac8dadd3b8" integrity sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA== -on-finished@2.4.1, on-finished@~2.4.1: +on-finished@2.4.1, on-finished@^2.4.1, on-finished@~2.4.1: version "2.4.1" resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz" integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== @@ -7795,7 +7851,7 @@ parse5@^7.0.0, parse5@^7.2.1: dependencies: entities "^6.0.0" -parseurl@~1.3.3: +parseurl@^1.3.3, parseurl@~1.3.3: version "1.3.3" resolved "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== @@ -8905,6 +8961,11 @@ readdirp@^4.0.1: resolved "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz" integrity sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg== +readdirp@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz#fbf1f71a727891d685bb1786f9ba74084f6e2f91" + integrity sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ== + readdirp@~3.6.0: version "3.6.0" resolved "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz" @@ -9391,6 +9452,23 @@ send@0.19.0: range-parser "~1.2.1" statuses "2.0.1" +send@^1.2.0: + version "1.2.1" + resolved "https://registry.npmjs.org/send/-/send-1.2.1.tgz#9eab743b874f3550f40a26867bf286ad60d3f3ed" + integrity sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ== + dependencies: + debug "^4.4.3" + encodeurl "^2.0.0" + escape-html "^1.0.3" + etag "^1.8.1" + fresh "^2.0.0" + http-errors "^2.0.1" + mime-types "^3.0.2" + ms "^2.1.3" + on-finished "^2.4.1" + range-parser "^1.2.1" + statuses "^2.0.2" + send@~0.19.0, send@~0.19.1: version "0.19.2" resolved "https://registry.npmjs.org/send/-/send-0.19.2.tgz#59bc0da1b4ea7ad42736fd642b1c4294e114ff29" @@ -9440,6 +9518,16 @@ serve-static@1.16.2: parseurl "~1.3.3" send "0.19.0" +serve-static@^2.2.1: + version "2.2.1" + resolved "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz#7f186a4a4e5f5b663ad7a4294ff1bf37cf0e98a9" + integrity sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw== + dependencies: + encodeurl "^2.0.0" + escape-html "^1.0.3" + parseurl "^1.3.3" + send "^1.2.0" + serve-static@~1.16.2: version "1.16.3" resolved "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz#a97b74d955778583f3862a4f0b841eb4d5d78cf9" @@ -9713,7 +9801,7 @@ statuses@2.0.1: resolved "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== -statuses@~2.0.1, statuses@~2.0.2: +statuses@^2.0.2, statuses@~2.0.1, statuses@~2.0.2: version "2.0.2" resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz#8f75eecef765b5e1cfcdc080da59409ed424e382" integrity sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw== @@ -10773,6 +10861,11 @@ ws@^8.18.0: resolved "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz" integrity sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg== +ws@^8.19.0: + version "8.20.0" + resolved "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz#4cd9532358eba60bc863aad1623dfb045a4d4af8" + integrity sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA== + xml-name-validator@^5.0.0: version "5.0.0" resolved "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz#82be9b957f7afdacf961e5980f1bf227c0bf7673" From 6ecfa97ec64b123fefa5434f5201a93c39f27a04 Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Wed, 8 Apr 2026 22:30:59 -1000 Subject: [PATCH 16/16] Fix RSC manifest and renderer review items --- config/webpack/rscWebpackConfig.js | 4 +- config/webpack/rspackRscPlugin.js | 86 ++++++++++++++++++--------- config/webpack/serverWebpackConfig.js | 6 +- react-on-rails-pro-node-renderer.js | 23 ++++++- 4 files changed, 84 insertions(+), 35 deletions(-) diff --git a/config/webpack/rscWebpackConfig.js b/config/webpack/rscWebpackConfig.js index a838a01e5..b4b83e1aa 100644 --- a/config/webpack/rscWebpackConfig.js +++ b/config/webpack/rscWebpackConfig.js @@ -4,8 +4,8 @@ // The RSC bundle runs server components in the Node renderer and produces // the Flight payload that React uses to hydrate on the client. // -// Unlike the server bundle (which uses ExecJS), the RSC bundle targets Node.js -// and can use Node.js built-in modules like os, fs, path, etc. +// Unlike the server bundle (which runs in the Node renderer VM sandbox), the +// RSC bundle targets Node.js and can use built-in modules like os, fs, path, etc. const path = require('path'); const { config } = require('shakapacker'); diff --git a/config/webpack/rspackRscPlugin.js b/config/webpack/rspackRscPlugin.js index b78992492..769bb53d6 100644 --- a/config/webpack/rspackRscPlugin.js +++ b/config/webpack/rspackRscPlugin.js @@ -7,6 +7,8 @@ // that the React flight protocol needs to resolve client component references. const fs = require('fs'); +const path = require('path'); +const { pathToFileURL } = require('url'); const { sources } = require('@rspack/core'); class RspackRscPlugin { @@ -18,7 +20,6 @@ class RspackRscPlugin { this.clientManifestFilename = options.isServer ? 'react-server-client-manifest.json' : 'react-client-manifest.json'; - this.ssrManifestFilename = 'react-ssr-manifest.json'; this.useClientCache = new Map(); } @@ -35,7 +36,15 @@ class RspackRscPlugin { stage: compilation.constructor.PROCESS_ASSETS_STAGE_REPORT || 5000, }, () => { - const manifest = {}; + const filePathToModuleMetadata = {}; + const configuredCrossOriginLoading = this._getCrossOriginLoading(compilation); + const manifest = { + moduleLoading: { + prefix: compilation.outputOptions.publicPath || '', + crossOrigin: configuredCrossOriginLoading, + }, + filePathToModuleMetadata, + }; for (const chunk of compilation.chunks) { const chunkFiles = []; @@ -51,11 +60,11 @@ class RspackRscPlugin { : []; for (const mod of modules) { - this._processModule(mod, chunk, chunkFiles, manifest, compilation); + this._processModule(mod, chunk, chunkFiles, filePathToModuleMetadata, compilation); // Handle concatenated modules if (mod.modules) { for (const innerMod of mod.modules) { - this._processModule(innerMod, chunk, chunkFiles, manifest, compilation); + this._processModule(innerMod, chunk, chunkFiles, filePathToModuleMetadata, compilation); } } } @@ -65,19 +74,21 @@ class RspackRscPlugin { this.clientManifestFilename, new sources.RawSource(JSON.stringify(manifest, null, 2)), ); - - // Emit SSR manifest (maps module IDs to SSR module data) - if (!this.isServer) { - compilation.emitAsset( - this.ssrManifestFilename, - new sources.RawSource(JSON.stringify({}, null, 2)), - ); - } }, ); }); } + _getCrossOriginLoading(compilation) { + const configuredCrossOriginLoading = compilation.outputOptions.crossOriginLoading; + + if (typeof configuredCrossOriginLoading !== 'string') { + return null; + } + + return configuredCrossOriginLoading === 'use-credentials' ? configuredCrossOriginLoading : 'anonymous'; + } + _hasUseClientDirective(filePath) { if (this.useClientCache.has(filePath)) return this.useClientCache.get(filePath); @@ -112,11 +123,12 @@ class RspackRscPlugin { return result; } - _processModule(mod, chunk, chunkFiles, manifest, compilation) { + _processModule(mod, chunk, chunkFiles, filePathToModuleMetadata, compilation) { const resource = mod.resource || mod.userRequest; if (!resource || !resource.match(/\.(js|jsx|ts|tsx)$/)) return; // Skip node_modules if (resource.includes('node_modules')) return; + if (!this._shouldIncludeInManifest(resource)) return; // Check original file for 'use client' directive if (!this._hasUseClientDirective(resource)) return; @@ -130,27 +142,43 @@ class RspackRscPlugin { chunks.push(chunk.id, file); } - // Build the module entry with all exported names - const ssrEntry = { + const key = pathToFileURL(resource).href; + const existingEntry = filePathToModuleMetadata[key]; + + if (existingEntry) { + const knownChunkIds = new Set(); + for (let index = 0; index < existingEntry.chunks.length; index += 2) { + knownChunkIds.add(existingEntry.chunks[index]); + } + + for (let index = 0; index < chunks.length; index += 2) { + if (!knownChunkIds.has(chunks[index])) { + existingEntry.chunks.push(chunks[index], chunks[index + 1]); + knownChunkIds.add(chunks[index]); + } + } + return; + } + + filePathToModuleMetadata[key] = { id: moduleId, chunks: chunks, name: '*', - async: false, }; + } - // Use resource path as the key (React flight protocol convention) - const key = resource; - if (!manifest[key]) { - manifest[key] = {}; - } - manifest[key]['*'] = ssrEntry; - manifest[key][''] = ssrEntry; - - // Also add default export entry - manifest[key]['default'] = { - ...ssrEntry, - name: 'default', - }; + _shouldIncludeInManifest(resource) { + const normalizedResource = path.normalize(resource); + const serverComponentsRoot = path.normalize('client/app/bundles/server-components/'); + const storesRegistrationPath = path.normalize('client/app/packs/stores-registration.js'); + + // The RSC runtime only needs client references that can appear inside the + // server-components demo tree. Pulling in unrelated app bundles causes the + // client and server manifests to diverge and breaks buildClientRenderer(). + return ( + normalizedResource.includes(serverComponentsRoot) || + normalizedResource.endsWith(storesRegistrationPath) + ); } } diff --git a/config/webpack/serverWebpackConfig.js b/config/webpack/serverWebpackConfig.js index dd20c06d8..3f0b2becc 100644 --- a/config/webpack/serverWebpackConfig.js +++ b/config/webpack/serverWebpackConfig.js @@ -176,16 +176,18 @@ const configureServer = () => { // The Node renderer runs bundles in a VM sandbox that lacks browser globals // like MessageChannel and TextEncoder. Inject polyfills at the top of the - // bundle so react-dom/server.browser can initialize. + // bundle so react-dom/server.browser can initialize with the async task + // boundary its scheduler expects. serverWebpackConfig.plugins.push( new bundler.BannerPlugin({ banner: [ 'if(typeof MessageChannel==="undefined"){', + ' var enqueueTask=typeof setImmediate==="function"?setImmediate:function(cb){setTimeout(cb,0);};', ' 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});', + ' var p=this._port1;enqueueTask(function(){if(p.onmessage)p.onmessage({data:msg});});', ' }};', ' this.port2._port1=this.port1;', ' }', diff --git a/react-on-rails-pro-node-renderer.js b/react-on-rails-pro-node-renderer.js index d80045899..0eed6c149 100644 --- a/react-on-rails-pro-node-renderer.js +++ b/react-on-rails-pro-node-renderer.js @@ -8,13 +8,32 @@ if (!rendererPassword) { throw new Error('RENDERER_PASSWORD must be set in production'); } +function parseIntegerEnv(name, defaultValue, { min, max = Number.MAX_SAFE_INTEGER }) { + const rawValue = process.env[name]; + if (rawValue == null || rawValue.trim() === '') { + return defaultValue; + } + + const normalized = rawValue.trim(); + if (!/^\d+$/.test(normalized)) { + throw new Error(`Invalid ${name}: "${rawValue}". Expected an integer.`); + } + + const parsed = Number.parseInt(normalized, 10); + if (parsed < min || parsed > max) { + throw new Error(`Invalid ${name}: "${rawValue}". Expected a value between ${min} and ${max}.`); + } + + return parsed; +} + const config = { serverBundleCachePath: path.resolve(__dirname, '.node-renderer-bundles'), logLevel: process.env.RENDERER_LOG_LEVEL || 'debug', password: rendererPassword, - port: process.env.RENDERER_PORT || 3800, + port: parseIntegerEnv('RENDERER_PORT', 3800, { min: 1, max: 65535 }), supportModules: true, - workersCount: Number(process.env.NODE_RENDERER_CONCURRENCY || 3), + workersCount: parseIntegerEnv('NODE_RENDERER_CONCURRENCY', 3, { min: 0 }), }; if (process.env.CI) {