react_on_rails 16.4.0.rc.1 → 16.4.0.rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +2 -0
  3. data/Gemfile.lock +1 -1
  4. data/lib/generators/react_on_rails/base_generator.rb +30 -10
  5. data/lib/generators/react_on_rails/dev_tests_generator.rb +9 -1
  6. data/lib/generators/react_on_rails/generator_helper.rb +101 -0
  7. data/lib/generators/react_on_rails/generator_messages.rb +2 -2
  8. data/lib/generators/react_on_rails/install_generator.rb +55 -19
  9. data/lib/generators/react_on_rails/js_dependency_manager.rb +115 -8
  10. data/lib/generators/react_on_rails/pro/USAGE +21 -0
  11. data/lib/generators/react_on_rails/pro_generator.rb +97 -0
  12. data/lib/generators/react_on_rails/pro_setup.rb +314 -0
  13. data/lib/generators/react_on_rails/rsc/USAGE +23 -0
  14. data/lib/generators/react_on_rails/rsc_generator.rb +100 -0
  15. data/lib/generators/react_on_rails/rsc_setup.rb +471 -0
  16. data/lib/generators/react_on_rails/templates/base/base/Procfile.dev +3 -3
  17. data/lib/generators/react_on_rails/templates/base/base/config/webpack/{generateWebpackConfigs.js.tt → ServerClientOrBoth.js.tt} +28 -3
  18. data/lib/generators/react_on_rails/templates/base/base/config/webpack/clientWebpackConfig.js.tt +8 -0
  19. data/lib/generators/react_on_rails/templates/base/base/config/webpack/development.js.tt +2 -2
  20. data/lib/generators/react_on_rails/templates/base/base/config/webpack/production.js.tt +2 -2
  21. data/lib/generators/react_on_rails/templates/base/base/config/webpack/serverWebpackConfig.js.tt +54 -0
  22. data/lib/generators/react_on_rails/templates/base/base/config/webpack/test.js.tt +2 -2
  23. data/lib/generators/react_on_rails/templates/dev_tests/spec/system/hello_server_spec.rb +17 -0
  24. data/lib/generators/react_on_rails/templates/pro/base/client/node-renderer.js +41 -0
  25. data/lib/generators/react_on_rails/templates/pro/base/config/initializers/react_on_rails_pro.rb.tt +25 -0
  26. data/lib/generators/react_on_rails/templates/rsc/base/app/controllers/hello_server_controller.rb +25 -0
  27. data/lib/generators/react_on_rails/templates/rsc/base/app/javascript/src/HelloServer/components/HelloServer.jsx +80 -0
  28. data/lib/generators/react_on_rails/templates/rsc/base/app/javascript/src/HelloServer/components/HelloServer.tsx +90 -0
  29. data/lib/generators/react_on_rails/templates/rsc/base/app/javascript/src/HelloServer/components/LikeButton.jsx +54 -0
  30. data/lib/generators/react_on_rails/templates/rsc/base/app/javascript/src/HelloServer/components/LikeButton.tsx +54 -0
  31. data/lib/generators/react_on_rails/templates/rsc/base/app/javascript/src/HelloServer/ror_components/HelloServer.jsx +10 -0
  32. data/lib/generators/react_on_rails/templates/rsc/base/app/javascript/src/HelloServer/ror_components/HelloServer.tsx +10 -0
  33. data/lib/generators/react_on_rails/templates/rsc/base/app/views/hello_server/index.html.erb +37 -0
  34. data/lib/generators/react_on_rails/templates/rsc/base/config/webpack/rscWebpackConfig.js.tt +64 -0
  35. data/lib/react_on_rails/engine.rb +5 -1
  36. data/lib/react_on_rails/version.rb +1 -1
  37. data/lib/react_on_rails/version_checker.rb +6 -1
  38. metadata +21 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9598839cf6698d283c5549d221dd23a2b67903cbc8620fb037012b74bc0d0a61
4
- data.tar.gz: 7b89e0d293d6aa02af9b404732adf5376a4c9411e180a880a538db48e8fa337c
3
+ metadata.gz: 880046575d7835f62a0a81e6613e38de78f23d923510af15ecff3ca6aaf30f82
4
+ data.tar.gz: 7090c54cae36e825fb88b25279c9152c9bd3cb7b0d38b072c43e5477fb050485
5
5
  SHA512:
6
- metadata.gz: 78464cfa9ffbaae7606fbb68cafb3189dba19a0aa731af101d6eae85ed8ce66fb9d44e5f5bf6444377d011621b599a63a742b25621213b744d711950c9ca123a
7
- data.tar.gz: 51b18f716f5d6cadcc777c14fee9430f3c40375932be3f9576e355564365995768f635a45ecd29c2605ad9eac146ee85a0a2190acdd700b10cd71aa58046fa59
6
+ metadata.gz: f1f7ddba8f47287950edae782fe3abca7caaf8ad683047962d0a92a085134ff28cda589f7c9fa6b2ce0467f5a7c199a0ec202630e160c1e76ec1230587d8e27a
7
+ data.tar.gz: 237295bfd09024fc32307555d9fde545d94e64c272547f3c653614f7bd49e3d8b5a65956bb19bc355456c4d54240c0e766880b4bde1868ee7dee2a3da56d374e
data/.rubocop.yml CHANGED
@@ -59,6 +59,8 @@ RSpec/BeforeAfterAll:
59
59
  Exclude:
60
60
  - 'spec/react_on_rails/generators/dev_tests_generator_spec.rb'
61
61
  - 'spec/react_on_rails/generators/install_generator_spec.rb'
62
+ - 'spec/react_on_rails/generators/pro_generator_spec.rb'
63
+ - 'spec/react_on_rails/generators/rsc_generator_spec.rb'
62
64
  - 'spec/react_on_rails/binstubs/dev_spec.rb'
63
65
  - 'spec/react_on_rails/binstubs/dev_static_spec.rb'
64
66
  - 'spec/react_on_rails/dev/**/*_spec.rb' # Dev module tests require global setup
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- react_on_rails (16.4.0.rc.1)
4
+ react_on_rails (16.4.0.rc.2)
5
5
  addressable
6
6
  connection_pool
7
7
  execjs (~> 2.5)
@@ -27,27 +27,46 @@ module ReactOnRails
27
27
  default: false,
28
28
  desc: "Use Rspack instead of Webpack as the bundler"
29
29
 
30
+ # --pro
31
+ class_option :pro,
32
+ type: :boolean,
33
+ default: false,
34
+ desc: "Setup React on Rails Pro with Node Renderer"
35
+
36
+ # --rsc
37
+ class_option :rsc,
38
+ type: :boolean,
39
+ default: false,
40
+ desc: "Setup React Server Components (requires Pro)"
41
+
30
42
  def add_hello_world_route
43
+ # RSC uses HelloServer instead of HelloWorld, but Redux still needs hello_world route
44
+ return if use_rsc? && !options.redux?
45
+
31
46
  route "get 'hello_world', to: 'hello_world#index'"
32
47
  end
33
48
 
34
49
  def create_react_directories
35
- # Create auto-bundling directory structure for non-Redux components only
36
- # Redux components handle their own directory structure
37
- return if options.redux?
50
+ # Skip HelloWorld directory for Redux (uses HelloWorldApp) or RSC (uses HelloServer)
51
+ return if options.redux? || use_rsc?
38
52
 
39
53
  empty_directory("app/javascript/src/HelloWorld/ror_components")
40
54
  end
41
55
 
42
56
  def copy_base_files
43
57
  base_path = "base/base/"
44
- base_files = %w[app/controllers/hello_world_controller.rb
45
- app/views/layouts/hello_world.html.erb
46
- Procfile.dev
58
+ base_files = %w[Procfile.dev
47
59
  Procfile.dev-static-assets
48
60
  Procfile.dev-prod-assets
49
61
  .dev-services.yml.example
50
62
  bin/shakapacker-precompile-hook]
63
+
64
+ # HelloWorld controller/layout only when not using RSC (RSC uses HelloServer)
65
+ # Exception: Redux still needs the HelloWorld controller even with RSC
66
+ unless use_rsc? && !options.redux?
67
+ base_files += %w[app/controllers/hello_world_controller.rb
68
+ app/views/layouts/hello_world.html.erb]
69
+ end
51
70
  base_templates = %w[config/initializers/react_on_rails.rb]
52
71
  base_files.each { |file| copy_file("#{base_path}#{file}", file) }
53
72
  base_templates.each do |file|
@@ -62,9 +81,10 @@ module ReactOnRails
62
81
  base_path = "base/base/"
63
82
  base_files = %w[app/javascript/packs/server-bundle.js]
64
83
 
65
- # Only copy HelloWorld.module.css for non-Redux components
66
- # Redux components handle their own CSS files
67
- base_files << "app/javascript/src/HelloWorld/ror_components/HelloWorld.module.css" unless options.redux?
84
+ # Skip HelloWorld CSS for Redux (uses HelloWorldApp) or RSC (uses HelloServer)
85
+ unless options.redux? || use_rsc?
86
+ base_files << "app/javascript/src/HelloWorld/ror_components/HelloWorld.module.css"
87
+ end
68
88
 
69
89
  base_files.each { |file| copy_file("#{base_path}#{file}", file) }
70
90
  end
@@ -79,7 +99,7 @@ module ReactOnRails
79
99
  config/webpack/development.js
80
100
  config/webpack/production.js
81
101
  config/webpack/serverWebpackConfig.js
82
- config/webpack/generateWebpackConfigs.js]
102
+ config/webpack/ServerClientOrBoth.js]
83
103
  config = {
84
104
  message: "// The source code including full typescript support is available at:"
85
105
  }
@@ -17,6 +17,12 @@ module ReactOnRails
17
17
  default: false,
18
18
  desc: "Setup prerender true for server rendered examples"
19
19
 
20
+ # --rsc
21
+ class_option :rsc,
22
+ type: :boolean,
23
+ default: false,
24
+ desc: "Include React Server Components test (hello_server_spec.rb)"
25
+
20
26
  def copy_rspec_files
21
27
  %w[.eslintrc
22
28
  spec/spec_helper.rb
@@ -26,7 +32,9 @@ module ReactOnRails
26
32
  end
27
33
 
28
34
  def copy_tests
29
- %w[spec/system/hello_world_spec.rb].each { |file| copy_file(file) }
35
+ files = %w[spec/system/hello_world_spec.rb]
36
+ files << "spec/system/hello_server_spec.rb" if options.rsc
37
+ files.each { |file| copy_file(file) }
30
38
  end
31
39
 
32
40
  def add_test_related_gems_to_gemfile
@@ -92,10 +92,85 @@ module GeneratorHelper
92
92
  "#{message} \n#{source}"
93
93
  end
94
94
 
95
+ def print_generator_messages
96
+ GeneratorMessages.messages.each do |message|
97
+ puts message
98
+ puts "" # Blank line after each message for readability
99
+ end
100
+ end
101
+
95
102
  def component_extension(options)
96
103
  options.typescript? ? "tsx" : "jsx"
97
104
  end
98
105
 
106
+ # Check if a gem is present in Gemfile.lock
107
+ # Always checks the target app's Gemfile.lock, not inherited BUNDLE_GEMFILE
108
+ # See: https://github.com/shakacode/react_on_rails/issues/2287
109
+ #
110
+ # @param gem_name [String] Name of the gem to check
111
+ # @return [Boolean] true if the gem is in Gemfile.lock
112
+ def gem_in_lockfile?(gem_name)
113
+ File.file?("Gemfile.lock") &&
114
+ File.foreach("Gemfile.lock").any? { |line| line.match?(/^\s{4}#{Regexp.escape(gem_name)}\s\(/) }
115
+ rescue StandardError
116
+ false
117
+ end
118
+
119
+ # Check if React on Rails Pro gem is installed
120
+ #
121
+ # Detection priority:
122
+ # 1. Gem.loaded_specs - gem is loaded in current Ruby process (most reliable)
123
+ # 2. Gemfile.lock - gem is resolved and installed
124
+ #
125
+ # @return [Boolean] true if react_on_rails_pro gem is installed
126
+ def pro_gem_installed?
127
+ return @pro_gem_installed if defined?(@pro_gem_installed)
128
+
129
+ @pro_gem_installed = Gem.loaded_specs.key?("react_on_rails_pro") || gem_in_lockfile?("react_on_rails_pro")
130
+ end
131
+
132
+ # Check if Pro features should be enabled
133
+ # Returns true if --pro flag is set OR --rsc flag is set (RSC implies Pro)
134
+ #
135
+ # @return [Boolean] true if Pro setup should be included
136
+ def use_pro?
137
+ options[:pro] || options[:rsc]
138
+ end
139
+
140
+ # Check if RSC (React Server Components) should be enabled
141
+ # Returns true only if --rsc flag is explicitly set
142
+ #
143
+ # @return [Boolean] true if RSC setup should be included
144
+ def use_rsc?
145
+ options[:rsc]
146
+ end
147
+
148
+ # Detect the installed React version from package.json
149
+ # Uses VERSION_PARTS_REGEX pattern from VersionChecker for consistency
150
+ #
151
+ # @return [String, nil] React version string (e.g., "19.0.3") or nil if not found/parseable
152
+ def detect_react_version
153
+ pj = package_json
154
+ return nil unless pj
155
+
156
+ dependencies = pj.fetch("dependencies", {})
157
+ react_version = dependencies["react"]
158
+ return nil unless react_version
159
+
160
+ # Skip non-version strings (workspace:*, file:, link:, http://, etc.)
161
+ return nil if react_version.include?("/") || react_version.start_with?("workspace:")
162
+
163
+ # Extract version using the same regex pattern as VersionChecker
164
+ # Handles: "19.0.3", "^19.0.3", "~19.0.3", "19.0.3-beta.1", etc.
165
+ match = react_version.match(/(\d+)\.(\d+)\.(\d+)(?:[-.]([0-9A-Za-z.-]+))?/)
166
+ return nil unless match
167
+
168
+ # Return the matched version (without pre-release suffix for comparison)
169
+ "#{match[1]}.#{match[2]}.#{match[3]}"
170
+ rescue StandardError
171
+ nil
172
+ end
173
+
99
174
  # Check if Shakapacker 9.0 or higher is available
100
175
  # Returns true if Shakapacker >= 9.0, false otherwise
101
176
  #
@@ -146,6 +221,32 @@ module GeneratorHelper
146
221
  @using_swc = detect_swc_configuration
147
222
  end
148
223
 
224
+ # Resolve the path to ServerClientOrBoth.js, handling the legacy name.
225
+ # Old installs may still use generateWebpackConfigs.js; this renames it
226
+ # and updates references in environment configs so downstream transforms
227
+ # can rely on the canonical name.
228
+ #
229
+ # @return [String, nil] relative config path, or nil if neither file exists
230
+ def resolve_server_client_or_both_path
231
+ new_path = "config/webpack/ServerClientOrBoth.js"
232
+ old_path = "config/webpack/generateWebpackConfigs.js"
233
+ full_new = File.join(destination_root, new_path)
234
+ full_old = File.join(destination_root, old_path)
235
+
236
+ if File.exist?(full_new)
237
+ new_path
238
+ elsif File.exist?(full_old)
239
+ FileUtils.mv(full_old, full_new)
240
+ %w[development.js production.js test.js].each do |env_file|
241
+ env_path = "config/webpack/#{env_file}"
242
+ if File.exist?(File.join(destination_root, env_path))
243
+ gsub_file(env_path, /generateWebpackConfigs/, "ServerClientOrBoth")
244
+ end
245
+ end
246
+ new_path
247
+ end
248
+ end
249
+
149
250
  private
150
251
 
151
252
  def detect_swc_configuration
@@ -69,8 +69,8 @@ module GeneratorMessages
69
69
  <%= javascript_pack_tag %>
70
70
  <%= stylesheet_pack_tag %>
71
71
 
72
- • Server-side rendering - Enabled with prerender option in app/views/hello_world/index.html.erb:
73
- <%= react_component("#{component_name}", props: @hello_world_props, prerender: true) %>
72
+ • Server-side rendering - Enabled with prerender option in app/views/#{route}/index.html.erb:
73
+ <%= react_component("#{component_name}", props: @#{route}_props, prerender: true) %>
74
74
 
75
75
  📚 LEARN MORE:
76
76
  ─────────────────────────────────────────────────────────────────────────
@@ -6,13 +6,20 @@ require "bundler"
6
6
  require_relative "generator_helper"
7
7
  require_relative "generator_messages"
8
8
  require_relative "js_dependency_manager"
9
+ require_relative "pro_setup"
10
+ require_relative "rsc_setup"
9
11
 
10
12
  module ReactOnRails
11
13
  module Generators
14
+ # TODO: Extract more modules to reduce class length below 150 lines.
15
+ # Candidates: ShakapackerSetup (~100 lines), TypeScriptSetup (~60 lines),
16
+ # ValidationHelpers (~80 lines for Node/package manager checks).
12
17
  # rubocop:disable Metrics/ClassLength
13
18
  class InstallGenerator < Rails::Generators::Base
14
19
  include GeneratorHelper
15
20
  include JsDependencyManager
21
+ include ProSetup
22
+ include RscSetup
16
23
 
17
24
  # fetch USAGE file for details generator description
18
25
  source_root(File.expand_path(__dir__))
@@ -43,6 +50,18 @@ module ReactOnRails
43
50
  default: false,
44
51
  desc: "Skip warnings. Default: false"
45
52
 
53
+ # --pro
54
+ class_option :pro,
55
+ type: :boolean,
56
+ default: false,
57
+ desc: "Install React on Rails Pro with Node Renderer. Default: false"
58
+
59
+ # --rsc
60
+ class_option :rsc,
61
+ type: :boolean,
62
+ default: false,
63
+ desc: "Install React Server Components support (includes Pro). Default: false"
64
+
46
65
  # Removed: --skip-shakapacker-install (Shakapacker is now a required dependency)
47
66
 
48
67
  # Main generator entry point
@@ -94,10 +113,6 @@ module ReactOnRails
94
113
 
95
114
  private
96
115
 
97
- def print_generator_messages
98
- GeneratorMessages.messages.each { |message| puts message }
99
- end
100
-
101
116
  def invoke_generators
102
117
  ensure_shakapacker_installed
103
118
  if options.typescript?
@@ -106,13 +121,29 @@ module ReactOnRails
106
121
  create_typescript_config
107
122
  end
108
123
  invoke "react_on_rails:base", [],
109
- { typescript: options.typescript?, redux: options.redux?, rspack: options.rspack? }
124
+ { typescript: options.typescript?, redux: options.redux?, rspack: options.rspack?,
125
+ pro: options.pro?, rsc: options.rsc? }
126
+
127
+ # Component generator logic:
128
+ # - --rsc without --redux: Skip HelloWorld, HelloServer will be generated in setup_rsc
129
+ # - --rsc with --redux: Generate HelloWorldApp (user explicitly wants Redux) + HelloServer
130
+ # - Without --rsc: Normal behavior (HelloWorld or HelloWorldApp based on --redux)
110
131
  if options.redux?
111
132
  invoke "react_on_rails:react_with_redux", [], { typescript: options.typescript? }
112
- else
133
+ elsif !use_rsc?
134
+ # Only generate HelloWorld if RSC is not enabled
135
+ # For RSC, HelloServer replaces HelloWorld as the example component
113
136
  invoke "react_on_rails:react_no_redux", [], { typescript: options.typescript? }
114
137
  end
138
+
115
139
  setup_react_dependencies
140
+
141
+ # Invoke standalone Pro/RSC generators when flags are used
142
+ # Pass invoked_by_install: true so they skip message printing (we handle it)
143
+ invoke "react_on_rails:pro", [], { invoked_by_install: true } if use_pro?
144
+ return unless use_rsc?
145
+
146
+ invoke "react_on_rails:rsc", [], { typescript: options.typescript?, invoked_by_install: true }
116
147
  end
117
148
 
118
149
  def setup_react_dependencies
@@ -123,7 +154,8 @@ module ReactOnRails
123
154
  # js(.coffee) are not checked by this method, but instead produce warning messages
124
155
  # and allow the build to continue
125
156
  def installation_prerequisites_met?
126
- !(missing_node? || missing_package_manager? || ReactOnRails::GitUtils.uncommitted_changes?(GeneratorMessages))
157
+ !(missing_node? || missing_package_manager? || missing_pro_gem? ||
158
+ ReactOnRails::GitUtils.uncommitted_changes?(GeneratorMessages))
127
159
  end
128
160
 
129
161
  def missing_node?
@@ -193,6 +225,11 @@ module ReactOnRails
193
225
  template_bin_path = "#{__dir__}/templates/base/base/bin"
194
226
  directory template_bin_path, "bin"
195
227
 
228
+ # For --rsc without --redux, hello_world doesn't exist — update DEFAULT_ROUTE
229
+ if use_rsc? && !options.redux?
230
+ gsub_file "bin/dev", 'DEFAULT_ROUTE = "hello_world"', 'DEFAULT_ROUTE = "hello_server"'
231
+ end
232
+
196
233
  # Make these and only these files executable
197
234
  files_to_copy = []
198
235
  Dir.chdir(template_bin_path) do
@@ -204,9 +241,15 @@ module ReactOnRails
204
241
  end
205
242
 
206
243
  def add_post_install_message
207
- # Determine what route will be created by the generator
208
- route = "hello_world" # This is the hardcoded route from base_generator.rb
209
- component_name = options.redux? ? "HelloWorldApp" : "HelloWorld"
244
+ # Determine what route and component will be created by the generator
245
+ if use_rsc? && !options.redux?
246
+ # RSC without Redux: HelloServer replaces HelloWorld
247
+ route = "hello_server"
248
+ component_name = "HelloServer"
249
+ else
250
+ route = "hello_world"
251
+ component_name = options.redux? ? "HelloWorldApp" : "HelloWorld"
252
+ end
210
253
 
211
254
  GeneratorMessages.add_info(GeneratorMessages.helpful_message_after_installation(
212
255
  component_name: component_name,
@@ -219,10 +262,7 @@ module ReactOnRails
219
262
  end
220
263
 
221
264
  def shakapacker_in_lockfile?(gem_name)
222
- # Always check the target app's Gemfile.lock, not inherited BUNDLE_GEMFILE
223
- # See: https://github.com/shakacode/react_on_rails/issues/2287
224
- File.file?("Gemfile.lock") &&
225
- File.foreach("Gemfile.lock").any? { |l| l.match?(/^\s{4}#{Regexp.escape(gem_name)}\s\(/) }
265
+ gem_in_lockfile?(gem_name)
226
266
  end
227
267
 
228
268
  def shakapacker_in_bundler_specs?(gem_name)
@@ -431,11 +471,7 @@ module ReactOnRails
431
471
  File.write("tsconfig.json", JSON.pretty_generate(tsconfig_content))
432
472
  puts Rainbow("✅ Created tsconfig.json").green
433
473
  end
434
-
435
- # Removed: Shakapacker auto-installation logic (now explicit dependency)
436
-
437
- # Removed: Shakapacker 8+ is now required as explicit dependency
438
- # rubocop:enable Metrics/ClassLength
439
474
  end
475
+ # rubocop:enable Metrics/ClassLength
440
476
  end
441
477
  end
@@ -106,6 +106,19 @@ module ReactOnRails
106
106
  swc-loader
107
107
  ].freeze
108
108
 
109
+ # React on Rails Pro dependencies (only installed when --pro or --rsc flag is used)
110
+ # These packages are published publicly on npmjs.org but require a license for production use
111
+ PRO_DEPENDENCIES = %w[
112
+ react-on-rails-pro
113
+ react-on-rails-pro-node-renderer
114
+ ].freeze
115
+
116
+ # React Server Components dependencies (only installed when --rsc flag is used)
117
+ # Requires React 19.0.x - see https://react.dev/reference/rsc/server-components
118
+ RSC_DEPENDENCIES = %w[
119
+ react-on-rails-rsc
120
+ ].freeze
121
+
109
122
  private
110
123
 
111
124
  def setup_js_dependencies
@@ -120,17 +133,23 @@ module ReactOnRails
120
133
  end
121
134
 
122
135
  def add_js_dependencies
123
- add_react_on_rails_package
136
+ using_pro = respond_to?(:use_pro?) && use_pro?
137
+ using_rsc = respond_to?(:use_rsc?) && use_rsc?
138
+ # Pro package includes react-on-rails, so skip base package when using Pro
139
+ add_react_on_rails_package unless using_pro
124
140
  add_react_dependencies
125
141
  add_css_dependencies
126
- # Rspack dependencies are only added when --rspack flag is used
127
- add_rspack_dependencies if respond_to?(:options) && options&.rspack?
128
- # SWC dependencies are only added when SWC is the configured transpiler
142
+ add_rspack_dependencies if using_rspack?
129
143
  add_swc_dependencies if using_swc?
130
- # Dev dependencies vary based on bundler choice
144
+ add_pro_dependencies if using_pro
145
+ add_rsc_dependencies if using_rsc
131
146
  add_dev_dependencies
132
147
  end
133
148
 
149
+ def using_rspack?
150
+ respond_to?(:options) && options&.rspack?
151
+ end
152
+
134
153
  def add_react_on_rails_package
135
154
  # Use exact version match between gem and npm package for all versions including pre-releases
136
155
  # Ruby gem versions use dots (16.2.0.beta.10) but npm requires hyphens (16.2.0-beta.10)
@@ -186,20 +205,29 @@ module ReactOnRails
186
205
 
187
206
  def add_react_dependencies
188
207
  puts "Installing React dependencies..."
189
- return if add_packages(REACT_DEPENDENCIES)
208
+
209
+ # RSC requires React 19.0.x specifically (not 19.1.x or later)
210
+ # Pin to ~19.0.4 to allow patch updates while staying within 19.0.x
211
+ react_deps = if respond_to?(:use_rsc?) && use_rsc?
212
+ %w[react@~19.0.4 react-dom@~19.0.4 prop-types]
213
+ else
214
+ REACT_DEPENDENCIES
215
+ end
216
+
217
+ return if add_packages(react_deps)
190
218
 
191
219
  GeneratorMessages.add_warning(<<~MSG.strip)
192
220
  ⚠️ Failed to add React dependencies.
193
221
 
194
222
  You can install them manually by running:
195
- npm install #{REACT_DEPENDENCIES.join(' ')}
223
+ npm install #{react_deps.join(' ')}
196
224
  MSG
197
225
  rescue StandardError => e
198
226
  GeneratorMessages.add_warning(<<~MSG.strip)
199
227
  ⚠️ Error adding React dependencies: #{e.message}
200
228
 
201
229
  You can install them manually by running:
202
- npm install #{REACT_DEPENDENCIES.join(' ')}
230
+ npm install #{react_deps.join(' ')}
203
231
  MSG
204
232
  end
205
233
 
@@ -280,6 +308,85 @@ module ReactOnRails
280
308
  MSG
281
309
  end
282
310
 
311
+ def add_pro_dependencies
312
+ puts "Installing React on Rails Pro dependencies..."
313
+
314
+ # When upgrading from base React on Rails to Pro, remove the base package first
315
+ # Pro package includes all base functionality, so having both causes validation errors
316
+ remove_base_package_if_present
317
+
318
+ # Pin to exact version matching the gem (converts Ruby format to npm format)
319
+ # Falls back to latest if version can't be determined
320
+ pro_packages = pro_packages_with_version
321
+ results = pro_packages.map { |pkg| add_package(pkg) }
322
+ return if results.all?
323
+
324
+ GeneratorMessages.add_warning(<<~MSG.strip)
325
+ ⚠️ Failed to add React on Rails Pro dependencies.
326
+
327
+ You can install them manually by running:
328
+ npm install #{pro_packages.join(' ')}
329
+ MSG
330
+ rescue StandardError => e
331
+ GeneratorMessages.add_warning(<<~MSG.strip)
332
+ ⚠️ Error adding React on Rails Pro dependencies: #{e.message}
333
+
334
+ You can install them manually by running:
335
+ npm install #{PRO_DEPENDENCIES.join(' ')}
336
+ MSG
337
+ end
338
+
339
+ # Returns Pro package names with version suffix matching the gem version.
340
+ # Uses VersionSyntaxConverter to handle Ruby->npm format conversion.
341
+ # Falls back to unversioned package names if version can't be determined.
342
+ def pro_packages_with_version
343
+ return PRO_DEPENDENCIES unless defined?(ReactOnRailsPro::VERSION)
344
+
345
+ npm_version = ReactOnRails::VersionSyntaxConverter.new.rubygem_to_npm(ReactOnRailsPro::VERSION)
346
+ PRO_DEPENDENCIES.map { |pkg| "#{pkg}@#{npm_version}" }
347
+ rescue StandardError
348
+ puts "WARNING: Could not determine Pro package version. Installing latest."
349
+ PRO_DEPENDENCIES
350
+ end
351
+
352
+ def add_rsc_dependencies
353
+ puts "Installing React Server Components dependencies..."
354
+ return if add_packages(RSC_DEPENDENCIES)
355
+
356
+ GeneratorMessages.add_warning(<<~MSG.strip)
357
+ ⚠️ Failed to add React Server Components dependencies.
358
+
359
+ You can install them manually by running:
360
+ npm install #{RSC_DEPENDENCIES.join(' ')}
361
+ MSG
362
+ rescue StandardError => e
363
+ GeneratorMessages.add_warning(<<~MSG.strip)
364
+ ⚠️ Error adding React Server Components dependencies: #{e.message}
365
+
366
+ You can install them manually by running:
367
+ npm install #{RSC_DEPENDENCIES.join(' ')}
368
+ MSG
369
+ end
370
+
371
+ def remove_base_package_if_present
372
+ pj = package_json
373
+ return unless pj
374
+
375
+ dependencies = pj.fetch("dependencies", {})
376
+ return unless dependencies.key?("react-on-rails")
377
+
378
+ puts "Removing base 'react-on-rails' package (Pro package includes all base functionality)..."
379
+ pj.manager.remove(["react-on-rails"])
380
+ puts "✅ Removed 'react-on-rails' package"
381
+ rescue StandardError => e
382
+ GeneratorMessages.add_warning(<<~MSG.strip)
383
+ ⚠️ Could not remove base 'react-on-rails' package: #{e.message}
384
+
385
+ Please remove it manually:
386
+ npm uninstall react-on-rails
387
+ MSG
388
+ end
389
+
283
390
  def add_dev_dependencies
284
391
  puts "Installing development dependencies..."
285
392
 
@@ -0,0 +1,21 @@
1
+ Description:
2
+ Add React on Rails Pro to an existing React on Rails application.
3
+
4
+ Example:
5
+ rails generate react_on_rails:pro
6
+
7
+ This will add:
8
+ - Pro initializer (config/initializers/react_on_rails_pro.rb)
9
+ - Node renderer (client/node-renderer.js)
10
+ - Node renderer process to Procfile.dev
11
+
12
+ Modifies:
13
+ - config/webpack/serverWebpackConfig.js (enables Pro settings: libraryTarget,
14
+ target, extractLoader, Babel SSR caller, object exports)
15
+ - config/webpack/ServerClientOrBoth.js (switches to destructured import)
16
+ - package.json (replaces react-on-rails with react-on-rails-pro and
17
+ react-on-rails-pro-node-renderer)
18
+
19
+ Prerequisites:
20
+ - React on Rails must be installed (rails g react_on_rails:install)
21
+ - react_on_rails_pro gem must be in your Gemfile