Skip to content

Commit c2ebf3d

Browse files
justin808claude
andcommitted
Automate Pro generator gem/package swap and import updates (#2822)
## Summary - Add first-class `--rsc-pro` install mode to the generator, unifying `--rsc --pro` into a single flag with matched Pro/RSC defaults, verification checklist, and recovery commands - Extend standalone `react_on_rails:pro` upgrade flow to automate Gemfile gem swap (`react_on_rails` → `react_on_rails_pro`) with mode-aware version requirements (`~>` for standard Pro, exact pin in RSC Pro mode when applicable), plus `bundle install` after swap and automatic rollback on failure - Rewrite JS/TS/Vue/Svelte imports from `react-on-rails` to `react-on-rails-pro` across common frontend roots using atomic file writes, with full comment-aware parsing (block comments, multiline imports, and inline template-literal masking to avoid false rewrites inside template strings) - Improve `package_json` add return handling — treat `nil` as success instead of falsy failure - Pin RSC dependencies to explicit versions (`react`/`react-dom` to `~19.0.4`, `react-on-rails-rsc` to `19.0.4`) with fallback to unpinned install when pinned install fails - Improve Pro flag detection: `use_pro?` and `use_rsc?` now respect `--rsc-pro`, and new `use_rsc_pro_mode?` helper controls RSC Pro–specific behavior - Add Pro gem version requirement helper (`pro_gem_version_requirement`) — exact pin for RSC Pro mode, pessimistic (`~>`) for standard Pro - Add prerelease messaging for RSC Pro mode when the gem version is a prerelease - Add RSC Pro post-install verification message with startup/streaming/hydration checklist Closes #2626 ## Test Plan - `bundle exec rspec react_on_rails/spec/react_on_rails/generators/pro_generator_spec.rb` - `bundle exec rspec react_on_rails/spec/react_on_rails/generators/generator_helper_spec.rb` - `bundle exec rspec react_on_rails/spec/react_on_rails/generators/install_generator_spec.rb` - `bundle exec rspec react_on_rails/spec/react_on_rails/generators/js_dependency_manager_spec.rb` - `bundle exec rubocop react_on_rails/lib/generators/react_on_rails/pro_generator.rb react_on_rails/lib/generators/react_on_rails/pro_setup.rb react_on_rails/lib/generators/react_on_rails/install_generator.rb` 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Medium risk: generator behavior now mutates `Gemfile`/runs `bundle install`, pins RSC/Pro versions, and rewrites frontend imports, which could impact existing apps if edge cases in parsing or version constraints are missed. > > **Overview** > Adds a first-class `--rsc-pro` installation mode (and `use_rsc_pro_mode?` helper) so install flows, sub-generator invocation, recovery commands, prerequisite errors, and post-install messaging treat RSC+Pro as a single, consistent mode. > > Enhances dependency handling by treating `package_json` `add` returning `nil` as success, pinning RSC React versions to `~19.0.4`, pinning `react-on-rails-rsc` to `19.0.4` with retry-to-latest fallback, and making Pro gem version requirements exact in RSC Pro mode. > > Extends `react_on_rails:pro` to automatically swap `react_on_rails` → `react_on_rails_pro` in `Gemfile` (with atomic writes, `bundle install`, and rollback on failure) and to rewrite JS/TS/Vue/Svelte imports from `react-on-rails` to `react-on-rails-pro`, with extensive spec coverage for edge cases. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 7c0ecb8. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added `--rsc-pro` CLI option to enable first-class React Server Components with Pro features in a single installation command. * Automatic Gemfile gem swap from `react_on_rails` to `react_on_rails_pro` with bundler integration. * Automatic rewriting of module imports from `react-on-rails` to `react-on-rails-pro` across JavaScript/TypeScript files. * Introduced React version pinning (~19.0.4) for RSC installations. * **Bug Fixes** * Improved npm dependency installation return value handling to correctly interpret `nil` responses as successful. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 4f37812 commit c2ebf3d

9 files changed

Lines changed: 2235 additions & 32 deletions

File tree

react_on_rails/lib/generators/react_on_rails/generator_helper.rb

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ def add_npm_dependencies(packages, dev: false)
3434
else
3535
pj.manager.add(packages, exact: true)
3636
end
37-
result ? true : false
37+
# package_json#add can return nil for successful side-effect operations.
38+
result != false
3839
rescue StandardError => e
3940
say_status :warning, "Could not add packages via package_json gem: #{e.message}", :yellow
4041
say_status :warning, "Will fall back to direct npm commands.", :yellow
@@ -151,20 +152,28 @@ def mark_pro_gem_installed!
151152
@pro_gem_installed = true
152153
end
153154

154-
# Check if Pro features should be enabled
155-
# Returns true if --pro flag is set OR --rsc flag is set (RSC implies Pro)
155+
# Check if first-class RSC Pro mode should be enabled.
156+
# Returns true when --rsc-pro is set, or when users explicitly pass both --rsc and --pro.
157+
#
158+
# @return [Boolean] true if RSC Pro mode semantics should be applied
159+
def use_rsc_pro_mode?
160+
options[:rsc_pro] || (options[:rsc] && options[:pro])
161+
end
162+
163+
# Check if Pro features should be enabled.
164+
# Returns true if --pro, --rsc, or --rsc-pro is set (RSC implies Pro).
156165
#
157166
# @return [Boolean] true if Pro setup should be included
158167
def use_pro?
159-
options[:pro] || options[:rsc]
168+
options[:pro] || options[:rsc] || options[:rsc_pro]
160169
end
161170

162171
# Check if RSC (React Server Components) should be enabled
163-
# Returns true only if --rsc flag is explicitly set
172+
# Returns true if --rsc or --rsc-pro is explicitly set
164173
#
165174
# @return [Boolean] true if RSC setup should be included
166175
def use_rsc?
167-
options[:rsc]
176+
options[:rsc] || options[:rsc_pro]
168177
end
169178

170179
# Determine if the project is using rspack as the bundler.

react_on_rails/lib/generators/react_on_rails/install_generator.rb

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,21 @@ class InstallGenerator < Rails::Generators::Base
5757
class_option :pro,
5858
type: :boolean,
5959
default: false,
60-
desc: "Install React on Rails Pro with Node Renderer. Default: false"
60+
desc: "Install React on Rails Pro with Node Renderer. " \
61+
"Combined with --rsc, uses --rsc-pro mode. Default: false"
6162

6263
# --rsc
6364
class_option :rsc,
6465
type: :boolean,
6566
default: false,
66-
desc: "Install React Server Components support (includes Pro). Default: false"
67+
desc: "Install React Server Components support (includes Pro). " \
68+
"Combined with --pro, uses --rsc-pro mode. Default: false"
69+
70+
# --rsc-pro
71+
class_option :rsc_pro,
72+
type: :boolean,
73+
default: false,
74+
desc: "Install first-class Pro RSC mode with matched Pro/RSC defaults. Default: false"
6775

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

9098
SHAKAPACKER_YML_PATH = "config/shakapacker.yml"
99+
HELLO_WORLD_ROUTE = "hello_world"
100+
HELLO_SERVER_ROUTE = "hello_server"
91101
# Matches the stock `bin/dev` written by Rails 8.x. Rails 7.1 commonly
92102
# generated a foreman-based shell script instead, which stock_rails_bin_dev?
93103
# also recognizes so the React on Rails template can replace either variant.
@@ -190,7 +200,7 @@ def invoke_generators
190200
# --pretend/--force/--skip must be forwarded explicitly at each boundary.
191201
invoke "react_on_rails:base", [],
192202
{ typescript: options.typescript?, redux: options.redux?, rspack: options.rspack?,
193-
pro: options.pro?, rsc: options.rsc?, new_app: options.new_app?,
203+
pro: use_pro?, rsc: use_rsc?, new_app: options.new_app?,
194204
shakapacker_just_installed: shakapacker_just_installed?,
195205
force: options[:force], skip: options[:skip], pretend: options[:pretend] }
196206

@@ -202,7 +212,7 @@ def invoke_generators
202212
invoke "react_on_rails:react_with_redux", [], { typescript: options.typescript?,
203213
invoked_by_install: true,
204214
new_app: options.new_app?,
205-
rsc: options.rsc?,
215+
rsc: use_rsc?,
206216
force: options[:force], skip: options[:skip],
207217
pretend: options[:pretend] }
208218
elsif !use_rsc?
@@ -274,11 +284,12 @@ def installation_prerequisites_met?
274284
# it on a clean worktree. On a dirty tree, use the read-only pro_gem_installed?
275285
# check to catch a missing gem without triggering auto-install.
276286
if has_worktree_issues && use_pro? && !pro_gem_installed?
287+
required_flag = pro_requirement_flag
277288
GeneratorMessages.add_error(<<~MSG.strip)
278-
🚫 react_on_rails_pro gem is required for #{options[:rsc] ? '--rsc' : '--pro'} but is not installed.
289+
🚫 react_on_rails_pro gem is required for #{required_flag} but is not installed.
279290
Auto-install was skipped because the worktree has uncommitted changes.
280291
Please add it manually:
281-
gem 'react_on_rails_pro', '~> #{recommended_pro_gem_version}'
292+
gem 'react_on_rails_pro', '#{pro_gem_version_requirement}'
282293
Then run: bundle install
283294
MSG
284295
return false
@@ -382,7 +393,7 @@ def add_bin_scripts
382393
if preserve_existing_bin_dev?
383394
if use_rsc? && !options.redux? && !options.new_app?
384395
say_status :warn,
385-
'Custom bin/dev detected: update DEFAULT_ROUTE to "hello_server" manually for --rsc',
396+
"Custom bin/dev detected: update DEFAULT_ROUTE to \"#{HELLO_SERVER_ROUTE}\" manually for --rsc",
386397
:yellow
387398
end
388399
else
@@ -463,10 +474,10 @@ def add_post_install_message
463474
# Determine what route and component will be created by the generator
464475
if use_rsc? && !options.redux?
465476
# RSC without Redux: HelloServer replaces HelloWorld
466-
route = "hello_server"
477+
route = HELLO_SERVER_ROUTE
467478
component_name = "HelloServer"
468479
else
469-
route = "hello_world"
480+
route = HELLO_WORLD_ROUTE
470481
component_name = options.redux? ? "HelloWorldApp" : "HelloWorld"
471482
end
472483

@@ -478,6 +489,7 @@ def add_post_install_message
478489
shakapacker_just_installed: shakapacker_just_installed?,
479490
landing_page: options.new_app? && new_app_root_route_available?
480491
))
492+
GeneratorMessages.add_info(rsc_pro_verification_message) if use_rsc_pro_mode?
481493
end
482494

483495
def shakapacker_setup_incomplete?
@@ -491,7 +503,9 @@ def recovery_install_command
491503
flags << "--typescript" if options.typescript?
492504
flags << "--rspack" if options.rspack?
493505

494-
if use_rsc?
506+
if use_rsc_pro_mode?
507+
flags << "--rsc-pro"
508+
elsif options.rsc?
495509
flags << "--rsc"
496510
elsif options.pro?
497511
flags << "--pro"
@@ -500,6 +514,17 @@ def recovery_install_command
500514
["rails generate react_on_rails:install", *flags].join(" ")
501515
end
502516

517+
def rsc_pro_verification_message
518+
<<~MSG
519+
520+
🔎 RSC Pro Verification:
521+
─────────────────────────────────────────────────────────────────────────
522+
1. Start all processes: #{Rainbow('bin/dev').cyan}
523+
2. Visit: #{Rainbow("http://localhost:<port>/#{HELLO_SERVER_ROUTE}").cyan.underline}
524+
3. Confirm the page streams and the Like button hydrates on click.
525+
MSG
526+
end
527+
503528
def recovery_working_tree_lines
504529
[
505530
"If this run created or changed files, clean up your working tree before rerunning",

react_on_rails/lib/generators/react_on_rails/js_dependency_manager.rb

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,10 @@ module JsDependencyManager
125125
react-on-rails-rsc
126126
].freeze
127127

128+
# RSC package releases follow the React 19.0.x line (independent from gem versioning).
129+
RSC_REACT_VERSION_RANGE = "~19.0.4"
130+
RSC_PACKAGE_VERSION_PIN = "19.0.4"
131+
128132
private
129133

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

384388
def add_rsc_dependencies
385389
say "Installing React Server Components dependencies..."
386-
return if add_packages(RSC_DEPENDENCIES)
390+
rsc_packages, used_version_pins = rsc_packages_with_version
391+
return if add_packages(rsc_packages)
392+
393+
manual_install_packages = rsc_packages
394+
if used_version_pins
395+
warning_msg = "Could not install version-pinned RSC dependency. Retrying latest available package."
396+
say_status :warning,
397+
warning_msg,
398+
:yellow
399+
GeneratorMessages.add_warning(
400+
"Warning: #{warning_msg} " \
401+
"The installed react-on-rails-rsc version may not match the expected compatibility pin."
402+
)
403+
return if add_packages(RSC_DEPENDENCIES)
404+
405+
manual_install_packages = RSC_DEPENDENCIES
406+
end
387407

388408
GeneratorMessages.add_warning(<<~MSG.strip)
389409
⚠️ Failed to add React Server Components dependencies.
390410
391411
You can install them manually by running:
392-
npm install #{RSC_DEPENDENCIES.join(' ')}
412+
npm install #{manual_install_packages.join(' ')}
393413
MSG
394414
rescue StandardError => e
395415
GeneratorMessages.add_warning(<<~MSG.strip)
@@ -400,6 +420,12 @@ def add_rsc_dependencies
400420
MSG
401421
end
402422

423+
# Returns [pinned_packages, used_version_pins]. used_version_pins is always true here;
424+
# subclasses may override to return [packages, false] when pinning should be skipped.
425+
def rsc_packages_with_version
426+
[RSC_DEPENDENCIES.map { |pkg| "#{pkg}@#{RSC_PACKAGE_VERSION_PIN}" }, true]
427+
end
428+
403429
def remove_base_package_if_present
404430
pj = package_json
405431
return unless pj

0 commit comments

Comments
 (0)