From 504f4998f15f0078f926b9667e4dd98366a398ac Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Mon, 24 Nov 2025 19:02:35 -1000 Subject: [PATCH 01/40] Move React/Shakapacker version compatibility to generator smoke tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This moves React and Shakapacker version compatibility testing from spec/dummy to the generator smoke tests, as suggested in PR #2114 review. Changes: - Update spec/dummy to always use latest React 19 and Shakapacker 9.4.0 - Add minimum version example apps (basic-minimum, basic-server-rendering-minimum) that use React 18.0.0 and Shakapacker 8.2.0 - Add ExampleType.minimum_versions flag to support version-specific examples - Add rake tasks for filtered testing: - run_rspec:shakapacker_examples_latest (for latest versions only) - run_rspec:shakapacker_examples_minimum (for minimum versions only) - Simplify script/convert to only handle Node.js tooling compatibility (removed React/Shakapacker version modifications) - Update CI workflows to run appropriate examples per dependency level Benefits: - Clearer separation: spec/dummy tests latest, generators test compatibility - Simpler CI configuration for integration tests - Better reflects real-world usage patterns Closes #2123 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .github/workflows/examples.yml | 10 ++++-- .github/workflows/integration-tests.yml | 4 +-- package.json | 9 +++--- react_on_rails/rakelib/example_type.rb | 9 ++++-- react_on_rails/rakelib/examples_config.yml | 7 +++++ react_on_rails/rakelib/run_rspec.rake | 19 ++++++++++++ .../rakelib/shakapacker_examples.rake | 31 +++++++++++++++++++ react_on_rails/spec/dummy/package.json | 4 +-- script/convert | 29 ++++++----------- 9 files changed, 89 insertions(+), 33 deletions(-) diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index 5d53b00f81..0c8522825f 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -137,7 +137,7 @@ jobs: echo "Node version: "; node -v echo "pnpm version: "; pnpm --version echo "Bundler version: "; bundle --version - - name: run conversion script to support shakapacker v6 + - name: Run conversion script for older Node compatibility if: matrix.dependency-level == 'minimum' run: script/convert - name: Save root ruby gems to cache @@ -180,8 +180,12 @@ jobs: - name: Set packer version environment variable run: | echo "CI_DEPENDENCY_LEVEL=${{ matrix.dependency-level }}" >> $GITHUB_ENV - - name: Main CI - run: cd react_on_rails && bundle exec rake run_rspec:shakapacker_examples + - name: Main CI - Latest version examples + if: matrix.dependency-level == 'latest' + run: cd react_on_rails && bundle exec rake run_rspec:shakapacker_examples_latest + - name: Main CI - Minimum version examples + if: matrix.dependency-level == 'minimum' + run: cd react_on_rails && bundle exec rake run_rspec:shakapacker_examples_minimum - name: Store test results uses: actions/upload-artifact@v4 with: diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index da229b1ec8..19347e58e7 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -140,7 +140,7 @@ jobs: echo "Node version: "; node -v echo "pnpm version: "; pnpm --version echo "Bundler version: "; bundle --version - - name: run conversion script to support shakapacker v6 + - name: Run conversion script for older Node compatibility if: matrix.dependency-level == 'minimum' run: script/convert - name: Install Node modules with pnpm for renderer package @@ -229,7 +229,7 @@ jobs: echo "Node version: "; node -v echo "pnpm version: "; pnpm --version echo "Bundler version: "; bundle --version - - name: run conversion script to support shakapacker v6 + - name: Run conversion script for older Node compatibility if: matrix.dependency-level == 'minimum' run: script/convert - name: Save root ruby gems to cache diff --git a/package.json b/package.json index f3a54ec927..f75b387d91 100644 --- a/package.json +++ b/package.json @@ -31,8 +31,8 @@ "@tsconfig/node14": "^14.1.2", "@types/jest": "^29.5.14", "@types/node": "^20.17.16", - "@types/react": "^18.3.18", - "@types/react-dom": "^18.3.5", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", "@types/turbolinks": "^5.2.2", "create-react-class": "^15.7.0", "eslint": "^9.24.0", @@ -58,8 +58,9 @@ "prettier": "^3.5.2", "prop-types": "^15.8.1", "publint": "^0.3.4", - "react": "18.0.0", - "react-dom": "18.0.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-on-rails-rsc": "19.0.2", "redux": "^4.2.1", "size-limit": "^12.0.0", "stylelint": "^16.14.0", diff --git a/react_on_rails/rakelib/example_type.rb b/react_on_rails/rakelib/example_type.rb index 85e0fe3734..dd9eadf431 100644 --- a/react_on_rails/rakelib/example_type.rb +++ b/react_on_rails/rakelib/example_type.rb @@ -14,12 +14,17 @@ def self.all @all ||= { shakapacker_examples: [] } end - attr_reader :packer_type, :name, :generator_options + # Minimum supported versions for compatibility testing + MINIMUM_REACT_VERSION = "18.0.0" + MINIMUM_SHAKAPACKER_VERSION = "8.2.0" - def initialize(packer_type: nil, name: nil, generator_options: nil) + attr_reader :packer_type, :name, :generator_options, :minimum_versions + + def initialize(packer_type: nil, name: nil, generator_options: nil, minimum_versions: false) @packer_type = packer_type @name = name @generator_options = generator_options + @minimum_versions = minimum_versions self.class.all[packer_type.to_sym] << self end diff --git a/react_on_rails/rakelib/examples_config.yml b/react_on_rails/rakelib/examples_config.yml index 731c9d9725..ec6a11b3cf 100644 --- a/react_on_rails/rakelib/examples_config.yml +++ b/react_on_rails/rakelib/examples_config.yml @@ -7,3 +7,10 @@ example_type_data: generator_options: --redux - name: redux-server-rendering generator_options: --redux --example-server-rendering + # Minimum version compatibility tests - tests React 18 and Shakapacker 8.2.0 + - name: basic-minimum + generator_options: '' + minimum_versions: true + - name: basic-server-rendering-minimum + generator_options: --example-server-rendering + minimum_versions: true diff --git a/react_on_rails/rakelib/run_rspec.rake b/react_on_rails/rakelib/run_rspec.rake index a61db47c8c..2953628fb3 100644 --- a/react_on_rails/rakelib/run_rspec.rake +++ b/react_on_rails/rakelib/run_rspec.rake @@ -91,6 +91,25 @@ namespace :run_rspec do ExampleType.all[:shakapacker_examples].each { |example_type| Rake::Task[example_type.rspec_task_name].invoke } end + # Helper methods for filtering examples + def latest_examples + ExampleType.all[:shakapacker_examples].reject(&:minimum_versions) + end + + def minimum_examples + ExampleType.all[:shakapacker_examples].select(&:minimum_versions) + end + + desc "Runs Rspec for latest version example apps only (excludes minimum version tests)" + task shakapacker_examples_latest: latest_examples.map(&:gen_task_name) do + latest_examples.each { |example_type| Rake::Task[example_type.rspec_task_name].invoke } + end + + desc "Runs Rspec for minimum version example apps only (React 18, Shakapacker 8.2.0)" + task shakapacker_examples_minimum: minimum_examples.map(&:gen_task_name) do + minimum_examples.each { |example_type| Rake::Task[example_type.rspec_task_name].invoke } + end + Coveralls::RakeTask.new if ENV["USE_COVERALLS"] == "TRUE" desc "run all tests no examples" diff --git a/react_on_rails/rakelib/shakapacker_examples.rake b/react_on_rails/rakelib/shakapacker_examples.rake index 5f5f1bb864..33ee45be63 100644 --- a/react_on_rails/rakelib/shakapacker_examples.rake +++ b/react_on_rails/rakelib/shakapacker_examples.rake @@ -8,6 +8,7 @@ require "yaml" require "rails/version" require "pathname" +require "json" require_relative "example_type" require_relative "task_helpers" @@ -15,6 +16,32 @@ require_relative "task_helpers" namespace :shakapacker_examples do # rubocop:disable Metrics/BlockLength include ReactOnRails::TaskHelpers + # Updates package.json to use minimum supported versions for compatibility testing + def apply_minimum_versions(dir) + package_json_path = File.join(dir, "package.json") + return unless File.exist?(package_json_path) + + package_json = JSON.parse(File.read(package_json_path)) + + # Update React versions to minimum supported + if package_json["dependencies"] + package_json["dependencies"]["react"] = ExampleType::MINIMUM_REACT_VERSION + package_json["dependencies"]["react-dom"] = ExampleType::MINIMUM_REACT_VERSION + end + + # Update Shakapacker to minimum supported version + if package_json["devDependencies"]&.key?("shakapacker") + package_json["devDependencies"]["shakapacker"] = ExampleType::MINIMUM_SHAKAPACKER_VERSION + elsif package_json["dependencies"]&.key?("shakapacker") + package_json["dependencies"]["shakapacker"] = ExampleType::MINIMUM_SHAKAPACKER_VERSION + end + + File.write(package_json_path, "#{JSON.pretty_generate(package_json)}\n") + puts " Updated package.json with minimum versions:" + puts " React: #{ExampleType::MINIMUM_REACT_VERSION}" + puts " Shakapacker: #{ExampleType::MINIMUM_SHAKAPACKER_VERSION}" + end + # Define tasks for each example type ExampleType.all[:shakapacker_examples].each do |example_type| relative_gem_root = Pathname(gem_root).relative_path_from(Pathname(example_type.dir)) @@ -46,6 +73,10 @@ namespace :shakapacker_examples do # rubocop:disable Metrics/BlockLength "REACT_ON_RAILS_SKIP_VALIDATION=true #{cmd}" end sh_in_dir(example_type.dir, generator_commands) + + # Apply minimum versions for compatibility testing examples + apply_minimum_versions(example_type.dir) if example_type.minimum_versions + sh_in_dir(example_type.dir, "npm install") # Generate the component packs after running the generator to ensure all # auto-bundled components have corresponding pack files created diff --git a/react_on_rails/spec/dummy/package.json b/react_on_rails/spec/dummy/package.json index e9998a52db..d41661f9b2 100644 --- a/react_on_rails/spec/dummy/package.json +++ b/react_on_rails/spec/dummy/package.json @@ -18,8 +18,8 @@ "node-libs-browser": "^2.2.1", "null-loader": "^4.0.0", "prop-types": "^15.7.2", - "react": "18.0.0", - "react-dom": "18.0.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", "react-helmet": "^6.1.0", "react-on-rails": "link:.yalc/react-on-rails", "react-redux": "^8.0.2", diff --git a/script/convert b/script/convert index 0ea3ea0b07..0e4622e2a5 100755 --- a/script/convert +++ b/script/convert @@ -1,6 +1,14 @@ #!/usr/bin/env ruby # frozen_string_literal: true +# This script converts the codebase to use minimum supported dependency versions. +# It's run during CI when testing the "minimum" dependency matrix to ensure +# backward compatibility. +# +# React/Shakapacker version compatibility is now tested through generator smoke tests +# (see examples_config.yml for minimum version examples like basic-minimum). +# This script only handles Node.js/tooling compatibility and Pro package test adjustments. + def gsub_file_content(path, old_content, new_content) path = File.expand_path(path, __dir__) @@ -17,19 +25,6 @@ def gsub_file_content(path, old_content, new_content) File.binwrite(path, content) end -def move(old_path, new_path) - old_path = File.expand_path(old_path, __dir__) - new_path = File.expand_path(new_path, __dir__) - File.rename(old_path, new_path) -end - -# Keep shakapacker.yml since we're using Shakapacker 8+ -# move("../react_on_rails/spec/dummy/config/shakapacker.yml", "../react_on_rails/spec/dummy/config/webpacker.yml") - -# Shakapacker - use version with async script loading support (8.2.0+) -gsub_file_content("../react_on_rails/Gemfile.development_dependencies", /gem "shakapacker", "[^"]*"/, 'gem "shakapacker", "8.2.0"') -gsub_file_content("../react_on_rails/spec/dummy/package.json", /"shakapacker": "[^"]*",/, '"shakapacker": "8.2.0",') - # The below packages don't work on the oldest supported Node version and aren't needed there anyway # Note: All dev dependencies remain in root package.json even after workspace migration gsub_file_content("../package.json", /"[^"]*eslint[^"]*": "[^"]*",?/, "") @@ -42,13 +37,7 @@ gsub_file_content("../package.json", %r{"@testing-library/[^"]*": "[^"]*",}, "") # Clean up any trailing commas before closing braces gsub_file_content("../package.json", /,(\s*})/, "\\1") -# Switch to minimum supported React version (React 18 since we removed PropTypes) -gsub_file_content("../package.json", /"react": "[^"]*",/, '"react": "18.0.0",') -gsub_file_content("../package.json", /"react-dom": "[^"]*",/, '"react-dom": "18.0.0",') -gsub_file_content("../packages/react-on-rails-pro/package.json", /"react": "[^"]*",/, '"react": "18.0.0",') -gsub_file_content("../packages/react-on-rails-pro/package.json", /"react-dom": "[^"]*",/, '"react-dom": "18.0.0",') -gsub_file_content("../react_on_rails/spec/dummy/package.json", /"react": "[^"]*",/, '"react": "18.0.0",') -gsub_file_content("../react_on_rails/spec/dummy/package.json", /"react-dom": "[^"]*",/, '"react-dom": "18.0.0",') +# Pro package: Skip RSC tests on React 18 since RSC requires React 19 gsub_file_content( "../packages/react-on-rails-pro/package.json", /"test:non-rsc": "(?:\\"|[^"])*",/, From 8869a2c019372820a6c32333586d0702d625073c Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Mon, 24 Nov 2025 19:20:45 -1000 Subject: [PATCH 02/40] Add JSON parse error handling in apply_minimum_versions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Improves error handling by catching and logging JSON parse errors when reading package.json during minimum version configuration. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- react_on_rails/rakelib/shakapacker_examples.rake | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/react_on_rails/rakelib/shakapacker_examples.rake b/react_on_rails/rakelib/shakapacker_examples.rake index 33ee45be63..61a92c7e4f 100644 --- a/react_on_rails/rakelib/shakapacker_examples.rake +++ b/react_on_rails/rakelib/shakapacker_examples.rake @@ -17,11 +17,16 @@ namespace :shakapacker_examples do # rubocop:disable Metrics/BlockLength include ReactOnRails::TaskHelpers # Updates package.json to use minimum supported versions for compatibility testing - def apply_minimum_versions(dir) + def apply_minimum_versions(dir) # rubocop:disable Metrics/CyclomaticComplexity package_json_path = File.join(dir, "package.json") return unless File.exist?(package_json_path) - package_json = JSON.parse(File.read(package_json_path)) + begin + package_json = JSON.parse(File.read(package_json_path)) + rescue JSON::ParserError => e + puts " ERROR: Failed to parse package.json in #{dir}: #{e.message}" + raise + end # Update React versions to minimum supported if package_json["dependencies"] From 6f5b0e5735684cb57d7c5ad8555277c466e364fc Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Mon, 24 Nov 2025 20:11:54 -1000 Subject: [PATCH 03/40] Add minimum_versions? predicate method for Ruby idiom MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a predicate method with ? suffix for boolean checks, following Ruby convention. Update run_rspec.rake to use the predicate method with &:minimum_versions? instead of &:minimum_versions. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- react_on_rails/rakelib/example_type.rb | 5 +++++ react_on_rails/rakelib/run_rspec.rake | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/react_on_rails/rakelib/example_type.rb b/react_on_rails/rakelib/example_type.rb index dd9eadf431..a2864146d4 100644 --- a/react_on_rails/rakelib/example_type.rb +++ b/react_on_rails/rakelib/example_type.rb @@ -20,6 +20,11 @@ def self.all attr_reader :packer_type, :name, :generator_options, :minimum_versions + # Ruby convention: predicate method with ? suffix for boolean checks + def minimum_versions? + minimum_versions + end + def initialize(packer_type: nil, name: nil, generator_options: nil, minimum_versions: false) @packer_type = packer_type @name = name diff --git a/react_on_rails/rakelib/run_rspec.rake b/react_on_rails/rakelib/run_rspec.rake index 2953628fb3..4131d6cc4b 100644 --- a/react_on_rails/rakelib/run_rspec.rake +++ b/react_on_rails/rakelib/run_rspec.rake @@ -93,11 +93,11 @@ namespace :run_rspec do # Helper methods for filtering examples def latest_examples - ExampleType.all[:shakapacker_examples].reject(&:minimum_versions) + ExampleType.all[:shakapacker_examples].reject(&:minimum_versions?) end def minimum_examples - ExampleType.all[:shakapacker_examples].select(&:minimum_versions) + ExampleType.all[:shakapacker_examples].select(&:minimum_versions?) end desc "Runs Rspec for latest version example apps only (excludes minimum version tests)" From be2c39e6c1ee7832918e10c27c5094db1515bd78 Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Mon, 24 Nov 2025 22:07:28 -1000 Subject: [PATCH 04/40] Update spec/dummy for React 19 compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Upgrade react-redux from ^8.0.2 to ^9.2.0 for React 19 support - Upgrade redux from ^4.0.1 to ^5.0.1 (required by react-redux 9.x) - Upgrade redux-thunk from ^2.2.0 to ^3.1.0 (required by redux 5.x) - Replace react-helmet@^6.1.0 with @dr.pogodin/react-helmet@^3.0.4 (thread-safe React 19 compatible fork) Code changes: - Update redux-thunk imports to use named export: { thunk } - Update react-helmet SSR to use HelmetProvider with context prop - Remove @types/react-helmet (new package has built-in types) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../app-react16/startup/ReduxApp.client.jsx | 4 +-- .../client/app/components/ReactHelmet.jsx | 5 +++- .../app/startup/ReactHelmetApp.server.jsx | 25 +++++++++++++++---- .../startup/ReactHelmetAppBroken.server.jsx | 13 +++++++--- .../client/app/startup/ReduxApp.client.jsx | 4 +-- .../client/app/startup/ReduxApp.server.jsx | 4 +-- .../client/app/stores/SharedReduxStore.jsx | 4 +-- react_on_rails/spec/dummy/package.json | 9 +++---- 8 files changed, 45 insertions(+), 23 deletions(-) diff --git a/react_on_rails/spec/dummy/client/app-react16/startup/ReduxApp.client.jsx b/react_on_rails/spec/dummy/client/app-react16/startup/ReduxApp.client.jsx index b7ab770286..a96f624be7 100644 --- a/react_on_rails/spec/dummy/client/app-react16/startup/ReduxApp.client.jsx +++ b/react_on_rails/spec/dummy/client/app-react16/startup/ReduxApp.client.jsx @@ -5,7 +5,7 @@ import React from 'react'; import { combineReducers, applyMiddleware, createStore } from 'redux'; import { Provider } from 'react-redux'; -import thunkMiddleware from 'redux-thunk'; +import { thunk } from 'redux-thunk'; import ReactDOM from 'react-dom'; import reducers from '../../app/reducers/reducersIndex'; @@ -29,7 +29,7 @@ export default (props, railsContext, domNodeId) => { // This is where we'll put in the middleware for the async function. Placeholder. // store will have helloWorldData as a top level property - const store = createStore(combinedReducer, combinedProps, applyMiddleware(thunkMiddleware)); + const store = createStore(combinedReducer, combinedProps, applyMiddleware(thunk)); // renderApp is a function required for hot reloading. see // https://github.com/retroalgic/react-on-rails-hot-minimal/blob/master/client/src/entry.js diff --git a/react_on_rails/spec/dummy/client/app/components/ReactHelmet.jsx b/react_on_rails/spec/dummy/client/app/components/ReactHelmet.jsx index bba49492df..ca82611935 100644 --- a/react_on_rails/spec/dummy/client/app/components/ReactHelmet.jsx +++ b/react_on_rails/spec/dummy/client/app/components/ReactHelmet.jsx @@ -1,7 +1,10 @@ import React from 'react'; -import { Helmet } from 'react-helmet'; +import { Helmet } from '@dr.pogodin/react-helmet'; import HelloWorld from '../startup/HelloWorld'; +// Note: This component expects to be wrapped in a HelmetProvider by its parent. +// For client-side rendering, wrap in HelmetProvider at the app root. +// For server-side rendering, the server entry point provides the HelmetProvider. const ReactHelmet = (props) => (
diff --git a/react_on_rails/spec/dummy/client/app/startup/ReactHelmetApp.server.jsx b/react_on_rails/spec/dummy/client/app/startup/ReactHelmetApp.server.jsx index f7c77e8a73..6546457bca 100644 --- a/react_on_rails/spec/dummy/client/app/startup/ReactHelmetApp.server.jsx +++ b/react_on_rails/spec/dummy/client/app/startup/ReactHelmetApp.server.jsx @@ -1,8 +1,8 @@ // Top level component for simple client side only rendering import React from 'react'; import { renderToString } from 'react-dom/server'; -import { Helmet } from 'react-helmet'; -import ReactHelmet from '../components/ReactHelmet'; +import { Helmet, HelmetProvider } from '@dr.pogodin/react-helmet'; +import HelloWorld from './HelloWorld'; /* * Export a function that takes the props and returns an object with { renderedHtml } @@ -16,12 +16,27 @@ import ReactHelmet from '../components/ReactHelmet'; * the function could get the property of `.renderFunction = true` added to it. */ export default (props, _railsContext) => { - const componentHtml = renderToString(); - const helmet = Helmet.renderStatic(); + // For server-side rendering with @dr.pogodin/react-helmet, we pass a context object + // to HelmetProvider to capture the helmet data per-request (thread-safe) + const helmetContext = {}; + + const componentHtml = renderToString( + +
+ + Custom page title + + Props: {JSON.stringify(props)} + +
+
, + ); + + const { helmet } = helmetContext; const renderedHtml = { componentHtml, - title: helmet.title.toString(), + title: helmet ? helmet.title.toString() : '', }; // Note that this function returns an Object for server rendering. diff --git a/react_on_rails/spec/dummy/client/app/startup/ReactHelmetAppBroken.server.jsx b/react_on_rails/spec/dummy/client/app/startup/ReactHelmetAppBroken.server.jsx index d1c72af50d..f0d68d3e18 100644 --- a/react_on_rails/spec/dummy/client/app/startup/ReactHelmetAppBroken.server.jsx +++ b/react_on_rails/spec/dummy/client/app/startup/ReactHelmetAppBroken.server.jsx @@ -3,7 +3,7 @@ // function. The point of this is to provide a good error. import React from 'react'; import { renderToString } from 'react-dom/server'; -import { Helmet } from 'react-helmet'; +import { HelmetProvider } from '@dr.pogodin/react-helmet'; import ReactHelmet from '../components/ReactHelmet'; /* @@ -18,12 +18,17 @@ import ReactHelmet from '../components/ReactHelmet'; * Alternately, the function could get the property of `.renderFunction = true` added to it. */ export default (props) => { - const componentHtml = renderToString(); - const helmet = Helmet.renderStatic(); + const helmetContext = {}; + const componentHtml = renderToString( + + + , + ); + const { helmet } = helmetContext; const renderedHtml = { componentHtml, - title: helmet.title.toString(), + title: helmet ? helmet.title.toString() : '', }; return { renderedHtml }; }; diff --git a/react_on_rails/spec/dummy/client/app/startup/ReduxApp.client.jsx b/react_on_rails/spec/dummy/client/app/startup/ReduxApp.client.jsx index 3e8ddc211b..cfe9e24a93 100644 --- a/react_on_rails/spec/dummy/client/app/startup/ReduxApp.client.jsx +++ b/react_on_rails/spec/dummy/client/app/startup/ReduxApp.client.jsx @@ -5,7 +5,7 @@ import React from 'react'; import { combineReducers, applyMiddleware, createStore } from 'redux'; import { Provider } from 'react-redux'; -import thunkMiddleware from 'redux-thunk'; +import { thunk } from 'redux-thunk'; import ReactDOMClient from 'react-dom/client'; import reducers from '../reducers/reducersIndex'; @@ -34,7 +34,7 @@ export default (props, railsContext, domNodeId) => { // This is where we'll put in the middleware for the async function. Placeholder. // store will have helloWorldData as a top level property - const store = createStore(combinedReducer, combinedProps, applyMiddleware(thunkMiddleware)); + const store = createStore(combinedReducer, combinedProps, applyMiddleware(thunk)); // renderApp is a function required for hot reloading. see // https://github.com/retroalgic/react-on-rails-hot-minimal/blob/master/client/src/entry.js diff --git a/react_on_rails/spec/dummy/client/app/startup/ReduxApp.server.jsx b/react_on_rails/spec/dummy/client/app/startup/ReduxApp.server.jsx index bdcc317962..a26b2969f0 100644 --- a/react_on_rails/spec/dummy/client/app/startup/ReduxApp.server.jsx +++ b/react_on_rails/spec/dummy/client/app/startup/ReduxApp.server.jsx @@ -6,7 +6,7 @@ import React from 'react'; import { combineReducers, applyMiddleware, createStore } from 'redux'; import { Provider } from 'react-redux'; -import middleware from 'redux-thunk'; +import { thunk } from 'redux-thunk'; // Uses the index import reducers from '../reducers/reducersIndex'; @@ -28,7 +28,7 @@ export default (props, railsContext) => { // This is where we'll put in the middleware for the async function. Placeholder. // store will have helloWorldData as a top level property - const store = applyMiddleware(middleware)(createStore)(combinedReducer, combinedProps); + const store = applyMiddleware(thunk)(createStore)(combinedReducer, combinedProps); // Provider uses the this.props.children, so we're not typical React syntax. // This allows redux to add additional props to the HelloWorldContainer. diff --git a/react_on_rails/spec/dummy/client/app/stores/SharedReduxStore.jsx b/react_on_rails/spec/dummy/client/app/stores/SharedReduxStore.jsx index 33dcc680ed..3d463ff891 100644 --- a/react_on_rails/spec/dummy/client/app/stores/SharedReduxStore.jsx +++ b/react_on_rails/spec/dummy/client/app/stores/SharedReduxStore.jsx @@ -1,5 +1,5 @@ import { combineReducers, applyMiddleware, createStore } from 'redux'; -import middleware from 'redux-thunk'; +import { thunk } from 'redux-thunk'; import reducers from '../reducers/reducersIndex'; @@ -12,5 +12,5 @@ export default (props, railsContext) => { delete props.prerender; const combinedReducer = combineReducers(reducers); const newProps = { ...props, railsContext }; - return applyMiddleware(middleware)(createStore)(combinedReducer, newProps); + return applyMiddleware(thunk)(createStore)(combinedReducer, newProps); }; diff --git a/react_on_rails/spec/dummy/package.json b/react_on_rails/spec/dummy/package.json index d41661f9b2..16f3f804fc 100644 --- a/react_on_rails/spec/dummy/package.json +++ b/react_on_rails/spec/dummy/package.json @@ -20,12 +20,12 @@ "prop-types": "^15.7.2", "react": "^19.0.0", "react-dom": "^19.0.0", - "react-helmet": "^6.1.0", + "@dr.pogodin/react-helmet": "^3.0.4", "react-on-rails": "link:.yalc/react-on-rails", - "react-redux": "^8.0.2", + "react-redux": "^9.2.0", "react-router-dom": "^6.0.0", - "redux": "^4.0.1", - "redux-thunk": "^2.2.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", "regenerator-runtime": "^0.13.4" }, "devDependencies": { @@ -38,7 +38,6 @@ "@rescript/react": "^0.13.0", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", - "@types/react-helmet": "^6.1.5", "babel-loader": "8.2.4", "babel-plugin-transform-react-remove-prop-types": "^0.4.24", "compression-webpack-plugin": "9", From 175322f19d12da8df28378350cfe3a69d9ff784a Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Mon, 24 Nov 2025 22:30:32 -1000 Subject: [PATCH 05/40] Fix client-side HelmetProvider requirement for @dr.pogodin/react-helmet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add HelmetProvider wrapper to client-side entry points for ReactHelmet components. The @dr.pogodin/react-helmet package requires HelmetProvider to wrap all Helmet components, on both server and client sides. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../dummy/client/app/startup/ReactHelmetApp.client.jsx | 8 +++++++- .../client/app/startup/ReactHelmetAppBroken.client.jsx | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/react_on_rails/spec/dummy/client/app/startup/ReactHelmetApp.client.jsx b/react_on_rails/spec/dummy/client/app/startup/ReactHelmetApp.client.jsx index 2c5f342d92..4dd6c26570 100644 --- a/react_on_rails/spec/dummy/client/app/startup/ReactHelmetApp.client.jsx +++ b/react_on_rails/spec/dummy/client/app/startup/ReactHelmetApp.client.jsx @@ -1,11 +1,17 @@ // Top level component for simple client side only rendering import React from 'react'; +import { HelmetProvider } from '@dr.pogodin/react-helmet'; import ReactHelmet from '../components/ReactHelmet'; // This works fine, React functional component: // export default (props) => ; -export default (props) => ; +// HelmetProvider is required by @dr.pogodin/react-helmet for both client and server rendering +export default (props) => ( + + + +); // Note, the server side has to be a Render-Function diff --git a/react_on_rails/spec/dummy/client/app/startup/ReactHelmetAppBroken.client.jsx b/react_on_rails/spec/dummy/client/app/startup/ReactHelmetAppBroken.client.jsx index 2c5f342d92..4dd6c26570 100644 --- a/react_on_rails/spec/dummy/client/app/startup/ReactHelmetAppBroken.client.jsx +++ b/react_on_rails/spec/dummy/client/app/startup/ReactHelmetAppBroken.client.jsx @@ -1,11 +1,17 @@ // Top level component for simple client side only rendering import React from 'react'; +import { HelmetProvider } from '@dr.pogodin/react-helmet'; import ReactHelmet from '../components/ReactHelmet'; // This works fine, React functional component: // export default (props) => ; -export default (props) => ; +// HelmetProvider is required by @dr.pogodin/react-helmet for both client and server rendering +export default (props) => ( + + + +); // Note, the server side has to be a Render-Function From 4eb4575b2629e31a8f485967e827de2b062a8648 Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Mon, 24 Nov 2025 22:39:04 -1000 Subject: [PATCH 06/40] Fix webpack-assets-manifest version for minimum Shakapacker compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Shakapacker 8.2.0 requires webpack-assets-manifest ^5.x, but the generator creates apps with ^6.x. The apply_minimum_versions function now also downgrades webpack-assets-manifest to be compatible with Shakapacker 8.2.0. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../rakelib/shakapacker_examples.rake | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/react_on_rails/rakelib/shakapacker_examples.rake b/react_on_rails/rakelib/shakapacker_examples.rake index 61a92c7e4f..3f2d4b42ac 100644 --- a/react_on_rails/rakelib/shakapacker_examples.rake +++ b/react_on_rails/rakelib/shakapacker_examples.rake @@ -17,7 +17,8 @@ namespace :shakapacker_examples do # rubocop:disable Metrics/BlockLength include ReactOnRails::TaskHelpers # Updates package.json to use minimum supported versions for compatibility testing - def apply_minimum_versions(dir) # rubocop:disable Metrics/CyclomaticComplexity + # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity + def apply_minimum_versions(dir) package_json_path = File.join(dir, "package.json") return unless File.exist?(package_json_path) @@ -28,17 +29,25 @@ namespace :shakapacker_examples do # rubocop:disable Metrics/BlockLength raise end + deps = package_json["dependencies"] + dev_deps = package_json["devDependencies"] + # Update React versions to minimum supported - if package_json["dependencies"] - package_json["dependencies"]["react"] = ExampleType::MINIMUM_REACT_VERSION - package_json["dependencies"]["react-dom"] = ExampleType::MINIMUM_REACT_VERSION + if deps + deps["react"] = ExampleType::MINIMUM_REACT_VERSION + deps["react-dom"] = ExampleType::MINIMUM_REACT_VERSION + # Shakapacker 8.2.0 requires webpack-assets-manifest ^5.x + deps["webpack-assets-manifest"] = "^5.0.6" if deps.key?("webpack-assets-manifest") end + # Shakapacker 8.2.0 requires webpack-assets-manifest ^5.x (check devDependencies too) + dev_deps["webpack-assets-manifest"] = "^5.0.6" if dev_deps&.key?("webpack-assets-manifest") + # Update Shakapacker to minimum supported version - if package_json["devDependencies"]&.key?("shakapacker") - package_json["devDependencies"]["shakapacker"] = ExampleType::MINIMUM_SHAKAPACKER_VERSION - elsif package_json["dependencies"]&.key?("shakapacker") - package_json["dependencies"]["shakapacker"] = ExampleType::MINIMUM_SHAKAPACKER_VERSION + if dev_deps&.key?("shakapacker") + dev_deps["shakapacker"] = ExampleType::MINIMUM_SHAKAPACKER_VERSION + elsif deps&.key?("shakapacker") + deps["shakapacker"] = ExampleType::MINIMUM_SHAKAPACKER_VERSION end File.write(package_json_path, "#{JSON.pretty_generate(package_json)}\n") @@ -46,6 +55,7 @@ namespace :shakapacker_examples do # rubocop:disable Metrics/BlockLength puts " React: #{ExampleType::MINIMUM_REACT_VERSION}" puts " Shakapacker: #{ExampleType::MINIMUM_SHAKAPACKER_VERSION}" end + # rubocop:enable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity # Define tasks for each example type ExampleType.all[:shakapacker_examples].each do |example_type| From 816ae7d1f65a27394dcac12f4d1c4ee38e08f2df Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Mon, 24 Nov 2025 22:59:47 -1000 Subject: [PATCH 07/40] Fix Shakapacker gem/npm version mismatch for minimum version testing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The minimum version testing was failing because: 1. Gemfile had 'shakapacker >= 8.2.0' which installs latest (9.4.0) 2. package.json was downgraded to shakapacker 8.2.0 3. The version mismatch caused Shakapacker to error on rake tasks Solution: - apply_minimum_versions now also updates Gemfile to pin shakapacker to the exact minimum version (8.2.0) - Re-run bundle install after updating Gemfile to install the pinned version - This ensures both gem and npm package are at the same version 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../rakelib/shakapacker_examples.rake | 77 ++++++++++++------- 1 file changed, 48 insertions(+), 29 deletions(-) diff --git a/react_on_rails/rakelib/shakapacker_examples.rake b/react_on_rails/rakelib/shakapacker_examples.rake index 3f2d4b42ac..753915fd83 100644 --- a/react_on_rails/rakelib/shakapacker_examples.rake +++ b/react_on_rails/rakelib/shakapacker_examples.rake @@ -16,49 +16,64 @@ require_relative "task_helpers" namespace :shakapacker_examples do # rubocop:disable Metrics/BlockLength include ReactOnRails::TaskHelpers - # Updates package.json to use minimum supported versions for compatibility testing - # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity + # Updates package.json and Gemfile to use minimum supported versions for compatibility testing + # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity def apply_minimum_versions(dir) + # Update package.json package_json_path = File.join(dir, "package.json") - return unless File.exist?(package_json_path) + if File.exist?(package_json_path) + begin + package_json = JSON.parse(File.read(package_json_path)) + rescue JSON::ParserError => e + puts " ERROR: Failed to parse package.json in #{dir}: #{e.message}" + raise + end - begin - package_json = JSON.parse(File.read(package_json_path)) - rescue JSON::ParserError => e - puts " ERROR: Failed to parse package.json in #{dir}: #{e.message}" - raise - end + deps = package_json["dependencies"] + dev_deps = package_json["devDependencies"] - deps = package_json["dependencies"] - dev_deps = package_json["devDependencies"] + # Update React versions to minimum supported + if deps + deps["react"] = ExampleType::MINIMUM_REACT_VERSION + deps["react-dom"] = ExampleType::MINIMUM_REACT_VERSION + # Shakapacker 8.2.0 requires webpack-assets-manifest ^5.x + deps["webpack-assets-manifest"] = "^5.0.6" if deps.key?("webpack-assets-manifest") + end - # Update React versions to minimum supported - if deps - deps["react"] = ExampleType::MINIMUM_REACT_VERSION - deps["react-dom"] = ExampleType::MINIMUM_REACT_VERSION - # Shakapacker 8.2.0 requires webpack-assets-manifest ^5.x - deps["webpack-assets-manifest"] = "^5.0.6" if deps.key?("webpack-assets-manifest") - end + # Shakapacker 8.2.0 requires webpack-assets-manifest ^5.x (check devDependencies too) + dev_deps["webpack-assets-manifest"] = "^5.0.6" if dev_deps&.key?("webpack-assets-manifest") - # Shakapacker 8.2.0 requires webpack-assets-manifest ^5.x (check devDependencies too) - dev_deps["webpack-assets-manifest"] = "^5.0.6" if dev_deps&.key?("webpack-assets-manifest") + # Update Shakapacker to minimum supported version in package.json + if dev_deps&.key?("shakapacker") + dev_deps["shakapacker"] = ExampleType::MINIMUM_SHAKAPACKER_VERSION + elsif deps&.key?("shakapacker") + deps["shakapacker"] = ExampleType::MINIMUM_SHAKAPACKER_VERSION + end + + File.write(package_json_path, "#{JSON.pretty_generate(package_json)}\n") + end - # Update Shakapacker to minimum supported version - if dev_deps&.key?("shakapacker") - dev_deps["shakapacker"] = ExampleType::MINIMUM_SHAKAPACKER_VERSION - elsif deps&.key?("shakapacker") - deps["shakapacker"] = ExampleType::MINIMUM_SHAKAPACKER_VERSION + # Update Gemfile to pin shakapacker to minimum version + # (must match the npm package version exactly) + gemfile_path = File.join(dir, "Gemfile") + if File.exist?(gemfile_path) + gemfile_content = File.read(gemfile_path) + # Replace any shakapacker gem line with exact version pin + gemfile_content = gemfile_content.gsub( + /gem ['"]shakapacker['"].*$/, + "gem 'shakapacker', '#{ExampleType::MINIMUM_SHAKAPACKER_VERSION}'" + ) + File.write(gemfile_path, gemfile_content) end - File.write(package_json_path, "#{JSON.pretty_generate(package_json)}\n") puts " Updated package.json with minimum versions:" puts " React: #{ExampleType::MINIMUM_REACT_VERSION}" puts " Shakapacker: #{ExampleType::MINIMUM_SHAKAPACKER_VERSION}" end - # rubocop:enable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity + # rubocop:enable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity # Define tasks for each example type - ExampleType.all[:shakapacker_examples].each do |example_type| + ExampleType.all[:shakapacker_examples].each do |example_type| # rubocop:disable Metrics/BlockLength relative_gem_root = Pathname(gem_root).relative_path_from(Pathname(example_type.dir)) # CLOBBER desc "Clobbers (deletes) #{example_type.name_pretty}" @@ -90,7 +105,11 @@ namespace :shakapacker_examples do # rubocop:disable Metrics/BlockLength sh_in_dir(example_type.dir, generator_commands) # Apply minimum versions for compatibility testing examples - apply_minimum_versions(example_type.dir) if example_type.minimum_versions + if example_type.minimum_versions + apply_minimum_versions(example_type.dir) + # Re-run bundle install since Gemfile was updated with pinned shakapacker version + bundle_install_in(example_type.dir) + end sh_in_dir(example_type.dir, "npm install") # Generate the component packs after running the generator to ensure all From 7bc7ff76f80bb1466b0f69251eb20800e93cecc0 Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Mon, 24 Nov 2025 23:11:53 -1000 Subject: [PATCH 08/40] Fix bundler isolation for generated example apps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use BUNDLE_GEMFILE to explicitly point to the generated app's Gemfile when running rake tasks. This prevents bundler from using gems from a parent workspace's vendor/bundle, which could have different versions. This fixes the Shakapacker version mismatch error in CI where the generated app had shakapacker 8.2.0 installed but Rails was loading shakapacker 9.4.0 from the parent react_on_rails vendor/bundle. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- react_on_rails/rakelib/shakapacker_examples.rake | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/react_on_rails/rakelib/shakapacker_examples.rake b/react_on_rails/rakelib/shakapacker_examples.rake index 753915fd83..4139db58f9 100644 --- a/react_on_rails/rakelib/shakapacker_examples.rake +++ b/react_on_rails/rakelib/shakapacker_examples.rake @@ -113,8 +113,11 @@ namespace :shakapacker_examples do # rubocop:disable Metrics/BlockLength sh_in_dir(example_type.dir, "npm install") # Generate the component packs after running the generator to ensure all - # auto-bundled components have corresponding pack files created - sh_in_dir(example_type.dir, "bundle exec rake react_on_rails:generate_packs") + # auto-bundled components have corresponding pack files created. + # Use BUNDLE_GEMFILE to ensure bundler uses the generated app's Gemfile + # and not any parent workspace's bundle, which could have different gem versions. + gemfile_path = File.join(example_type.dir, "Gemfile") + sh_in_dir(example_type.dir, "BUNDLE_GEMFILE=#{gemfile_path} bundle exec rake react_on_rails:generate_packs") end end From d62b9aa06757f2a9b728a2ad6cafc30cfe3af96c Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Mon, 24 Nov 2025 23:19:27 -1000 Subject: [PATCH 09/40] Only use BUNDLE_GEMFILE isolation for minimum version examples MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The BUNDLE_GEMFILE env var was causing issues for latest version examples because bundler was detecting Gemfile/lockfile mismatches in frozen mode. Changes: - Only set BUNDLE_GEMFILE and BUNDLE_FROZEN=false for minimum version examples - Latest examples continue using standard bundle exec This ensures: - Latest examples work normally without bundler isolation issues - Minimum examples are properly isolated from parent workspace gems 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- react_on_rails/rakelib/shakapacker_examples.rake | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/react_on_rails/rakelib/shakapacker_examples.rake b/react_on_rails/rakelib/shakapacker_examples.rake index 4139db58f9..3d121e0a1e 100644 --- a/react_on_rails/rakelib/shakapacker_examples.rake +++ b/react_on_rails/rakelib/shakapacker_examples.rake @@ -114,10 +114,17 @@ namespace :shakapacker_examples do # rubocop:disable Metrics/BlockLength sh_in_dir(example_type.dir, "npm install") # Generate the component packs after running the generator to ensure all # auto-bundled components have corresponding pack files created. - # Use BUNDLE_GEMFILE to ensure bundler uses the generated app's Gemfile - # and not any parent workspace's bundle, which could have different gem versions. - gemfile_path = File.join(example_type.dir, "Gemfile") - sh_in_dir(example_type.dir, "BUNDLE_GEMFILE=#{gemfile_path} bundle exec rake react_on_rails:generate_packs") + if example_type.minimum_versions + # For minimum version examples, use BUNDLE_GEMFILE to ensure bundler uses + # the generated app's Gemfile and not any parent workspace's bundle, + # which could have different gem versions. Also use BUNDLE_FROZEN=false + # to allow the lockfile to be updated. + gemfile_path = File.join(example_type.dir, "Gemfile") + sh_in_dir(example_type.dir, + "BUNDLE_GEMFILE=#{gemfile_path} BUNDLE_FROZEN=false bundle exec rake react_on_rails:generate_packs") + else + sh_in_dir(example_type.dir, "bundle exec rake react_on_rails:generate_packs") + end end end From d881bf118334cd27d398ece4eddbdab7a15cc094 Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Tue, 25 Nov 2025 21:59:55 -1000 Subject: [PATCH 10/40] Update pnpm lockfile for React 19.0.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The lockfile was generated with React 18.0.0 but package.json specifies ^19.0.0. This caused CI failures with --frozen-lockfile. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- pnpm-lock.yaml | 187 +++++++++++++++---------------------------------- 1 file changed, 58 insertions(+), 129 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e47b2c184e..b79d764a20 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -56,7 +56,7 @@ importers: version: 6.9.1 '@testing-library/react': specifier: ^16.2.0 - version: 16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.0.0(react@18.0.0))(react@18.0.0) + version: 16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@tsconfig/node14': specifier: ^14.1.2 version: 14.1.8 @@ -98,7 +98,7 @@ importers: version: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-jest: specifier: ^28.11.0 - version: 28.14.0(@typescript-eslint/eslint-plugin@8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(jest@29.7.0(@types/node@20.19.25)(babel-plugin-macros@3.1.0))(typescript@5.9.3) + version: 28.14.0(@typescript-eslint/eslint-plugin@8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(jest@29.7.0(@types/node@20.19.25))(typescript@5.9.3) eslint-plugin-jsx-a11y: specifier: ^6.10.2 version: 6.10.2(eslint@9.39.1(jiti@2.6.1)) @@ -119,7 +119,7 @@ importers: version: 16.5.0 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@20.19.25)(babel-plugin-macros@3.1.0) + version: 29.7.0(@types/node@20.19.25) jest-environment-jsdom: specifier: ^29.7.0 version: 29.7.0 @@ -148,11 +148,14 @@ importers: specifier: ^0.3.4 version: 0.3.15 react: - specifier: 18.0.0 - version: 18.0.0 + specifier: ^19.0.0 + version: 19.2.0 react-dom: - specifier: 18.0.0 - version: 18.0.0(react@18.0.0) + specifier: ^19.0.0 + version: 19.2.0(react@19.2.0) + react-on-rails-rsc: + specifier: 19.0.2 + version: 19.0.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(webpack@5.103.0(@swc/core@1.15.3)) redux: specifier: ^4.2.1 version: 4.2.1 @@ -170,7 +173,7 @@ importers: version: 0.2.6(@swc/core@1.15.3)(webpack@5.103.0(@swc/core@1.15.3)) ts-jest: specifier: ^29.2.5 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.25)(babel-plugin-macros@3.1.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.25))(typescript@5.9.3) typescript: specifier: ^5.8.3 version: 5.9.3 @@ -186,12 +189,24 @@ importers: react-dom: specifier: '>= 16' version: 19.2.0(react@19.2.0) + react-on-rails-rsc: + specifier: 19.0.2 + version: 19.0.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(webpack@5.103.0(@swc/core@1.15.3)) packages/react-on-rails-pro: dependencies: + react: + specifier: '>= 16' + version: 19.2.0 + react-dom: + specifier: '>= 16' + version: 19.2.0(react@19.2.0) react-on-rails: specifier: workspace:* version: link:../react-on-rails + react-on-rails-rsc: + specifier: 19.0.2 + version: 19.0.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(webpack@5.103.0(@swc/core@1.15.3)) devDependencies: '@types/mock-fs': specifier: ^4.13.4 @@ -199,15 +214,6 @@ importers: mock-fs: specifier: ^5.5.0 version: 5.5.0 - react: - specifier: ^19.0.1 - version: 19.2.0 - react-dom: - specifier: ^19.0.1 - version: 19.2.0(react@19.2.0) - react-on-rails-rsc: - specifier: ^19.0.3 - version: 19.0.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(webpack@5.103.0(@swc/core@1.15.3)) packages/react-on-rails-pro-node-renderer: dependencies: @@ -1647,12 +1653,6 @@ packages: '@types/node@20.19.25': resolution: {integrity: sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==} - '@types/node@24.10.1': - resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==} - - '@types/parse-json@4.0.2': - resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} - '@types/prop-types@15.7.15': resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} @@ -2137,10 +2137,6 @@ packages: resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - babel-plugin-macros@3.1.0: - resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} - engines: {node: '>=10', npm: '>=6'} - babel-plugin-polyfill-corejs2@0.4.14: resolution: {integrity: sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==} peerDependencies: @@ -2406,10 +2402,6 @@ packages: core-js-compat@3.47.0: resolution: {integrity: sha512-IGfuznZ/n7Kp9+nypamBhvwdwLsW6KC8IOaURw2doAK5e98AG3acVLdh0woOnEqCfUtS+Vu882JE4k/DAm3ItQ==} - cosmiconfig@7.1.0: - resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} - engines: {node: '>=10'} - cosmiconfig@9.0.0: resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} engines: {node: '>=14'} @@ -4414,11 +4406,6 @@ packages: randombytes@2.1.0: resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} - react-dom@18.0.0: - resolution: {integrity: sha512-XqX7uzmFo0pUceWFCt7Gff6IyIMzFUn7QMZrbrQfGxtaxXZIcGQzoNpRLE3fQLnS4XzLLPMZX2T9TRcSrasicw==} - peerDependencies: - react: ^18.0.0 - react-dom@19.2.0: resolution: {integrity: sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==} peerDependencies: @@ -4433,17 +4420,13 @@ packages: react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} - react-on-rails-rsc@19.0.3: - resolution: {integrity: sha512-g+89U83+WjZDbtLvYQbjld0pWdUXpKageSoeKsX8cj1SkmULMAzbxgvH6vdzOuQUSwchkbDgwFO9umlHDhiyug==} + react-on-rails-rsc@19.0.2: + resolution: {integrity: sha512-0q26jcWcr6v9nfYfB4wxtAdTwEC4PCDSb/5U7TPperP4Ac9U2K7nt3uLOSVh7BX4bacX3PrpDeI1C30cIkBPog==} peerDependencies: - react: ^19.0.1 - react-dom: ^19.0.1 + react: ^19.0.0 + react-dom: ^19.0.0 webpack: ^5.59.0 - react@18.0.0: - resolution: {integrity: sha512-x+VL6wbT4JRVPm7EGxXhZ8w8LTROaxPXOqhlGyVSrv0sB1jkyFGgXxJ8LVoPRLvPR6/CIZGFmfzqUa2NYeMr2A==} - engines: {node: '>=0.10.0'} - react@19.2.0: resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==} engines: {node: '>=0.10.0'} @@ -4589,9 +4572,6 @@ packages: resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} engines: {node: '>=v12.22.7'} - scheduler@0.21.0: - resolution: {integrity: sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ==} - scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -5115,9 +5095,6 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - undici-types@7.16.0: - resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} - unicode-canonical-property-names-ecmascript@2.0.1: resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==} engines: {node: '>=4'} @@ -5361,10 +5338,6 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - yaml@1.10.2: - resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} - engines: {node: '>= 6'} - yargs-parser@15.0.3: resolution: {integrity: sha512-/MVEVjTXy/cGAjdtQf8dW3V9b97bPN7rNn8ETj6BmAQL7ibC7O1Q9SPJbGjgh3SlwoBNXMzj/ZGIj8mBgl12YA==} @@ -6417,7 +6390,7 @@ snapshots: jest-util: 29.7.0 slash: 3.0.0 - '@jest/core@29.7.0(babel-plugin-macros@3.1.0)': + '@jest/core@29.7.0': dependencies: '@jest/console': 29.7.0 '@jest/reporters': 29.7.0 @@ -6431,7 +6404,7 @@ snapshots: exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@20.19.25)(babel-plugin-macros@3.1.0) + jest-config: 29.7.0(@types/node@20.19.25) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -6891,12 +6864,12 @@ snapshots: picocolors: 1.1.1 redent: 3.0.0 - '@testing-library/react@16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.0.0(react@18.0.0))(react@18.0.0)': + '@testing-library/react@16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 '@testing-library/dom': 10.4.1 - react: 18.0.0 - react-dom: 18.0.0(react@18.0.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) optionalDependencies: '@types/react': 18.3.27 '@types/react-dom': 18.3.7(@types/react@18.3.27) @@ -6942,11 +6915,11 @@ snapshots: '@types/body-parser@1.19.6': dependencies: '@types/connect': 3.4.38 - '@types/node': 24.10.1 + '@types/node': 20.19.25 '@types/connect@3.4.38': dependencies: - '@types/node': 24.10.1 + '@types/node': 20.19.25 '@types/eslint-scope@3.7.7': dependencies: @@ -6962,7 +6935,7 @@ snapshots: '@types/express-serve-static-core@5.1.0': dependencies: - '@types/node': 24.10.1 + '@types/node': 20.19.25 '@types/qs': 6.14.0 '@types/range-parser': 1.2.7 '@types/send': 1.2.1 @@ -7032,13 +7005,6 @@ snapshots: dependencies: undici-types: 6.21.0 - '@types/node@24.10.1': - dependencies: - undici-types: 7.16.0 - - '@types/parse-json@4.0.2': - optional: true - '@types/prop-types@15.7.15': {} '@types/qs@6.14.0': {} @@ -7057,16 +7023,16 @@ snapshots: '@types/send@0.17.6': dependencies: '@types/mime': 1.3.5 - '@types/node': 24.10.1 + '@types/node': 20.19.25 '@types/send@1.2.1': dependencies: - '@types/node': 24.10.1 + '@types/node': 20.19.25 '@types/serve-static@1.15.10': dependencies: '@types/http-errors': 2.0.5 - '@types/node': 24.10.1 + '@types/node': 20.19.25 '@types/send': 0.17.6 '@types/stack-utils@2.0.3': {} @@ -7565,13 +7531,6 @@ snapshots: '@types/babel__core': 7.20.5 '@types/babel__traverse': 7.28.0 - babel-plugin-macros@3.1.0: - dependencies: - '@babel/runtime': 7.28.4 - cosmiconfig: 7.1.0 - resolve: 1.22.11 - optional: true - babel-plugin-polyfill-corejs2@0.4.14(@babel/core@7.28.5): dependencies: '@babel/compat-data': 7.28.5 @@ -7846,15 +7805,6 @@ snapshots: dependencies: browserslist: 4.28.0 - cosmiconfig@7.1.0: - dependencies: - '@types/parse-json': 4.0.2 - import-fresh: 3.3.1 - parse-json: 5.2.0 - path-type: 4.0.0 - yaml: 1.10.2 - optional: true - cosmiconfig@9.0.0(typescript@5.9.3): dependencies: env-paths: 2.2.1 @@ -7864,13 +7814,13 @@ snapshots: optionalDependencies: typescript: 5.9.3 - create-jest@29.7.0(@types/node@20.19.25)(babel-plugin-macros@3.1.0): + create-jest@29.7.0(@types/node@20.19.25): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@20.19.25)(babel-plugin-macros@3.1.0) + jest-config: 29.7.0(@types/node@20.19.25) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -7987,9 +7937,7 @@ snapshots: decimal.js@10.6.0: {} - dedent@1.7.0(babel-plugin-macros@3.1.0): - optionalDependencies: - babel-plugin-macros: 3.1.0 + dedent@1.7.0: {} deep-is@0.1.4: {} @@ -8336,13 +8284,13 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-jest@28.14.0(@typescript-eslint/eslint-plugin@8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(jest@29.7.0(@types/node@20.19.25)(babel-plugin-macros@3.1.0))(typescript@5.9.3): + eslint-plugin-jest@28.14.0(@typescript-eslint/eslint-plugin@8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(jest@29.7.0(@types/node@20.19.25))(typescript@5.9.3): dependencies: '@typescript-eslint/utils': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) optionalDependencies: '@typescript-eslint/eslint-plugin': 8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - jest: 29.7.0(@types/node@20.19.25)(babel-plugin-macros@3.1.0) + jest: 29.7.0(@types/node@20.19.25) transitivePeerDependencies: - supports-color - typescript @@ -9187,7 +9135,7 @@ snapshots: jest-util: 29.7.0 p-limit: 3.1.0 - jest-circus@29.7.0(babel-plugin-macros@3.1.0): + jest-circus@29.7.0: dependencies: '@jest/environment': 29.7.0 '@jest/expect': 29.7.0 @@ -9196,7 +9144,7 @@ snapshots: '@types/node': 20.19.25 chalk: 4.1.2 co: 4.6.0 - dedent: 1.7.0(babel-plugin-macros@3.1.0) + dedent: 1.7.0 is-generator-fn: 2.1.0 jest-each: 29.7.0 jest-matcher-utils: 29.7.0 @@ -9213,16 +9161,16 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@20.19.25)(babel-plugin-macros@3.1.0): + jest-cli@29.7.0(@types/node@20.19.25): dependencies: - '@jest/core': 29.7.0(babel-plugin-macros@3.1.0) + '@jest/core': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@20.19.25)(babel-plugin-macros@3.1.0) + create-jest: 29.7.0(@types/node@20.19.25) exit: 0.1.2 import-local: 3.2.0 - jest-config: 29.7.0(@types/node@20.19.25)(babel-plugin-macros@3.1.0) + jest-config: 29.7.0(@types/node@20.19.25) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -9232,7 +9180,7 @@ snapshots: - supports-color - ts-node - jest-config@29.7.0(@types/node@20.19.25)(babel-plugin-macros@3.1.0): + jest-config@29.7.0(@types/node@20.19.25): dependencies: '@babel/core': 7.28.5 '@jest/test-sequencer': 29.7.0 @@ -9243,7 +9191,7 @@ snapshots: deepmerge: 4.3.1 glob: 7.2.3 graceful-fs: 4.2.11 - jest-circus: 29.7.0(babel-plugin-macros@3.1.0) + jest-circus: 29.7.0 jest-environment-node: 29.7.0 jest-get-type: 29.6.3 jest-regex-util: 29.6.3 @@ -9501,7 +9449,7 @@ snapshots: jest-worker@27.5.1: dependencies: - '@types/node': 24.10.1 + '@types/node': 20.19.25 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -9512,12 +9460,12 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0(@types/node@20.19.25)(babel-plugin-macros@3.1.0): + jest@29.7.0(@types/node@20.19.25): dependencies: - '@jest/core': 29.7.0(babel-plugin-macros@3.1.0) + '@jest/core': 29.7.0 '@jest/types': 29.6.3 import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@20.19.25)(babel-plugin-macros@3.1.0) + jest-cli: 29.7.0(@types/node@20.19.25) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -10355,12 +10303,6 @@ snapshots: dependencies: safe-buffer: 5.2.1 - react-dom@18.0.0(react@18.0.0): - dependencies: - loose-envify: 1.4.0 - react: 18.0.0 - scheduler: 0.21.0 - react-dom@19.2.0(react@19.2.0): dependencies: react: 19.2.0 @@ -10372,7 +10314,7 @@ snapshots: react-is@18.3.1: {} - react-on-rails-rsc@19.0.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(webpack@5.103.0(@swc/core@1.15.3)): + react-on-rails-rsc@19.0.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(webpack@5.103.0(@swc/core@1.15.3)): dependencies: acorn-loose: 8.5.2 neo-async: 2.6.2 @@ -10381,10 +10323,6 @@ snapshots: webpack: 5.103.0(@swc/core@1.15.3) webpack-sources: 3.3.3 - react@18.0.0: - dependencies: - loose-envify: 1.4.0 - react@19.2.0: {} readline-sync@1.4.10: {} @@ -10534,10 +10472,6 @@ snapshots: dependencies: xmlchars: 2.2.0 - scheduler@0.21.0: - dependencies: - loose-envify: 1.4.0 - scheduler@0.27.0: {} schema-utils@4.3.3: @@ -11049,12 +10983,12 @@ snapshots: dependencies: typescript: 5.9.3 - ts-jest@29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.25)(babel-plugin-macros@3.1.0))(typescript@5.9.3): + ts-jest@29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.25))(typescript@5.9.3): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 handlebars: 4.7.8 - jest: 29.7.0(@types/node@20.19.25)(babel-plugin-macros@3.1.0) + jest: 29.7.0(@types/node@20.19.25) json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 @@ -11154,8 +11088,6 @@ snapshots: undici-types@6.21.0: {} - undici-types@7.16.0: {} - unicode-canonical-property-names-ecmascript@2.0.1: {} unicode-emoji-modifier-base@1.0.0: {} @@ -11421,9 +11353,6 @@ snapshots: yallist@3.1.1: {} - yaml@1.10.2: - optional: true - yargs-parser@15.0.3: dependencies: camelcase: 5.3.1 From b255da821a24dc7fbaae31d8daac43da3133a112 Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Tue, 25 Nov 2025 22:13:38 -1000 Subject: [PATCH 11/40] Trigger CI From 7582e68cc54030cfbadc54b99892d5bdd98f6ebd Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Tue, 25 Nov 2025 22:25:45 -1000 Subject: [PATCH 12/40] Fix bundler isolation for generate_packs in example apps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use unbundled_sh_in_dir to properly isolate the bundler environment when running generate_packs in generated example apps. The previous approach with BUNDLE_GEMFILE env var didn't work because bundler still used cached gem paths from the parent rake context. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- react_on_rails/rakelib/shakapacker_examples.rake | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/react_on_rails/rakelib/shakapacker_examples.rake b/react_on_rails/rakelib/shakapacker_examples.rake index 3d121e0a1e..31455b3c7a 100644 --- a/react_on_rails/rakelib/shakapacker_examples.rake +++ b/react_on_rails/rakelib/shakapacker_examples.rake @@ -114,17 +114,9 @@ namespace :shakapacker_examples do # rubocop:disable Metrics/BlockLength sh_in_dir(example_type.dir, "npm install") # Generate the component packs after running the generator to ensure all # auto-bundled components have corresponding pack files created. - if example_type.minimum_versions - # For minimum version examples, use BUNDLE_GEMFILE to ensure bundler uses - # the generated app's Gemfile and not any parent workspace's bundle, - # which could have different gem versions. Also use BUNDLE_FROZEN=false - # to allow the lockfile to be updated. - gemfile_path = File.join(example_type.dir, "Gemfile") - sh_in_dir(example_type.dir, - "BUNDLE_GEMFILE=#{gemfile_path} BUNDLE_FROZEN=false bundle exec rake react_on_rails:generate_packs") - else - sh_in_dir(example_type.dir, "bundle exec rake react_on_rails:generate_packs") - end + # Use unbundled_sh_in_dir to ensure we're using the generated app's Gemfile + # and gem versions, not the parent workspace's bundle context. + unbundled_sh_in_dir(example_type.dir, "bundle exec rake react_on_rails:generate_packs") end end From f666831a33103a055dfabc2744aca0b5b75be65c Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Tue, 25 Nov 2025 22:31:49 -1000 Subject: [PATCH 13/40] Run RSpec for minimum version examples with bundler isolation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add unbundled option to run_tests_in to use unbundled_sh_in_dir for minimum version example tests. This ensures the example app's Gemfile and gem versions are used (e.g., Shakapacker 8.2.0) instead of the parent workspace's bundle context (which has Shakapacker 9.4.0). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- react_on_rails/rakelib/run_rspec.rake | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/react_on_rails/rakelib/run_rspec.rake b/react_on_rails/rakelib/run_rspec.rake index 4131d6cc4b..1584180c51 100644 --- a/react_on_rails/rakelib/run_rspec.rake +++ b/react_on_rails/rakelib/run_rspec.rake @@ -82,7 +82,10 @@ namespace :run_rspec do puts "Creating #{example_type.rspec_task_name} task" desc "Runs RSpec for #{example_type.name_pretty} only" task example_type.rspec_task_name_short => example_type.gen_task_name do - run_tests_in(File.join(examples_dir, example_type.name)) # have to use relative path + # Use unbundled mode for minimum version examples to ensure the example app's + # Gemfile and gem versions are used, not the parent workspace's bundle + run_tests_in(File.join(examples_dir, example_type.name), + unbundled: example_type.minimum_versions?) end end @@ -158,11 +161,17 @@ end # If string is passed and it's not absolute, it's converted relative to root of the gem. # TEST_ENV_COMMAND_NAME is used to make SimpleCov.command_name unique in order to # prevent a name collision. Defaults to the given directory's name. +# Options: +# :command_name - name for SimpleCov (default: dir basename) +# :rspec_args - additional rspec arguments (default: "") +# :env_vars - additional environment variables (default: "") +# :unbundled - run with unbundled_sh_in_dir for Bundler isolation (default: false) def run_tests_in(dir, options = {}) path = calc_path(dir) command_name = options.fetch(:command_name, path.basename) rspec_args = options.fetch(:rspec_args, "") + unbundled = options.fetch(:unbundled, false) # Build environment variables as an array for proper spacing env_tokens = [] @@ -171,5 +180,11 @@ def run_tests_in(dir, options = {}) env_tokens << "COVERAGE=true" if ENV["USE_COVERALLS"] env_vars = env_tokens.join(" ") - sh_in_dir(path.realpath, "#{env_vars} bundle exec rspec #{rspec_args}") + command = "#{env_vars} bundle exec rspec #{rspec_args}" + + if unbundled + unbundled_sh_in_dir(path.realpath, command) + else + sh_in_dir(path.realpath, command) + end end From 3ca2f9f5b8c3c81a3e351a55aef165d2a188d150 Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Tue, 25 Nov 2025 22:38:36 -1000 Subject: [PATCH 14/40] Remove deprecated chromedriver-helper from dev_tests generator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit chromedriver-helper was deprecated in March 2019 and is incompatible with modern selenium-webdriver (4.x). The gem tries to set Selenium::WebDriver::Chrome.driver_path which no longer exists. Modern selenium-webdriver uses webdriver-manager internally for driver management. GitHub Actions and most CI environments have Chrome and ChromeDriver pre-installed, so no driver helper gem is needed. This fixes the minimum version example tests which were failing with: "undefined method `driver_path=' for Selenium::WebDriver::Chrome:Module" 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../lib/generators/react_on_rails/dev_tests_generator.rb | 3 ++- .../spec/react_on_rails/generators/dev_tests_generator_spec.rb | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/react_on_rails/lib/generators/react_on_rails/dev_tests_generator.rb b/react_on_rails/lib/generators/react_on_rails/dev_tests_generator.rb index 253a2e2476..fd63c05867 100644 --- a/react_on_rails/lib/generators/react_on_rails/dev_tests_generator.rb +++ b/react_on_rails/lib/generators/react_on_rails/dev_tests_generator.rb @@ -31,7 +31,8 @@ def copy_tests def add_test_related_gems_to_gemfile gem("rspec-rails", group: :test) - gem("chromedriver-helper", group: :test) + # NOTE: chromedriver-helper was deprecated in 2019. Modern selenium-webdriver (4.x) + # and GitHub Actions have built-in driver management, so no driver helper is needed. gem("coveralls", require: false) end diff --git a/react_on_rails/spec/react_on_rails/generators/dev_tests_generator_spec.rb b/react_on_rails/spec/react_on_rails/generators/dev_tests_generator_spec.rb index 5a100a5dba..e13ced2178 100644 --- a/react_on_rails/spec/react_on_rails/generators/dev_tests_generator_spec.rb +++ b/react_on_rails/spec/react_on_rails/generators/dev_tests_generator_spec.rb @@ -34,7 +34,8 @@ assert_file("Gemfile") do |contents| expect(contents).to match("gem \"rspec-rails\", group: :test") expect(contents).to match("gem \"coveralls\", require: false") - expect(contents).to match("gem \"chromedriver-helper\", group: :test") + # chromedriver-helper was removed as it's deprecated since 2019 + # Modern selenium-webdriver (4.x) handles driver management automatically end end end From 12fc9d25dbcf2e41aafb5f201dcbb36ef18abc1e Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Thu, 4 Dec 2025 18:53:30 -1000 Subject: [PATCH 15/40] Address PR review comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Update @types/react and @types/react-dom to v19 to match React 19 runtime - Fix Gemfile regex to handle multi-line gem declarations - Document why bundle isolation is only needed for minimum version examples - Improve script/convert fallback logic to handle all file path patterns with warning when file not found 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- pnpm-lock.yaml | 38 ++++++++----------- react_on_rails/rakelib/run_rspec.rake | 6 +++ .../rakelib/shakapacker_examples.rake | 6 ++- script/convert | 23 +++++++++-- 4 files changed, 47 insertions(+), 26 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b79d764a20..53dc9d710e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -56,7 +56,7 @@ importers: version: 6.9.1 '@testing-library/react': specifier: ^16.2.0 - version: 16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@tsconfig/node14': specifier: ^14.1.2 version: 14.1.8 @@ -67,11 +67,11 @@ importers: specifier: ^20.17.16 version: 20.19.25 '@types/react': - specifier: ^18.3.18 - version: 18.3.27 + specifier: ^19.0.0 + version: 19.2.7 '@types/react-dom': - specifier: ^18.3.5 - version: 18.3.7(@types/react@18.3.27) + specifier: ^19.0.0 + version: 19.2.3(@types/react@19.2.7) '@types/turbolinks': specifier: ^5.2.2 version: 5.2.2 @@ -1653,22 +1653,19 @@ packages: '@types/node@20.19.25': resolution: {integrity: sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==} - '@types/prop-types@15.7.15': - resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} - '@types/qs@6.14.0': resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} '@types/range-parser@1.2.7': resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} - '@types/react-dom@18.3.7': - resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: - '@types/react': ^18.0.0 + '@types/react': ^19.2.0 - '@types/react@18.3.27': - resolution: {integrity: sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==} + '@types/react@19.2.7': + resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==} '@types/send@0.17.6': resolution: {integrity: sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==} @@ -6864,15 +6861,15 @@ snapshots: picocolors: 1.1.1 redent: 3.0.0 - '@testing-library/react@16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@testing-library/react@16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 '@testing-library/dom': 10.4.1 react: 19.2.0 react-dom: 19.2.0(react@19.2.0) optionalDependencies: - '@types/react': 18.3.27 - '@types/react-dom': 18.3.7(@types/react@18.3.27) + '@types/react': 19.2.7 + '@types/react-dom': 19.2.3(@types/react@19.2.7) '@tootallnate/once@1.1.2': {} @@ -7005,19 +7002,16 @@ snapshots: dependencies: undici-types: 6.21.0 - '@types/prop-types@15.7.15': {} - '@types/qs@6.14.0': {} '@types/range-parser@1.2.7': {} - '@types/react-dom@18.3.7(@types/react@18.3.27)': + '@types/react-dom@19.2.3(@types/react@19.2.7)': dependencies: - '@types/react': 18.3.27 + '@types/react': 19.2.7 - '@types/react@18.3.27': + '@types/react@19.2.7': dependencies: - '@types/prop-types': 15.7.15 csstype: 3.2.3 '@types/send@0.17.6': diff --git a/react_on_rails/rakelib/run_rspec.rake b/react_on_rails/rakelib/run_rspec.rake index 1584180c51..7569f3bb7d 100644 --- a/react_on_rails/rakelib/run_rspec.rake +++ b/react_on_rails/rakelib/run_rspec.rake @@ -166,6 +166,12 @@ end # :rspec_args - additional rspec arguments (default: "") # :env_vars - additional environment variables (default: "") # :unbundled - run with unbundled_sh_in_dir for Bundler isolation (default: false) +# This is required for minimum version examples because they have different +# gem versions (e.g., Shakapacker 8.2.0) pinned in their Gemfile than the +# parent workspace (Shakapacker 9.x). Without bundle isolation, Bundler +# would inherit the parent's gem resolution and use the wrong versions. +# Latest version examples don't need this because they use the same versions +# as the parent workspace. def run_tests_in(dir, options = {}) path = calc_path(dir) diff --git a/react_on_rails/rakelib/shakapacker_examples.rake b/react_on_rails/rakelib/shakapacker_examples.rake index 31455b3c7a..d0d7fbb067 100644 --- a/react_on_rails/rakelib/shakapacker_examples.rake +++ b/react_on_rails/rakelib/shakapacker_examples.rake @@ -59,8 +59,12 @@ namespace :shakapacker_examples do # rubocop:disable Metrics/BlockLength if File.exist?(gemfile_path) gemfile_content = File.read(gemfile_path) # Replace any shakapacker gem line with exact version pin + # Handle both single-line: gem 'shakapacker', '>= 8.2.0' + # And multi-line declarations: + # gem 'shakapacker', + # '>= 8.2.0' gemfile_content = gemfile_content.gsub( - /gem ['"]shakapacker['"].*$/, + /gem ['"]shakapacker['"][^\n]*(?:\n\s+[^g\n][^\n]*)*$/m, "gem 'shakapacker', '#{ExampleType::MINIMUM_SHAKAPACKER_VERSION}'" ) File.write(gemfile_path, gemfile_content) diff --git a/script/convert b/script/convert index 0e4622e2a5..17ed6ab21b 100755 --- a/script/convert +++ b/script/convert @@ -15,9 +15,26 @@ def gsub_file_content(path, old_content, new_content) # Support both old structure (files at root) and new structure (files in react_on_rails/) # This allows the script to work before and after the monorepo reorganization unless File.exist?(path) - # Try the old location by removing one level of nesting - old_path = path.sub(%r{/react_on_rails/Gemfile}, "/Gemfile") - path = old_path if File.exist?(old_path) + # Try alternate locations: + # 1. For react_on_rails subdirectory paths, try removing that nesting + # 2. For packages/* paths, try the old node_package/ location + alternate_paths = [ + path.sub(%r{/react_on_rails/}, "/"), + path.sub(%r{/packages/react-on-rails/}, "/node_package/"), + path.sub(%r{/packages/react-on-rails-pro/}, "/react_on_rails_pro/node_package/") + ] + + alternate_paths.each do |alt_path| + if File.exist?(alt_path) + path = alt_path + break + end + end + end + + unless File.exist?(path) + warn "Warning: File not found: #{path}" + return end content = File.binread(path) From f36c4ff66a7d03b47bb08bbb976df678c7eb0353 Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Sat, 6 Dec 2025 23:09:43 -1000 Subject: [PATCH 16/40] Address PR review nitpicks and style suggestions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Refactor apply_minimum_versions into focused helper methods: - update_react_dependencies: Updates React and React-DOM versions - update_shakapacker_dependency: Updates Shakapacker in deps or devDeps - update_package_json_versions: Parses JSON and coordinates updates - update_gemfile_versions: Updates Gemfile with pinned version - apply_minimum_versions: Orchestrates both file updates This eliminates all RuboCop metric disables and improves testability. 2. Improve error message to include full package.json path instead of just the directory, making debugging easier. 3. Fix task description symmetry - both latest and minimum version task descriptions now specify exact versions for clarity: - latest: "React 19, Shakapacker 9.4.0" - minimum: "React 18, Shakapacker 8.2.0" 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- react_on_rails/rakelib/run_rspec.rake | 2 +- .../rakelib/shakapacker_examples.rake | 107 ++++++++++-------- 2 files changed, 60 insertions(+), 49 deletions(-) diff --git a/react_on_rails/rakelib/run_rspec.rake b/react_on_rails/rakelib/run_rspec.rake index 7569f3bb7d..53e0b8d1f8 100644 --- a/react_on_rails/rakelib/run_rspec.rake +++ b/react_on_rails/rakelib/run_rspec.rake @@ -103,7 +103,7 @@ namespace :run_rspec do ExampleType.all[:shakapacker_examples].select(&:minimum_versions?) end - desc "Runs Rspec for latest version example apps only (excludes minimum version tests)" + desc "Runs Rspec for latest version example apps only (React 19, Shakapacker 9.4.0)" task shakapacker_examples_latest: latest_examples.map(&:gen_task_name) do latest_examples.each { |example_type| Rake::Task[example_type.rspec_task_name].invoke } end diff --git a/react_on_rails/rakelib/shakapacker_examples.rake b/react_on_rails/rakelib/shakapacker_examples.rake index d0d7fbb067..0265f014d6 100644 --- a/react_on_rails/rakelib/shakapacker_examples.rake +++ b/react_on_rails/rakelib/shakapacker_examples.rake @@ -16,65 +16,74 @@ require_relative "task_helpers" namespace :shakapacker_examples do # rubocop:disable Metrics/BlockLength include ReactOnRails::TaskHelpers - # Updates package.json and Gemfile to use minimum supported versions for compatibility testing - # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity - def apply_minimum_versions(dir) - # Update package.json - package_json_path = File.join(dir, "package.json") - if File.exist?(package_json_path) - begin - package_json = JSON.parse(File.read(package_json_path)) - rescue JSON::ParserError => e - puts " ERROR: Failed to parse package.json in #{dir}: #{e.message}" - raise - end + # Updates React-related dependencies to minimum supported versions + def update_react_dependencies(deps) + return unless deps + + deps["react"] = ExampleType::MINIMUM_REACT_VERSION + deps["react-dom"] = ExampleType::MINIMUM_REACT_VERSION + # Shakapacker 8.2.0 requires webpack-assets-manifest ^5.x + deps["webpack-assets-manifest"] = "^5.0.6" if deps.key?("webpack-assets-manifest") + end - deps = package_json["dependencies"] - dev_deps = package_json["devDependencies"] + # Updates Shakapacker to minimum supported version in either dependencies or devDependencies + def update_shakapacker_dependency(deps, dev_deps) + if dev_deps&.key?("shakapacker") + dev_deps["shakapacker"] = ExampleType::MINIMUM_SHAKAPACKER_VERSION + elsif deps&.key?("shakapacker") + deps["shakapacker"] = ExampleType::MINIMUM_SHAKAPACKER_VERSION + end + end - # Update React versions to minimum supported - if deps - deps["react"] = ExampleType::MINIMUM_REACT_VERSION - deps["react-dom"] = ExampleType::MINIMUM_REACT_VERSION - # Shakapacker 8.2.0 requires webpack-assets-manifest ^5.x - deps["webpack-assets-manifest"] = "^5.0.6" if deps.key?("webpack-assets-manifest") - end + # Updates dependencies in package.json to use minimum supported versions + def update_package_json_versions(package_json_path) + return unless File.exist?(package_json_path) - # Shakapacker 8.2.0 requires webpack-assets-manifest ^5.x (check devDependencies too) - dev_deps["webpack-assets-manifest"] = "^5.0.6" if dev_deps&.key?("webpack-assets-manifest") + begin + package_json = JSON.parse(File.read(package_json_path)) + rescue JSON::ParserError => e + puts " ERROR: Failed to parse #{package_json_path}: #{e.message}" + raise + end - # Update Shakapacker to minimum supported version in package.json - if dev_deps&.key?("shakapacker") - dev_deps["shakapacker"] = ExampleType::MINIMUM_SHAKAPACKER_VERSION - elsif deps&.key?("shakapacker") - deps["shakapacker"] = ExampleType::MINIMUM_SHAKAPACKER_VERSION - end + deps = package_json["dependencies"] + dev_deps = package_json["devDependencies"] - File.write(package_json_path, "#{JSON.pretty_generate(package_json)}\n") - end + update_react_dependencies(deps) + # Shakapacker 8.2.0 requires webpack-assets-manifest ^5.x (check devDependencies too) + dev_deps["webpack-assets-manifest"] = "^5.0.6" if dev_deps&.key?("webpack-assets-manifest") + update_shakapacker_dependency(deps, dev_deps) - # Update Gemfile to pin shakapacker to minimum version - # (must match the npm package version exactly) - gemfile_path = File.join(dir, "Gemfile") - if File.exist?(gemfile_path) - gemfile_content = File.read(gemfile_path) - # Replace any shakapacker gem line with exact version pin - # Handle both single-line: gem 'shakapacker', '>= 8.2.0' - # And multi-line declarations: - # gem 'shakapacker', - # '>= 8.2.0' - gemfile_content = gemfile_content.gsub( - /gem ['"]shakapacker['"][^\n]*(?:\n\s+[^g\n][^\n]*)*$/m, - "gem 'shakapacker', '#{ExampleType::MINIMUM_SHAKAPACKER_VERSION}'" - ) - File.write(gemfile_path, gemfile_content) - end + File.write(package_json_path, "#{JSON.pretty_generate(package_json)}\n") + end + + # Updates Gemfile to pin shakapacker to minimum version + # (must match the npm package version exactly) + def update_gemfile_versions(gemfile_path) + return unless File.exist?(gemfile_path) + + gemfile_content = File.read(gemfile_path) + # Replace any shakapacker gem line with exact version pin + # Handle both single-line: gem 'shakapacker', '>= 8.2.0' + # And multi-line declarations: + # gem 'shakapacker', + # '>= 8.2.0' + gemfile_content = gemfile_content.gsub( + /gem ['"]shakapacker['"][^\n]*(?:\n\s+[^g\n][^\n]*)*$/m, + "gem 'shakapacker', '#{ExampleType::MINIMUM_SHAKAPACKER_VERSION}'" + ) + File.write(gemfile_path, gemfile_content) + end + + # Updates package.json and Gemfile to use minimum supported versions for compatibility testing + def apply_minimum_versions(dir) + update_package_json_versions(File.join(dir, "package.json")) + update_gemfile_versions(File.join(dir, "Gemfile")) puts " Updated package.json with minimum versions:" puts " React: #{ExampleType::MINIMUM_REACT_VERSION}" puts " Shakapacker: #{ExampleType::MINIMUM_SHAKAPACKER_VERSION}" end - # rubocop:enable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity # Define tasks for each example type ExampleType.all[:shakapacker_examples].each do |example_type| # rubocop:disable Metrics/BlockLength @@ -107,6 +116,8 @@ namespace :shakapacker_examples do # rubocop:disable Metrics/BlockLength "REACT_ON_RAILS_SKIP_VALIDATION=true #{cmd}" end sh_in_dir(example_type.dir, generator_commands) + # Re-run bundle install since dev_tests generator adds rspec-rails and coveralls to Gemfile + bundle_install_in(example_type.dir) # Apply minimum versions for compatibility testing examples if example_type.minimum_versions From a07f02223fc112788d4d9159ec71c728110a21c6 Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Sat, 6 Dec 2025 23:18:29 -1000 Subject: [PATCH 17/40] Fix TypeScript compatibility with @types/react-dom@19 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit React 19's type definitions removed the deprecated hydrate, render, and unmountComponentAtNode APIs. Added a LegacyReactDOM interface to provide types for these methods that still exist at runtime for React 16/17 compatibility. Also removes unnecessary eslint-disable directives and fixes ReactElement type parameter warnings. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- packages/react-on-rails/src/ClientRenderer.ts | 1 - packages/react-on-rails/src/reactApis.cts | 25 +++++++++++++------ packages/react-on-rails/src/types/index.ts | 4 +-- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/packages/react-on-rails/src/ClientRenderer.ts b/packages/react-on-rails/src/ClientRenderer.ts index 6b97ba6fc4..ed15dbcf43 100644 --- a/packages/react-on-rails/src/ClientRenderer.ts +++ b/packages/react-on-rails/src/ClientRenderer.ts @@ -174,7 +174,6 @@ function unmountAllComponents(): void { root.unmount(); } else { // React 16-17 legacy API - // eslint-disable-next-line @typescript-eslint/no-deprecated unmountComponentAtNode(domNode); } } catch (error) { diff --git a/packages/react-on-rails/src/reactApis.cts b/packages/react-on-rails/src/reactApis.cts index d4a258b120..ee115e16e5 100644 --- a/packages/react-on-rails/src/reactApis.cts +++ b/packages/react-on-rails/src/reactApis.cts @@ -4,6 +4,14 @@ import * as ReactDOM from 'react-dom'; import type { ReactElement } from 'react'; import type { RenderReturnType } from './types/index.ts' with { 'resolution-mode': 'import' }; +// Type for legacy React DOM APIs (React 16/17) that were removed from @types/react-dom@19 +// These are only used at runtime when supportsRootApi is false +interface LegacyReactDOM { + hydrate(element: ReactElement, container: Element): void; + render(element: ReactElement, container: Element): RenderReturnType; + unmountComponentAtNode(container: Element): boolean; +} + const reactMajorVersion = Number(ReactDOM.version?.split('.')[0]) || 16; // TODO: once we require React 18, we can remove this and inline everything guarded by it. @@ -29,12 +37,14 @@ if (supportsRootApi) { type HydrateOrRenderType = (domNode: Element, reactElement: ReactElement) => RenderReturnType; -/* eslint-disable @typescript-eslint/no-deprecated,@typescript-eslint/no-non-null-assertion,react/no-deprecated -- - * while we need to support React 16 - */ +// Cast ReactDOM to include legacy APIs for React 16/17 compatibility +// These methods exist at runtime but are removed from @types/react-dom@19 +const legacyReactDOM = ReactDOM as unknown as LegacyReactDOM; + +/* eslint-disable @typescript-eslint/no-non-null-assertion -- reactDomClient is always defined when supportsRootApi is true */ export const reactHydrate: HydrateOrRenderType = supportsRootApi ? reactDomClient!.hydrateRoot - : (domNode, reactElement) => ReactDOM.hydrate(reactElement, domNode); + : (domNode, reactElement) => legacyReactDOM.hydrate(reactElement, domNode); export function reactRender(domNode: Element, reactElement: ReactElement): RenderReturnType { if (supportsRootApi) { @@ -43,14 +53,13 @@ export function reactRender(domNode: Element, reactElement: ReactElement): Rende return root; } - // eslint-disable-next-line react/no-render-return-value - return ReactDOM.render(reactElement, domNode); + return legacyReactDOM.render(reactElement, domNode); } -export const unmountComponentAtNode: typeof ReactDOM.unmountComponentAtNode = supportsRootApi +export const unmountComponentAtNode = supportsRootApi ? // not used if we use root API () => false - : ReactDOM.unmountComponentAtNode; + : (container: Element) => legacyReactDOM.unmountComponentAtNode(container); export const ensureReactUseAvailable = () => { if (!('use' in React) || typeof React.use !== 'function') { diff --git a/packages/react-on-rails/src/types/index.ts b/packages/react-on-rails/src/types/index.ts index 22f5b962bd..98901ed470 100644 --- a/packages/react-on-rails/src/types/index.ts +++ b/packages/react-on-rails/src/types/index.ts @@ -125,9 +125,9 @@ interface ServerRenderResult { error?: Error; } -type CreateReactOutputSyncResult = ServerRenderResult | ReactElement; +type CreateReactOutputSyncResult = ServerRenderResult | ReactElement; -type CreateReactOutputAsyncResult = Promise>; +type CreateReactOutputAsyncResult = Promise; type CreateReactOutputResult = CreateReactOutputSyncResult | CreateReactOutputAsyncResult; From 4f868c54af4825863a8f6110e901674d81596b81 Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Sat, 6 Dec 2025 23:26:44 -1000 Subject: [PATCH 18/40] Fix ConnectionPool.new to use keyword arguments for Ruby 3.4 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ruby 3.4 changed how hash-to-keyword argument conversion works. Changed from passing a hash as a positional argument to explicit keyword arguments to ensure compatibility. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../server_rendering_pool/ruby_embedded_java_script.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/react_on_rails/lib/react_on_rails/server_rendering_pool/ruby_embedded_java_script.rb b/react_on_rails/lib/react_on_rails/server_rendering_pool/ruby_embedded_java_script.rb index 8c4fd1863c..34202cacd6 100644 --- a/react_on_rails/lib/react_on_rails/server_rendering_pool/ruby_embedded_java_script.rb +++ b/react_on_rails/lib/react_on_rails/server_rendering_pool/ruby_embedded_java_script.rb @@ -9,11 +9,10 @@ module ServerRenderingPool class RubyEmbeddedJavaScript class << self def reset_pool - options = { + @js_context_pool = ConnectionPool.new( size: ReactOnRails.configuration.server_renderer_pool_size, timeout: ReactOnRails.configuration.server_renderer_timeout - } - @js_context_pool = ConnectionPool.new(options) { create_js_context } + ) { create_js_context } end def reset_pool_if_server_bundle_was_modified From 7288eeff7e75cea5291efc16125417c50b5dd2d7 Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Sun, 7 Dec 2025 21:24:09 -1000 Subject: [PATCH 19/40] Update pnpm lockfile after rebase MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- pnpm-lock.yaml | 37 +++++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 53dc9d710e..b7db392c03 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -189,24 +189,12 @@ importers: react-dom: specifier: '>= 16' version: 19.2.0(react@19.2.0) - react-on-rails-rsc: - specifier: 19.0.2 - version: 19.0.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(webpack@5.103.0(@swc/core@1.15.3)) packages/react-on-rails-pro: dependencies: - react: - specifier: '>= 16' - version: 19.2.0 - react-dom: - specifier: '>= 16' - version: 19.2.0(react@19.2.0) react-on-rails: specifier: workspace:* version: link:../react-on-rails - react-on-rails-rsc: - specifier: 19.0.2 - version: 19.0.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(webpack@5.103.0(@swc/core@1.15.3)) devDependencies: '@types/mock-fs': specifier: ^4.13.4 @@ -214,6 +202,15 @@ importers: mock-fs: specifier: ^5.5.0 version: 5.5.0 + react: + specifier: ^19.0.1 + version: 19.2.0 + react-dom: + specifier: ^19.0.1 + version: 19.2.0(react@19.2.0) + react-on-rails-rsc: + specifier: ^19.0.3 + version: 19.0.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(webpack@5.103.0(@swc/core@1.15.3)) packages/react-on-rails-pro-node-renderer: dependencies: @@ -4424,6 +4421,13 @@ packages: react-dom: ^19.0.0 webpack: ^5.59.0 + react-on-rails-rsc@19.0.3: + resolution: {integrity: sha512-g+89U83+WjZDbtLvYQbjld0pWdUXpKageSoeKsX8cj1SkmULMAzbxgvH6vdzOuQUSwchkbDgwFO9umlHDhiyug==} + peerDependencies: + react: ^19.0.1 + react-dom: ^19.0.1 + webpack: ^5.59.0 + react@19.2.0: resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==} engines: {node: '>=0.10.0'} @@ -10317,6 +10321,15 @@ snapshots: webpack: 5.103.0(@swc/core@1.15.3) webpack-sources: 3.3.3 + react-on-rails-rsc@19.0.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(webpack@5.103.0(@swc/core@1.15.3)): + dependencies: + acorn-loose: 8.5.2 + neo-async: 2.6.2 + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + webpack: 5.103.0(@swc/core@1.15.3) + webpack-sources: 3.3.3 + react@19.2.0: {} readline-sync@1.4.10: {} From a68cd63e9f2138636021a1086a7b6d59109106cd Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Mon, 8 Dec 2025 17:37:19 -1000 Subject: [PATCH 20/40] Fix minimum version examples by adding npm overrides for React 18 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The yalc-linked react-on-rails package was causing npm to install React 19 as a transitive dependency, even when package.json specified React 18.0.0. Added npm overrides to force React 18 versions for minimum version compatibility testing. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- react_on_rails/rakelib/shakapacker_examples.rake | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/react_on_rails/rakelib/shakapacker_examples.rake b/react_on_rails/rakelib/shakapacker_examples.rake index 0265f014d6..0fb3760c15 100644 --- a/react_on_rails/rakelib/shakapacker_examples.rake +++ b/react_on_rails/rakelib/shakapacker_examples.rake @@ -54,6 +54,13 @@ namespace :shakapacker_examples do # rubocop:disable Metrics/BlockLength dev_deps["webpack-assets-manifest"] = "^5.0.6" if dev_deps&.key?("webpack-assets-manifest") update_shakapacker_dependency(deps, dev_deps) + # Add npm overrides to force React 18 versions, preventing yalc-linked + # react-on-rails from pulling in React 19 as a transitive dependency + package_json["overrides"] = { + "react" => ExampleType::MINIMUM_REACT_VERSION, + "react-dom" => ExampleType::MINIMUM_REACT_VERSION + } + File.write(package_json_path, "#{JSON.pretty_generate(package_json)}\n") end @@ -126,7 +133,10 @@ namespace :shakapacker_examples do # rubocop:disable Metrics/BlockLength bundle_install_in(example_type.dir) end - sh_in_dir(example_type.dir, "npm install") + # Use --legacy-peer-deps for minimum version examples to avoid peer dependency + # conflicts when yalc-linked react-on-rails expects newer React versions + npm_install_cmd = example_type.minimum_versions? ? "npm install --legacy-peer-deps" : "npm install" + sh_in_dir(example_type.dir, npm_install_cmd) # Generate the component packs after running the generator to ensure all # auto-bundled components have corresponding pack files created. # Use unbundled_sh_in_dir to ensure we're using the generated app's Gemfile From 33d6fc3be432d5ff4f19c6166322bd8448951a64 Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Mon, 8 Dec 2025 17:42:08 -1000 Subject: [PATCH 21/40] Add React 17 and 18 compatibility testing to generator examples MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added react_version parameter to ExampleType for specifying React versions - Added REACT_VERSIONS constant mapping major versions to specific versions - Updated examples_config.yml with React 17 and React 18 test configurations - Renamed test examples from *-minimum to *-react18 and *-react17 for clarity - Added new rake tasks: shakapacker_examples_react17, shakapacker_examples_react18, and shakapacker_examples_pinned - Updated CI workflow to run all pinned version tests (React 17 and 18) - Added npm overrides to force specific React versions in generated apps - Use --legacy-peer-deps for all pinned React version examples This ensures the react-on-rails package works correctly with: - React 19 (latest, uses Root API) - React 18 (uses Root API) - React 17 (uses legacy render/hydrate API) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .github/workflows/examples.yml | 4 +- packages/react-on-rails/src/reactApis.cts | 5 ++- react_on_rails/rakelib/example_type.rb | 34 +++++++++++---- react_on_rails/rakelib/examples_config.yml | 20 ++++++--- react_on_rails/rakelib/run_rspec.rake | 42 +++++++++++++++---- .../rakelib/shakapacker_examples.rake | 42 +++++++++---------- 6 files changed, 100 insertions(+), 47 deletions(-) diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index 0c8522825f..ef3b705cb3 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -183,9 +183,9 @@ jobs: - name: Main CI - Latest version examples if: matrix.dependency-level == 'latest' run: cd react_on_rails && bundle exec rake run_rspec:shakapacker_examples_latest - - name: Main CI - Minimum version examples + - name: Main CI - Pinned version examples (React 17 and 18) if: matrix.dependency-level == 'minimum' - run: cd react_on_rails && bundle exec rake run_rspec:shakapacker_examples_minimum + run: cd react_on_rails && bundle exec rake run_rspec:shakapacker_examples_pinned - name: Store test results uses: actions/upload-artifact@v4 with: diff --git a/packages/react-on-rails/src/reactApis.cts b/packages/react-on-rails/src/reactApis.cts index ee115e16e5..f3b5e93467 100644 --- a/packages/react-on-rails/src/reactApis.cts +++ b/packages/react-on-rails/src/reactApis.cts @@ -56,9 +56,10 @@ export function reactRender(domNode: Element, reactElement: ReactElement): Rende return legacyReactDOM.render(reactElement, domNode); } -export const unmountComponentAtNode = supportsRootApi +export const unmountComponentAtNode: (container: Element) => boolean = supportsRootApi ? // not used if we use root API - () => false + // eslint-disable-next-line @typescript-eslint/no-unused-vars + (_container: Element) => false : (container: Element) => legacyReactDOM.unmountComponentAtNode(container); export const ensureReactUseAvailable = () => { diff --git a/react_on_rails/rakelib/example_type.rb b/react_on_rails/rakelib/example_type.rb index a2864146d4..bf39b75c5b 100644 --- a/react_on_rails/rakelib/example_type.rb +++ b/react_on_rails/rakelib/example_type.rb @@ -14,22 +14,42 @@ def self.all @all ||= { shakapacker_examples: [] } end - # Minimum supported versions for compatibility testing - MINIMUM_REACT_VERSION = "18.0.0" + # Supported React versions for compatibility testing + REACT_VERSIONS = { + "17" => "17.0.2", + "18" => "18.0.0", + "19" => nil # nil means use latest (default) + }.freeze + + # Minimum Shakapacker version for compatibility testing MINIMUM_SHAKAPACKER_VERSION = "8.2.0" - attr_reader :packer_type, :name, :generator_options, :minimum_versions + attr_reader :packer_type, :name, :generator_options, :react_version - # Ruby convention: predicate method with ? suffix for boolean checks + # Returns true if this example uses a pinned (non-latest) React version + def pinned_react_version? + !react_version.nil? + end + + # Returns the actual React version string to use + def react_version_string + return nil unless react_version + + REACT_VERSIONS[react_version.to_s] || react_version + end + + # Legacy method for backward compatibility - true if React 18 (minimum supported) def minimum_versions? - minimum_versions + react_version == "18" end - def initialize(packer_type: nil, name: nil, generator_options: nil, minimum_versions: false) + def initialize(packer_type: nil, name: nil, generator_options: nil, minimum_versions: false, react_version: nil) @packer_type = packer_type @name = name @generator_options = generator_options - @minimum_versions = minimum_versions + # Support both legacy minimum_versions flag and new react_version parameter + # minimum_versions: true is equivalent to react_version: "18" + @react_version = react_version || (minimum_versions ? "18" : nil) self.class.all[packer_type.to_sym] << self end diff --git a/react_on_rails/rakelib/examples_config.yml b/react_on_rails/rakelib/examples_config.yml index ec6a11b3cf..ead23e767b 100644 --- a/react_on_rails/rakelib/examples_config.yml +++ b/react_on_rails/rakelib/examples_config.yml @@ -1,4 +1,5 @@ example_type_data: + # Latest versions (React 19, Shakapacker 9.x) - name: basic generator_options: '' - name: basic-server-rendering @@ -7,10 +8,19 @@ example_type_data: generator_options: --redux - name: redux-server-rendering generator_options: --redux --example-server-rendering - # Minimum version compatibility tests - tests React 18 and Shakapacker 8.2.0 - - name: basic-minimum + + # React 18 compatibility tests (minimum supported version with Root API) + - name: basic-react18 generator_options: '' - minimum_versions: true - - name: basic-server-rendering-minimum + react_version: '18' + - name: basic-server-rendering-react18 generator_options: --example-server-rendering - minimum_versions: true + react_version: '18' + + # React 17 compatibility tests (legacy render/hydrate API) + - name: basic-react17 + generator_options: '' + react_version: '17' + - name: basic-server-rendering-react17 + generator_options: --example-server-rendering + react_version: '17' diff --git a/react_on_rails/rakelib/run_rspec.rake b/react_on_rails/rakelib/run_rspec.rake index 53e0b8d1f8..df19c1d69f 100644 --- a/react_on_rails/rakelib/run_rspec.rake +++ b/react_on_rails/rakelib/run_rspec.rake @@ -82,10 +82,10 @@ namespace :run_rspec do puts "Creating #{example_type.rspec_task_name} task" desc "Runs RSpec for #{example_type.name_pretty} only" task example_type.rspec_task_name_short => example_type.gen_task_name do - # Use unbundled mode for minimum version examples to ensure the example app's + # Use unbundled mode for pinned React version examples to ensure the example app's # Gemfile and gem versions are used, not the parent workspace's bundle run_tests_in(File.join(examples_dir, example_type.name), - unbundled: example_type.minimum_versions?) + unbundled: example_type.pinned_react_version?) end end @@ -94,23 +94,47 @@ namespace :run_rspec do ExampleType.all[:shakapacker_examples].each { |example_type| Rake::Task[example_type.rspec_task_name].invoke } end - # Helper methods for filtering examples + # Helper methods for filtering examples by React version def latest_examples - ExampleType.all[:shakapacker_examples].reject(&:minimum_versions?) + ExampleType.all[:shakapacker_examples].reject(&:pinned_react_version?) end - def minimum_examples - ExampleType.all[:shakapacker_examples].select(&:minimum_versions?) + def react18_examples + ExampleType.all[:shakapacker_examples].select { |e| e.react_version == "18" } end - desc "Runs Rspec for latest version example apps only (React 19, Shakapacker 9.4.0)" + def react17_examples + ExampleType.all[:shakapacker_examples].select { |e| e.react_version == "17" } + end + + def pinned_version_examples + ExampleType.all[:shakapacker_examples].select(&:pinned_react_version?) + end + + desc "Runs Rspec for latest version example apps only (React 19, Shakapacker 9.x)" task shakapacker_examples_latest: latest_examples.map(&:gen_task_name) do latest_examples.each { |example_type| Rake::Task[example_type.rspec_task_name].invoke } end + desc "Runs Rspec for React 18 example apps only (Shakapacker 8.2.0)" + task shakapacker_examples_react18: react18_examples.map(&:gen_task_name) do + react18_examples.each { |example_type| Rake::Task[example_type.rspec_task_name].invoke } + end + + desc "Runs Rspec for React 17 example apps only (legacy render API)" + task shakapacker_examples_react17: react17_examples.map(&:gen_task_name) do + react17_examples.each { |example_type| Rake::Task[example_type.rspec_task_name].invoke } + end + + desc "Runs Rspec for all pinned version example apps (React 17 and 18)" + task shakapacker_examples_pinned: pinned_version_examples.map(&:gen_task_name) do + pinned_version_examples.each { |example_type| Rake::Task[example_type.rspec_task_name].invoke } + end + + # Legacy alias for backward compatibility desc "Runs Rspec for minimum version example apps only (React 18, Shakapacker 8.2.0)" - task shakapacker_examples_minimum: minimum_examples.map(&:gen_task_name) do - minimum_examples.each { |example_type| Rake::Task[example_type.rspec_task_name].invoke } + task shakapacker_examples_minimum: react18_examples.map(&:gen_task_name) do + react18_examples.each { |example_type| Rake::Task[example_type.rspec_task_name].invoke } end Coveralls::RakeTask.new if ENV["USE_COVERALLS"] == "TRUE" diff --git a/react_on_rails/rakelib/shakapacker_examples.rake b/react_on_rails/rakelib/shakapacker_examples.rake index 0fb3760c15..cb34f8a941 100644 --- a/react_on_rails/rakelib/shakapacker_examples.rake +++ b/react_on_rails/rakelib/shakapacker_examples.rake @@ -16,14 +16,12 @@ require_relative "task_helpers" namespace :shakapacker_examples do # rubocop:disable Metrics/BlockLength include ReactOnRails::TaskHelpers - # Updates React-related dependencies to minimum supported versions - def update_react_dependencies(deps) + # Updates React-related dependencies to a specific version + def update_react_dependencies(deps, react_version) return unless deps - deps["react"] = ExampleType::MINIMUM_REACT_VERSION - deps["react-dom"] = ExampleType::MINIMUM_REACT_VERSION - # Shakapacker 8.2.0 requires webpack-assets-manifest ^5.x - deps["webpack-assets-manifest"] = "^5.0.6" if deps.key?("webpack-assets-manifest") + deps["react"] = react_version + deps["react-dom"] = react_version end # Updates Shakapacker to minimum supported version in either dependencies or devDependencies @@ -35,8 +33,8 @@ namespace :shakapacker_examples do # rubocop:disable Metrics/BlockLength end end - # Updates dependencies in package.json to use minimum supported versions - def update_package_json_versions(package_json_path) + # Updates dependencies in package.json to use specific React version + def update_package_json_for_react_version(package_json_path, react_version) return unless File.exist?(package_json_path) begin @@ -49,16 +47,16 @@ namespace :shakapacker_examples do # rubocop:disable Metrics/BlockLength deps = package_json["dependencies"] dev_deps = package_json["devDependencies"] - update_react_dependencies(deps) + update_react_dependencies(deps, react_version) # Shakapacker 8.2.0 requires webpack-assets-manifest ^5.x (check devDependencies too) dev_deps["webpack-assets-manifest"] = "^5.0.6" if dev_deps&.key?("webpack-assets-manifest") update_shakapacker_dependency(deps, dev_deps) - # Add npm overrides to force React 18 versions, preventing yalc-linked + # Add npm overrides to force specific React version, preventing yalc-linked # react-on-rails from pulling in React 19 as a transitive dependency package_json["overrides"] = { - "react" => ExampleType::MINIMUM_REACT_VERSION, - "react-dom" => ExampleType::MINIMUM_REACT_VERSION + "react" => react_version, + "react-dom" => react_version } File.write(package_json_path, "#{JSON.pretty_generate(package_json)}\n") @@ -82,13 +80,13 @@ namespace :shakapacker_examples do # rubocop:disable Metrics/BlockLength File.write(gemfile_path, gemfile_content) end - # Updates package.json and Gemfile to use minimum supported versions for compatibility testing - def apply_minimum_versions(dir) - update_package_json_versions(File.join(dir, "package.json")) + # Updates package.json and Gemfile to use specific React version for compatibility testing + def apply_react_version(dir, react_version) + update_package_json_for_react_version(File.join(dir, "package.json"), react_version) update_gemfile_versions(File.join(dir, "Gemfile")) - puts " Updated package.json with minimum versions:" - puts " React: #{ExampleType::MINIMUM_REACT_VERSION}" + puts " Updated package.json for compatibility testing:" + puts " React: #{react_version}" puts " Shakapacker: #{ExampleType::MINIMUM_SHAKAPACKER_VERSION}" end @@ -126,16 +124,16 @@ namespace :shakapacker_examples do # rubocop:disable Metrics/BlockLength # Re-run bundle install since dev_tests generator adds rspec-rails and coveralls to Gemfile bundle_install_in(example_type.dir) - # Apply minimum versions for compatibility testing examples - if example_type.minimum_versions - apply_minimum_versions(example_type.dir) + # Apply specific React version for compatibility testing examples + if example_type.pinned_react_version? + apply_react_version(example_type.dir, example_type.react_version_string) # Re-run bundle install since Gemfile was updated with pinned shakapacker version bundle_install_in(example_type.dir) end - # Use --legacy-peer-deps for minimum version examples to avoid peer dependency + # Use --legacy-peer-deps for pinned React version examples to avoid peer dependency # conflicts when yalc-linked react-on-rails expects newer React versions - npm_install_cmd = example_type.minimum_versions? ? "npm install --legacy-peer-deps" : "npm install" + npm_install_cmd = example_type.pinned_react_version? ? "npm install --legacy-peer-deps" : "npm install" sh_in_dir(example_type.dir, npm_install_cmd) # Generate the component packs after running the generator to ensure all # auto-bundled components have corresponding pack files created. From 5532c5d02ac96330be824e6264393fd9074fa018 Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Mon, 8 Dec 2025 17:51:00 -1000 Subject: [PATCH 22/40] Regenerate Shakapacker binstubs after version downgrade MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When downgrading from Shakapacker 9.x to 8.2.x, the bin/shakapacker binstub needs to be regenerated as the format may differ between major versions. Without this, the binstub fails immediately when running webpack. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- react_on_rails/rakelib/shakapacker_examples.rake | 3 +++ 1 file changed, 3 insertions(+) diff --git a/react_on_rails/rakelib/shakapacker_examples.rake b/react_on_rails/rakelib/shakapacker_examples.rake index cb34f8a941..cb6d3cb3cf 100644 --- a/react_on_rails/rakelib/shakapacker_examples.rake +++ b/react_on_rails/rakelib/shakapacker_examples.rake @@ -129,6 +129,9 @@ namespace :shakapacker_examples do # rubocop:disable Metrics/BlockLength apply_react_version(example_type.dir, example_type.react_version_string) # Re-run bundle install since Gemfile was updated with pinned shakapacker version bundle_install_in(example_type.dir) + # Regenerate Shakapacker binstubs after downgrading from 9.x to 8.2.x + # The binstub format may differ between major versions + unbundled_sh_in_dir(example_type.dir, "bundle exec rake shakapacker:binstubs") end # Use --legacy-peer-deps for pinned React version examples to avoid peer dependency From f353f28dc6b1ef39538ea0b522f8328f7b6bbf0d Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Mon, 8 Dec 2025 18:23:16 -1000 Subject: [PATCH 23/40] Fix workflow step name to document minimum supported versions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update the pinned version examples step name from "React 17 and 18" to "minimum: React 18.0.0, Shakapacker 8.2.0" for accuracy. This makes the workflow label correctly document the actual minimum supported versions. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .github/workflows/examples.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index ef3b705cb3..fe3604665d 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -183,7 +183,7 @@ jobs: - name: Main CI - Latest version examples if: matrix.dependency-level == 'latest' run: cd react_on_rails && bundle exec rake run_rspec:shakapacker_examples_latest - - name: Main CI - Pinned version examples (React 17 and 18) + - name: Main CI - Pinned version examples (minimum: React 18.0.0, Shakapacker 8.2.0) if: matrix.dependency-level == 'minimum' run: cd react_on_rails && bundle exec rake run_rspec:shakapacker_examples_pinned - name: Store test results From a96560ab4de149e44ce2fc8350d66127e5ba939e Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Mon, 8 Dec 2025 18:25:24 -1000 Subject: [PATCH 24/40] Fix Shakapacker version mismatch by running npm install before binstubs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The shakapacker:binstubs rake task loads the Rails environment, which validates that the gem and npm package versions match. Previously, npm install ran AFTER the binstubs task, causing version mismatch errors when testing minimum supported versions (gem 8.2.0 vs npm 9.4.0). Reorder operations for pinned React version examples: 1. Update package.json with shakapacker 8.2.0 2. Run bundle install (gem 8.2.0) 3. Run npm install (npm 8.2.0) - MOVED BEFORE binstubs 4. Run shakapacker:binstubs (now versions match) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- react_on_rails/rakelib/shakapacker_examples.rake | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/react_on_rails/rakelib/shakapacker_examples.rake b/react_on_rails/rakelib/shakapacker_examples.rake index cb6d3cb3cf..8f5683c2ce 100644 --- a/react_on_rails/rakelib/shakapacker_examples.rake +++ b/react_on_rails/rakelib/shakapacker_examples.rake @@ -129,15 +129,18 @@ namespace :shakapacker_examples do # rubocop:disable Metrics/BlockLength apply_react_version(example_type.dir, example_type.react_version_string) # Re-run bundle install since Gemfile was updated with pinned shakapacker version bundle_install_in(example_type.dir) + # Run npm install BEFORE shakapacker:binstubs to ensure the npm shakapacker version + # matches the gem version. The binstubs task loads the Rails environment which + # validates version matching between gem and npm package. + # Use --legacy-peer-deps to avoid peer dependency conflicts when yalc-linked + # react-on-rails expects newer React versions + sh_in_dir(example_type.dir, "npm install --legacy-peer-deps") # Regenerate Shakapacker binstubs after downgrading from 9.x to 8.2.x # The binstub format may differ between major versions unbundled_sh_in_dir(example_type.dir, "bundle exec rake shakapacker:binstubs") + else + sh_in_dir(example_type.dir, "npm install") end - - # Use --legacy-peer-deps for pinned React version examples to avoid peer dependency - # conflicts when yalc-linked react-on-rails expects newer React versions - npm_install_cmd = example_type.pinned_react_version? ? "npm install --legacy-peer-deps" : "npm install" - sh_in_dir(example_type.dir, npm_install_cmd) # Generate the component packs after running the generator to ensure all # auto-bundled components have corresponding pack files created. # Use unbundled_sh_in_dir to ensure we're using the generated app's Gemfile From d48f2291cb87ef4cbc9e51ac9dc2a2264ba0e58c Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Mon, 8 Dec 2025 18:26:00 -1000 Subject: [PATCH 25/40] Add runtime validation for legacy React APIs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add runtime validation when using legacy ReactDOM APIs (React < 18) to catch mismatched React versions early with clear error messages. This addresses the type safety concern where `as unknown as LegacyReactDOM` could silently fail at runtime if React structure changes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- packages/react-on-rails/src/reactApis.cts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/react-on-rails/src/reactApis.cts b/packages/react-on-rails/src/reactApis.cts index f3b5e93467..707b19ae7b 100644 --- a/packages/react-on-rails/src/reactApis.cts +++ b/packages/react-on-rails/src/reactApis.cts @@ -41,6 +41,19 @@ type HydrateOrRenderType = (domNode: Element, reactElement: ReactElement) => Ren // These methods exist at runtime but are removed from @types/react-dom@19 const legacyReactDOM = ReactDOM as unknown as LegacyReactDOM; +// Validate legacy APIs exist at runtime when needed (React < 18) +if (!supportsRootApi) { + if (typeof legacyReactDOM.hydrate !== 'function') { + throw new Error('React legacy hydrate API not available. Expected React 16/17.'); + } + if (typeof legacyReactDOM.render !== 'function') { + throw new Error('React legacy render API not available. Expected React 16/17.'); + } + if (typeof legacyReactDOM.unmountComponentAtNode !== 'function') { + throw new Error('React legacy unmountComponentAtNode API not available. Expected React 16/17.'); + } +} + /* eslint-disable @typescript-eslint/no-non-null-assertion -- reactDomClient is always defined when supportsRootApi is true */ export const reactHydrate: HydrateOrRenderType = supportsRootApi ? reactDomClient!.hydrateRoot From 22493522aa78c17ca34940c08643aa7c2973a097 Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Mon, 8 Dec 2025 18:53:02 -1000 Subject: [PATCH 26/40] Fix YAML syntax error in examples workflow step name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Quote step name containing colon to prevent YAML parser from interpreting "minimum:" as a mapping key. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .github/workflows/examples.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index fe3604665d..8eeebeae58 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -183,7 +183,7 @@ jobs: - name: Main CI - Latest version examples if: matrix.dependency-level == 'latest' run: cd react_on_rails && bundle exec rake run_rspec:shakapacker_examples_latest - - name: Main CI - Pinned version examples (minimum: React 18.0.0, Shakapacker 8.2.0) + - name: "Main CI - Pinned version examples (minimum: React 18.0.0, Shakapacker 8.2.0)" if: matrix.dependency-level == 'minimum' run: cd react_on_rails && bundle exec rake run_rspec:shakapacker_examples_pinned - name: Store test results From a28f819fbd4a5bcd0c1dddf2d34dac702ca1e58b Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Mon, 8 Dec 2025 20:05:41 -1000 Subject: [PATCH 27/40] Remove outdated commented code from script/convert MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clean up historical comments that were kept for reference but are no longer relevant. The remaining comment explains why test:rsc script doesn't need modification. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- script/convert | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/script/convert b/script/convert index 17ed6ab21b..ce4d7d3489 100755 --- a/script/convert +++ b/script/convert @@ -63,18 +63,3 @@ gsub_file_content( ) # test:rsc script now automatically detects React version and skips on React 18 # No override needed - the script checks React version and exits cleanly if < 19 -# Keep modern JSX transform for React 18+ -# gsub_file_content("../tsconfig.json", "react-jsx", "react") -# gsub_file_content("../spec/dummy/babel.config.js", "runtime: 'automatic'", "runtime: 'classic'") -# Keep modern ReScript configuration for React 18+ -# gsub_file_content("../spec/dummy/rescript.json", '"version": 4', '"version": 4, "mode": "classic"') -# Skip React 16 file replacements since we're using React 18+ -# Dir.glob(File.expand_path("../spec/dummy/**/app-react16/**/*.*", __dir__)).each do |file| -# move(file, file.gsub("-react16", "")) -# end - -# These replacements were incorrect - generateWebpackConfig() is the correct function from shakapacker -# gsub_file_content("../spec/dummy/config/webpack/commonWebpackConfig.js", /generateWebpackConfig(\(\))?/, -# "webpackConfig") -# -# gsub_file_content("../spec/dummy/config/webpack/webpack.config.js", /generateWebpackConfig(\(\))?/, "webpackConfig") From 7c10881d68b4b571d030e01c9ecc303b888e0a30 Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Mon, 8 Dec 2025 20:35:02 -1000 Subject: [PATCH 28/40] Output webpack build errors to stderr for CI visibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use warn() instead of puts() and flush stderr before exit!(1) to ensure error messages are visible in CI logs. The previous implementation may have been buffering output that wasn't flushed before the hard exit. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- react_on_rails/lib/react_on_rails/utils.rb | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/react_on_rails/lib/react_on_rails/utils.rb b/react_on_rails/lib/react_on_rails/utils.rb index 264feaae49..4737ec621d 100644 --- a/react_on_rails/lib/react_on_rails/utils.rb +++ b/react_on_rails/lib/react_on_rails/utils.rb @@ -98,9 +98,12 @@ def self.invoke_and_exit_if_failed(cmd, failure_message) exitstatus: #{status.exitstatus}#{stdout_msg}#{stderr_msg} MSG - puts wrap_message(msg) - puts "" - puts default_troubleshooting_section + # Use warn to ensure output is visible in CI logs (goes to stderr) + # and flush immediately before calling exit! + warn wrap_message(msg) + warn "" + warn default_troubleshooting_section + $stderr.flush # Rspec catches exit without! in the exit callbacks exit!(1) From aac91b857477ac545b91b6d1c038cd34e8c8875d Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Mon, 8 Dec 2025 20:41:35 -1000 Subject: [PATCH 29/40] Add babel-loader dependency for Shakapacker 8.2.0 compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Shakapacker 8.2.0 requires babel-loader to be explicitly installed when babel is configured in the webpack setup. In 9.x, this requirement was relaxed or the package structure changed, so the dependency isn't needed. When downgrading to 8.2.0 for minimum version testing, we need to add babel-loader to devDependencies. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- react_on_rails/rakelib/shakapacker_examples.rake | 3 +++ 1 file changed, 3 insertions(+) diff --git a/react_on_rails/rakelib/shakapacker_examples.rake b/react_on_rails/rakelib/shakapacker_examples.rake index 8f5683c2ce..4964d774c5 100644 --- a/react_on_rails/rakelib/shakapacker_examples.rake +++ b/react_on_rails/rakelib/shakapacker_examples.rake @@ -50,6 +50,9 @@ namespace :shakapacker_examples do # rubocop:disable Metrics/BlockLength update_react_dependencies(deps, react_version) # Shakapacker 8.2.0 requires webpack-assets-manifest ^5.x (check devDependencies too) dev_deps["webpack-assets-manifest"] = "^5.0.6" if dev_deps&.key?("webpack-assets-manifest") + # Shakapacker 8.2.0 requires babel-loader to be explicitly installed as a devDependency + # (in 9.x this requirement was relaxed or the package structure changed) + dev_deps["babel-loader"] = "^9.1.3" if dev_deps update_shakapacker_dependency(deps, dev_deps) # Add npm overrides to force specific React version, preventing yalc-linked From 3fb3a4367dcfa7e33986cb380df99bd17974dafe Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Tue, 9 Dec 2025 17:07:52 -1000 Subject: [PATCH 30/40] Fix workflow to use correct rake task for minimum version examples MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change from run_rspec:shakapacker_examples_pinned (which runs all pinned versions including React 17) to run_rspec:shakapacker_examples_minimum (which runs only React 18 minimum supported examples) to match the step label and matrix condition. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .github/workflows/examples.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index 8eeebeae58..002aa594e4 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -185,7 +185,7 @@ jobs: run: cd react_on_rails && bundle exec rake run_rspec:shakapacker_examples_latest - name: "Main CI - Pinned version examples (minimum: React 18.0.0, Shakapacker 8.2.0)" if: matrix.dependency-level == 'minimum' - run: cd react_on_rails && bundle exec rake run_rspec:shakapacker_examples_pinned + run: cd react_on_rails && bundle exec rake run_rspec:shakapacker_examples_minimum - name: Store test results uses: actions/upload-artifact@v4 with: From 9432cbbd27e189dcc6f215ddccb08f92f9725d18 Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Tue, 9 Dec 2025 17:11:17 -1000 Subject: [PATCH 31/40] Fix webpack-assets-manifest for Shakapacker 8.2.0 compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous condition only added webpack-assets-manifest ^5.0.6 if the key already existed in devDependencies. However, when generating example apps, webpack-assets-manifest is installed as a transitive dependency from shakapacker, not directly in package.json. This meant the v6.x transitive dependency was being used, which fails with "WebpackAssetsManifest is not a constructor" because v6.x uses ESM. Now we always add webpack-assets-manifest ^5.0.6 to devDependencies when generating pinned React version examples, ensuring the compatible CommonJS version is used with Shakapacker 8.2.0. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- react_on_rails/rakelib/shakapacker_examples.rake | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/react_on_rails/rakelib/shakapacker_examples.rake b/react_on_rails/rakelib/shakapacker_examples.rake index 4964d774c5..3cc995e510 100644 --- a/react_on_rails/rakelib/shakapacker_examples.rake +++ b/react_on_rails/rakelib/shakapacker_examples.rake @@ -48,8 +48,9 @@ namespace :shakapacker_examples do # rubocop:disable Metrics/BlockLength dev_deps = package_json["devDependencies"] update_react_dependencies(deps, react_version) - # Shakapacker 8.2.0 requires webpack-assets-manifest ^5.x (check devDependencies too) - dev_deps["webpack-assets-manifest"] = "^5.0.6" if dev_deps&.key?("webpack-assets-manifest") + # Shakapacker 8.2.0 requires webpack-assets-manifest ^5.x (v6.x uses ESM and breaks) + # Always add this explicitly since the transitive dependency from shakapacker may be v6.x + dev_deps["webpack-assets-manifest"] = "^5.0.6" if dev_deps # Shakapacker 8.2.0 requires babel-loader to be explicitly installed as a devDependency # (in 9.x this requirement was relaxed or the package structure changed) dev_deps["babel-loader"] = "^9.1.3" if dev_deps From 7169feda7b5c8f44df5e10d8f27cc4bb3136d212 Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Tue, 9 Dec 2025 17:11:44 -1000 Subject: [PATCH 32/40] Fix test to expect error output on stderr instead of stdout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The invoke_and_exit_if_failed method uses warn() which outputs to stderr, but the test was expecting the error message on stdout. This was introduced when error output was changed to use stderr for better CI visibility in commit 7c10881d6. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../react_on_rails/test_helper/webpack_assets_compiler_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/react_on_rails/spec/react_on_rails/test_helper/webpack_assets_compiler_spec.rb b/react_on_rails/spec/react_on_rails/test_helper/webpack_assets_compiler_spec.rb index 049ebb5061..aa8340b4ad 100644 --- a/react_on_rails/spec/react_on_rails/test_helper/webpack_assets_compiler_spec.rb +++ b/react_on_rails/spec/react_on_rails/test_helper/webpack_assets_compiler_spec.rb @@ -35,7 +35,7 @@ described_class.new.compile_assets rescue SystemExit # No op - end.to output(/#{expected_output}/).to_stdout + end.to output(/#{expected_output}/).to_stderr end end end From 8d18c1bcecc193d4d68b5bab0e1784aba20de3f1 Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Tue, 9 Dec 2025 17:20:57 -1000 Subject: [PATCH 33/40] Add React 16 compatibility testing support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add React 16.14.0 to the version compatibility test matrix: - Add React 16 to REACT_VERSIONS constant in example_type.rb - Add basic-react16 and basic-server-rendering-react16 examples - Add react16_examples helper and shakapacker_examples_react16 rake task - Update shakapacker_examples_pinned to include React 16, 17, and 18 This extends the existing React version compatibility testing infrastructure to cover React 16, ensuring the legacy render/hydrate API continues to work correctly. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- react_on_rails/rakelib/example_type.rb | 1 + react_on_rails/rakelib/examples_config.yml | 8 ++++++++ react_on_rails/rakelib/run_rspec.rake | 11 ++++++++++- 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/react_on_rails/rakelib/example_type.rb b/react_on_rails/rakelib/example_type.rb index bf39b75c5b..7ebd50bd0b 100644 --- a/react_on_rails/rakelib/example_type.rb +++ b/react_on_rails/rakelib/example_type.rb @@ -16,6 +16,7 @@ def self.all # Supported React versions for compatibility testing REACT_VERSIONS = { + "16" => "16.14.0", "17" => "17.0.2", "18" => "18.0.0", "19" => nil # nil means use latest (default) diff --git a/react_on_rails/rakelib/examples_config.yml b/react_on_rails/rakelib/examples_config.yml index ead23e767b..25f5b5c484 100644 --- a/react_on_rails/rakelib/examples_config.yml +++ b/react_on_rails/rakelib/examples_config.yml @@ -24,3 +24,11 @@ example_type_data: - name: basic-server-rendering-react17 generator_options: --example-server-rendering react_version: '17' + + # React 16 compatibility tests (oldest supported legacy API) + - name: basic-react16 + generator_options: '' + react_version: '16' + - name: basic-server-rendering-react16 + generator_options: --example-server-rendering + react_version: '16' diff --git a/react_on_rails/rakelib/run_rspec.rake b/react_on_rails/rakelib/run_rspec.rake index df19c1d69f..195484e26f 100644 --- a/react_on_rails/rakelib/run_rspec.rake +++ b/react_on_rails/rakelib/run_rspec.rake @@ -107,6 +107,10 @@ namespace :run_rspec do ExampleType.all[:shakapacker_examples].select { |e| e.react_version == "17" } end + def react16_examples + ExampleType.all[:shakapacker_examples].select { |e| e.react_version == "16" } + end + def pinned_version_examples ExampleType.all[:shakapacker_examples].select(&:pinned_react_version?) end @@ -126,7 +130,12 @@ namespace :run_rspec do react17_examples.each { |example_type| Rake::Task[example_type.rspec_task_name].invoke } end - desc "Runs Rspec for all pinned version example apps (React 17 and 18)" + desc "Runs Rspec for React 16 example apps only (oldest supported legacy API)" + task shakapacker_examples_react16: react16_examples.map(&:gen_task_name) do + react16_examples.each { |example_type| Rake::Task[example_type.rspec_task_name].invoke } + end + + desc "Runs Rspec for all pinned version example apps (React 16, 17, and 18)" task shakapacker_examples_pinned: pinned_version_examples.map(&:gen_task_name) do pinned_version_examples.each { |example_type| Rake::Task[example_type.rspec_task_name].invoke } end From 819327d95cc0e444bee113459811ebb9eb6af964 Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Tue, 9 Dec 2025 17:27:16 -1000 Subject: [PATCH 34/40] Run all pinned React versions (16, 17, 18) in minimum CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address review feedback: - Update CI workflow to run shakapacker_examples_pinned instead of shakapacker_examples_minimum, testing React 16, 17, and 18 - Clarify CI step name to reflect all pinned versions being tested - Add comprehensive documentation to examples_config.yml explaining: - Which examples run on latest vs minimum CI - Terminology clarification (latest vs pinned vs minimum supported) - Note about React 16/17 being past EOL but still tested This ensures all React version compatibility is tested in CI on master branch pushes, not just React 18. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .github/workflows/examples.yml | 4 ++-- react_on_rails/rakelib/examples_config.yml | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index 002aa594e4..0ed6b578d2 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -183,9 +183,9 @@ jobs: - name: Main CI - Latest version examples if: matrix.dependency-level == 'latest' run: cd react_on_rails && bundle exec rake run_rspec:shakapacker_examples_latest - - name: "Main CI - Pinned version examples (minimum: React 18.0.0, Shakapacker 8.2.0)" + - name: "Main CI - Pinned version examples (React 16, 17, 18 with Shakapacker 8.2.0)" if: matrix.dependency-level == 'minimum' - run: cd react_on_rails && bundle exec rake run_rspec:shakapacker_examples_minimum + run: cd react_on_rails && bundle exec rake run_rspec:shakapacker_examples_pinned - name: Store test results uses: actions/upload-artifact@v4 with: diff --git a/react_on_rails/rakelib/examples_config.yml b/react_on_rails/rakelib/examples_config.yml index 25f5b5c484..501503add8 100644 --- a/react_on_rails/rakelib/examples_config.yml +++ b/react_on_rails/rakelib/examples_config.yml @@ -1,3 +1,22 @@ +# Example Type Configuration for React on Rails Generator Tests +# +# CI Test Coverage: +# ----------------- +# - Latest CI (PRs): Runs shakapacker_examples_latest (React 19, Shakapacker 9.x) +# Examples: basic, basic-server-rendering, redux, redux-server-rendering +# +# - Minimum CI (master): Runs shakapacker_examples_pinned (React 16, 17, 18 with Shakapacker 8.2.0) +# Examples: basic-react16, basic-server-rendering-react16, +# basic-react17, basic-server-rendering-react17, +# basic-react18, basic-server-rendering-react18 +# +# Terminology: +# - "Latest" = Current React version (19) with latest Shakapacker (9.x) +# - "Pinned" = Specific older React versions (16, 17, 18) for compatibility testing +# - "Minimum supported" = React 18.0.0 + Shakapacker 8.2.0 (oldest officially supported) +# +# Note: React 16 and 17 are past EOL but still tested for legacy compatibility. + example_type_data: # Latest versions (React 19, Shakapacker 9.x) - name: basic From 90d20692d7fe3486b5b7477de696346c39c68a0a Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Tue, 9 Dec 2025 18:22:22 -1000 Subject: [PATCH 35/40] Add @babel/plugin-transform-runtime for Shakapacker 8.2.0 compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The default babel config requires @babel/plugin-transform-runtime but it's not automatically included as a dependency in older Shakapacker versions. This was causing webpack build failures in the React 18 pinned version examples with the error: Cannot find package '@babel/plugin-transform-runtime' 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- react_on_rails/rakelib/shakapacker_examples.rake | 3 +++ 1 file changed, 3 insertions(+) diff --git a/react_on_rails/rakelib/shakapacker_examples.rake b/react_on_rails/rakelib/shakapacker_examples.rake index 3cc995e510..3431471084 100644 --- a/react_on_rails/rakelib/shakapacker_examples.rake +++ b/react_on_rails/rakelib/shakapacker_examples.rake @@ -54,6 +54,9 @@ namespace :shakapacker_examples do # rubocop:disable Metrics/BlockLength # Shakapacker 8.2.0 requires babel-loader to be explicitly installed as a devDependency # (in 9.x this requirement was relaxed or the package structure changed) dev_deps["babel-loader"] = "^9.1.3" if dev_deps + # @babel/plugin-transform-runtime is required by the default babel config but not + # automatically included as a dependency in older Shakapacker versions + dev_deps["@babel/plugin-transform-runtime"] = "^7.24.0" if dev_deps update_shakapacker_dependency(deps, dev_deps) # Add npm overrides to force specific React version, preventing yalc-linked From 2c5d108c79b17d8203548bbd75121383e31ff18e Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Tue, 9 Dec 2025 21:51:54 -1000 Subject: [PATCH 36/40] Add rake task verification step to CI workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a verification step before running example tests to ensure the required rake task exists. This prevents silent failures where CI would skip tests if a task name is misspelled or doesn't exist. The step: - Lists all available shakapacker_examples tasks for debugging - Verifies the specific task needed for the matrix configuration exists - Fails fast with a clear error message if the task is missing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .github/workflows/examples.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index 0ed6b578d2..49a9cbe891 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -180,6 +180,18 @@ jobs: - name: Set packer version environment variable run: | echo "CI_DEPENDENCY_LEVEL=${{ matrix.dependency-level }}" >> $GITHUB_ENV + - name: Verify rake tasks exist + run: | + cd react_on_rails + echo "Available shakapacker_examples tasks:" + bundle exec rake -T | grep shakapacker_examples || true + # Verify the specific task we need exists + TASK_NAME="run_rspec:shakapacker_examples_${{ matrix.dependency-level == 'latest' && 'latest' || 'pinned' }}" + if ! bundle exec rake -T | grep -q "$TASK_NAME"; then + echo "ERROR: Required rake task '$TASK_NAME' not found!" + exit 1 + fi + echo "✓ Found required task: $TASK_NAME" - name: Main CI - Latest version examples if: matrix.dependency-level == 'latest' run: cd react_on_rails && bundle exec rake run_rspec:shakapacker_examples_latest From 2c7532a364d08964dbd8da5c6b2bc255127c12b9 Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Tue, 9 Dec 2025 21:53:27 -1000 Subject: [PATCH 37/40] Add version constants and validation to ExampleType MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add MINIMUM_SUPPORTED_REACT_VERSION and LATEST_REACT_MAJOR_VERSION constants for documentation and reference - Add validation in initialize to catch invalid react_version values early with a clear error message listing valid versions This prevents configuration errors in examples_config.yml from causing confusing failures later in the build process. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- react_on_rails/rakelib/example_type.rb | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/react_on_rails/rakelib/example_type.rb b/react_on_rails/rakelib/example_type.rb index 7ebd50bd0b..d1bb4ade5d 100644 --- a/react_on_rails/rakelib/example_type.rb +++ b/react_on_rails/rakelib/example_type.rb @@ -15,6 +15,7 @@ def self.all end # Supported React versions for compatibility testing + # Keys are major version strings, values are specific version to pin to (nil = latest) REACT_VERSIONS = { "16" => "16.14.0", "17" => "17.0.2", @@ -22,6 +23,10 @@ def self.all "19" => nil # nil means use latest (default) }.freeze + # Semantic version constants for documentation and reference + MINIMUM_SUPPORTED_REACT_VERSION = "18.0.0" + LATEST_REACT_MAJOR_VERSION = "19" + # Minimum Shakapacker version for compatibility testing MINIMUM_SHAKAPACKER_VERSION = "8.2.0" @@ -51,6 +56,14 @@ def initialize(packer_type: nil, name: nil, generator_options: nil, minimum_vers # Support both legacy minimum_versions flag and new react_version parameter # minimum_versions: true is equivalent to react_version: "18" @react_version = react_version || (minimum_versions ? "18" : nil) + + # Validate react_version is a known version to catch configuration errors early + if @react_version && !REACT_VERSIONS.key?(@react_version.to_s) + valid_versions = REACT_VERSIONS.keys.join(", ") + raise ArgumentError, "Invalid react_version '#{@react_version}' for example '#{name}'. " \ + "Valid versions: #{valid_versions}" + end + self.class.all[packer_type.to_sym] << self end From c8b0ac708a6f94d7dce7063f9343f36092aef437 Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Tue, 9 Dec 2025 21:55:05 -1000 Subject: [PATCH 38/40] Fix MINIMUM_SUPPORTED_REACT_VERSION to reflect React 16 support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit React 16.14.0 is the actual minimum supported version since we test and support it in the compatibility matrix. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- react_on_rails/rakelib/example_type.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/react_on_rails/rakelib/example_type.rb b/react_on_rails/rakelib/example_type.rb index d1bb4ade5d..b22798f2a6 100644 --- a/react_on_rails/rakelib/example_type.rb +++ b/react_on_rails/rakelib/example_type.rb @@ -24,7 +24,7 @@ def self.all }.freeze # Semantic version constants for documentation and reference - MINIMUM_SUPPORTED_REACT_VERSION = "18.0.0" + MINIMUM_SUPPORTED_REACT_VERSION = "16.14.0" LATEST_REACT_MAJOR_VERSION = "19" # Minimum Shakapacker version for compatibility testing From d0512f2ff43b6b4bd11d9cb10a47310a8bcf62c1 Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Tue, 9 Dec 2025 21:57:54 -1000 Subject: [PATCH 39/40] Use major version for MINIMUM_SUPPORTED_REACT constant MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We advertise support for major versions (React 16, 17, 18, 19) and test with the latest patch release of each. Renamed constant to reflect this. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- react_on_rails/rakelib/example_type.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/react_on_rails/rakelib/example_type.rb b/react_on_rails/rakelib/example_type.rb index b22798f2a6..17d7ebadbb 100644 --- a/react_on_rails/rakelib/example_type.rb +++ b/react_on_rails/rakelib/example_type.rb @@ -23,8 +23,8 @@ def self.all "19" => nil # nil means use latest (default) }.freeze - # Semantic version constants for documentation and reference - MINIMUM_SUPPORTED_REACT_VERSION = "16.14.0" + # Supported React major versions (we test with latest patch of each) + MINIMUM_SUPPORTED_REACT_MAJOR_VERSION = "16" LATEST_REACT_MAJOR_VERSION = "19" # Minimum Shakapacker version for compatibility testing From 312405296a763e6f9ba61f753c142bebafbaa65b Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Tue, 9 Dec 2025 22:14:07 -1000 Subject: [PATCH 40/40] Clean up naming: use 'pinned' consistently, remove legacy code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove legacy minimum_versions parameter and minimum_versions? method from ExampleType (no longer needed with react_version parameter) - Remove legacy shakapacker_examples_minimum rake task alias - Update all comments to use "pinned" instead of "minimum" terminology - Update examples_config.yml documentation: - Change "Minimum CI" to "Pinned CI" - Remove outdated "minimum supported = React 18" text - Clarify we support React 16+ and test with latest patch of each major This completes the terminology cleanup to consistently use: - "Latest" = React 19 + Shakapacker 9.x (runs on all PRs) - "Pinned" = React 16, 17, 18 + Shakapacker 8.2.0 (runs on master) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- react_on_rails/rakelib/example_type.rb | 11 ++--------- react_on_rails/rakelib/examples_config.yml | 11 +++++------ react_on_rails/rakelib/run_rspec.rake | 8 +------- 3 files changed, 8 insertions(+), 22 deletions(-) diff --git a/react_on_rails/rakelib/example_type.rb b/react_on_rails/rakelib/example_type.rb index 17d7ebadbb..540f80fa14 100644 --- a/react_on_rails/rakelib/example_type.rb +++ b/react_on_rails/rakelib/example_type.rb @@ -44,18 +44,11 @@ def react_version_string REACT_VERSIONS[react_version.to_s] || react_version end - # Legacy method for backward compatibility - true if React 18 (minimum supported) - def minimum_versions? - react_version == "18" - end - - def initialize(packer_type: nil, name: nil, generator_options: nil, minimum_versions: false, react_version: nil) + def initialize(packer_type: nil, name: nil, generator_options: nil, react_version: nil) @packer_type = packer_type @name = name @generator_options = generator_options - # Support both legacy minimum_versions flag and new react_version parameter - # minimum_versions: true is equivalent to react_version: "18" - @react_version = react_version || (minimum_versions ? "18" : nil) + @react_version = react_version # Validate react_version is a known version to catch configuration errors early if @react_version && !REACT_VERSIONS.key?(@react_version.to_s) diff --git a/react_on_rails/rakelib/examples_config.yml b/react_on_rails/rakelib/examples_config.yml index 501503add8..c4c569a6fc 100644 --- a/react_on_rails/rakelib/examples_config.yml +++ b/react_on_rails/rakelib/examples_config.yml @@ -2,20 +2,19 @@ # # CI Test Coverage: # ----------------- -# - Latest CI (PRs): Runs shakapacker_examples_latest (React 19, Shakapacker 9.x) +# - Latest CI (all PRs): Runs shakapacker_examples_latest (React 19, Shakapacker 9.x) # Examples: basic, basic-server-rendering, redux, redux-server-rendering # -# - Minimum CI (master): Runs shakapacker_examples_pinned (React 16, 17, 18 with Shakapacker 8.2.0) +# - Pinned CI (master): Runs shakapacker_examples_pinned (React 16, 17, 18 with Shakapacker 8.2.0) # Examples: basic-react16, basic-server-rendering-react16, # basic-react17, basic-server-rendering-react17, # basic-react18, basic-server-rendering-react18 # # Terminology: # - "Latest" = Current React version (19) with latest Shakapacker (9.x) -# - "Pinned" = Specific older React versions (16, 17, 18) for compatibility testing -# - "Minimum supported" = React 18.0.0 + Shakapacker 8.2.0 (oldest officially supported) +# - "Pinned" = Specific older React versions (16, 17, 18) for backward compatibility testing # -# Note: React 16 and 17 are past EOL but still tested for legacy compatibility. +# Note: We support React 16+ but test with latest patch of each major version. example_type_data: # Latest versions (React 19, Shakapacker 9.x) @@ -28,7 +27,7 @@ example_type_data: - name: redux-server-rendering generator_options: --redux --example-server-rendering - # React 18 compatibility tests (minimum supported version with Root API) + # React 18 compatibility tests (uses Root API introduced in React 18) - name: basic-react18 generator_options: '' react_version: '18' diff --git a/react_on_rails/rakelib/run_rspec.rake b/react_on_rails/rakelib/run_rspec.rake index 195484e26f..869f34b812 100644 --- a/react_on_rails/rakelib/run_rspec.rake +++ b/react_on_rails/rakelib/run_rspec.rake @@ -140,12 +140,6 @@ namespace :run_rspec do pinned_version_examples.each { |example_type| Rake::Task[example_type.rspec_task_name].invoke } end - # Legacy alias for backward compatibility - desc "Runs Rspec for minimum version example apps only (React 18, Shakapacker 8.2.0)" - task shakapacker_examples_minimum: react18_examples.map(&:gen_task_name) do - react18_examples.each { |example_type| Rake::Task[example_type.rspec_task_name].invoke } - end - Coveralls::RakeTask.new if ENV["USE_COVERALLS"] == "TRUE" desc "run all tests no examples" @@ -199,7 +193,7 @@ end # :rspec_args - additional rspec arguments (default: "") # :env_vars - additional environment variables (default: "") # :unbundled - run with unbundled_sh_in_dir for Bundler isolation (default: false) -# This is required for minimum version examples because they have different +# This is required for pinned version examples because they have different # gem versions (e.g., Shakapacker 8.2.0) pinned in their Gemfile than the # parent workspace (Shakapacker 9.x). Without bundle isolation, Bundler # would inherit the parent's gem resolution and use the wrong versions.