Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions .github/workflows/examples.yml
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ jobs:
echo "Node version: "; node -v
echo "Yarn version: "; yarn --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
Expand Down Expand Up @@ -172,8 +172,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:
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ jobs:
echo "Node version: "; node -v
echo "Yarn version: "; yarn --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 Yarn for renderer package
Expand Down Expand Up @@ -211,7 +211,7 @@ jobs:
echo "Node version: "; node -v
echo "Yarn version: "; yarn --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
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@
"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",
"stylelint": "^16.14.0",
Expand Down
14 changes: 12 additions & 2 deletions react_on_rails/rakelib/example_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,22 @@ 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

# 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
@generator_options = generator_options
@minimum_versions = minimum_versions
self.class.all[packer_type.to_sym] << self
end

Expand Down
7 changes: 7 additions & 0 deletions react_on_rails/rakelib/examples_config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
19 changes: 19 additions & 0 deletions react_on_rails/rakelib/run_rspec.rake
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,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"
Expand Down
67 changes: 66 additions & 1 deletion react_on_rails/rakelib/shakapacker_examples.rake
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,72 @@
require "yaml"
require "rails/version"
require "pathname"
require "json"

require_relative "example_type"
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

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

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

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|
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}"
Expand Down Expand Up @@ -46,6 +103,14 @@ 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
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
# auto-bundled components have corresponding pack files created
Expand Down
4 changes: 2 additions & 2 deletions react_on_rails/spec/dummy/Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,7 @@ GEM
rubyzip (>= 1.2.2, < 3.0)
websocket (~> 1.0)
semantic_range (3.1.0)
shakapacker (9.3.0)
shakapacker (9.4.0)
activesupport (>= 5.2)
package_json
rack-proxy (>= 0.6.1)
Expand Down Expand Up @@ -461,7 +461,7 @@ DEPENDENCIES
sass-rails (~> 6.0)
sdoc
selenium-webdriver (= 4.9.0)
shakapacker (= 9.3.0)
shakapacker (= 9.4.0)
spring (~> 4.0)
sprockets (~> 4.0)
sqlite3 (~> 1.6)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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) => (
<div>
<Helmet>
Expand Down
Original file line number Diff line number Diff line change
@@ -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) => <ReactHelmet {...props} />;

export default (props) => <ReactHelmet {...props} />;
// HelmetProvider is required by @dr.pogodin/react-helmet for both client and server rendering
export default (props) => (
<HelmetProvider>
<ReactHelmet {...props} />
</HelmetProvider>
);

// Note, the server side has to be a Render-Function

Expand Down
Original file line number Diff line number Diff line change
@@ -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 }
Expand All @@ -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(<ReactHelmet {...props} />);
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(
<HelmetProvider context={helmetContext}>
<div>
<Helmet>
<title>Custom page title</title>
</Helmet>
Props: {JSON.stringify(props)}
<HelloWorld {...props} />
</div>
</HelmetProvider>,
);

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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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) => <ReactHelmet {...props} />;

export default (props) => <ReactHelmet {...props} />;
// HelmetProvider is required by @dr.pogodin/react-helmet for both client and server rendering
export default (props) => (
<HelmetProvider>
<ReactHelmet {...props} />
</HelmetProvider>
);

// Note, the server side has to be a Render-Function

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/*
Expand All @@ -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(<ReactHelmet {...props} />);
const helmet = Helmet.renderStatic();
const helmetContext = {};
const componentHtml = renderToString(
<HelmetProvider context={helmetContext}>
<ReactHelmet {...props} />
</HelmetProvider>,
);
const { helmet } = helmetContext;

const renderedHtml = {
componentHtml,
title: helmet.title.toString(),
title: helmet ? helmet.title.toString() : '',
};
return { renderedHtml };
};
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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.
Expand Down
Loading
Loading