From 5551af9f198c8f28ac0116755dbd6331306092d5 Mon Sep 17 00:00:00 2001 From: Ihab Adham <71561048+ihabadham@users.noreply.github.com> Date: Mon, 20 Apr 2026 22:17:31 +0200 Subject: [PATCH 1/7] Pro RSC migration 1/3: swap gem and npm to react_on_rails_pro (#726) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First of three stacked sub-PRs enabling React Server Components in this tutorial, since `enable_rsc_support` is a Pro-only configuration. Pure dependency swap — ExecJS server rendering and existing behavior unchanged. - Gemfile: `react_on_rails 16.6.0.rc.0` → `react_on_rails_pro 16.6.0` - package.json: `react-on-rails 16.6.0-rc.0` → `react-on-rails-pro 16.6.0` - shakapacker: `10.0.0.rc.0` → `10.0.0` stable (lockstep with the Pro gem) - First-party imports: `'react-on-rails'` → `'react-on-rails-pro'` across 10 files (matches `react_on_rails :pro` generator's `update_imports_to_pro_package`) - `config/webpack/commonWebpackConfig.js`: alias `'react-on-rails$'` → `'react-on-rails-pro'` as a third-party shim. Without it, `rescript-react-on-rails`'s `import ReactOnRails from "react-on-rails"` resolves to core, core and Pro coexist in the bundle, and SSR fails with the ExecJS error `Cannot mix react-on-rails (core) with react-on-rails-pro` (verified by repro). Co-Authored-By: Claude Opus 4.7 (1M context) --- Gemfile | 4 +- Gemfile.lock | 40 +++++++++++++++++-- .../ror_components/SimpleCommentScreen.jsx | 2 +- .../startup/App/ror_components/App.jsx | 2 +- .../startup/ClientRouterAppExpress.jsx | 2 +- .../ror_components/NavigationBarApp.jsx | 2 +- .../ror_components/RouterApp.client.jsx | 2 +- .../ror_components/RouterApp.server.jsx | 2 +- .../comments/startup/serverRegistration.jsx | 2 +- client/app/libs/requestsManager.js | 2 +- client/app/packs/stimulus-bundle.js | 2 +- client/app/packs/stores-registration.js | 2 +- config/webpack/commonWebpackConfig.js | 10 +++++ package.json | 4 +- yarn.lock | 31 ++++++++++---- 15 files changed, 83 insertions(+), 26 deletions(-) diff --git a/Gemfile b/Gemfile index e9eb85053..ded32b042 100644 --- a/Gemfile +++ b/Gemfile @@ -5,8 +5,8 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" } ruby "3.4.6" -gem "react_on_rails", "16.6.0.rc.0" -gem "shakapacker", "10.0.0.rc.0" +gem "react_on_rails_pro", "16.6.0" +gem "shakapacker", "10.0.0" # Bundle edge Rails instead: gem "rails", github: "rails/rails" gem "rails", "~> 8.1.2" diff --git a/Gemfile.lock b/Gemfile.lock index b290c3986..da8af3800 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.39.0) + 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.6) + http-2 (>= 1.1.3) i18n (1.14.8) concurrent-ruby (~> 1.0) interception (0.5) io-console (0.8.2) + io-event (1.15.1) 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.6.0) addressable connection_pool execjs (~> 2.5) rails (>= 5.2) rainbow (~> 3.0) shakapacker (>= 6.0) + react_on_rails_pro (16.6.0) + 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.6.0) redcarpet (3.6.0) redis (5.3.0) redis-client (>= 0.22.0) @@ -387,7 +418,7 @@ GEM websocket (~> 1.0) semantic_range (3.1.1) sexp_processor (4.17.1) - shakapacker (10.0.0.rc.0) + shakapacker (10.0.0) activesupport (>= 5.2) package_json rack-proxy (>= 0.6.1) @@ -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.6.0) redcarpet redis (~> 5.0) rspec-rails (~> 6.0.0) @@ -498,7 +530,7 @@ DEPENDENCIES scss_lint sdoc selenium-webdriver (~> 4) - shakapacker (= 10.0.0.rc.0) + shakapacker (= 10.0.0) spring spring-commands-rspec stimulus-rails (~> 1.3) 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 dd382d567..097cd7508 100644 --- a/client/app/bundles/comments/components/SimpleCommentScreen/ror_components/SimpleCommentScreen.jsx +++ b/client/app/bundles/comments/components/SimpleCommentScreen/ror_components/SimpleCommentScreen.jsx @@ -3,7 +3,7 @@ import React from 'react'; import request from 'axios'; import Immutable from 'immutable'; import _ from 'lodash'; -import ReactOnRails from 'react-on-rails'; +import ReactOnRails from 'react-on-rails-pro'; import { IntlProvider, injectIntl } from 'react-intl'; import BaseComponent from 'libs/components/BaseComponent'; import SelectLanguage from 'libs/i18n/selectLanguage'; diff --git a/client/app/bundles/comments/startup/App/ror_components/App.jsx b/client/app/bundles/comments/startup/App/ror_components/App.jsx index fed57772a..63eb65329 100644 --- a/client/app/bundles/comments/startup/App/ror_components/App.jsx +++ b/client/app/bundles/comments/startup/App/ror_components/App.jsx @@ -1,6 +1,6 @@ import { Provider } from 'react-redux'; import React from 'react'; -import ReactOnRails from 'react-on-rails'; +import ReactOnRails from 'react-on-rails-pro'; import NonRouterCommentsContainer from '../../../containers/NonRouterCommentsContainer.jsx'; import 'intl/locale-data/jsonp/en'; diff --git a/client/app/bundles/comments/startup/ClientRouterAppExpress.jsx b/client/app/bundles/comments/startup/ClientRouterAppExpress.jsx index 6d2c7342a..a0ab70cc8 100644 --- a/client/app/bundles/comments/startup/ClientRouterAppExpress.jsx +++ b/client/app/bundles/comments/startup/ClientRouterAppExpress.jsx @@ -1,7 +1,7 @@ // Compare to ../ServerRouterApp.jsx import { Provider } from 'react-redux'; import React from 'react'; -import ReactOnRails from 'react-on-rails'; +import ReactOnRails from 'react-on-rails-pro'; import { Router, browserHistory } from 'react-router'; import { syncHistoryWithStore } from 'react-router-redux'; diff --git a/client/app/bundles/comments/startup/NavigationBarApp/ror_components/NavigationBarApp.jsx b/client/app/bundles/comments/startup/NavigationBarApp/ror_components/NavigationBarApp.jsx index 2afd10acc..911861bed 100644 --- a/client/app/bundles/comments/startup/NavigationBarApp/ror_components/NavigationBarApp.jsx +++ b/client/app/bundles/comments/startup/NavigationBarApp/ror_components/NavigationBarApp.jsx @@ -3,7 +3,7 @@ import { Provider } from 'react-redux'; import React from 'react'; -import ReactOnRails from 'react-on-rails'; +import ReactOnRails from 'react-on-rails-pro'; import NavigationBar from '../../../components/NavigationBar/NavigationBar.jsx'; import NavigationBarContainer from '../../../containers/NavigationBarContainer.jsx'; diff --git a/client/app/bundles/comments/startup/RouterApp/ror_components/RouterApp.client.jsx b/client/app/bundles/comments/startup/RouterApp/ror_components/RouterApp.client.jsx index 057984b15..caa2e06d3 100644 --- a/client/app/bundles/comments/startup/RouterApp/ror_components/RouterApp.client.jsx +++ b/client/app/bundles/comments/startup/RouterApp/ror_components/RouterApp.client.jsx @@ -1,7 +1,7 @@ // Compare to ./RouterApp.server.jsx import { Provider } from 'react-redux'; import React from 'react'; -import ReactOnRails from 'react-on-rails'; +import ReactOnRails from 'react-on-rails-pro'; import { BrowserRouter } from 'react-router-dom'; import routes from '../../../routes/routes.jsx'; 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 4e6fc8b92..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 @@ -2,7 +2,7 @@ import { Provider } from 'react-redux'; import React from 'react'; import { StaticRouter } from 'react-router-dom/server'; -import ReactOnRails from 'react-on-rails'; +import ReactOnRails from 'react-on-rails-pro'; import routes from '../../../routes/routes.jsx'; function ServerRouterApp(_props, railsContext) { diff --git a/client/app/bundles/comments/startup/serverRegistration.jsx b/client/app/bundles/comments/startup/serverRegistration.jsx index 11e0fd40e..c7db967ab 100644 --- a/client/app/bundles/comments/startup/serverRegistration.jsx +++ b/client/app/bundles/comments/startup/serverRegistration.jsx @@ -1,5 +1,5 @@ // Example of React + Redux -import ReactOnRails from 'react-on-rails'; +import ReactOnRails from 'react-on-rails-pro'; import App from './App/ror_components/App'; import RouterApp from './RouterApp/ror_components/RouterApp.server'; 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/stimulus-bundle.js b/client/app/packs/stimulus-bundle.js index a11ccf149..2664fee2b 100644 --- a/client/app/packs/stimulus-bundle.js +++ b/client/app/packs/stimulus-bundle.js @@ -1,4 +1,4 @@ -import ReactOnRails from 'react-on-rails'; +import ReactOnRails from 'react-on-rails-pro'; import 'jquery-ujs'; import { Turbo } from '@hotwired/turbo-rails'; diff --git a/client/app/packs/stores-registration.js b/client/app/packs/stores-registration.js index 435653379..d03732dc3 100644 --- a/client/app/packs/stores-registration.js +++ b/client/app/packs/stores-registration.js @@ -1,4 +1,4 @@ -import ReactOnRails from 'react-on-rails'; +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/webpack/commonWebpackConfig.js b/config/webpack/commonWebpackConfig.js index 1a99ddbc5..5c14f01fd 100644 --- a/config/webpack/commonWebpackConfig.js +++ b/config/webpack/commonWebpackConfig.js @@ -8,6 +8,16 @@ const commonOptions = { resolve: { // Add .res.js extension for ReScript-compiled modules (modern ReScript convention) extensions: ['.css', '.ts', '.tsx', '.res.js'], + // Shim for third-party packages (notably rescript-react-on-rails) that import + // 'react-on-rails' directly and can't be source-rewritten to react-on-rails-pro. + // Without this, Pro and core coexist in the bundle and trigger the runtime error + // "Cannot mix react-on-rails (core) with react-on-rails-pro". + // First-party code imports 'react-on-rails-pro' directly, so this alias is only + // needed for the third-party exact-specifier case — hence the `$` suffix (no + // sub-path rewrites, since Pro's package exports differ from core's). + alias: { + 'react-on-rails$': 'react-on-rails-pro', + }, }, }; diff --git a/package.json b/package.json index d52d2980c..989e4e166 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "react": "^19.0.0", "react-dom": "^19.0.0", "react-intl": "^6.4.4", - "react-on-rails": "16.6.0-rc.0", + "react-on-rails-pro": "16.6.0", "react-redux": "^8.1.0", "react-router": "^6.13.0", "react-router-dom": "^6.13.0", @@ -95,7 +95,7 @@ "sass": "^1.58.3", "sass-loader": "^13.3.2", "sass-resources-loader": "^2.2.5", - "shakapacker": "10.0.0-rc.0", + "shakapacker": "10.0.0", "stimulus": "^3.0.1", "style-loader": "^3.3.1", "swc-loader": "^0.2.6", diff --git a/yarn.lock b/yarn.lock index 14b990d67..ceb7d3f4a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7455,6 +7455,13 @@ p-try@^2.0.0: resolved "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== +pack-config-diff@^0.1.0: + version "0.1.0" + resolved "https://registry.npmjs.org/pack-config-diff/-/pack-config-diff-0.1.0.tgz#c15025e196ff85009806890cca0841e96aaa6a1b" + integrity sha512-gx60G4pnT4GFQ7WECdf7SCq9vdsvBhodLXXzwisy37/t0zkAZ3hMgLCdOGivudF1l0gcLZhIWVYreS89be15QA== + dependencies: + js-yaml "^4.1.0" + package-json-from-dist@^1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz" @@ -8403,10 +8410,17 @@ 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.6.0: + version "16.6.0" + resolved "https://registry.npmjs.org/react-on-rails-pro/-/react-on-rails-pro-16.6.0.tgz#19a5ea99d7b397dd56f14cff1f31955211b4d0a2" + integrity sha512-Uc8o3gdHyIETvY5J9wVUyONKOhnkw9kGJDREMHQb/IuXoB5/Vo51UK487Rcep2Z+Dzz/bEvNoF+GuZohORZ7Zw== + dependencies: + react-on-rails "16.6.0" + +react-on-rails@16.6.0: + version "16.6.0" + resolved "https://registry.npmjs.org/react-on-rails/-/react-on-rails-16.6.0.tgz#da7f117fec14f420f7f6ffe6bdb34b7fc2e01b3a" + integrity sha512-LqLi7A0n0Tv5c3yMYlwS9s6rE82gvXNMj3sscmK2LOgIJ+mLQlOX65n0Jq5ZJ4Nsl9SRgxEOOQmcrfvDeB9F1g== react-proxy@^1.1.7: version "1.1.8" @@ -9059,12 +9073,13 @@ setprototypeof@1.2.0, setprototypeof@~1.2.0: resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz" integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== -shakapacker@10.0.0-rc.0: - version "10.0.0-rc.0" - resolved "https://registry.npmjs.org/shakapacker/-/shakapacker-10.0.0-rc.0.tgz#472258e85c7aef3f705d36b6e06f303e680df8fa" - integrity sha512-Sv58iSE+z1N73o0ksAHPE2BrMjynTySttsc9EmiAdsqeW5U5KYC/N0CdCEXQxQoT5ybAYA2t0j+NJHdWIM/PCg== +shakapacker@10.0.0: + version "10.0.0" + resolved "https://registry.npmjs.org/shakapacker/-/shakapacker-10.0.0.tgz#870f823531c7362836975ab9bb16732234a580a0" + integrity sha512-bLKU/xGxIpC/oXvTBRLnpH6bjVK/wyYeWiwE35zd3xxbcg6WfbbQxls0L/AmO8p560RR2PgfR3QtHb7klBmu0A== dependencies: js-yaml "^4.1.0" + pack-config-diff "^0.1.0" path-complete-extname "^1.0.0" webpack-merge "^5.8.0" yargs "^17.7.2" From 767c51371dd857c89e28048ffbafb7a24c07301f Mon Sep 17 00:00:00 2001 From: Ihab Adham <71561048+ihabadham@users.noreply.github.com> Date: Wed, 22 Apr 2026 09:00:10 +0200 Subject: [PATCH 2/7] Pro RSC migration 2/3: switch SSR to the Pro Node renderer on webpack (#728) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Switch SSR to the Pro Node renderer on webpack Second of three stacked sub-PRs in the Pro RSC migration. Routes all server rendering through the Pro Node renderer (port 3800) instead of ExecJS, and flips the asset bundler from rspack to webpack — scoped reversal of #702, needed because rspack 2.0.0-beta.7's webpack compatibility layer doesn't cover the APIs upstream RSCWebpackPlugin requires. We flip back to rspack once shakacode/react_on_rails_rsc#29 ships a native rspack RSC plugin. The bundler flip and NodeRenderer wiring ship atomically: the server bundle produced by the Pro webpack transforms (target: 'node' + libraryTarget: 'commonjs2') is not evaluable by ExecJS, so the initializer pointing server_renderer at the NodeRenderer must land at the same time. Key changes: - config/shakapacker.yml: assets_bundler: rspack → webpack - config/webpack/bundlerUtils.js: return @rspack/core or webpack based on the shakapacker setting (was rspack-only and threw otherwise); spec updated in parallel - config/webpack/serverWebpackConfig.js: Pro transforms per the :pro generator's update_webpack_config_for_pro and the marketplace/dummy references — target: 'node' + node: false, libraryTarget: 'commonjs2', extractLoader helper, babelLoader.options.caller = { ssr: true }, destructured module.exports so Sub-PR 3's rscWebpackConfig.js can derive from serverWebpackConfig(true). RSCWebpackPlugin({ isServer: true }) when !rscBundle emits the server manifest; inert until Sub-PR 3 activates RSC support - config/initializers/react_on_rails_pro.rb: NodeRenderer-only config (no RSC fields — those move in Sub-PR 3) - renderer/node-renderer.js: launcher with strict integer env parsing, CI worker cap, and additionalContext: { URL, AbortController } so react-router-dom's NavLink.encodeLocation does not throw "ReferenceError: URL is not defined" at SSR time - Procfile.dev: renderer: NODE_ENV=development node renderer/node-renderer.js - package.json: react-on-rails-pro-node-renderer 16.6.0 and react-on-rails-rsc ^19.0.4 (Pro peer dep; required for the RSCWebpackPlugin import) - .gitignore: /renderer/.node-renderer-bundles/ - .env.example: REACT_ON_RAILS_PRO_LICENSE, RENDERER_PASSWORD, and REACT_RENDERER_URL with dev vs prod guidance - .github/workflows/rspec_test.yml: start the Node renderer before rspec with PID liveness and port-ready checks plus log capture on failure Verified locally: webpack build compiles cleanly. `bin/rails s` on 3000 with `node renderer/node-renderer.js` on 3800 renders GET / at HTTP 200; Rails log shows "Node Renderer responded" and the renderer log emits "[SERVER] RENDERED Footer to dom node with id: ..." — confirming SSR went through the Pro path rather than falling back to ExecJS. Co-Authored-By: Claude Opus 4.7 (1M context) * Tighten Pro webpack + initializer setup from reference audit Four fixes from auditing Sub-PR 2 against the Pro dummy, the :pro generator, and the Pro configuration docs. - config/initializers/react_on_rails_pro.rb: renderer_password now raises in non-local envs instead of falling back to the dev string. Previously any production deploy that forgot to set the env var would silently run with a known-public password; now it fails loudly. Matches the safer pattern from PR #723's final state. - config/webpack/serverWebpackConfig.js: pass clientReferences to RSCWebpackPlugin({ isServer: true }), matching the Pro dummy's serverWebpackConfig at `react_on_rails_pro/spec/dummy/config/webpack/ serverWebpackConfig.js`. Without it, the plugin may walk into node_modules and hit unlodaed .tsx source files and re-scan modules we don't need. Locks client-ref discovery to client/app/**. - config/webpack/serverWebpackConfig.js: drop publicPath from the server-bundle output. Server bundles are loaded by the Node renderer via the filesystem, never served over HTTP — the URL is unused. Matches the Pro dummy's comment. - package.json: pin react-on-rails-rsc to 19.0.4 stable (was ^19.0.4 range) and add "node-renderer" npm script as a convenience shortcut for `node renderer/node-renderer.js`. - .github/workflows/rspec_test.yml: set RENDERER_PASSWORD explicitly in CI to the shared dev default so both the initializer and the launcher use the same concrete value, avoiding silent drift if either side's default is ever touched. Re-verified: webpack build clean; renderer + Rails boot; GET / returns HTTP 200 with "Node Renderer responded" in the Rails log and "[SERVER] RENDERED" in the renderer log, confirming SSR still goes through the Pro path. Co-Authored-By: Claude Opus 4.7 (1M context) * Align renderer_password initializer with Pro docs + source Previous commit added Rails.env-branching + explicit raise in non-local envs. That duplicates what Pro's configuration.rb already does at boot (validate_renderer_password_for_production) and diverges from the documented pattern in pro/node-renderer.md and pro/installation.md. Revert to the simple ENV.fetch with a dev default. Pro handles the prod-enforcement itself — if RENDERER_PASSWORD is unset in a production-like env, Pro raises at boot with a helpful error message pointing at exactly this fix. Co-Authored-By: Claude Opus 4.7 (1M context) * Pin react and react-dom to 19.0.4 Journey plan's Sub-PR 1 KEEP table explicitly called for the 19.0.4 pin from PR #723's final state — missed in the Sub-PR 1 migration commit. 19.0.4 is the minimum React version documented as required for React Server Components (per the Pro RSC tutorial doc, CVE-safe floor). Tightens from the inherited "^19.0.0" range. Boot-verified: webpack build clean; renderer + Rails boot; GET / returns HTTP 200 with "Node Renderer responded" in Rails log and "[SERVER] RENDERED" in renderer log — SSR path unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) * Rename Procfile.dev renderer process to node-renderer Match the Pro dummy's Procfile.dev and PR #723's naming. Marketplace uses `renderer:`, but the dummy is the canonical Pro reference and aligns with how the :pro generator's `add_pro_to_procfile` names it. Co-Authored-By: Claude Opus 4.7 (1M context) * Default renderer logLevel to 'info' Pro docs (docs/oss/building-features/node-renderer/container-deployment.md) prescribe `logLevel: 'info'` as the general-renderer-logs default, and the Pro dummy's renderer/node-renderer.js also uses 'info'. The inherited 'debug' default is too verbose for normal operation (emits the full VM context setup messages like "Adding Buffer, TextDecoder, ..."). Debug remains reachable via RENDERER_LOG_LEVEL=debug env var as docs describe for active debugging (docs/oss/building-features/node-renderer/ debugging.md). Co-Authored-By: Claude Opus 4.7 (1M context) * Trim over-verbose comments across Sub-PR 2 files Per the repo's CLAUDE.md guidance ("default to writing no comments; only when the WHY is non-obvious"), stripping comments that explain WHAT the code does or reference the current-PR context. Kept (non-obvious WHYs): the `additionalContext: { URL, AbortController }` reason (react-router-dom NavLink url-is-not-defined), the CI worker-count cap reason, `clientReferences` scoping rationale, `libraryTarget: 'commonjs2'` requirement for the renderer, `target: 'node'` fix for SSR-breaking libs, the renderer-fallback-disabled reason, the renderer-password-must-match pointer, the bundler-utils dual-support blurb, and the shakapacker.yml tactical-reversal tag. Dropped (WHAT explanations, redundant with identifiers): JSDoc blocks describing `configureServer` and `extractLoader`, per-config-key descriptions in the renderer launcher, Procfile.dev process-purpose comment, verbose `.env.example` prose, initializer multi-line explanations of each field. No code changes — comments only. Co-Authored-By: Claude Opus 4.7 (1M context) * Restore license setup guidance in initializer comments Over-trimmed in the previous cleanup commit. The longer wording has non-obvious info (where to get the JWT, which envs skip license, what Pro does when no license is set) that readers genuinely benefit from — beyond what the generator template's one-line version captures. Co-Authored-By: Claude Opus 4.7 (1M context) * Wire the Pro Node renderer for Control Plane deploys Sub-PR 2 already wires dev (Procfile.dev) and CI (rspec_test.yml). Without this commit, merging base → master with the renderer active on the Rails side but no renderer process in the deployed container would break the live tutorial (Rails tries to hit localhost:3800, nothing listens, HTTPX::ConnectionError → 500 with renderer_use_fallback_exec_js = false). Matches the react-server-components-marketplace-demo deploy pattern (Option 1 "Single Container" from docs/oss/building-features/ node-renderer/container-deployment.md): - .controlplane/Dockerfile: CMD starts the Node renderer and Rails in the same container with `wait -n ; exit 1` so any child exit restarts the whole workload. Preserves the existing Thruster HTTP/2 proxy for Rails — the only deviation from marketplace's literal CMD. - .controlplane/templates/app.yml: add RENDERER_PORT, RENDERER_LOG_LEVEL, RENDERER_WORKERS_COUNT, RENDERER_URL as plain env, plus RENDERER_PASSWORD and REACT_ON_RAILS_PRO_LICENSE as Control Plane secret references keyed by {{APP_NAME}}-secrets (matches the repo's existing {{APP_NAME}} templating convention in DATABASE_URL / REDIS_URL). - .controlplane/templates/rails.yml: bump memory 512Mi → 2Gi. The container now runs two processes; 512Mi OOMs fast. 2Gi matches the marketplace demo's rails.yml. - config/initializers/react_on_rails_pro.rb: read ENV["RENDERER_URL"] instead of ENV["REACT_RENDERER_URL"]. Aligns with docs/oss/ configuration/configuration-pro.md (which uses RENDERER_URL), the Pro dummy's initializer, and the marketplace initializer — all of which use RENDERER_URL without a REACT_ prefix. The REACT_ prefix was inherited from PR #723 and is non-canonical. - .env.example: matching REACT_RENDERER_URL → RENDERER_URL rename. Prerequisite before first deploy (outside PR scope, one-time per app per org, performed manually via cpln or the Control Plane UI): create a Secret named `-secrets` with keys RENDERER_PASSWORD (strong random) and REACT_ON_RAILS_PRO_LICENSE (JWT from pro.reactonrails.com). Affects react-webpack-rails-tutorial-production, react-webpack-rails-tutorial-staging, and any qa-* review apps. Co-Authored-By: Claude Opus 4.7 (1M context) * Use {{APP_SECRETS}} for renderer + license secret references {{APP_NAME}}-secrets expanded to a per-app secret name, which would require a new Control Plane Secret for every review app PR — wrong per cpflow's own conventions. cpflow exposes {{APP_SECRETS}} (lib/core/template_parser.rb:49, lib/core/config.rb:51-52) which expands to `{APP_PREFIX}-secrets`. Per our controlplane.yml, APP_PREFIX is: - `react-webpack-rails-tutorial-production` for the prod app - `react-webpack-rails-tutorial-staging` for the staging app - `qa-react-webpack-rails-tutorial` for all qa-* review apps (because match_if_app_name_starts_with: true) So review apps all share `qa-react-webpack-rails-tutorial-secrets` instead of each PR needing its own. Three secrets total across two orgs instead of one per PR. Matches the `{APP_PREFIX}-secrets` default documented at shakacode/control-plane-flow/docs/secrets-and-env-values.md. Co-Authored-By: Claude Opus 4.7 (1M context) * Fix env var name mismatch: NODE_RENDERER_CONCURRENCY → RENDERER_WORKERS_COUNT The launcher was reading NODE_RENDERER_CONCURRENCY (inherited from PR #723's f55dcc29), but app.yml sets RENDERER_WORKERS_COUNT (canonical per marketplace + Pro renderer library). Result: prod/staging would ignore the deployed value and always use the launcher default. RENDERER_WORKERS_COUNT is the canonical env var name per: - docs/oss/building-features/node-renderer/js-configuration.md "workersCount (default: process.env.RENDERER_WORKERS_COUNT ...)" - packages/react-on-rails-pro-node-renderer/src/shared/configBuilder.ts:200 "workersCount: env.RENDERER_WORKERS_COUNT ? parseInt(...) : defaultWorkersCount()" - react-server-components-marketplace-demo/node-renderer.js NODE_RENDERER_CONCURRENCY was a non-canonical name invented by PR #723. Co-Authored-By: Claude Opus 4.7 (1M context) * Right-size rails workload: 1Gi + capacityAI Previous commit bumped memory 512Mi → 2Gi matching the marketplace demo, but cherry-picked only the memory dimension: CPU stayed at 300m and capacityAI stayed false. Net result was a wasteful fixed 2Gi allocation with no autoscale — the pattern Justin flagged. Better right-size for this tutorial (smaller Rails surface than the marketplace RSC demo): - memory: 2Gi → 1Gi. Enough headroom for Rails + the Pro Node renderer in one container without reserving capacity that won't be used. - capacityAI: false → true. Adjusts CPU/memory within the single replica based on observed usage, so the 1Gi/300m baseline grows if actual workload warrants it. Matches the marketplace demo's capacityAI posture without copying its oversized static baseline. Co-Authored-By: Claude Opus 4.7 (1M context) * Replace org.yml placeholder with renderer + license schema Document the actual keys the qa-* dictionary needs (RENDERER_PASSWORD, REACT_ON_RAILS_PRO_LICENSE) instead of the unused SOME_ENV placeholder, and warn against re-applying the template after real values are populated. Co-Authored-By: Claude Opus 4.7 (1M context) * Fix stale Rspack comments in Procfile.dev The rspack→webpack bundler flip in this PR left two comment lines referring to Rspack that now misdescribe the active bundler. Co-Authored-By: Claude Opus 4.7 (1M context) * Treat blank RENDERER_PASSWORD as unset in Rails initializer ENV.fetch returns "" when the env var is set to empty string, while the JS renderer's `process.env.RENDERER_PASSWORD || fallback` treats "" as falsy. Result: copying .env.example verbatim (ships with a blank RENDERER_PASSWORD= line) leaves Rails sending "" in the auth header while the renderer expects the dev default, silently failing SSR auth. Switch to ENV["RENDERER_PASSWORD"].presence || default so blank values route to the fallback on both sides. Co-Authored-By: Claude Opus 4.7 (1M context) * Document RENDERER_PORT, LOG_LEVEL, WORKERS_COUNT in .env.example app.yml and the renderer launcher read three additional env vars that .env.example didn't mention, leaving developers with no single place to see which renderer knobs exist. Add them commented-out with their defaults and brief descriptions. Co-Authored-By: Claude Opus 4.7 (1M context) * Derive RSCWebpackPlugin clientReferences from config.source_path Previously hardcoded path.resolve(__dirname, '../../client/app'), which would silently point at the wrong directory if shakapacker.yml's source_path were ever changed. The same file already derives the server-bundle entry's path from config.source_path (line 33); apply the same pattern here so the two stay in sync. Co-Authored-By: Claude Opus 4.7 (1M context) * Dump Node renderer log on CI rspec failure The renderer startup step only cats /tmp/node-renderer.log when the startup wait loop times out (30s). If the renderer starts fine but crashes mid-test, the log is never surfaced, making the rspec failure impossible to debug from the Actions UI. Add an `if: failure()` step after rspec that cats the log so any crash during the test run is visible. Co-Authored-By: Claude Opus 4.7 (1M context) * Forward SIGTERM to children in Dockerfile CMD Bash as PID 1 doesn't forward signals to its children by default, so Control Plane's rolling-deploy SIGTERM never reaches Puma or the Node renderer's cluster manager. Both handle SIGTERM automatically (drain in-flight requests, stop accepting new ones), but only if they receive the signal. Without a trap, the graceful period expires and SIGKILL drops in-flight requests on every rolling deploy. Add `trap 'kill -TERM $RENDERER_PID $RAILS_PID' TERM INT` before `wait -n`. Uses the PID captures that were previously assigned but unused, turning dead code into a real graceful-shutdown mechanism aligned with the graceful-shutdown section of container-deployment.md. Co-Authored-By: Claude Opus 4.7 (1M context) * Revert "Forward SIGTERM to children in Dockerfile CMD" This reverts commit 1bd5e86e7d5bb499627e6bcb2c6e2fadc9395335. The trap addition was incomplete. On the shutdown signal (Control Plane sends SIGINT per docs.controlplane.com/reference/workload/termination.md), the trap fires, `wait -n` returns 130, and `exit 1` then runs unconditionally. The container exits with code 1 on every rolling deploy instead of a code that reflects the signal-initiated shutdown. More importantly, the trap was trying to solve a problem Control Plane already handles. CP's default preStop hook runs `sh -c "sleep 45"` before any signal reaches PID 1, and the sidecar stops accepting new inbound connections ~80 seconds ahead of termination. That sleep plus the routing drain is the graceful-shutdown window; signal forwarding inside the container is marginal on top of it. Match the reference deployments verbatim: the react-on-rails-demo- marketplace-rsc Dockerfile and the hichee production Dockerfile both use this exact bash-c pattern without a trap. If deeper graceful shutdown is ever needed, the right tool is extending preStop or switching to a process manager (overmind, tini), not a bash trap on wait -n. Co-Authored-By: Claude Opus 4.7 (1M context) * Guard against dev-default RENDERER_PASSWORD in production The Pro renderer's JS side raises if RENDERER_PASSWORD is unset in production, but accepts the literal "local-dev-renderer-password" value. A CP secret misconfigured to that string (easy to happen when copying from .env.example) would let both sides "match" while running with no real authentication. Split the branch by Rails.env.local?: - dev/test keeps the .presence || default fallback so blank env vars still work for local development - production fetches strict and raises if blank, unset, or the literal dev default Co-Authored-By: Claude Opus 4.7 (1M context) * Fold CI workers cap into parseIntegerEnv default The post-hoc `if (process.env.CI && env == null) config.workersCount = 2` block mutated an already-constructed config and used a narrower definition of "unset" (`== null`) than parseIntegerEnv's own check (treats "" as unset too). Folding the CI default into the second argument of parseIntegerEnv: workersCount: parseIntegerEnv('RENDERER_WORKERS_COUNT', process.env.CI ? 2 : 3, { min: 0 }) keeps the same behaviour for explicit values, uses one definition of "unset" consistently, and drops the mutation. Co-Authored-By: Claude Opus 4.7 (1M context) * Document rscBundle parameter's Sub-PR 3 role The parameter is unused until Sub-PR 3 wires rscWebpackConfig.js to call configureServer(true). Adding a short comment so it isn't mistaken for dead code during review. Co-Authored-By: Claude Opus 4.7 (1M context) * Revert "Document rscBundle parameter's Sub-PR 3 role" This reverts commit a7734472b8e9e2bf80088a85ef0b7b9dd76b44a0. The `rscBundle = false` parameter shape follows the documented Pro pattern: docs/oss/migrating/rsc-preparing-app.md:244 and docs/pro/react-server-components/upgrading-existing-pro-app.md:106 both name it by parameter name and show the exact `if (!rscBundle) { ... RSCWebpackPlugin ... }` guard. The Pro dummy (react_on_rails_pro/spec/dummy/config/webpack/serverWebpackConfig.js) uses the same shape with no inline comment. The reverted commit restated what the docs already cover and added a Sub-PR 3 reference that would rot once Sub-PR 3 merges. Code follows documented conventions; readers who need context can read the doc. Co-Authored-By: Claude Opus 4.7 (1M context) * Loosen react pin from exact 19.0.4 to ~19.0.4 The exact pin locked out every 19.0.x patch including 19.0.5 and future security patches within the 19.0 line. Pro's own install docs (docs/pro/react-server-components/upgrading-existing-pro-app.md:26-28 and create-without-ssr.md:37) prescribe `react@~19.0.4` — tilde range that keeps the CVE floor while allowing 19.0.x patches. react-on-rails-rsc@19.0.4's peer dep is `react: ^19.0.3`, so 19.0.5 satisfies it. After this change, yarn resolves react to 19.0.5. Co-Authored-By: Claude Opus 4.7 (1M context) * Trim renderer_password initializer comment The previous comment restated in English what the code below already expressed. Cut to the one non-obvious WHY (why the prod branch exists despite Pro's own JS-side guard). Method names + the raise message cover the rest. Co-Authored-By: Claude Opus 4.7 (1M context) * Align CI renderer log handling with Pro CI Pro's own integration CI (react_on_rails_pro/.github/workflows/ pro-integration-tests.yml) runs the renderer with `pnpm run node-renderer &` — no log redirect — so renderer output interleaves with job stdout and is always visible without a special dump step. Our workflow inherited a redirect-to-/tmp/node-renderer.log pattern from PR #723 plus a follow-up if-failure cat step (commit 7dea0947) that dumped the file after rspec. Both existed to work around the redirect; neither was in any reference. Drop the redirect and the associated cat calls (startup-failure cat, timeout cat, post-rspec failure dump). Renderer logs now appear inline with job output, same shape as Pro CI. Co-Authored-By: Claude Opus 4.7 (1M context) * Reframe shakapacker bundler note as TODO "Tactical:" read as a weird label; "TODO:" is the standard signal that the current state is meant to be reverted once the upstream blocker (shakacode/react_on_rails_rsc#29) ships. Same content, leads with the action. Co-Authored-By: Claude Opus 4.7 (1M context) * Remove RENDERER_PASSWORD prod-default guard The guard added in 0eb94af9 raised at every Rails boot in production, which breaks the Docker build step that runs `bin/rails react_on_rails:locale` during image bake (RENDERER_PASSWORD isn't in the build environment — secrets are mounted at runtime only, not at build time). Deploy on Control Plane fails before it can even produce an image. Reference check on the guard: zero of Pro dummy, marketplace demo (react-on-rails-demo-marketplace-rsc), Justin's PR 723, and hichee have a "raise on literal dev-default value" guard. Each uses ENV.fetch with a default or hardcodes the password. The guard was a reviewer-driven divergence with no reference backing. Revert the prod branch. Keep the .presence fallback from 7254b1a1 (that one responds to our specific .env.example shape, not a reference divergence). Co-Authored-By: Claude Opus 4.7 (1M context) --------- Co-authored-by: Claude Opus 4.7 (1M context) --- .controlplane/Dockerfile | 9 +- .controlplane/templates/app.yml | 19 + .controlplane/templates/org.yml | 17 +- .controlplane/templates/rails.yml | 11 +- .env.example | 22 + .github/workflows/rspec_test.yml | 22 + .gitignore | 3 + Procfile.dev | 7 +- client/__tests__/webpack/bundlerUtils.spec.js | 36 +- config/initializers/react_on_rails_pro.rb | 19 + config/shakapacker.yml | 7 +- config/webpack/bundlerUtils.js | 51 +- config/webpack/serverWebpackConfig.js | 144 +++-- config/webpack/webpackConfig.js | 2 +- package.json | 9 +- renderer/node-renderer.js | 46 ++ yarn.lock | 499 +++++++++++++++++- 17 files changed, 751 insertions(+), 172 deletions(-) create mode 100644 .env.example create mode 100644 config/initializers/react_on_rails_pro.rb create mode 100644 renderer/node-renderer.js diff --git a/.controlplane/Dockerfile b/.controlplane/Dockerfile index a36aae827..9a955122d 100644 --- a/.controlplane/Dockerfile +++ b/.controlplane/Dockerfile @@ -80,7 +80,8 @@ RUN SECRET_KEY_BASE=precompile_placeholder yarn res:build && \ # For Kubernetes and ControlPlane, this is the command on the workload. ENTRYPOINT ["./.controlplane/entrypoint.sh"] -# Default args to pass to the entry point that can be overridden -# For Kubernetes and ControlPlane, these are the "workload args" -# Use Thruster HTTP/2 proxy for optimized performance -CMD ["bundle", "exec", "thrust", "bin/rails", "server"] +# Run the Pro Node renderer and Rails in a single container (Option 1 from +# docs/oss/building-features/node-renderer/container-deployment.md). +# `wait -n` exits on first child failure → container exits → Control Plane +# restarts the whole unit. Keeps the existing Thruster HTTP/2 proxy for Rails. +CMD ["bash", "-c", "node renderer/node-renderer.js & RENDERER_PID=$! ; bundle exec thrust bin/rails server & RAILS_PID=$! ; wait -n ; exit 1"] diff --git a/.controlplane/templates/app.yml b/.controlplane/templates/app.yml index add22f1f3..0ff2ac87c 100644 --- a/.controlplane/templates/app.yml +++ b/.controlplane/templates/app.yml @@ -24,6 +24,25 @@ spec: # set to a secure random value using: openssl rand -hex 64 # Production apps should configure this manually after app creation via a secret. value: 'placeholder_secret_key_base_for_test_apps_only' + # Pro Node renderer settings. The renderer process runs in the same + # container as Rails (see .controlplane/Dockerfile CMD). + - name: RENDERER_PORT + value: '3800' + - name: RENDERER_LOG_LEVEL + value: info + - name: RENDERER_WORKERS_COUNT + value: '2' + - name: RENDERER_URL + value: http://localhost:3800 + # RENDERER_PASSWORD and REACT_ON_RAILS_PRO_LICENSE must be created in the + # Control Plane Secret named by {{APP_SECRETS}} before deploy. cpflow + # resolves {{APP_SECRETS}} to `{APP_PREFIX}-secrets` — which means review + # apps all share one `qa-react-webpack-rails-tutorial-secrets` (thanks to + # match_if_app_name_starts_with: true on the qa template). + - name: RENDERER_PASSWORD + value: cpln://secret/{{APP_SECRETS}}.RENDERER_PASSWORD + - name: REACT_ON_RAILS_PRO_LICENSE + value: cpln://secret/{{APP_SECRETS}}.REACT_ON_RAILS_PRO_LICENSE # Part of standard configuration staticPlacement: locationLinks: diff --git a/.controlplane/templates/org.yml b/.controlplane/templates/org.yml index 6616376de..c04129e87 100644 --- a/.controlplane/templates/org.yml +++ b/.controlplane/templates/org.yml @@ -4,14 +4,25 @@ # other sensitive information that is shared across multiple apps # in the same organization. -# This is how you apply this once (not during CI) -# cpl apply-template secrets -a qa-react-webpack-rails-tutorial --org shakacode-open-source-examples-staging +# The qa-* dictionary is bootstrapped via this template; prod and +# staging dictionaries are created manually with real values. + +# Initial bootstrap (once, manually, not in CI): +# cpflow apply-template secrets -a qa-react-webpack-rails-tutorial --org shakacode-open-source-examples-staging +# +# Populate real values with `cpln apply -f ` or `cpln secret edit`. +# Do NOT re-apply this template after real values are set: it will +# overwrite them with the placeholders below. kind: secret name: {{APP_SECRETS}} type: dictionary data: - SOME_ENV: "123456" + # Both sides of the Rails/Node renderer handshake must match. + # Generate with `openssl rand -hex 32`. + RENDERER_PASSWORD: "replace-with-openssl-rand-hex-32" + # JWT from https://pro.reactonrails.com/; same token across envs. + REACT_ON_RAILS_PRO_LICENSE: "replace-with-pro-license-jwt" --- diff --git a/.controlplane/templates/rails.yml b/.controlplane/templates/rails.yml index 49fe19091..ed686ed18 100644 --- a/.controlplane/templates/rails.yml +++ b/.controlplane/templates/rails.yml @@ -15,8 +15,11 @@ spec: # Inherit other ENV values from GVC inheritEnv: true image: {{APP_IMAGE_LINK}} - # 512 corresponds to a standard 1x dyno type - memory: 512Mi + # 1Gi (up from 512Mi) gives the single container enough headroom for + # Rails + the Pro Node renderer. Below the marketplace demo's 2Gi + # because this tutorial's Rails surface is smaller; capacityAI below + # adjusts upward if actual usage warrants it. + memory: 1Gi ports: - number: 3000 protocol: http @@ -27,7 +30,9 @@ spec: autoscaling: # Max of 1 effectively disables autoscaling, so a like a Heroku dyno count of 1 maxScale: 1 - capacityAI: false + # CapacityAI adjusts CPU/memory within the single replica based on observed + # usage, so the 1Gi/300m baseline can grow if Rails + renderer need more. + capacityAI: true firewallConfig: external: # Default to allow public access to Rails server diff --git a/.env.example b/.env.example new file mode 100644 index 000000000..346c99817 --- /dev/null +++ b/.env.example @@ -0,0 +1,22 @@ +# React on Rails Pro license (JWT token from https://pro.reactonrails.com/). +# Required in production; optional in development/test. +REACT_ON_RAILS_PRO_LICENSE= + +# Shared secret between Rails and the Node renderer. Must match on both sides. +# Dev/test default to `local-dev-renderer-password` if unset; production must +# set this explicitly (Pro raises at boot otherwise). +RENDERER_PASSWORD= + +# Node renderer endpoint. Defaults to http://localhost:3800. +# RENDERER_URL=http://localhost:3800 + +# Port the Node renderer listens on. Defaults to 3800. +# RENDERER_PORT=3800 + +# Log verbosity for the Node renderer (debug, info, warn, error). +# Defaults to info. +# RENDERER_LOG_LEVEL=info + +# Number of renderer worker processes. `0` is single-process mode, useful +# for local debugging. Defaults to 3; the app's CI workflow caps to 2. +# RENDERER_WORKERS_COUNT=3 diff --git a/.github/workflows/rspec_test.yml b/.github/workflows/rspec_test.yml index d117458a6..83a676981 100644 --- a/.github/workflows/rspec_test.yml +++ b/.github/workflows/rspec_test.yml @@ -33,6 +33,9 @@ jobs: DRIVER: selenium_chrome CHROME_BIN: /usr/bin/google-chrome USE_COVERALLS: true + # Must match config.renderer_password in config/initializers/react_on_rails_pro.rb. + # Setting explicitly avoids silent drift if the two-sided defaults ever diverge. + RENDERER_PASSWORD: local-dev-renderer-password steps: - name: Install Chrome @@ -82,6 +85,25 @@ jobs: - name: Build shakapacker chunks run: NODE_ENV=development bundle exec bin/shakapacker + - name: Start Node renderer for SSR + run: | + node renderer/node-renderer.js & + RENDERER_PID=$! + echo "Waiting for Node renderer (PID $RENDERER_PID) on port 3800..." + for i in $(seq 1 30); do + if ! kill -0 $RENDERER_PID 2>/dev/null; then + echo "Node renderer process exited unexpectedly (see output above)." + exit 1 + fi + 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 (see output above)." + exit 1 + - name: Run rspec with xvfb uses: coactions/setup-xvfb@v1 with: diff --git a/.gitignore b/.gitignore index 366e6a9b7..198c879e9 100644 --- a/.gitignore +++ b/.gitignore @@ -57,6 +57,9 @@ client/app/bundles/comments/rescript/**/*.bs.js # Using React on Rails default directory /ssr-generated/ +# Pro Node renderer bundle cache +/renderer/.node-renderer-bundles/ + # Generated React on Rails packs **/generated/** diff --git a/Procfile.dev b/Procfile.dev index 102c0a8df..20cd0f7b6 100644 --- a/Procfile.dev +++ b/Procfile.dev @@ -2,13 +2,14 @@ # You can run these commands in separate shells # # Note: bin/dev runs precompile tasks (rescript + locale) BEFORE starting these processes. -# This ensures all generated files exist before Rspack starts watching. +# This ensures all generated files exist before webpack starts watching. # # ReScript watch mode (no clean - bin/dev already did the clean build) rescript: yarn res:watch # redis: redis-server # Run Redis as a system service instead (brew services start redis) rails: bundle exec thrust bin/rails server -p 3000 -# Client Rspack dev server with HMR +# Client webpack dev server with HMR wp-client: RAILS_ENV=development NODE_ENV=development bin/shakapacker-dev-server -# Server Rspack watcher for SSR bundle +# Server webpack watcher for SSR bundle wp-server: SERVER_BUNDLE_ONLY=yes bin/shakapacker --watch +node-renderer: NODE_ENV=development node renderer/node-renderer.js diff --git a/client/__tests__/webpack/bundlerUtils.spec.js b/client/__tests__/webpack/bundlerUtils.spec.js index 0e710c701..7e7def5f3 100644 --- a/client/__tests__/webpack/bundlerUtils.spec.js +++ b/client/__tests__/webpack/bundlerUtils.spec.js @@ -1,7 +1,9 @@ /* eslint-disable max-classes-per-file */ /* eslint-disable global-require */ /** - * Unit tests for bundlerUtils.js in Rspack-only mode. + * Unit tests for bundlerUtils.js. The utility returns the active bundler + * module based on shakapacker.yml's `assets_bundler` setting; it supports + * both webpack and rspack. */ jest.mock('@rspack/core', () => ({ @@ -10,12 +12,18 @@ jest.mock('@rspack/core', () => ({ optimize: { LimitChunkCountPlugin: class MockRspackLimitChunkCount {} }, })); +jest.mock('webpack', () => ({ + ProvidePlugin: class MockWebpackProvidePlugin {}, + DefinePlugin: class MockWebpackDefinePlugin {}, + optimize: { LimitChunkCountPlugin: class MockWebpackLimitChunkCount {} }, +})); + describe('bundlerUtils', () => { let mockConfig; beforeEach(() => { jest.resetModules(); - mockConfig = { assets_bundler: 'rspack' }; + mockConfig = { assets_bundler: 'webpack' }; }); afterEach(() => { @@ -35,32 +43,30 @@ describe('bundlerUtils', () => { expect(bundler.CssExtractRspackPlugin.name).toBe('MockCssExtractRspackPlugin'); }); - it('throws when assets_bundler is webpack', () => { + it('returns webpack when assets_bundler is webpack', () => { mockConfig.assets_bundler = 'webpack'; jest.doMock('shakapacker', () => ({ config: mockConfig })); const utils = require('../../../config/webpack/bundlerUtils'); - expect(() => utils.getBundler()).toThrow('configured for Rspack only'); + const bundler = utils.getBundler(); + + expect(bundler).toBeDefined(); + expect(bundler.DefinePlugin).toBeDefined(); + expect(bundler.DefinePlugin.name).toBe('MockWebpackDefinePlugin'); }); - it('throws when assets_bundler is undefined', () => { + it('returns webpack when assets_bundler is undefined', () => { mockConfig.assets_bundler = undefined; jest.doMock('shakapacker', () => ({ config: mockConfig })); const utils = require('../../../config/webpack/bundlerUtils'); - expect(() => utils.getBundler()).toThrow('configured for Rspack only'); - }); - - it('throws when assets_bundler is invalid', () => { - mockConfig.assets_bundler = 'invalid-bundler'; - jest.doMock('shakapacker', () => ({ config: mockConfig })); - const utils = require('../../../config/webpack/bundlerUtils'); + const bundler = utils.getBundler(); - expect(() => utils.getBundler()).toThrow('configured for Rspack only'); + expect(bundler.DefinePlugin).toBeDefined(); }); it('returns cached bundler on subsequent calls', () => { - mockConfig.assets_bundler = 'rspack'; + mockConfig.assets_bundler = 'webpack'; jest.doMock('shakapacker', () => ({ config: mockConfig })); const utils = require('../../../config/webpack/bundlerUtils'); @@ -106,7 +112,7 @@ describe('bundlerUtils', () => { jest.doMock('shakapacker', () => ({ config: mockConfig })); const utils = require('../../../config/webpack/bundlerUtils'); - expect(() => utils.getCssExtractPlugin()).toThrow('configured for Rspack only'); + expect(() => utils.getCssExtractPlugin()).toThrow('only available when assets_bundler is rspack'); }); }); }); diff --git a/config/initializers/react_on_rails_pro.rb b/config/initializers/react_on_rails_pro.rb new file mode 100644 index 000000000..4e4f03890 --- /dev/null +++ b/config/initializers/react_on_rails_pro.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +# See https://reactonrails.com/docs/configuration/configuration-pro +# Production license: set REACT_ON_RAILS_PRO_LICENSE to the JWT token from +# https://pro.reactonrails.com/. Development and test environments don't +# require a license — the Pro engine logs an info-level notice only. +ReactOnRailsPro.configure do |config| + config.server_renderer = "NodeRenderer" + # Raise loudly when the renderer is unavailable instead of silently + # degrading to ExecJS (default is true). + config.renderer_use_fallback_exec_js = false + + config.renderer_url = ENV.fetch("RENDERER_URL", "http://localhost:3800") + + # Must match the password in renderer/node-renderer.js. Use .presence + # so a blank env var (.env.example ships with `RENDERER_PASSWORD=`) + # falls back to the dev default, matching the JS side's `||`. + config.renderer_password = ENV["RENDERER_PASSWORD"].presence || "local-dev-renderer-password" +end diff --git a/config/shakapacker.yml b/config/shakapacker.yml index 9b2484521..a56bb8100 100644 --- a/config/shakapacker.yml +++ b/config/shakapacker.yml @@ -9,7 +9,12 @@ default: &default webpack_compile_output: true nested_entries: true javascript_transpiler: swc - assets_bundler: rspack + # TODO: flip back to rspack once shakacode/react_on_rails_rsc#29 ships a + # native rspack RSC plugin. On webpack for now because rspack + # 2.0.0-beta.7's webpack-compat layer doesn't cover the APIs the upstream + # RSCWebpackPlugin needs (contextModuleFactory.resolveDependencies, + # ModuleDependency). + assets_bundler: webpack # Additional paths webpack should lookup modules # ['app/assets', 'engine/foo/app/assets'] diff --git a/config/webpack/bundlerUtils.js b/config/webpack/bundlerUtils.js index a403abb66..2a483f20f 100644 --- a/config/webpack/bundlerUtils.js +++ b/config/webpack/bundlerUtils.js @@ -1,55 +1,34 @@ -/** - * Bundler utilities for Rspack-only configuration. - * - * This repository standardizes on Rspack with Shakapacker. - */ +// Returns the active bundler module per shakapacker.yml's `assets_bundler`. +// Supports both webpack and rspack so this project can switch between them +// without touching every config file. const { config } = require('shakapacker'); -// Cache for bundler module let _cachedBundler = null; -const ensureRspack = () => { - if (config.assets_bundler !== 'rspack') { - throw new Error( - `Invalid assets_bundler: "${config.assets_bundler}". ` + - 'This project is configured for Rspack only. ' + - 'Set assets_bundler: rspack in config/shakapacker.yml', - ); - } -}; - -/** - * Gets the Rspack module for the current build. - * - * @returns {Object} @rspack/core module - * @throws {Error} If assets_bundler is not 'rspack' - */ const getBundler = () => { - ensureRspack(); - if (_cachedBundler) { return _cachedBundler; } - _cachedBundler = require('@rspack/core'); + _cachedBundler = config.assets_bundler === 'rspack' ? require('@rspack/core') : require('webpack'); return _cachedBundler; }; -/** - * Checks whether the configured bundler is Rspack. - * - * @returns {boolean} True when assets_bundler is rspack - */ const isRspack = () => config.assets_bundler === 'rspack'; -/** - * Gets the CSS extraction plugin for Rspack. - * - * @returns {Object} CssExtractRspackPlugin - */ -const getCssExtractPlugin = () => getBundler().CssExtractRspackPlugin; +// Only meaningful on rspack — webpack projects use mini-css-extract-plugin +// via shakapacker's generated config. +const getCssExtractPlugin = () => { + if (!isRspack()) { + throw new Error( + 'getCssExtractPlugin() is only available when assets_bundler is rspack. ' + + "On webpack, rely on shakapacker's generated MiniCssExtractPlugin configuration.", + ); + } + return getBundler().CssExtractRspackPlugin; +}; module.exports = { getBundler, diff --git a/config/webpack/serverWebpackConfig.js b/config/webpack/serverWebpackConfig.js index 8dada6496..edcd38127 100644 --- a/config/webpack/serverWebpackConfig.js +++ b/config/webpack/serverWebpackConfig.js @@ -1,28 +1,22 @@ // The source code including full typescript support is available at: // https://github.com/shakacode/react_on_rails_tutorial_with_ssr_and_hmr_fast_refresh/blob/master/config/webpack/serverWebpackConfig.js +/* eslint-disable no-param-reassign */ const path = require('path'); const { config } = require('shakapacker'); +const { RSCWebpackPlugin } = require('react-on-rails-rsc/WebpackPlugin'); const commonWebpackConfig = require('./commonWebpackConfig'); const { getBundler } = require('./bundlerUtils'); -/** - * Generates the server-side rendering (SSR) bundle configuration. - * - * This creates a separate bundle optimized for server-side rendering: - * - Single chunk (no code splitting for Node.js execution) - * - CSS extraction disabled (uses exportOnlyLocals for class name mapping) - * - No asset hashing (not served directly to clients) - * - Outputs to ssr-generated/ directory - * - * Key differences from client config: - * - Removes CSS extraction loaders (mini-css-extract-plugin/CssExtractRspackPlugin) - * - Preserves CSS Modules configuration but adds exportOnlyLocals: true - * - Disables optimization/minification for faster builds and better debugging - * - * @returns {Object} Webpack/Rspack configuration object for server bundle - */ -const configureServer = () => { +function extractLoader(rule, loaderName) { + if (!Array.isArray(rule.use)) return undefined; + return rule.use.find((item) => { + const testValue = typeof item === 'string' ? item : item?.loader; + return typeof testValue === 'string' && testValue.includes(loaderName); + }); +} + +const configureServer = (rscBundle = false) => { const bundler = getBundler(); // We need to use "merge" because the clientConfigObject, EVEN after running @@ -31,7 +25,6 @@ const configureServer = () => { // Using webpack-merge into an empty object avoids this issue. const serverWebpackConfig = commonWebpackConfig(); - // We just want the single server bundle entry const serverEntry = { 'server-bundle': serverWebpackConfig.entry['server-bundle'], }; @@ -43,51 +36,49 @@ 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`, ); } serverWebpackConfig.entry = serverEntry; - // Remove the mini-css-extract-plugin from the style loaders because - // the client build will handle exporting CSS. - // replace file-loader with null-loader - serverWebpackConfig.module.rules.forEach((loader) => { - if (loader.use && loader.use.filter) { - loader.use = loader.use.filter( - (item) => !(typeof item === 'string' && item.match(/mini-css-extract-plugin/)), - ); - } - }); - - // No splitting of chunks for a server bundle serverWebpackConfig.optimization = { minimize: false, }; serverWebpackConfig.plugins.unshift(new bundler.optimize.LimitChunkCountPlugin({ maxChunks: 1 })); - // Custom output for the server-bundle that matches the config in - // config/initializers/react_on_rails.rb - // Output to a private directory for SSR bundles (not in public/) - // Using the default React on Rails path: ssr-generated + if (!rscBundle) { + // Scope client-reference discovery to the app source dir. Without this, + // the plugin can walk into node_modules and hit .tsx source files that + // aren't configured for a loader. Derive from config.source_path so the + // scope follows shakapacker.yml instead of hardcoding client/app. + const clientReferencesDir = path.resolve(config.source_path || 'client/app'); + serverWebpackConfig.plugins.push( + new RSCWebpackPlugin({ + isServer: true, + clientReferences: [ + { directory: clientReferencesDir, recursive: true, include: /\.(js|ts|jsx|tsx)$/ }, + ], + }), + ); + } + + // libraryTarget: 'commonjs2' is required by the Pro Node renderer so it can + // `require()` the evaluated bundle. No publicPath: the server bundle is + // loaded from the filesystem, never served over HTTP. serverWebpackConfig.output = { filename: 'server-bundle.js', globalObject: 'this', - // If using the React on Rails Pro node server renderer, uncomment the next line - // libraryTarget: 'commonjs2', + libraryTarget: 'commonjs2', path: path.resolve(__dirname, '../../ssr-generated'), - publicPath: config.publicPath, - // https://webpack.js.org/configuration/output/#outputglobalobject }; - // Don't hash the server bundle b/c would conflict with the client manifest - // And no need for CSS extraction plugins (MiniCssExtractPlugin or CssExtractRspackPlugin) serverWebpackConfig.plugins = serverWebpackConfig.plugins.filter( (plugin) => plugin.constructor.name !== 'WebpackAssetsManifest' && @@ -96,64 +87,49 @@ const configureServer = () => { plugin.constructor.name !== 'ForkTsCheckerWebpackPlugin', ); - // Configure loader rules for SSR - // Remove the mini-css-extract-plugin/CssExtractRspackPlugin from the style loaders because - // the client build will handle exporting CSS. - // replace file-loader with null-loader - const rules = serverWebpackConfig.module.rules; - rules.forEach((rule) => { + serverWebpackConfig.module.rules.forEach((rule) => { if (Array.isArray(rule.use)) { - // remove the mini-css-extract-plugin/CssExtractRspackPlugin and style-loader rule.use = rule.use.filter((item) => { - let testValue; - if (typeof item === 'string') { - testValue = item; - } else if (typeof item.loader === 'string') { - testValue = item.loader; - } + const testValue = typeof item === 'string' ? item : item?.loader; + if (typeof testValue !== 'string') return true; return !( - testValue?.match(/mini-css-extract-plugin/) || - testValue?.match(/CssExtractRspackPlugin/) || - testValue?.includes('cssExtractLoader') || + testValue.match(/mini-css-extract-plugin/) || + testValue.match(/CssExtractRspackPlugin/) || + testValue.includes('cssExtractLoader') || testValue === 'style-loader' ); }); - const cssLoader = rule.use.find((item) => { - let testValue; - - if (typeof item === 'string') { - testValue = item; - } else if (typeof item.loader === 'string') { - testValue = item.loader; - } - return testValue?.includes('css-loader'); - }); - if (cssLoader && cssLoader.options && cssLoader.options.modules) { - // Preserve existing modules config but add exportOnlyLocals for SSR + const cssLoader = extractLoader(rule, 'css-loader'); + if (cssLoader?.options?.modules) { cssLoader.options.modules = { ...cssLoader.options.modules, exportOnlyLocals: true, }; } - // Skip writing image files during SSR by setting emitFile to false + const babelLoader = extractLoader(rule, 'babel-loader'); + if (babelLoader) { + babelLoader.options = babelLoader.options || {}; + babelLoader.options.caller = { ssr: true }; + } } else if (rule.use && (rule.use.loader === 'url-loader' || rule.use.loader === 'file-loader')) { rule.use.options.emitFile = false; } }); - // eval works well for the SSR bundle because it's the fastest and shows - // lines in the server bundle which is good for debugging SSR - // 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' + // target: 'node' fixes SSR breakage in libraries (Emotion, loadable-components, etc.) + // that don't behave under the default 'web' target. node: false disables the + // polyfill shims that only matter when targeting 'web'. + serverWebpackConfig.target = 'node'; + serverWebpackConfig.node = false; return serverWebpackConfig; }; -module.exports = configureServer; +module.exports = { + default: configureServer, + extractLoader, +}; diff --git a/config/webpack/webpackConfig.js b/config/webpack/webpackConfig.js index 4f68574e2..44327e8b1 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 serverWebpackConfig = require('./serverWebpackConfig'); +const { default: serverWebpackConfig } = require('./serverWebpackConfig'); const webpackConfig = (envSpecific) => { const clientConfig = clientWebpackConfig(); diff --git a/package.json b/package.json index 989e4e166..1ae8c2c79 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 renderer/node-renderer.js" }, "dependencies": { "@babel/cli": "^7.21.0", @@ -77,10 +78,12 @@ "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-pro": "16.6.0", + "react-on-rails-pro-node-renderer": "16.6.0", + "react-on-rails-rsc": "19.0.4", "react-redux": "^8.1.0", "react-router": "^6.13.0", "react-router-dom": "^6.13.0", diff --git a/renderer/node-renderer.js b/renderer/node-renderer.js new file mode 100644 index 000000000..a4cb3a347 --- /dev/null +++ b/renderer/node-renderer.js @@ -0,0 +1,46 @@ +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'); +} + +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 || 'info', + password: rendererPassword, + port: parseIntegerEnv('RENDERER_PORT', 3800, { min: 1, max: 65535 }), + supportModules: true, + // CI hosts report more CPUs than allocated to the container; default to + // 2 workers on CI to avoid oversubscribing memory. Explicit + // RENDERER_WORKERS_COUNT still wins on either path. + workersCount: parseIntegerEnv('RENDERER_WORKERS_COUNT', process.env.CI ? 2 : 3, { min: 0 }), + // Expose globals the VM sandbox doesn't auto-provide but that downstream + // deps rely on during SSR. Without URL, react-router-dom's NavLink throws + // `ReferenceError: URL is not defined` via encodeLocation. + additionalContext: { URL, AbortController }, +}; + +reactOnRailsProNodeRenderer(config); diff --git a/yarn.lock b/yarn.lock index ceb7d3f4a..833bca340 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" @@ -2840,6 +2920,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" @@ -2875,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" @@ -2907,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" @@ -3149,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" @@ -3168,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" @@ -3407,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" @@ -3780,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" @@ -4329,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" @@ -4993,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" @@ -5019,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== @@ -5034,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.3: + version "5.8.5" + resolved "https://registry.npmjs.org/fastify/-/fastify-5.8.5.tgz#c452224295e0ca550bcd0efc3f7d3e90e9c11955" + integrity sha512-Yqptv59pQzPgQUSIm87hMqHJmdkb1+GPxdE6vW6FRyVE9G86mt7rOghitiU4JHRaTyDUk9pfeKmDeu70lAwM4Q== + 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" @@ -5103,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" @@ -5201,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" @@ -5758,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== @@ -6605,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" @@ -6657,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" @@ -6667,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" @@ -6724,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" @@ -6865,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" @@ -6875,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" @@ -6894,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" @@ -7190,9 +7483,9 @@ 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" + resolved "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== no-case@^3.0.4: @@ -7343,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 +7908,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 +8622,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 +8691,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" @@ -8372,12 +8738,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.5" + resolved "https://registry.npmjs.org/react-dom/-/react-dom-19.0.5.tgz#7666ca4385dd1f1d2ac2445423077b2f232aa3c0" + integrity sha512-yqJj7o8tlj5FiLpycpClCCTp1f1FXvMgCkFej41N1iTmVDiTeDIay6Y69sn8w9JXSCzZyCLP3fotgEhZagDZWw== dependencies: - scheduler "^0.27.0" + scheduler "^0.25.0" react-intl@^6.4.4: version "6.8.9" @@ -8410,6 +8776,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.6.0: + version "16.6.0" + resolved "https://registry.npmjs.org/react-on-rails-pro-node-renderer/-/react-on-rails-pro-node-renderer-16.6.0.tgz#c13ca0f156566531d7c6e005759459b0f19472a8" + integrity sha512-fBZ0lKRaEe8LyVTdUsXx364zQfL6hGJuE+2qQsKo+bXm0aTVq2RtO49gzq0m7Y4xuhBTVmnPQUP0O1v1cGRzLg== + dependencies: + "@fastify/formbody" "^7.4.0 || ^8.0.2" + "@fastify/multipart" "^8.3.1 || ^9.0.3" + 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.6.0: version "16.6.0" resolved "https://registry.npmjs.org/react-on-rails-pro/-/react-on-rails-pro-16.6.0.tgz#19a5ea99d7b397dd56f14cff1f31955211b4d0a2" @@ -8417,6 +8796,15 @@ react-on-rails-pro@16.6.0: dependencies: react-on-rails "16.6.0" +react-on-rails-rsc@19.0.4: + version "19.0.4" + resolved "https://registry.npmjs.org/react-on-rails-rsc/-/react-on-rails-rsc-19.0.4.tgz#a605fbaa82a0bece504de1ef0b8d5c6fae5d6be3" + integrity sha512-KtHYz0opcXJk+Zw5aabjNiaszzpTdFgs1YfSLIZJSzU5SpddUw7b08epkxeJq/TCpR60Q9vsjxX3Q3OWJ97tMg== + dependencies: + acorn-loose "^8.3.0" + neo-async "^2.6.1" + webpack-sources "^3.2.0" + react-on-rails@16.6.0: version "16.6.0" resolved "https://registry.npmjs.org/react-on-rails/-/react-on-rails-16.6.0.tgz#da7f117fec14f420f7f6ffe6bdb34b7fc2e01b3a" @@ -8485,10 +8873,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.5" + resolved "https://registry.npmjs.org/react/-/react-19.0.5.tgz#b9406da29c7085e446e4c2372dcfe4f7c4801aec" + integrity sha512-yIoQWl4moQfHFKNGmyJavhOki09GwCRcMFuXv3y3KMXoQrGnDi0ZHGe4H9EtQE+jrMWU4hgxaILMS4rxTkJdGw== read-cache@^1.0.0: version "1.0.0" @@ -8531,6 +8919,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" @@ -8741,6 +9134,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" @@ -8764,6 +9162,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" @@ -8808,7 +9211,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== @@ -8840,6 +9243,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.1" + resolved "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.1.1.tgz#a5f3a6e35b8d84d0f41fa22efd5b6d30b367bbc7" + integrity sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA== + 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" @@ -8892,10 +9307,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" @@ -8926,6 +9341,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" @@ -8954,6 +9374,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" @@ -9037,6 +9462,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" @@ -9182,6 +9612,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" @@ -9252,6 +9689,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" @@ -9670,6 +10112,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" @@ -9704,6 +10160,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" @@ -10116,7 +10577,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 f1fa1d703002c66666ea34e5fd32c9a5b792da9b Mon Sep 17 00:00:00 2001 From: Ihab Adham <71561048+ihabadham@users.noreply.github.com> Date: Tue, 28 Apr 2026 22:17:56 +0300 Subject: [PATCH 3/7] Pro RSC migration 3/3: React Server Components demo on webpack MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a React Server Components demo page at /server-components, riding on top of Sub-PR 2's NodeRenderer + webpack setup. The page demonstrates four RSC capabilities: - Server Environment: ServerInfo reads Node's `os` module and lodash on the server; neither reaches the browser. - Interactive Client Component: a 'use client' TogglePanel nested inside a Server Component tree, demonstrating the donut pattern. - Live Server Activity: client-driven server-component re-fetching via useRSC().refetchComponent + RSCRoute, with react-error-boundary catching simulated errors and a Retry button that re-primes the cache before resetting the boundary. Local Suspense fallback prevents the in-flight RSC fetch from collapsing the page. - Streamed Comments: async Server Component receiving comments as props from the controller per the canonical Pro data-fetching pattern, streaming in via Suspense after the page shell. Renderer config (renderer/node-renderer.js) needs stubTimers: false (the default `true` no-ops setTimeout, which React's RSC server renderer uses for Flight-protocol yielding — without the override, RSC streams emit zero chunks and hang until the idle timeout fires) and replayServerAsyncOperationLogs: true (surfaces async-Server-Component console output through consoleReplayScript). Build setup uses upstream RSCWebpackPlugin and WebpackLoader. The RSC bundle reuses the server-bundle entry; entry name, RSC loader injection, and a few resolve overrides (react-server condition, react-dom/server: false) are the only differences from the SSR build. The webpack-config dispatch gains an RSC_BUNDLE_ONLY env-var gate matching the existing SERVER_BUNDLE_ONLY / CLIENT_BUNDLE_ONLY pattern; default builds now produce three bundles (client + server + RSC). Covered by request + system specs at spec/requests/server_components_spec.rb and spec/system/server_components_demo_spec.rb. Co-Authored-By: Claude Opus 4.7 (1M context) --- .controlplane/templates/app.yml | 4 + Procfile.dev | 2 + app/controllers/pages_controller.rb | 9 +- app/views/pages/server_components.html.erb | 5 + .../Footer/ror_components/Footer.jsx | 2 + .../NavigationBar/NavigationBar.jsx | 8 + .../ror_components/SimpleCommentScreen.jsx | 2 + .../app/bundles/comments/constants/paths.js | 1 + .../ror_components/RescriptShow.jsx | 2 + .../startup/App/ror_components/App.jsx | 2 + .../ror_components/NavigationBarApp.jsx | 2 + .../ror_components/RouterApp.client.jsx | 2 + .../ror_components/RouterApp.server.jsx | 2 + .../components/CommentsFeed.jsx | 84 ++++++++++ .../components/LiveActivityRefresher.jsx | 96 +++++++++++ .../components/ServerInfo.jsx | 56 +++++++ .../components/TogglePanel.jsx | 40 +++++ .../ror_components/LiveActivity.jsx | 49 ++++++ .../ror_components/ServerComponentsPage.jsx | 150 ++++++++++++++++++ client/app/packs/stimulus-bundle.js | 2 + client/app/packs/stores-registration.js | 3 + config/initializers/react_on_rails_pro.rb | 4 + config/routes.rb | 3 + config/webpack/clientWebpackConfig.js | 13 ++ config/webpack/rscWebpackConfig.js | 53 +++++++ config/webpack/webpackConfig.js | 10 +- package.json | 1 + renderer/node-renderer.js | 11 ++ spec/requests/server_components_spec.rb | 49 ++++++ spec/system/server_components_demo_spec.rb | 44 +++++ yarn.lock | 7 + 31 files changed, 714 insertions(+), 4 deletions(-) create mode 100644 app/views/pages/server_components.html.erb create mode 100644 client/app/bundles/server-components/components/CommentsFeed.jsx create mode 100644 client/app/bundles/server-components/components/LiveActivityRefresher.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/bundles/server-components/ror_components/LiveActivity.jsx create mode 100644 client/app/bundles/server-components/ror_components/ServerComponentsPage.jsx create mode 100644 config/webpack/rscWebpackConfig.js create mode 100644 spec/requests/server_components_spec.rb create mode 100644 spec/system/server_components_demo_spec.rb diff --git a/.controlplane/templates/app.yml b/.controlplane/templates/app.yml index 0ff2ac87c..5ed7c689f 100644 --- a/.controlplane/templates/app.yml +++ b/.controlplane/templates/app.yml @@ -34,6 +34,10 @@ spec: value: '2' - name: RENDERER_URL value: http://localhost:3800 + # Enable the artificial Suspense demo delay so the streaming fallback is + # visible on the review-app. Off by default in production deployments. + - name: RSC_SUSPENSE_DEMO_DELAY + value: 'true' # RENDERER_PASSWORD and REACT_ON_RAILS_PRO_LICENSE must be created in the # Control Plane Secret named by {{APP_SECRETS}} before deploy. cpflow # resolves {{APP_SECRETS}} to `{APP_PREFIX}-secrets` — which means review diff --git a/Procfile.dev b/Procfile.dev index 20cd0f7b6..71c6a4b13 100644 --- a/Procfile.dev +++ b/Procfile.dev @@ -12,4 +12,6 @@ rails: bundle exec thrust bin/rails server -p 3000 wp-client: RAILS_ENV=development NODE_ENV=development bin/shakapacker-dev-server # Server webpack watcher for SSR bundle wp-server: SERVER_BUNDLE_ONLY=yes bin/shakapacker --watch +# RSC webpack watcher for React Server Components bundle +wp-rsc: RSC_BUNDLE_ONLY=yes bin/shakapacker --watch node-renderer: NODE_ENV=development node renderer/node-renderer.js diff --git a/app/controllers/pages_controller.rb b/app/controllers/pages_controller.rb index 507cc6cf7..d668bd8cd 100644 --- a/app/controllers/pages_controller.rb +++ b/app/controllers/pages_controller.rb @@ -2,7 +2,8 @@ class PagesController < ApplicationController include ReactOnRails::Controller - before_action :set_comments + include ReactOnRailsPro::Stream + before_action :set_comments, only: %i[index no_router] def index # NOTE: The below notes apply if you want to set the value of the props in the controller, as @@ -38,6 +39,12 @@ def simple; end def rescript; end + def server_components + @server_components_comments = Comment.order(id: :desc).limit(10) + .as_json(only: %i[id author text created_at updated_at]) + stream_view_containing_react_components(template: "/pages/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..029cfce1b --- /dev/null +++ b/app/views/pages/server_components.html.erb @@ -0,0 +1,5 @@ +<%= stream_react_component("ServerComponentsPage", + props: { comments: @server_components_comments }, + prerender: true, + auto_load_bundle: true, + trace: Rails.env.development?) %> 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 + +
  • fallback is visible in the demo. +// Set RSC_SUSPENSE_DEMO_DELAY=true to enable; defaults off in production. +async function CommentsFeed({ comments = [] }) { + if (process.env.RSC_SUSPENSE_DEMO_DELAY === 'true') { + await new Promise((resolve) => { + setTimeout(resolve, 800); + }); + } + + if (comments.length === 0) { + return ( +
    +

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

    +
    + ); + } + + return ( +
    + {comments.map((comment) => { + // marked + sanitize-html (~200KB combined) stay server-side. + 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'], + }); + + 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 */} + {/* eslint-disable-next-line react/no-danger */} +
    + +

    {comment.text}

    +
    + ); + })} +

    + {comments.length} comment{comments.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/LiveActivityRefresher.jsx b/client/app/bundles/server-components/components/LiveActivityRefresher.jsx new file mode 100644 index 000000000..0c7165339 --- /dev/null +++ b/client/app/bundles/server-components/components/LiveActivityRefresher.jsx @@ -0,0 +1,96 @@ +'use client'; + +import React, { useState, Suspense } from 'react'; +import { ErrorBoundary } from 'react-error-boundary'; +import RSCRoute from 'react-on-rails-pro/RSCRoute'; +import { useRSC } from 'react-on-rails-pro/RSCProvider'; + +// Same shape and dimensions as the rendered LiveActivity card. Local Suspense +// fallback prevents the RSCRoute suspension from bubbling to an outer +// boundary, which would collapse the whole page during in-flight fetches. +const ActivityCardSkeleton = () => ( +
    +
    + {['Server Time', 'Free RAM', 'Uptime (hrs)'].map((label) => ( +
    +
    + {label} +
    +
    +
    + ))} +
    +
    +); + +const LiveActivityRefresher = () => { + const [refreshKey, setRefreshKey] = useState(0); + const [simulateError, setSimulateError] = useState(false); + const { refetchComponent } = useRSC(); + + const handleRefresh = () => { + setSimulateError(false); + setRefreshKey((k) => k + 1); + }; + + const handleSimulateError = () => { + setSimulateError(true); + setRefreshKey((k) => k + 1); + }; + + // refetchComponent primes the cache with corrected props before resetting + // the boundary, so the post-reset render hits cache instead of re-fetching. + const buildRetry = (resetErrorBoundary) => () => { + const newKey = refreshKey + 1; + setSimulateError(false); + setRefreshKey(newKey); + refetchComponent('LiveActivity', { simulateError: false, refreshKey: newKey }) + // eslint-disable-next-line no-console + .catch((err) => console.error('Retry refetch failed:', err)) + .finally(() => resetErrorBoundary()); + }; + + return ( +
    +
    + + + Refresh count: {refreshKey} +
    + ( +
    +

    Server component fetch failed

    +

    {error.message}

    + +
    + )} + resetKeys={[refreshKey]} + > + }> + + +
    +
    + ); +}; + +export default LiveActivityRefresher; 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..e09fa1d98 --- /dev/null +++ b/client/app/bundles/server-components/components/ServerInfo.jsx @@ -0,0 +1,56 @@ +// Server Component - uses Node.js os module, which only exists on the server. +// This component and its dependencies are never sent to the browser. + +import React from 'react'; +import os from 'os'; +import _ from 'lodash'; + +function ServerInfo() { + const serverInfo = { + platform: os.platform(), + arch: os.arch(), + nodeVersion: process.version, + uptime: Math.floor(os.uptime() / 3600), + totalMemory: (os.totalmem() / (1024 * 1024 * 1024)).toFixed(1), + freeMemory: (os.freemem() / (1024 * 1024 * 1024)).toFixed(1), + cpus: os.cpus().length, + }; + + // Using lodash on the server — this 70KB+ library stays server-side + const infoEntries = _.toPairs(serverInfo); + const grouped = _.chunk(infoEntries, 4); + + const labels = { + platform: 'Platform', + arch: 'Architecture', + nodeVersion: 'Node.js', + uptime: 'Uptime (hrs)', + totalMemory: 'Total RAM (GB)', + freeMemory: 'Free RAM (GB)', + cpus: 'CPU Cores', + }; + + return ( +
    +

    + 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) => ( +
    k).join('-')} className="space-y-1"> + {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..1336b56b3 --- /dev/null +++ b/client/app/bundles/server-components/components/TogglePanel.jsx @@ -0,0 +1,40 @@ +'use client'; + +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; + +const TogglePanel = ({ title, children }) => { + const [isOpen, setIsOpen] = useState(false); + + return ( +
    + + {isOpen && ( +
    + {children} +
    + )} +
    + ); +}; + +TogglePanel.propTypes = { + title: PropTypes.string.isRequired, + children: PropTypes.node.isRequired, +}; + +export default TogglePanel; diff --git a/client/app/bundles/server-components/ror_components/LiveActivity.jsx b/client/app/bundles/server-components/ror_components/LiveActivity.jsx new file mode 100644 index 000000000..a76f7de34 --- /dev/null +++ b/client/app/bundles/server-components/ror_components/LiveActivity.jsx @@ -0,0 +1,49 @@ +import React from 'react'; +import os from 'os'; + +async function LiveActivity({ simulateError = false }) { + if (simulateError) { + throw new Error('Simulated server-side render failure (demo)'); + } + + // Opt-in delay so the refresh-in-flight state is visible in the demo. + // Matches the gate in CommentsFeed; off by default in production. + if (process.env.RSC_SUSPENSE_DEMO_DELAY === 'true') { + await new Promise((resolve) => { + setTimeout(resolve, 300); + }); + } + + const stats = { + serverTime: new Date().toISOString(), + freeMemoryMB: Math.round(os.freemem() / (1024 * 1024)), + uptimeHours: Math.floor(os.uptime() / 3600), + }; + + return ( +
    +
    +
    +
    + Server Time +
    +
    {stats.serverTime}
    +
    +
    +
    + Free RAM +
    +
    {stats.freeMemoryMB} MB
    +
    +
    +
    + Uptime (hrs) +
    +
    {stats.uptimeHours}
    +
    +
    +
    + ); +} + +export default LiveActivity; diff --git a/client/app/bundles/server-components/ror_components/ServerComponentsPage.jsx b/client/app/bundles/server-components/ror_components/ServerComponentsPage.jsx new file mode 100644 index 000000000..e6b8df849 --- /dev/null +++ b/client/app/bundles/server-components/ror_components/ServerComponentsPage.jsx @@ -0,0 +1,150 @@ +// Server Component - this entire component runs on the server. +// It can use Node.js APIs and server-only dependencies directly. +// None of these imports are shipped to the client bundle. + +import React, { Suspense } from 'react'; +import ServerInfo from '../components/ServerInfo'; +import CommentsFeed from '../components/CommentsFeed'; +import TogglePanel from '../components/TogglePanel'; +import LiveActivityRefresher from '../components/LiveActivityRefresher'; + +const ServerComponentsPage = ({ comments = [] }) => { + 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
    • +
    +
    +
    +
    + + {/* Client-fetched server component via RSCRoute + ErrorBoundary */} +
    +

    + Live Server Activity + + RSCRoute + ErrorBoundary + +

    +

    + Click Refresh to fetch a new RSC payload — the server re-renders + this section and streams the result back, no client-side JSON parsing or loading + state plumbing. Click Simulate Error to make the server component + throw; the failure surfaces as ServerComponentFetchError and is + caught by <ErrorBoundary>, which renders a Retry button that + calls refetchComponent with corrected props. +

    + +
    + + {/* Async data fetching with Suspense streaming */} +
    +

    + Streamed Comments + + Async + Suspense + +

    +

    + Comments come from the Rails controller as props — the canonical React on Rails Pro + pattern. The page shell renders immediately while this section streams in + progressively as Suspense boundaries resolve. +

    + + {[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/packs/stimulus-bundle.js b/client/app/packs/stimulus-bundle.js index 2664fee2b..07dedc01f 100644 --- a/client/app/packs/stimulus-bundle.js +++ b/client/app/packs/stimulus-bundle.js @@ -1,3 +1,5 @@ +'use client'; + import ReactOnRails from 'react-on-rails-pro'; import 'jquery-ujs'; import { Turbo } from '@hotwired/turbo-rails'; diff --git a/client/app/packs/stores-registration.js b/client/app/packs/stores-registration.js index d03732dc3..a069ac620 100644 --- a/client/app/packs/stores-registration.js +++ b/client/app/packs/stores-registration.js @@ -1,3 +1,6 @@ +// 'use client' keeps this pack and its store imports out of the RSC bundle. +'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 index 4e4f03890..fc5f39000 100644 --- a/config/initializers/react_on_rails_pro.rb +++ b/config/initializers/react_on_rails_pro.rb @@ -16,4 +16,8 @@ # so a blank env var (.env.example ships with `RENDERER_PASSWORD=`) # falls back to the dev default, matching the JS side's `||`. config.renderer_password = ENV["RENDERER_PASSWORD"].presence || "local-dev-renderer-password" + + config.enable_rsc_support = true + config.rsc_bundle_js_file = "rsc-bundle.js" + config.rsc_payload_generation_url_path = "rsc_payload/" end diff --git a/config/routes.rb b/config/routes.rb index 1d8c7b7a5..353819a32 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,12 +1,15 @@ # frozen_string_literal: true Rails.application.routes.draw do + rsc_payload_route + # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html # Serve websocket cable requests in-process # mount ActionCable.server => '/cable' root "pages#index" + get "server-components", to: "pages#server_components" get "simple", to: "pages#simple" get "rescript", to: "pages#rescript" diff --git a/config/webpack/clientWebpackConfig.js b/config/webpack/clientWebpackConfig.js index 6352208fb..ea5957c0f 100644 --- a/config/webpack/clientWebpackConfig.js +++ b/config/webpack/clientWebpackConfig.js @@ -1,6 +1,9 @@ // The source code including full typescript support is available at: // https://github.com/shakacode/react_on_rails_tutorial_with_ssr_and_hmr_fast_refresh/blob/master/config/webpack/clientWebpackConfig.js +const path = require('path'); +const { config } = require('shakapacker'); +const { RSCWebpackPlugin } = require('react-on-rails-rsc/WebpackPlugin'); const commonWebpackConfig = require('./commonWebpackConfig'); const { getBundler } = require('./bundlerUtils'); @@ -22,6 +25,16 @@ const configureClient = () => { // client config is going to try to load chunks. delete clientConfig.entry['server-bundle']; + const clientReferencesDir = path.resolve(config.source_path || 'client/app'); + clientConfig.plugins.push( + new RSCWebpackPlugin({ + isServer: false, + clientReferences: [ + { directory: clientReferencesDir, recursive: true, include: /\.(js|ts|jsx|tsx)$/ }, + ], + }), + ); + return clientConfig; }; diff --git a/config/webpack/rscWebpackConfig.js b/config/webpack/rscWebpackConfig.js new file mode 100644 index 000000000..6d0ac252c --- /dev/null +++ b/config/webpack/rscWebpackConfig.js @@ -0,0 +1,53 @@ +const { default: serverWebpackConfig, extractLoader } = require('./serverWebpackConfig'); + +const configureRsc = () => { + const rscConfig = serverWebpackConfig(true); + + const rscEntry = { + 'rsc-bundle': rscConfig.entry['server-bundle'], + }; + rscConfig.entry = rscEntry; + + // Runs before babel/swc (webpack loaders execute right-to-left) to detect + // 'use client' directives in raw source before transpilation. Shakapacker + // generates rule.use as an array for Babel and as a function for SWC, so + // handle both forms. + const { rules } = rscConfig.module; + rules.forEach((rule) => { + if (typeof rule.use === 'function') { + const originalUse = rule.use; + rule.use = function rscLoaderWrapper(data) { + const result = originalUse.call(this, data); + const resultArray = Array.isArray(result) ? result : result ? [result] : []; + const resolvedRule = { use: resultArray }; + const jsLoader = + extractLoader(resolvedRule, 'babel-loader') || extractLoader(resolvedRule, 'swc-loader'); + if (jsLoader) { + return [...resultArray, { loader: 'react-on-rails-rsc/WebpackLoader' }]; + } + return result; + }; + } else if (Array.isArray(rule.use)) { + const jsLoader = extractLoader(rule, 'babel-loader') || extractLoader(rule, 'swc-loader'); + if (jsLoader) { + rule.use = [...rule.use, { loader: 'react-on-rails-rsc/WebpackLoader' }]; + } + } + }); + + rscConfig.resolve = { + ...rscConfig.resolve, + conditionNames: ['react-server', '...'], + alias: { + ...rscConfig.resolve?.alias, + // RSC payload generation doesn't need react-dom/server; importing + // it in the react-server environment causes a runtime error. + 'react-dom/server': false, + }, + }; + + rscConfig.output.filename = 'rsc-bundle.js'; + return rscConfig; +}; + +module.exports = configureRsc; diff --git a/config/webpack/webpackConfig.js b/config/webpack/webpackConfig.js index 44327e8b1..b1783513a 100644 --- a/config/webpack/webpackConfig.js +++ b/config/webpack/webpackConfig.js @@ -3,6 +3,7 @@ const clientWebpackConfig = require('./clientWebpackConfig'); const { default: serverWebpackConfig } = require('./serverWebpackConfig'); +const rscWebpackConfig = require('./rscWebpackConfig'); const webpackConfig = (envSpecific) => { const clientConfig = clientWebpackConfig(); @@ -22,11 +23,14 @@ const webpackConfig = (envSpecific) => { // 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) { + // eslint-disable-next-line no-console + console.log('[React on Rails] Creating only the RSC bundle.'); + result = rscWebpackConfig(); } else { - // default is the standard client and server build // 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, rscWebpackConfig()]; } // To debug, uncomment next line and inspect "result" diff --git a/package.json b/package.json index 1ae8c2c79..3f2801592 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,7 @@ "prop-types": "^15.8.1", "react": "~19.0.4", "react-dom": "~19.0.4", + "react-error-boundary": "^4.1.2", "react-intl": "^6.4.4", "react-on-rails-pro": "16.6.0", "react-on-rails-pro-node-renderer": "16.6.0", diff --git a/renderer/node-renderer.js b/renderer/node-renderer.js index a4cb3a347..b29a5d032 100644 --- a/renderer/node-renderer.js +++ b/renderer/node-renderer.js @@ -41,6 +41,17 @@ const config = { // deps rely on during SSR. Without URL, react-router-dom's NavLink throws // `ReferenceError: URL is not defined` via encodeLocation. additionalContext: { URL, AbortController }, + // RSC requires a real setTimeout. The renderer's default stubTimers:true + // replaces setTimeout with a no-op to prevent legacy SSR from leaking + // timers, but React's RSC server renderer uses setTimeout internally for + // Flight-protocol yielding — with it stubbed, the RSC stream silently + // emits zero chunks and hangs until the Fastify idle timeout fires. + stubTimers: false, + // Surface console output from async server-component code. Without this, + // `console.error` calls from within async Server Components (e.g. + // CommentsFeed's catch block) are silently dropped by the VM, making + // runtime failures in RSC components invisible. + replayServerAsyncOperationLogs: true, }; reactOnRailsProNodeRenderer(config); diff --git a/spec/requests/server_components_spec.rb b/spec/requests/server_components_spec.rb new file mode 100644 index 000000000..5527c218f --- /dev/null +++ b/spec/requests/server_components_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require "rails_helper" + +describe "Server Components" do + it "GET /server-components returns the demo page shell" do + get "/server-components" + expect(response).to have_http_status(:ok) + expect(response.body).to include("React Server Components Demo") + end + + describe "RSC payload endpoint" do + def parsed_chunks + response.body.each_line.filter_map do |line| + stripped = line.strip + next if stripped.empty? + + JSON.parse(stripped) + end + end + + def expect_valid_rsc_payload + expect(response).to have_http_status(:ok) + expect(response.media_type).to eq("application/x-ndjson") + chunks = parsed_chunks + expect(chunks).not_to be_empty + expect(chunks.any? { |chunk| chunk.key?("html") }).to be(true) + end + + it "streams a valid RSC payload for ServerComponentsPage" do + get "/rsc_payload/ServerComponentsPage", params: { props: "{}" } + expect_valid_rsc_payload + end + + it "streams a valid RSC payload for ServerComponentsPage with populated comments" do + now = 1.minute.ago.iso8601 + comments = [ + { id: 1, author: "Alice", text: "Hello **markdown**", created_at: now, updated_at: now }, + ] + get "/rsc_payload/ServerComponentsPage", params: { props: { comments: comments }.to_json } + expect_valid_rsc_payload + end + + it "streams a valid RSC payload for LiveActivity" do + get "/rsc_payload/LiveActivity", params: { props: "{}" } + expect_valid_rsc_payload + end + end +end diff --git a/spec/system/server_components_demo_spec.rb b/spec/system/server_components_demo_spec.rb new file mode 100644 index 000000000..6361fc680 --- /dev/null +++ b/spec/system/server_components_demo_spec.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require "rails_helper" + +describe "Server Components demo" do + before { visit "/server-components" } + + it "renders the four demo sections" do + expect(page).to have_selector("h2", text: "Server Environment") + expect(page).to have_selector("h2", text: "Interactive Client Component") + expect(page).to have_selector("h2", text: "Live Server Activity") + expect(page).to have_selector("h2", text: "Streamed Comments") + end + + it "shows server-side data in ServerInfo" do + expect(page).to have_content("Platform") + expect(page).to have_content("Architecture") + expect(page).to have_content("Node.js") + expect(page).to have_content("CPU Cores") + end + + describe "Live Server Activity (RSCRoute)" do + it "shows the initial activity card with the live stats labels" do + expect(page).to have_content("SERVER TIME") + expect(page).to have_content("FREE RAM") + expect(page).to have_content("UPTIME (HRS)") + expect(page).to have_content("Refresh count: 0") + end + + it "updates content when Refresh is clicked" do + click_button "Refresh" + expect(page).to have_content("Refresh count: 1") + end + + it "shows the ErrorBoundary fallback when Simulate Error is clicked, then recovers on Retry" do + click_button "Simulate Error" + expect(page).to have_content("Server component fetch failed") + + click_button "Retry" + expect(page).to have_content("SERVER TIME") + expect(page).to have_no_content("Server component fetch failed") + end + end +end diff --git a/yarn.lock b/yarn.lock index 833bca340..d1e20864a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8745,6 +8745,13 @@ react-dom@~19.0.4: dependencies: scheduler "^0.25.0" +react-error-boundary@^4.1.2: + version "4.1.2" + resolved "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.1.2.tgz#bc750ad962edb8b135d6ae922c046051eb58f289" + integrity sha512-GQDxZ5Jd+Aq/qUxbCm1UtzmL/s++V7zKgE8yMktJiCQXCCFZnMZh9ng+6/Ne6PjNSXH0L9CjeOEREfRnq6Duag== + dependencies: + "@babel/runtime" "^7.12.5" + react-intl@^6.4.4: version "6.8.9" resolved "https://registry.npmjs.org/react-intl/-/react-intl-6.8.9.tgz" From 227fd25f83b8791caf94ca931fd12c0162e5fcff Mon Sep 17 00:00:00 2001 From: ihabadham Date: Tue, 28 Apr 2026 22:48:03 +0300 Subject: [PATCH 4/7] Drop trailing comma in server_components spec populated-comments fixture Style/TrailingCommaInArrayLiteral offense in the populated-comments RSC payload test. Didn't surface on #729 because lint_test.yml only triggers on PRs targeting master, so sub-PR CI skipped lint entirely. Co-Authored-By: Claude Opus 4.7 (1M context) --- spec/requests/server_components_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/requests/server_components_spec.rb b/spec/requests/server_components_spec.rb index 5527c218f..1be18e6e6 100644 --- a/spec/requests/server_components_spec.rb +++ b/spec/requests/server_components_spec.rb @@ -35,7 +35,7 @@ def expect_valid_rsc_payload it "streams a valid RSC payload for ServerComponentsPage with populated comments" do now = 1.minute.ago.iso8601 comments = [ - { id: 1, author: "Alice", text: "Hello **markdown**", created_at: now, updated_at: now }, + { id: 1, author: "Alice", text: "Hello **markdown**", created_at: now, updated_at: now } ] get "/rsc_payload/ServerComponentsPage", params: { props: { comments: comments }.to_json } expect_valid_rsc_payload From 4c2a3e9b15667e989f5012cb7122dbe9c4085781 Mon Sep 17 00:00:00 2001 From: ihabadham Date: Tue, 28 Apr 2026 23:35:37 +0300 Subject: [PATCH 5/7] Update README to document Pro and React Server Components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Headline + lead bullet bumped to React on Rails Pro + Node Renderer - New "Demoed Functionality" entries for Pro NodeRenderer, RSC features, and the donut pattern - Technologies + Version Targets refreshed (Pro 16.6.0, shakapacker 10.0.0, react ~19.0.4, SWC; removed Turbolinks) - New "React Server Components (RSC)" section: demo overview, three- bundle build architecture, env-var dispatch, setup notes, and links to reactonrails.com docs - Configuration Files list expanded with rscWebpackConfig.js and bundlerUtils.js - Small "currently on webpack, temporarily" note under "Why Rspack" pointing at the upstream blocker (no section rename — flips back once shakacode/react_on_rails_rsc#29 ships) - Process Management section lists the six Procfile.dev processes with bundler-agnostic descriptions - TOC: added RSC entry, removed stale Rendering with Express Server entry (no matching section in content) Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 100 +++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 80 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 395f35569..e558099dd 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ [![Code Climate](https://codeclimate.com/github/shakacode/react-webpack-rails-tutorial/badges/gpa.svg)](https://codeclimate.com/github/shakacode/react-webpack-rails-tutorial) [![Coverage Status](https://coveralls.io/repos/shakacode/react-webpack-rails-tutorial/badge.svg?branch=master&service=github)](https://coveralls.io/github/shakacode/react-webpack-rails-tutorial?branch=master) -# React, Redux, Tailwind CSS, ES2024, Rspack, Ruby on Rails Demo +# React, Redux, Tailwind CSS, React Server Components, ES2024, Rspack, Ruby on Rails Demo -* Server-Side Rendering of React via the [react_on_rails gem](https://github.com/shakacode/react_on_rails) +* Server-Side Rendering and React Server Components via [React on Rails Pro](https://www.shakacode.com/react-on-rails-pro/) with the Node Renderer * Live at [www.reactrails.com](http://www.reactrails.com/) ## Control Plane Deployment Example @@ -82,12 +82,11 @@ You can see this tutorial live here: [http://reactrails.com/](http://reactrails. + [Rspack](#rspack-with-shakapacker) + [Configuration Files](#configuration-files) + [Additional Resources](#additional-resources) ++ [React Server Components (RSC)](#react-server-components-rsc) + [Thruster HTTP/2 Proxy](#thruster-http2-proxy) + [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,10 @@ 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 Node Renderer for server-side rendering and React Server Components. +- Example of [React Server Components](#react-server-components-rsc) with streaming, Suspense fallbacks, error boundaries, and client-driven re-fetching — see the `/server-components` page. +- Example of `'use client'` directives splitting a tree between server-rendered and client-hydrated components (the "donut pattern"). +- Example of using the [react_on_rails gem](https://github.com/shakacode/react_on_rails) (via React on Rails Pro) for React + Rspack integration with Rails. - 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 +112,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 the Node Renderer 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 +184,12 @@ 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` +- `react-on-rails-pro` npm package: `16.6.0` +- `react-on-rails-pro-node-renderer` npm package: `16.6.0` +- `shakapacker` gem/npm package: `10.0.0` +- `@rspack/core` and `@rspack/cli`: `2.0.0-beta.7` +- `react`: `~19.0.4` (minimum for React Server Components) ### Why Rspack @@ -194,19 +197,70 @@ assets_bundler: rspack - Better incremental rebuild performance for local development - One bundler path for browser bundles and SSR bundles +> **Currently on webpack, temporarily.** The repo is built on webpack instead of rspack while [shakacode/react_on_rails_rsc#29](https://github.com/shakacode/react_on_rails_rsc/pull/29) ships rspack support for the RSC plugin. Tracked as a `TODO` in `config/shakapacker.yml`; flips back once that lands. + ### Configuration Files All bundler configuration is in `config/webpack/`: -- `webpackConfig.js` - Main Shakapacker entry point -- `commonWebpackConfig.js` - Shared configuration -- `clientWebpackConfig.js` - Client bundle settings -- `serverWebpackConfig.js` - Server-side rendering bundle -- `development.js`, `production.js`, `test.js` - Environment-specific settings +- `webpackConfig.js` — Composer; produces all bundles (client + SSR + RSC) +- `commonWebpackConfig.js` — Shared base configuration +- `clientWebpackConfig.js` — Client bundle (browser, with HMR + RSC client-references) +- `serverWebpackConfig.js` — SSR bundle (runs in the Pro Node Renderer) +- `rscWebpackConfig.js` — React Server Components bundle (runs in the Pro Node Renderer with the `react-server` resolve condition) +- `bundlerUtils.js` — Bundler detection helper (webpack vs rspack) +- `development.js`, `production.js`, `test.js` — Environment-specific tweaks ### Additional Resources - [Shakapacker Documentation](https://github.com/shakacode/shakapacker) - [Rspack Documentation](https://rspack.dev/) +## React Server Components (RSC) + +This project demonstrates React Server Components running on top of the React on Rails Pro Node Renderer. With the dev stack running, visit [`/server-components`](http://localhost:3000/server-components) to see the demo. + +### What the demo shows + +| Section | What it demonstrates | +|---------|----------------------| +| Server Environment | A server-only component reads Node's `os` module and `lodash`; neither library reaches the browser. | +| Interactive Client Component | A `'use client'` component nested inside a server-component tree, hydrated normally — the "donut pattern". | +| Live Server Activity | Client-driven server-component re-fetching via `useRSC().refetchComponent` + `RSCRoute`, with `react-error-boundary` catching simulated errors and a Retry button. | +| Streamed Comments | An async server component receives comments as props from the controller and streams in via `` after the page shell. | + +### How the build works + +The app produces three bundles, all sharing the `client/app/packs/server-bundle.js` entry: + +- **Client bundle** — Browser JavaScript with HMR; emits `react-client-manifest.json` for client-component resolution. +- **SSR bundle** — Traditional server-side rendering; runs in the Pro Node Renderer (port 3800). +- **RSC bundle** — RSC payload generation; runs in the Pro Node Renderer with the `react-server` resolve condition and an extra loader that classifies `'use client'` modules as client references. + +The three bundles are gated by env vars: + +| Env var | Result | +|---------|--------| +| (default for `bin/shakapacker-dev-server`) | Client bundle only | +| `SERVER_BUNDLE_ONLY=yes` | SSR bundle only | +| `RSC_BUNDLE_ONLY=yes` | RSC bundle only | +| (none, default for `bin/shakapacker`) | All three | + +`Procfile.dev` runs three watchers (`wp-client`, `wp-server`, `wp-rsc`), each gated by its own env var, alongside the Rails server and the Pro Node Renderer. + +### Setup notes + +In addition to the [Basic Demo Setup](#basic-demo-setup) prerequisites: + +- A `REACT_ON_RAILS_PRO_LICENSE` environment variable. Development and test environments don't require one (the Pro engine logs an info-level notice instead). Production deploys must set it to a JWT from [pro.reactonrails.com](https://www.shakacode.com/react-on-rails-pro/). +- `RENDERER_PASSWORD` (shared between Rails and the Node Renderer for SSR auth). The dev/test default is provided in `.env.example`; production must override. +- Optional: `RSC_SUSPENSE_DEMO_DELAY=true` adds a small artificial server delay so Suspense fallbacks are visible during the demo. Off by default; enabled on the QA review-app. + +### Further reading + +- [How React Server Components work](https://www.reactonrails.com/docs/pro/react-server-components/how-react-server-components-work) — RoR Pro's RSC overview +- [Preparing your app for RSC](https://www.reactonrails.com/docs/migrating/rsc-preparing-app) — `'use client'` directive rules +- [Component patterns](https://www.reactonrails.com/docs/migrating/rsc-component-patterns) — async server components and Suspense +- [Data fetching](https://www.reactonrails.com/docs/migrating/rsc-data-fetching) — controller-props pattern (used by `CommentsFeed`) + ## Thruster HTTP/2 Proxy This project uses [Thruster](https://github.com/basecamp/thruster), a zero-config HTTP/2 proxy from Basecamp, for optimized asset delivery and improved performance. @@ -270,12 +324,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 the full development stack with Hot Reloading. Six processes: + - `rescript` — ReScript watch mode + - `rails` — Rails server via Thruster on port 3000 + - `wp-client` — Shakapacker dev server with HMR (client bundle) + - `wp-server` — Shakapacker watcher for the SSR bundle + - `wp-rsc` — Shakapacker watcher for the 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 7b9093e54fbfd363f41fabcda0d2c6b76ce7b7d5 Mon Sep 17 00:00:00 2001 From: ihabadham Date: Tue, 28 Apr 2026 23:58:20 +0300 Subject: [PATCH 6/7] Fix ESLint failures surfaced on the base PR's first lint run Sub-PR workflows skip lint_test.yml (it triggers only on PRs targeting master), so these slipped through into the integration branch: - SimpleCommentScreen.jsx: pre-existing line-form disable for max-classes-per-file broke when #729 inserted `'use client'` ahead of it (the rule reports at file position 1:1; line-form disable on the import below no longer covered). Switch to a block-form disable at file top. - ServerComponentsPage.jsx, LiveActivity.jsx, CommentsFeed.jsx: block-form disable for react/prop-types. React 19 removed runtime propTypes validation; the new components rely on ES default destructuring rather than dead PropTypes declarations. - LiveActivityRefresher.jsx: line-form disable for react/no-unstable-nested-components on the ErrorBoundary's fallbackRender prop. The render-prop API is canonical for react-error-boundary when the fallback needs a closure; it cannot be hoisted without losing access to parent state (refreshKey, setRefreshKey, refetchComponent via buildRetry). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../SimpleCommentScreen/ror_components/SimpleCommentScreen.jsx | 2 +- .../app/bundles/server-components/components/CommentsFeed.jsx | 2 ++ .../server-components/components/LiveActivityRefresher.jsx | 3 +++ .../bundles/server-components/ror_components/LiveActivity.jsx | 2 ++ .../server-components/ror_components/ServerComponentsPage.jsx | 2 ++ 5 files changed, 10 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 157943dab..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,6 +1,6 @@ +/* eslint-disable max-classes-per-file */ 'use client'; -// 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 76129b337..7e0e27367 100644 --- a/client/app/bundles/server-components/components/CommentsFeed.jsx +++ b/client/app/bundles/server-components/components/CommentsFeed.jsx @@ -1,3 +1,5 @@ +/* eslint-disable react/prop-types -- React 19 no longer validates propTypes at runtime; using ES default destructuring instead */ + import React from 'react'; import { Marked } from 'marked'; import { gfmHeadingId } from 'marked-gfm-heading-id'; diff --git a/client/app/bundles/server-components/components/LiveActivityRefresher.jsx b/client/app/bundles/server-components/components/LiveActivityRefresher.jsx index 0c7165339..b2ee2c5b1 100644 --- a/client/app/bundles/server-components/components/LiveActivityRefresher.jsx +++ b/client/app/bundles/server-components/components/LiveActivityRefresher.jsx @@ -70,6 +70,9 @@ const LiveActivityRefresher = () => { Refresh count: {refreshKey}
    (

    Server component fetch failed

    diff --git a/client/app/bundles/server-components/ror_components/LiveActivity.jsx b/client/app/bundles/server-components/ror_components/LiveActivity.jsx index a76f7de34..30641fe41 100644 --- a/client/app/bundles/server-components/ror_components/LiveActivity.jsx +++ b/client/app/bundles/server-components/ror_components/LiveActivity.jsx @@ -1,3 +1,5 @@ +/* eslint-disable react/prop-types -- React 19 no longer validates propTypes at runtime; using ES default destructuring instead */ + import React from 'react'; import os from 'os'; diff --git a/client/app/bundles/server-components/ror_components/ServerComponentsPage.jsx b/client/app/bundles/server-components/ror_components/ServerComponentsPage.jsx index e6b8df849..7d8b6b4e7 100644 --- a/client/app/bundles/server-components/ror_components/ServerComponentsPage.jsx +++ b/client/app/bundles/server-components/ror_components/ServerComponentsPage.jsx @@ -2,6 +2,8 @@ // It can use Node.js APIs and server-only dependencies directly. // None of these imports are shipped to the client bundle. +/* eslint-disable react/prop-types -- React 19 no longer validates propTypes at runtime; using ES default destructuring instead */ + import React, { Suspense } from 'react'; import ServerInfo from '../components/ServerInfo'; import CommentsFeed from '../components/CommentsFeed'; From d29de5f3c5604252da5e018debaa21553f85652e Mon Sep 17 00:00:00 2001 From: ihabadham Date: Wed, 29 Apr 2026 00:13:24 +0300 Subject: [PATCH 7/7] Disable react/prop-types project-wide; drop dead PropTypes from TogglePanel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit React 19 removed runtime propTypes validation. The rule still ships in plugin:react/recommended (jsx-eslint/eslint-plugin-react#3753 tracks removing it) but no longer reflects this codebase's reality. - .eslintrc: set react/prop-types: 0 with rationale pointing at the upstream issue. - ServerComponentsPage / LiveActivity / CommentsFeed: drop the per-file `/* eslint-disable react/prop-types */` workarounds from 7b9093e5; the rule is now off everywhere. - TogglePanel: drop the prop-types import and the `TogglePanel.propTypes` declaration we introduced; React 19 doesn't validate them. Existing components elsewhere with their own PropTypes are unaffected — the rule only ever required declarations, never validated; disabling just stops the requirement. Co-Authored-By: Claude Opus 4.7 (1M context) --- .eslintrc | 5 +++++ .../bundles/server-components/components/CommentsFeed.jsx | 2 -- .../bundles/server-components/components/TogglePanel.jsx | 6 ------ .../server-components/ror_components/LiveActivity.jsx | 2 -- .../ror_components/ServerComponentsPage.jsx | 2 -- 5 files changed, 5 insertions(+), 12 deletions(-) diff --git a/.eslintrc b/.eslintrc index 2233d5895..061eb4f81 100644 --- a/.eslintrc +++ b/.eslintrc @@ -37,6 +37,11 @@ rules: # currently deprecated https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/no-onchange.md jsx-a11y/no-onchange: 0 + # React 19 removed runtime propTypes validation; the rule is still in + # plugin:react/recommended (jsx-eslint/eslint-plugin-react#3753 tracks + # removing it) but no longer relevant in this codebase. + react/prop-types: 0 + settings: import/core-modules: - react-redux diff --git a/client/app/bundles/server-components/components/CommentsFeed.jsx b/client/app/bundles/server-components/components/CommentsFeed.jsx index 7e0e27367..76129b337 100644 --- a/client/app/bundles/server-components/components/CommentsFeed.jsx +++ b/client/app/bundles/server-components/components/CommentsFeed.jsx @@ -1,5 +1,3 @@ -/* eslint-disable react/prop-types -- React 19 no longer validates propTypes at runtime; using ES default destructuring instead */ - import React from 'react'; import { Marked } from 'marked'; import { gfmHeadingId } from 'marked-gfm-heading-id'; diff --git a/client/app/bundles/server-components/components/TogglePanel.jsx b/client/app/bundles/server-components/components/TogglePanel.jsx index 1336b56b3..f5a38a9eb 100644 --- a/client/app/bundles/server-components/components/TogglePanel.jsx +++ b/client/app/bundles/server-components/components/TogglePanel.jsx @@ -1,7 +1,6 @@ 'use client'; import React, { useState } from 'react'; -import PropTypes from 'prop-types'; const TogglePanel = ({ title, children }) => { const [isOpen, setIsOpen] = useState(false); @@ -32,9 +31,4 @@ const TogglePanel = ({ title, children }) => { ); }; -TogglePanel.propTypes = { - title: PropTypes.string.isRequired, - children: PropTypes.node.isRequired, -}; - export default TogglePanel; diff --git a/client/app/bundles/server-components/ror_components/LiveActivity.jsx b/client/app/bundles/server-components/ror_components/LiveActivity.jsx index 30641fe41..a76f7de34 100644 --- a/client/app/bundles/server-components/ror_components/LiveActivity.jsx +++ b/client/app/bundles/server-components/ror_components/LiveActivity.jsx @@ -1,5 +1,3 @@ -/* eslint-disable react/prop-types -- React 19 no longer validates propTypes at runtime; using ES default destructuring instead */ - import React from 'react'; import os from 'os'; diff --git a/client/app/bundles/server-components/ror_components/ServerComponentsPage.jsx b/client/app/bundles/server-components/ror_components/ServerComponentsPage.jsx index 7d8b6b4e7..e6b8df849 100644 --- a/client/app/bundles/server-components/ror_components/ServerComponentsPage.jsx +++ b/client/app/bundles/server-components/ror_components/ServerComponentsPage.jsx @@ -2,8 +2,6 @@ // It can use Node.js APIs and server-only dependencies directly. // None of these imports are shipped to the client bundle. -/* eslint-disable react/prop-types -- React 19 no longer validates propTypes at runtime; using ES default destructuring instead */ - import React, { Suspense } from 'react'; import ServerInfo from '../components/ServerInfo'; import CommentsFeed from '../components/CommentsFeed';