Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
c14431e
Add first-class --rsc-pro install mode
justin808 Mar 24, 2026
4343d86
Automate Pro generator gem and import upgrade steps
justin808 Mar 24, 2026
6fda382
Handle failed npm add results so RSC installs recover
justin808 Mar 24, 2026
97ba6ee
Fix rsc-pro gem pinning and generator review issues
justin808 Mar 24, 2026
eb33f31
Harden Pro gem swap and import rewrite behavior
justin808 Mar 24, 2026
55d2b39
Harden Pro generator Gemfile swap and import rewrites
justin808 Mar 24, 2026
90a48c3
Harden Pro Gemfile swap and bundle install flow
justin808 Mar 24, 2026
bbce91a
Harden Pro generator gem swap and import rewrites
justin808 Mar 24, 2026
3770f5c
Fix Gemfile multiline parsing edge case in pro generator
justin808 Mar 24, 2026
f6a42ec
Handle magic-comment dynamic imports in pro import rewrite
justin808 Mar 24, 2026
aa86193
Harden pro Gemfile swap and import rewrite edge cases
justin808 Mar 24, 2026
5bcf491
Harden pro generator import and Gemfile rewrite flows
justin808 Mar 24, 2026
28747cb
Clarify preserved Pro Gemfile entries during swap
justin808 Mar 24, 2026
45da4bb
Harden Pro generator multiline parsing and atomic writes
justin808 Mar 24, 2026
7970be6
Harden Pro generator import rewrites and error handling
justin808 Mar 24, 2026
88fe947
Cover Pro generator multiline and rewrite edge cases
justin808 Mar 24, 2026
d68f7db
Handle block-comment and multiline static import rewrites
justin808 Mar 24, 2026
eed2fb0
Preserve Gemfile metadata in Pro swap edge cases
justin808 Mar 24, 2026
07b9593
Fix parenthesized Gemfile swap edge cases
justin808 Mar 24, 2026
bd71df2
Assert no orphan parenthesis in Gemfile rewrite
justin808 Mar 24, 2026
88ca44c
Harden Pro gem swap parsing and import rewrite guards
justin808 Mar 24, 2026
97252bb
Fix backtick detection for quoted string literals
justin808 Mar 24, 2026
237ceb0
Handle mixed-line gem calls and template-state edge cases
justin808 Mar 24, 2026
84c806d
Fix rsc-pro recovery flags and rsc dependency assertion
justin808 Mar 24, 2026
d84f57e
Fix #2821 review follow-ups for RSC-Pro generator
justin808 Mar 24, 2026
341a990
Address review feedback for RSC Pro dependency handling
justin808 Mar 24, 2026
919a1b1
Harden RSC pin fallback messaging and Pro flag checks
justin808 Mar 24, 2026
95efd3f
Rollback Gemfile swap on bundle failure and atomically rewrite imports
justin808 Mar 28, 2026
24bbb51
Fix pro generator swap consistency and template-literal rewrites
justin808 Mar 28, 2026
422ff98
Fix regex backtracking and strip all version constraints in gem swap
justin808 Mar 29, 2026
27137fc
Avoid partial pro upgrades and stale generator rewrites
justin808 Mar 30, 2026
62040f9
Avoid partial standalone Pro upgrades
justin808 Mar 30, 2026
c9d36f8
Address review: node_modules exclusion, block comment tracking, narro…
justin808 Mar 31, 2026
b78c4dd
Fix unresolved review findings on pro generator swap/import handling
justin808 Apr 2, 2026
6be8925
Avoid rewriting React on Rails imports inside template literals
justin808 Apr 2, 2026
7c6cd2b
Avoid duplicate Pro gem entries when parenthesized Gemfile declaratio…
justin808 Apr 2, 2026
7c0ecb8
Fix RSC React dependency warning spec to simulate fallback failure
justin808 Apr 2, 2026
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
21 changes: 15 additions & 6 deletions react_on_rails/lib/generators/react_on_rails/generator_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ def add_npm_dependencies(packages, dev: false)
else
pj.manager.add(packages, exact: true)
end
result ? true : false
# package_json#add can return nil for successful side-effect operations.
result != false
rescue StandardError => e
say_status :warning, "Could not add packages via package_json gem: #{e.message}", :yellow
say_status :warning, "Will fall back to direct npm commands.", :yellow
Expand Down Expand Up @@ -151,20 +152,28 @@ def mark_pro_gem_installed!
@pro_gem_installed = true
end

# Check if Pro features should be enabled
# Returns true if --pro flag is set OR --rsc flag is set (RSC implies Pro)
# Check if first-class RSC Pro mode should be enabled.
# Returns true when --rsc-pro is set, or when users explicitly pass both --rsc and --pro.
#
# @return [Boolean] true if RSC Pro mode semantics should be applied
def use_rsc_pro_mode?
options[:rsc_pro] || (options[:rsc] && options[:pro])
end

# Check if Pro features should be enabled.
# Returns true if --pro, --rsc, or --rsc-pro is set (RSC implies Pro).
#
# @return [Boolean] true if Pro setup should be included
def use_pro?
options[:pro] || options[:rsc]
options[:pro] || options[:rsc] || options[:rsc_pro]
end

# Check if RSC (React Server Components) should be enabled
# Returns true only if --rsc flag is explicitly set
# Returns true if --rsc or --rsc-pro is explicitly set
#
# @return [Boolean] true if RSC setup should be included
def use_rsc?
options[:rsc]
options[:rsc] || options[:rsc_pro]
end

# Determine if the project is using rspack as the bundler.
Expand Down
45 changes: 35 additions & 10 deletions react_on_rails/lib/generators/react_on_rails/install_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,21 @@ class InstallGenerator < Rails::Generators::Base
class_option :pro,
type: :boolean,
default: false,
desc: "Install React on Rails Pro with Node Renderer. Default: false"
desc: "Install React on Rails Pro with Node Renderer. " \
"Combined with --rsc, uses --rsc-pro mode. Default: false"

# --rsc
class_option :rsc,
type: :boolean,
default: false,
desc: "Install React Server Components support (includes Pro). Default: false"
desc: "Install React Server Components support (includes Pro). " \
"Combined with --pro, uses --rsc-pro mode. Default: false"

# --rsc-pro
class_option :rsc_pro,
type: :boolean,
default: false,
desc: "Install first-class Pro RSC mode with matched Pro/RSC defaults. Default: false"

# Hidden option: allows tests (and advanced users) to signal that Shakapacker
# was just installed, triggering force-overwrite of shakapacker.yml with RoR's template.
Expand All @@ -88,6 +96,8 @@ class InstallGenerator < Rails::Generators::Base
# Removed: --skip-shakapacker-install (Shakapacker is now a required dependency)

SHAKAPACKER_YML_PATH = "config/shakapacker.yml"
HELLO_WORLD_ROUTE = "hello_world"
HELLO_SERVER_ROUTE = "hello_server"
# Matches the stock `bin/dev` written by Rails 8.x. Rails 7.1 commonly
# generated a foreman-based shell script instead, which stock_rails_bin_dev?
# also recognizes so the React on Rails template can replace either variant.
Expand Down Expand Up @@ -190,7 +200,7 @@ def invoke_generators
# --pretend/--force/--skip must be forwarded explicitly at each boundary.
invoke "react_on_rails:base", [],
{ typescript: options.typescript?, redux: options.redux?, rspack: options.rspack?,
pro: options.pro?, rsc: options.rsc?, new_app: options.new_app?,
pro: use_pro?, rsc: use_rsc?, new_app: options.new_app?,
Comment thread
justin808 marked this conversation as resolved.
shakapacker_just_installed: shakapacker_just_installed?,
force: options[:force], skip: options[:skip], pretend: options[:pretend] }

Expand All @@ -202,7 +212,7 @@ def invoke_generators
invoke "react_on_rails:react_with_redux", [], { typescript: options.typescript?,
invoked_by_install: true,
new_app: options.new_app?,
rsc: options.rsc?,
rsc: use_rsc?,
force: options[:force], skip: options[:skip],
pretend: options[:pretend] }
elsif !use_rsc?
Expand Down Expand Up @@ -274,11 +284,12 @@ def installation_prerequisites_met?
# it on a clean worktree. On a dirty tree, use the read-only pro_gem_installed?
# check to catch a missing gem without triggering auto-install.
if has_worktree_issues && use_pro? && !pro_gem_installed?
required_flag = pro_requirement_flag
GeneratorMessages.add_error(<<~MSG.strip)
🚫 react_on_rails_pro gem is required for #{options[:rsc] ? '--rsc' : '--pro'} but is not installed.
🚫 react_on_rails_pro gem is required for #{required_flag} but is not installed.
Auto-install was skipped because the worktree has uncommitted changes.
Please add it manually:
gem 'react_on_rails_pro', '~> #{recommended_pro_gem_version}'
gem 'react_on_rails_pro', '#{pro_gem_version_requirement}'
Then run: bundle install
MSG
return false
Expand Down Expand Up @@ -382,7 +393,7 @@ def add_bin_scripts
if preserve_existing_bin_dev?
if use_rsc? && !options.redux? && !options.new_app?
say_status :warn,
'Custom bin/dev detected: update DEFAULT_ROUTE to "hello_server" manually for --rsc',
"Custom bin/dev detected: update DEFAULT_ROUTE to \"#{HELLO_SERVER_ROUTE}\" manually for --rsc",
:yellow
end
else
Expand Down Expand Up @@ -463,10 +474,10 @@ def add_post_install_message
# Determine what route and component will be created by the generator
if use_rsc? && !options.redux?
# RSC without Redux: HelloServer replaces HelloWorld
route = "hello_server"
route = HELLO_SERVER_ROUTE
component_name = "HelloServer"
else
route = "hello_world"
route = HELLO_WORLD_ROUTE
component_name = options.redux? ? "HelloWorldApp" : "HelloWorld"
end

Expand All @@ -478,6 +489,7 @@ def add_post_install_message
shakapacker_just_installed: shakapacker_just_installed?,
landing_page: options.new_app? && new_app_root_route_available?
))
GeneratorMessages.add_info(rsc_pro_verification_message) if use_rsc_pro_mode?
end

def shakapacker_setup_incomplete?
Expand All @@ -491,7 +503,9 @@ def recovery_install_command
flags << "--typescript" if options.typescript?
flags << "--rspack" if options.rspack?

if use_rsc?
if use_rsc_pro_mode?
flags << "--rsc-pro"
elsif options.rsc?
flags << "--rsc"
elsif options.pro?
flags << "--pro"
Expand All @@ -500,6 +514,17 @@ def recovery_install_command
["rails generate react_on_rails:install", *flags].join(" ")
end

def rsc_pro_verification_message
<<~MSG
Comment thread
justin808 marked this conversation as resolved.

🔎 RSC Pro Verification:
─────────────────────────────────────────────────────────────────────────
1. Start all processes: #{Rainbow('bin/dev').cyan}
2. Visit: #{Rainbow("http://localhost:<port>/#{HELLO_SERVER_ROUTE}").cyan.underline}
3. Confirm the page streams and the Like button hydrates on click.
MSG
end

def recovery_working_tree_lines
[
"If this run created or changed files, clean up your working tree before rerunning",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ module JsDependencyManager
react-on-rails-rsc
].freeze

# RSC package releases follow the React 19.0.x line (independent from gem versioning).
RSC_REACT_VERSION_RANGE = "~19.0.4"
RSC_PACKAGE_VERSION_PIN = "19.0.4"
Comment thread
justin808 marked this conversation as resolved.
Comment thread
justin808 marked this conversation as resolved.

private

def setup_js_dependencies
Expand Down Expand Up @@ -218,7 +222,7 @@ def add_react_dependencies
# RSC requires React 19.0.x specifically (not 19.1.x or later)
# Pin to ~19.0.4 to allow patch updates while staying within 19.0.x
react_deps = if respond_to?(:use_rsc?) && use_rsc?
%w[react@~19.0.4 react-dom@~19.0.4 prop-types]
["react@#{RSC_REACT_VERSION_RANGE}", "react-dom@#{RSC_REACT_VERSION_RANGE}", "prop-types"]
else
REACT_DEPENDENCIES
end
Expand Down Expand Up @@ -383,13 +387,29 @@ def pro_packages_with_version

def add_rsc_dependencies
say "Installing React Server Components dependencies..."
return if add_packages(RSC_DEPENDENCIES)
rsc_packages, used_version_pins = rsc_packages_with_version
return if add_packages(rsc_packages)

manual_install_packages = rsc_packages
if used_version_pins
warning_msg = "Could not install version-pinned RSC dependency. Retrying latest available package."
say_status :warning,
warning_msg,
:yellow
GeneratorMessages.add_warning(
"Warning: #{warning_msg} " \
"The installed react-on-rails-rsc version may not match the expected compatibility pin."
)
return if add_packages(RSC_DEPENDENCIES)
Comment thread
justin808 marked this conversation as resolved.

manual_install_packages = RSC_DEPENDENCIES
end

GeneratorMessages.add_warning(<<~MSG.strip)
⚠️ Failed to add React Server Components dependencies.

You can install them manually by running:
npm install #{RSC_DEPENDENCIES.join(' ')}
npm install #{manual_install_packages.join(' ')}
MSG
rescue StandardError => e
GeneratorMessages.add_warning(<<~MSG.strip)
Expand All @@ -400,6 +420,12 @@ def add_rsc_dependencies
MSG
end

# Returns [pinned_packages, used_version_pins]. used_version_pins is always true here;
# subclasses may override to return [packages, false] when pinning should be skipped.
def rsc_packages_with_version
[RSC_DEPENDENCIES.map { |pkg| "#{pkg}@#{RSC_PACKAGE_VERSION_PIN}" }, true]
Comment thread
justin808 marked this conversation as resolved.
end
Comment on lines +423 to +427
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Subclasses may override" is pre-emptive documentation

There are currently no subclasses of JsDependencyManager that override this method, and no existing callers passing [packages, false]. The comment describes a future extension point that doesn't exist yet, which adds reading overhead without actionable guidance.

If this override hook is genuinely part of the design, consider either:

  1. Leaving it undocumented until there's a concrete subclass, or
  2. Adding a note about which subclass would use it and why, so it's clearly intentional rather than accidentally dead code

The constant RSC_PACKAGE_VERSION_PIN being hardcoded ("19.0.4") while the React version range is derived from RSC_REACT_VERSION_RANGE is also worth noting: when updating the React version, both constants need to be updated in sync. A comment (or derive one from the other) would reduce the chance of drift.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the suggestion. I’m keeping the comment for now as intentional context for the extension point; no functional issue to address in this PR.


def remove_base_package_if_present
pj = package_json
return unless pj
Expand Down
Loading
Loading