react_on_rails 16.2.0.beta.8 → 16.2.0.beta.11

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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +19 -13
  3. data/CLAUDE.md +136 -5
  4. data/CONTRIBUTING.md +3 -1
  5. data/Gemfile.lock +1 -1
  6. data/Steepfile +4 -0
  7. data/analysis/rake-task-duplicate-analysis.md +149 -0
  8. data/bin/ci-run-failed-specs +6 -4
  9. data/bin/ci-switch-config +4 -3
  10. data/knip.ts +1 -1
  11. data/lib/generators/react_on_rails/base_generator.rb +5 -119
  12. data/lib/generators/react_on_rails/generator_helper.rb +29 -0
  13. data/lib/generators/react_on_rails/install_generator.rb +5 -180
  14. data/lib/generators/react_on_rails/js_dependency_manager.rb +354 -0
  15. data/lib/generators/react_on_rails/templates/base/base/config/initializers/react_on_rails.rb.tt +19 -0
  16. data/lib/generators/react_on_rails/templates/base/base/config/{shakapacker.yml → shakapacker.yml.tt} +18 -2
  17. data/lib/generators/react_on_rails/templates/base/base/config/webpack/serverWebpackConfig.js.tt +38 -4
  18. data/lib/react_on_rails/configuration.rb +82 -8
  19. data/lib/react_on_rails/dev/pack_generator.rb +1 -0
  20. data/lib/react_on_rails/dev/server_manager.rb +1 -0
  21. data/lib/react_on_rails/doctor.rb +94 -4
  22. data/lib/react_on_rails/engine.rb +2 -5
  23. data/lib/react_on_rails/system_checker.rb +7 -4
  24. data/lib/react_on_rails/utils.rb +54 -0
  25. data/lib/react_on_rails/version.rb +1 -1
  26. data/react_on_rails_pro/Gemfile.lock +3 -3
  27. data/react_on_rails_pro/lib/react_on_rails_pro/version.rb +1 -1
  28. data/react_on_rails_pro/package.json +1 -1
  29. data/react_on_rails_pro/spec/dummy/Gemfile.lock +3 -3
  30. data/react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook +19 -0
  31. data/react_on_rails_pro/spec/dummy/config/shakapacker.yml +5 -0
  32. data/sig/react_on_rails/dev/file_manager.rbs +15 -0
  33. data/sig/react_on_rails/dev/pack_generator.rbs +19 -0
  34. data/sig/react_on_rails/dev/process_manager.rbs +22 -0
  35. data/sig/react_on_rails/dev/server_manager.rbs +39 -0
  36. data/sig/react_on_rails/generators/js_dependency_manager.rbs +123 -0
  37. metadata +11 -3
@@ -95,4 +95,33 @@ module GeneratorHelper
95
95
  def component_extension(options)
96
96
  options.typescript? ? "tsx" : "jsx"
97
97
  end
98
+
99
+ # Check if Shakapacker 9.0 or higher is available
100
+ # Returns true if Shakapacker >= 9.0, false otherwise
101
+ #
102
+ # This method is used during code generation to determine which configuration
103
+ # patterns to use in generated files (e.g., config.privateOutputPath vs hardcoded paths).
104
+ #
105
+ # @return [Boolean] true if Shakapacker 9.0+ is available or likely to be installed
106
+ #
107
+ # @note Default behavior: Returns true when Shakapacker is not yet installed
108
+ # Rationale: During fresh installations, we optimistically assume users will install
109
+ # the latest Shakapacker version. This ensures new projects get best-practice configs.
110
+ # If users later install an older version, the generated webpack config includes
111
+ # fallback logic (e.g., `config.privateOutputPath || hardcodedPath`) that prevents
112
+ # breakage, and validation warnings guide them to fix any misconfigurations.
113
+ def shakapacker_version_9_or_higher?
114
+ return @shakapacker_version_9_or_higher if defined?(@shakapacker_version_9_or_higher)
115
+
116
+ @shakapacker_version_9_or_higher = begin
117
+ # If Shakapacker is not available yet (fresh install), default to true
118
+ # since we're likely installing the latest version
119
+ return true unless defined?(ReactOnRails::PackerUtils)
120
+
121
+ ReactOnRails::PackerUtils.shakapacker_version_requirement_met?("9.0.0")
122
+ rescue StandardError
123
+ # If we can't determine version, assume latest
124
+ true
125
+ end
126
+ end
98
127
  end
@@ -4,12 +4,14 @@ require "rails/generators"
4
4
  require "json"
5
5
  require_relative "generator_helper"
6
6
  require_relative "generator_messages"
7
+ require_relative "js_dependency_manager"
7
8
 
8
9
  module ReactOnRails
9
10
  module Generators
10
11
  # rubocop:disable Metrics/ClassLength
11
12
  class InstallGenerator < Rails::Generators::Base
12
13
  include GeneratorHelper
14
+ include JsDependencyManager
13
15
 
14
16
  # fetch USAGE file for details generator description
15
17
  source_root(File.expand_path(__dir__))
@@ -113,10 +115,7 @@ module ReactOnRails
113
115
  end
114
116
 
115
117
  def setup_react_dependencies
116
- @added_dependencies_to_package_json ||= false
117
- @ran_direct_installs ||= false
118
- add_js_dependencies
119
- install_js_dependencies if @added_dependencies_to_package_json && !@ran_direct_installs
118
+ setup_js_dependencies
120
119
  end
121
120
 
122
121
  # NOTE: other requirements for existing files such as .gitignore or application.
@@ -366,29 +365,8 @@ module ReactOnRails
366
365
 
367
366
  def install_typescript_dependencies
368
367
  puts Rainbow("📝 Installing TypeScript dependencies...").yellow
369
-
370
- # Install TypeScript and React type definitions
371
- typescript_packages = %w[
372
- typescript
373
- @types/react
374
- @types/react-dom
375
- @babel/preset-typescript
376
- ]
377
-
378
- # Try using GeneratorHelper first (package manager agnostic)
379
- return if add_npm_dependencies(typescript_packages, dev: true)
380
-
381
- # Fallback to npm if GeneratorHelper fails
382
- success = system("npm", "install", "--save-dev", *typescript_packages)
383
- return if success
384
-
385
- warning = <<~MSG.strip
386
- ⚠️ Failed to install TypeScript dependencies automatically.
387
-
388
- Please run manually:
389
- npm install --save-dev #{typescript_packages.join(' ')}
390
- MSG
391
- GeneratorMessages.add_warning(warning)
368
+ # Delegate to shared module for consistent dependency management
369
+ add_typescript_dependencies
392
370
  end
393
371
 
394
372
  def create_css_module_types
@@ -450,159 +428,6 @@ module ReactOnRails
450
428
  puts Rainbow("✅ Created tsconfig.json").green
451
429
  end
452
430
 
453
- def add_js_dependencies
454
- add_react_on_rails_package
455
- add_react_dependencies
456
- add_css_dependencies
457
- add_rspack_dependencies if options.rspack?
458
- add_dev_dependencies
459
- end
460
-
461
- def add_react_on_rails_package
462
- major_minor_patch_only = /\A\d+\.\d+\.\d+\z/
463
-
464
- # Try to use package_json gem first, fall back to direct npm commands
465
- react_on_rails_pkg = if ReactOnRails::VERSION.match?(major_minor_patch_only)
466
- ["react-on-rails@#{ReactOnRails::VERSION}"]
467
- else
468
- puts "Adding the latest react-on-rails NPM module. " \
469
- "Double check this is correct in package.json"
470
- ["react-on-rails"]
471
- end
472
-
473
- puts "Installing React on Rails package..."
474
- if add_npm_dependencies(react_on_rails_pkg)
475
- @added_dependencies_to_package_json = true
476
- return
477
- end
478
-
479
- puts "Using direct npm commands as fallback"
480
- success = system("npm", "install", *react_on_rails_pkg)
481
- @ran_direct_installs = true if success
482
- handle_npm_failure("react-on-rails package", react_on_rails_pkg) unless success
483
- end
484
-
485
- def add_react_dependencies
486
- puts "Installing React dependencies..."
487
- react_deps = %w[
488
- react
489
- react-dom
490
- @babel/preset-react
491
- prop-types
492
- babel-plugin-transform-react-remove-prop-types
493
- babel-plugin-macros
494
- ]
495
- if add_npm_dependencies(react_deps)
496
- @added_dependencies_to_package_json = true
497
- return
498
- end
499
-
500
- success = system("npm", "install", *react_deps)
501
- @ran_direct_installs = true if success
502
- handle_npm_failure("React dependencies", react_deps) unless success
503
- end
504
-
505
- def add_css_dependencies
506
- puts "Installing CSS handling dependencies..."
507
- css_deps = %w[
508
- css-loader
509
- css-minimizer-webpack-plugin
510
- mini-css-extract-plugin
511
- style-loader
512
- ]
513
- if add_npm_dependencies(css_deps)
514
- @added_dependencies_to_package_json = true
515
- return
516
- end
517
-
518
- success = system("npm", "install", *css_deps)
519
- @ran_direct_installs = true if success
520
- handle_npm_failure("CSS dependencies", css_deps) unless success
521
- end
522
-
523
- def add_rspack_dependencies
524
- puts "Installing Rspack core dependencies..."
525
- rspack_deps = %w[
526
- @rspack/core
527
- rspack-manifest-plugin
528
- ]
529
- if add_npm_dependencies(rspack_deps)
530
- @added_dependencies_to_package_json = true
531
- return
532
- end
533
-
534
- success = system("npm", "install", *rspack_deps)
535
- @ran_direct_installs = true if success
536
- handle_npm_failure("Rspack dependencies", rspack_deps) unless success
537
- end
538
-
539
- def add_dev_dependencies
540
- puts "Installing development dependencies..."
541
- dev_deps = if options.rspack?
542
- %w[
543
- @rspack/cli
544
- @rspack/plugin-react-refresh
545
- react-refresh
546
- ]
547
- else
548
- %w[
549
- @pmmmwh/react-refresh-webpack-plugin
550
- react-refresh
551
- ]
552
- end
553
- if add_npm_dependencies(dev_deps, dev: true)
554
- @added_dependencies_to_package_json = true
555
- return
556
- end
557
-
558
- success = system("npm", "install", "--save-dev", *dev_deps)
559
- @ran_direct_installs = true if success
560
- handle_npm_failure("development dependencies", dev_deps, dev: true) unless success
561
- end
562
-
563
- def install_js_dependencies
564
- # Detect which package manager to use
565
- success = if File.exist?(File.join(destination_root, "yarn.lock"))
566
- system("yarn", "install")
567
- elsif File.exist?(File.join(destination_root, "pnpm-lock.yaml"))
568
- system("pnpm", "install")
569
- elsif File.exist?(File.join(destination_root, "package-lock.json")) ||
570
- File.exist?(File.join(destination_root, "package.json"))
571
- # Use npm for package-lock.json or as default fallback
572
- system("npm", "install")
573
- else
574
- true # No package manager detected, skip
575
- end
576
-
577
- unless success
578
- GeneratorMessages.add_warning(<<~MSG.strip)
579
- ⚠️ JavaScript dependencies installation failed.
580
-
581
- This could be due to network issues or missing package manager.
582
- You can install dependencies manually later by running:
583
- • npm install (if using npm)
584
- • yarn install (if using yarn)
585
- • pnpm install (if using pnpm)
586
- MSG
587
- end
588
-
589
- success
590
- end
591
-
592
- def handle_npm_failure(dependency_type, packages, dev: false)
593
- install_command = dev ? "npm install --save-dev" : "npm install"
594
- GeneratorMessages.add_warning(<<~MSG.strip)
595
- ⚠️ Failed to install #{dependency_type}.
596
-
597
- The following packages could not be installed automatically:
598
- #{packages.map { |pkg| " • #{pkg}" }.join("\n")}
599
-
600
- This could be due to network issues or missing package manager.
601
- You can install them manually later by running:
602
- #{install_command} #{packages.join(' ')}
603
- MSG
604
- end
605
-
606
431
  # Removed: Shakapacker auto-installation logic (now explicit dependency)
607
432
 
608
433
  # Removed: Shakapacker 8+ is now required as explicit dependency
@@ -0,0 +1,354 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "generator_messages"
4
+
5
+ # rubocop:disable Metrics/ModuleLength
6
+ module ReactOnRails
7
+ module Generators
8
+ # Shared module for managing JavaScript dependencies across generators
9
+ # This module provides common functionality for adding and installing
10
+ # JS dependencies to avoid code duplication between generators.
11
+ #
12
+ # Since react_on_rails requires shakapacker, and shakapacker includes
13
+ # package_json as a dependency, the package_json gem is always available.
14
+ #
15
+ # == Required Methods
16
+ # Including classes must include GeneratorHelper module which provides:
17
+ # - add_npm_dependencies(packages, dev: false): Add packages via package_json gem
18
+ # - package_json: Access to PackageJson instance (always available via shakapacker)
19
+ # - destination_root: Generator destination directory
20
+ #
21
+ # == Optional Methods
22
+ # Including classes may define:
23
+ # - options.rspack?: Returns true if --rspack flag is set (for Rspack support)
24
+ # - options.typescript?: Returns true if --typescript flag is set (for TypeScript support)
25
+ #
26
+ # == Installation Behavior
27
+ # The module ALWAYS runs package manager install after adding dependencies.
28
+ # This is safe because package_json gem's install method is idempotent - it only
29
+ # installs what's actually needed from package.json. This prevents edge cases
30
+ # where package.json was modified but dependencies weren't installed.
31
+ #
32
+ # == Error Handling Philosophy
33
+ # All dependency addition methods use a graceful degradation approach:
34
+ # - Methods return false on failure instead of raising exceptions
35
+ # - StandardError is caught at the lowest level (add_package) and higher levels (add_*_dependencies)
36
+ # - Failures trigger user-facing warnings via GeneratorMessages
37
+ # - Warnings provide clear manual installation instructions
38
+ #
39
+ # This ensures the generator ALWAYS completes successfully, even when:
40
+ # - Network connectivity issues prevent package downloads
41
+ # - Package manager (npm/yarn/pnpm) has permission errors
42
+ # - package_json gem encounters unexpected states
43
+ #
44
+ # Users can manually run package installation commands after generator completion.
45
+ # This is preferable to generator crashes that leave Rails apps in incomplete states.
46
+ #
47
+ # == Usage
48
+ # Include this module in generator classes and call setup_js_dependencies
49
+ # to handle all JS dependency installation via package_json gem.
50
+ module JsDependencyManager
51
+ # Core React dependencies required for React on Rails
52
+ # Note: @babel/preset-react and babel plugins are NOT included here because:
53
+ # - Shakapacker handles JavaScript transpiler configuration (babel, swc, or esbuild)
54
+ # - Users configure their preferred transpiler via shakapacker.yml javascript_transpiler setting
55
+ # - SWC is now the default and doesn't need Babel presets
56
+ # - For Babel users, shakapacker will install babel-loader and its dependencies
57
+ REACT_DEPENDENCIES = %w[
58
+ react
59
+ react-dom
60
+ prop-types
61
+ ].freeze
62
+
63
+ # CSS processing dependencies for webpack
64
+ CSS_DEPENDENCIES = %w[
65
+ css-loader
66
+ css-minimizer-webpack-plugin
67
+ mini-css-extract-plugin
68
+ style-loader
69
+ ].freeze
70
+
71
+ # Development-only dependencies for hot reloading (Webpack)
72
+ DEV_DEPENDENCIES = %w[
73
+ @pmmmwh/react-refresh-webpack-plugin
74
+ react-refresh
75
+ ].freeze
76
+
77
+ # Rspack core dependencies (only installed when --rspack flag is used)
78
+ RSPACK_DEPENDENCIES = %w[
79
+ @rspack/core
80
+ rspack-manifest-plugin
81
+ ].freeze
82
+
83
+ # Rspack development dependencies for hot reloading
84
+ RSPACK_DEV_DEPENDENCIES = %w[
85
+ @rspack/cli
86
+ @rspack/plugin-react-refresh
87
+ react-refresh
88
+ ].freeze
89
+
90
+ # TypeScript dependencies (only installed when --typescript flag is used)
91
+ # Note: @babel/preset-typescript is NOT included because:
92
+ # - SWC is now the default javascript_transpiler (has built-in TypeScript support)
93
+ # - Shakapacker handles the transpiler configuration via shakapacker.yml
94
+ # - If users choose javascript_transpiler: 'babel', they should manually add @babel/preset-typescript
95
+ # and configure it in their babel.config.js
96
+ TYPESCRIPT_DEPENDENCIES = %w[
97
+ typescript
98
+ @types/react
99
+ @types/react-dom
100
+ ].freeze
101
+
102
+ private
103
+
104
+ def setup_js_dependencies
105
+ add_js_dependencies
106
+
107
+ # Always run install to ensure all dependencies are properly installed.
108
+ # The package_json gem's install method is idempotent and safe to call
109
+ # even if packages were already added - it will only install what's needed.
110
+ # This ensures edge cases where package.json was modified but install wasn't
111
+ # run are handled correctly.
112
+ install_js_dependencies
113
+ end
114
+
115
+ def add_js_dependencies
116
+ add_react_on_rails_package
117
+ add_react_dependencies
118
+ add_css_dependencies
119
+ # Rspack dependencies are only added when --rspack flag is used
120
+ add_rspack_dependencies if respond_to?(:options) && options&.rspack?
121
+ # Dev dependencies vary based on bundler choice
122
+ add_dev_dependencies
123
+ end
124
+
125
+ def add_react_on_rails_package
126
+ # Use exact version match between gem and npm package for all versions including pre-releases
127
+ # Ruby gem versions use dots (16.2.0.beta.10) but npm requires hyphens (16.2.0-beta.10)
128
+ # This method converts between the two formats.
129
+ #
130
+ # The regex matches:
131
+ # - Stable: 16.2.0
132
+ # - Beta (Ruby): 16.2.0.beta.10 or (npm): 16.2.0-beta.10
133
+ # - RC (Ruby): 16.1.0.rc.1 or (npm): 16.1.0-rc.1
134
+ # - Alpha (Ruby): 16.0.0.alpha.5 or (npm): 16.0.0-alpha.5
135
+ # This ensures beta/rc versions use the exact version instead of "latest" which would
136
+ # install the latest stable release and cause version mismatches.
137
+
138
+ # Accept both dot and hyphen separators for pre-release versions
139
+ version_with_optional_prerelease = /\A(\d+\.\d+\.\d+)([-.]([a-zA-Z0-9.]+))?\z/
140
+
141
+ react_on_rails_pkg = if (match = ReactOnRails::VERSION.match(version_with_optional_prerelease))
142
+ base_version = match[1]
143
+ prerelease = match[3]
144
+
145
+ # Convert Ruby gem format (dot) to npm semver format (hyphen)
146
+ npm_version = if prerelease
147
+ "#{base_version}-#{prerelease}"
148
+ else
149
+ base_version
150
+ end
151
+
152
+ "react-on-rails@#{npm_version}"
153
+ else
154
+ puts "WARNING: Unrecognized version format #{ReactOnRails::VERSION}. " \
155
+ "Adding the latest react-on-rails NPM module. " \
156
+ "Double check this is correct in package.json"
157
+ "react-on-rails"
158
+ end
159
+
160
+ puts "Installing React on Rails package..."
161
+ return if add_package(react_on_rails_pkg)
162
+
163
+ GeneratorMessages.add_warning(<<~MSG.strip)
164
+ ⚠️ Failed to add react-on-rails package.
165
+
166
+ You can install it manually by running:
167
+ npm install #{react_on_rails_pkg}
168
+ MSG
169
+ rescue StandardError => e
170
+ GeneratorMessages.add_warning(<<~MSG.strip)
171
+ ⚠️ Error adding react-on-rails package: #{e.message}
172
+
173
+ You can install it manually by running:
174
+ npm install #{react_on_rails_pkg}
175
+ MSG
176
+ end
177
+
178
+ def add_react_dependencies
179
+ puts "Installing React dependencies..."
180
+ return if add_packages(REACT_DEPENDENCIES)
181
+
182
+ GeneratorMessages.add_warning(<<~MSG.strip)
183
+ ⚠️ Failed to add React dependencies.
184
+
185
+ You can install them manually by running:
186
+ npm install #{REACT_DEPENDENCIES.join(' ')}
187
+ MSG
188
+ rescue StandardError => e
189
+ GeneratorMessages.add_warning(<<~MSG.strip)
190
+ ⚠️ Error adding React dependencies: #{e.message}
191
+
192
+ You can install them manually by running:
193
+ npm install #{REACT_DEPENDENCIES.join(' ')}
194
+ MSG
195
+ end
196
+
197
+ def add_css_dependencies
198
+ puts "Installing CSS handling dependencies..."
199
+ return if add_packages(CSS_DEPENDENCIES)
200
+
201
+ GeneratorMessages.add_warning(<<~MSG.strip)
202
+ ⚠️ Failed to add CSS dependencies.
203
+
204
+ You can install them manually by running:
205
+ npm install #{CSS_DEPENDENCIES.join(' ')}
206
+ MSG
207
+ rescue StandardError => e
208
+ GeneratorMessages.add_warning(<<~MSG.strip)
209
+ ⚠️ Error adding CSS dependencies: #{e.message}
210
+
211
+ You can install them manually by running:
212
+ npm install #{CSS_DEPENDENCIES.join(' ')}
213
+ MSG
214
+ end
215
+
216
+ def add_rspack_dependencies
217
+ puts "Installing Rspack core dependencies..."
218
+ return if add_packages(RSPACK_DEPENDENCIES)
219
+
220
+ GeneratorMessages.add_warning(<<~MSG.strip)
221
+ ⚠️ Failed to add Rspack dependencies.
222
+
223
+ You can install them manually by running:
224
+ npm install #{RSPACK_DEPENDENCIES.join(' ')}
225
+ MSG
226
+ rescue StandardError => e
227
+ GeneratorMessages.add_warning(<<~MSG.strip)
228
+ ⚠️ Error adding Rspack dependencies: #{e.message}
229
+
230
+ You can install them manually by running:
231
+ npm install #{RSPACK_DEPENDENCIES.join(' ')}
232
+ MSG
233
+ end
234
+
235
+ def add_typescript_dependencies
236
+ puts "Installing TypeScript dependencies..."
237
+ return if add_packages(TYPESCRIPT_DEPENDENCIES, dev: true)
238
+
239
+ GeneratorMessages.add_warning(<<~MSG.strip)
240
+ ⚠️ Failed to add TypeScript dependencies.
241
+
242
+ You can install them manually by running:
243
+ npm install --save-dev #{TYPESCRIPT_DEPENDENCIES.join(' ')}
244
+ MSG
245
+ rescue StandardError => e
246
+ GeneratorMessages.add_warning(<<~MSG.strip)
247
+ ⚠️ Error adding TypeScript dependencies: #{e.message}
248
+
249
+ You can install them manually by running:
250
+ npm install --save-dev #{TYPESCRIPT_DEPENDENCIES.join(' ')}
251
+ MSG
252
+ end
253
+
254
+ def add_dev_dependencies
255
+ puts "Installing development dependencies..."
256
+
257
+ # Use Rspack-specific dev dependencies if --rspack flag is set
258
+ dev_deps = if respond_to?(:options) && options&.rspack?
259
+ RSPACK_DEV_DEPENDENCIES
260
+ else
261
+ DEV_DEPENDENCIES
262
+ end
263
+
264
+ return if add_packages(dev_deps, dev: true)
265
+
266
+ GeneratorMessages.add_warning(<<~MSG.strip)
267
+ ⚠️ Failed to add development dependencies.
268
+
269
+ You can install them manually by running:
270
+ npm install --save-dev #{dev_deps.join(' ')}
271
+ MSG
272
+ rescue StandardError => e
273
+ GeneratorMessages.add_warning(<<~MSG.strip)
274
+ ⚠️ Error adding development dependencies: #{e.message}
275
+
276
+ You can install them manually by running:
277
+ npm install --save-dev #{dev_deps.join(' ')}
278
+ MSG
279
+ end
280
+
281
+ # Add a single dependency using package_json gem
282
+ #
283
+ # This method is used internally for adding the react-on-rails package
284
+ # with version-specific handling (react-on-rails@VERSION).
285
+ # For batch operations, use add_packages instead.
286
+ #
287
+ # The exact: true flag ensures version pinning aligns with the gem version,
288
+ # preventing version mismatches between the Ruby gem and NPM package.
289
+ #
290
+ # @param package [String] Package specifier (e.g., "react-on-rails@16.0.0")
291
+ # @param dev [Boolean] Whether to add as dev dependency
292
+ # @return [Boolean] true if successful, false otherwise
293
+ def add_package(package, dev: false)
294
+ pj = package_json
295
+ return false unless pj
296
+
297
+ begin
298
+ # Ensure package is in array format for package_json gem
299
+ packages_array = [package]
300
+ if dev
301
+ pj.manager.add(packages_array, type: :dev, exact: true)
302
+ else
303
+ pj.manager.add(packages_array, exact: true)
304
+ end
305
+ true
306
+ rescue StandardError
307
+ # Return false to trigger warning in calling method
308
+ false
309
+ end
310
+ end
311
+
312
+ # Add multiple dependencies at once using package_json gem
313
+ #
314
+ # This method delegates to GeneratorHelper's add_npm_dependencies for
315
+ # better package manager abstraction and batch processing efficiency.
316
+ #
317
+ # @param packages [Array<String>] Package names to add
318
+ # @param dev [Boolean] Whether to add as dev dependencies
319
+ # @return [Boolean] true if successful, false otherwise
320
+ def add_packages(packages, dev: false)
321
+ # Use the add_npm_dependencies helper from GeneratorHelper
322
+ add_npm_dependencies(packages, dev: dev)
323
+ end
324
+
325
+ def install_js_dependencies
326
+ # Use package_json gem's install method (always available via shakapacker)
327
+ # package_json is guaranteed to be available because:
328
+ # 1. react_on_rails gemspec requires shakapacker
329
+ # 2. shakapacker gemspec requires package_json
330
+ # 3. GeneratorHelper provides package_json method
331
+ pj = package_json
332
+ unless pj
333
+ GeneratorMessages.add_warning("package_json not available, skipping dependency installation")
334
+ return false
335
+ end
336
+
337
+ pj.manager.install
338
+ true
339
+ rescue StandardError => e
340
+ GeneratorMessages.add_warning(<<~MSG.strip)
341
+ ⚠️ JavaScript dependencies installation failed: #{e.message}
342
+
343
+ This could be due to network issues or package manager problems.
344
+ You can install dependencies manually later by running:
345
+ • npm install (if using npm)
346
+ • yarn install (if using yarn)
347
+ • pnpm install (if using pnpm)
348
+ MSG
349
+ false
350
+ end
351
+ end
352
+ end
353
+ end
354
+ # rubocop:enable Metrics/ModuleLength
@@ -12,6 +12,25 @@ ReactOnRails.configure do |config|
12
12
  # Set to "" if you're not using server rendering
13
13
  config.server_bundle_js_file = "server-bundle.js"
14
14
 
15
+ # ⚠️ RECOMMENDED: Use Shakapacker 9.0+ private_output_path instead
16
+ #
17
+ # If using Shakapacker 9.0+, add to config/shakapacker.yml:
18
+ # private_output_path: ssr-generated
19
+ #
20
+ # React on Rails will auto-detect this value, eliminating the need to set it here.
21
+ # This keeps your webpack and Rails configs in sync automatically.
22
+ #
23
+ # For older Shakapacker versions or custom setups, manually configure:
24
+ # config.server_bundle_output_path = "ssr-generated"
25
+ #
26
+ # The path is relative to Rails.root and should point to a private directory
27
+ # (outside of public/) for security. Run 'rails react_on_rails:doctor' to verify.
28
+
29
+ # Enforce that server bundles are only loaded from private (non-public) directories.
30
+ # When true, server bundles will only be loaded from the configured server_bundle_output_path.
31
+ # This is recommended for production to prevent server-side code from being exposed.
32
+ config.enforce_private_server_bundles = true
33
+
15
34
  ################################################################################
16
35
  # Test Configuration (Optional)
17
36
  ################################################################################
@@ -29,6 +29,15 @@ default: &default
29
29
  # Location for manifest.json, defaults to {public_output_path}/manifest.json if unset
30
30
  # manifest_path: public/packs/manifest.json
31
31
 
32
+ # Location for private server-side bundles (e.g., for SSR)
33
+ # These bundles are not served publicly, unlike public_output_path
34
+ # Shakapacker 9.0+ feature - automatically detected by React on Rails
35
+ <% if shakapacker_version_9_or_higher? -%>
36
+ private_output_path: ssr-generated
37
+ <% else -%>
38
+ # private_output_path: ssr-generated # Uncomment to enable (requires Shakapacker 9.0+)
39
+ <% end -%>
40
+
32
41
  # Additional paths webpack should look up modules
33
42
  # ['app/assets', 'engine/foo/app/assets']
34
43
  additional_paths: []
@@ -36,8 +45,15 @@ default: &default
36
45
  # Reload manifest.json on all requests so we reload latest compiled packs
37
46
  cache_manifest: false
38
47
 
39
- # Select loader to use, available options are 'babel' (default), 'swc' or 'esbuild'
40
- webpack_loader: 'babel'
48
+ # Select JavaScript transpiler to use
49
+ # Available options: 'swc' (default, 20x faster), 'babel', 'esbuild', or 'none'
50
+ # Use 'none' when providing a completely custom webpack configuration
51
+ # Note: When using rspack, swc is used automatically regardless of this setting
52
+ javascript_transpiler: "swc"
53
+
54
+ # Select assets bundler to use
55
+ # Available options: 'webpack' (default) or 'rspack'
56
+ assets_bundler: "webpack"
41
57
 
42
58
  # Raises an error if there is a mismatch in the shakapacker gem and npm package being used
43
59
  ensure_consistent_versioning: true