react_on_rails 16.2.0.beta.10 → 16.2.0.beta.12

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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -21
  3. data/CLAUDE.md +180 -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/analysis/v8-crash-retry-solution.md +148 -0
  9. data/bin/ci-run-failed-specs +6 -4
  10. data/bin/ci-switch-config +4 -3
  11. data/lib/generators/react_on_rails/base_generator.rb +2 -1
  12. data/lib/generators/react_on_rails/generator_helper.rb +29 -0
  13. data/lib/generators/react_on_rails/js_dependency_manager.rb +29 -7
  14. data/lib/generators/react_on_rails/templates/base/base/config/initializers/react_on_rails.rb.tt +19 -0
  15. data/lib/generators/react_on_rails/templates/base/base/config/{shakapacker.yml → shakapacker.yml.tt} +9 -0
  16. data/lib/generators/react_on_rails/templates/base/base/config/webpack/serverWebpackConfig.js.tt +38 -4
  17. data/lib/react_on_rails/configuration.rb +89 -10
  18. data/lib/react_on_rails/dev/pack_generator.rb +49 -11
  19. data/lib/react_on_rails/dev/server_manager.rb +1 -0
  20. data/lib/react_on_rails/doctor.rb +94 -4
  21. data/lib/react_on_rails/helper.rb +32 -5
  22. data/lib/react_on_rails/packs_generator.rb +47 -35
  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/lib/tasks/generate_packs.rake +12 -6
  27. data/react_on_rails_pro/CHANGELOG.md +11 -4
  28. data/react_on_rails_pro/Gemfile.lock +3 -3
  29. data/react_on_rails_pro/app/helpers/react_on_rails_pro_helper.rb +2 -1
  30. data/react_on_rails_pro/lib/react_on_rails_pro/version.rb +1 -1
  31. data/react_on_rails_pro/package.json +1 -1
  32. data/react_on_rails_pro/spec/dummy/Gemfile.lock +3 -3
  33. data/react_on_rails_pro/spec/dummy/spec/helpers/react_on_rails_pro_helper_spec.rb +15 -7
  34. data/react_on_rails_pro/spec/dummy/spec/requests/renderer_console_logging_spec.rb +9 -4
  35. data/sig/react_on_rails/dev/file_manager.rbs +15 -0
  36. data/sig/react_on_rails/dev/pack_generator.rbs +19 -0
  37. data/sig/react_on_rails/dev/process_manager.rbs +22 -0
  38. data/sig/react_on_rails/dev/server_manager.rbs +39 -0
  39. metadata +9 -3
@@ -139,10 +139,12 @@ echo ""
139
139
 
140
140
  # Determine the working directory (check if we need to be in spec/dummy)
141
141
  WORKING_DIR="."
142
- if [ ${#UNIQUE_SPECS[@]} -gt 0 ] && ([[ "${UNIQUE_SPECS[0]}" == *"spec/system"* ]] || [[ "${UNIQUE_SPECS[0]}" == *"spec/helpers"* ]]); then
143
- if [ -d "spec/dummy" ]; then
144
- WORKING_DIR="spec/dummy"
145
- echo -e "${BLUE}Running from spec/dummy directory${NC}"
142
+ if [ ${#UNIQUE_SPECS[@]} -gt 0 ]; then
143
+ if [[ "${UNIQUE_SPECS[0]}" == *"spec/system"* ]] || [[ "${UNIQUE_SPECS[0]}" == *"spec/helpers"* ]]; then
144
+ if [ -d "spec/dummy" ]; then
145
+ WORKING_DIR="spec/dummy"
146
+ echo -e "${BLUE}Running from spec/dummy directory${NC}"
147
+ fi
146
148
  fi
147
149
  fi
148
150
 
data/bin/ci-switch-config CHANGED
@@ -255,9 +255,10 @@ EOF
255
255
  set_node_version "20.18.1" "$VERSION_MANAGER"
256
256
 
257
257
  # Run conversion script
258
- # NOTE: This uses whatever 'ruby' is in PATH after version manager updates above.
259
- # The version manager may not have reloaded yet, so ensure your current Ruby is
260
- # compatible with script/convert (Ruby 2.6+ should work).
258
+ # NOTE: This executes 'ruby' before the version manager reloads in your current shell.
259
+ # The script/convert file requires Ruby 2.6+ and uses basic file I/O operations.
260
+ # Most modern Ruby versions (2.6+) are compatible. The version manager changes above
261
+ # only take effect after shell reload, so this uses your current Ruby installation.
261
262
  print_header "Running script/convert to downgrade dependencies"
262
263
  cd "$PROJECT_ROOT"
263
264
  ruby script/convert
@@ -101,7 +101,8 @@ module ReactOnRails
101
101
  puts "Adding Shakapacker #{ReactOnRails::PackerUtils.shakapacker_version} config"
102
102
  base_path = "base/base/"
103
103
  config = "config/shakapacker.yml"
104
- copy_file("#{base_path}#{config}", config)
104
+ # Use template to enable version-aware configuration
105
+ template("#{base_path}#{config}.tt", config)
105
106
  configure_rspack_in_shakapacker if options.rspack?
106
107
  end
107
108
 
@@ -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
@@ -123,14 +123,36 @@ module ReactOnRails
123
123
  end
124
124
 
125
125
  def add_react_on_rails_package
126
- # Use exact version match between gem and npm package for stable releases
127
- # For pre-release versions (e.g., 16.1.0-rc.1), use latest to avoid installing
128
- # a version that may not exist in the npm registry
129
- major_minor_patch_only = /\A\d+\.\d+\.\d+\z/
130
- react_on_rails_pkg = if ReactOnRails::VERSION.match?(major_minor_patch_only)
131
- "react-on-rails@#{ReactOnRails::VERSION}"
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}"
132
153
  else
133
- puts "Adding the latest react-on-rails NPM module. " \
154
+ puts "WARNING: Unrecognized version format #{ReactOnRails::VERSION}. " \
155
+ "Adding the latest react-on-rails NPM module. " \
134
156
  "Double check this is correct in package.json"
135
157
  "react-on-rails"
136
158
  end
@@ -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: []
@@ -44,19 +44,53 @@ const configureServer = () => {
44
44
  };
45
45
  serverWebpackConfig.plugins.unshift(new bundler.optimize.LimitChunkCountPlugin({ maxChunks: 1 }));
46
46
 
47
- // Custom output for the server-bundle that matches the config in
48
- // config/initializers/react_on_rails.rb
49
- // Server bundles are output to a private directory (not public) for security
47
+ // Custom output for the server-bundle
48
+ <% if shakapacker_version_9_or_higher? -%>
49
+ // Using Shakapacker 9.0+ privateOutputPath for automatic sync with shakapacker.yml
50
+ // This eliminates manual path configuration and keeps configs in sync.
51
+ // Falls back to hardcoded path if private_output_path is not configured.
52
+ const serverBundleOutputPath = config.privateOutputPath ||
53
+ require('path').resolve(__dirname, '../../ssr-generated');
54
+ <% else -%>
55
+ // Using hardcoded path (Shakapacker < 9.0)
56
+ // For Shakapacker 9.0+, consider using config.privateOutputPath instead
57
+ // to automatically sync with shakapacker.yml private_output_path.
58
+ const serverBundleOutputPath = require('path').resolve(__dirname, '../../ssr-generated');
59
+ <% end -%>
60
+
50
61
  serverWebpackConfig.output = {
51
62
  filename: 'server-bundle.js',
52
63
  globalObject: 'this',
53
64
  // If using the React on Rails Pro node server renderer, uncomment the next line
54
65
  // libraryTarget: 'commonjs2',
55
- path: require('path').resolve(__dirname, '../../ssr-generated'),
66
+ path: serverBundleOutputPath,
56
67
  // No publicPath needed since server bundles are not served via web
57
68
  // https://webpack.js.org/configuration/output/#outputglobalobject
58
69
  };
59
70
 
71
+ // Validate server bundle output path configuration
72
+ <% if shakapacker_version_9_or_higher? -%>
73
+ // For Shakapacker 9.0+, verify privateOutputPath is configured in shakapacker.yml
74
+ if (!config.privateOutputPath) {
75
+ console.warn('āš ļø Shakapacker 9.0+ detected but private_output_path not configured in shakapacker.yml');
76
+ console.warn(' Add to config/shakapacker.yml:');
77
+ console.warn(' private_output_path: ssr-generated');
78
+ console.warn(' Run: rails react_on_rails:doctor to validate your configuration');
79
+ }
80
+ <% else -%>
81
+ // For Shakapacker < 9.0, verify hardcoded path syncs with Rails config
82
+ // 1. Ensure config/initializers/react_on_rails.rb has: config.server_bundle_output_path = "ssr-generated"
83
+ // 2. Run: rails react_on_rails:doctor to verify configuration
84
+ const fs = require('fs');
85
+ if (!fs.existsSync(serverBundleOutputPath)) {
86
+ console.warn(`āš ļø Server bundle output directory does not exist: ${serverBundleOutputPath}`);
87
+ console.warn(' It will be created during build, but ensure React on Rails is configured:');
88
+ console.warn(' config.server_bundle_output_path = "ssr-generated" in config/initializers/react_on_rails.rb');
89
+ console.warn(' Run: rails react_on_rails:doctor to validate your configuration');
90
+ }
91
+ <% end -%>
92
+
93
+
60
94
  // Don't hash the server bundle b/c would conflict with the client manifest
61
95
  // And no need for the MiniCssExtractPlugin
62
96
  serverWebpackConfig.plugins = serverWebpackConfig.plugins.filter(
@@ -1,5 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_support/core_ext/enumerable"
4
+ require "active_support/core_ext/object/blank"
5
+
6
+ # Polyfill for compact_blank (added in Rails 6.1) to support Rails 5.2-6.0
7
+ unless [].respond_to?(:compact_blank)
8
+ module Enumerable
9
+ def compact_blank
10
+ reject(&:blank?)
11
+ end
12
+ end
13
+
14
+ class Array
15
+ def compact_blank
16
+ reject(&:blank?)
17
+ end
18
+ end
19
+ end
20
+
3
21
  # rubocop:disable Metrics/ClassLength
4
22
 
5
23
  module ReactOnRails
@@ -10,6 +28,7 @@ module ReactOnRails
10
28
 
11
29
  DEFAULT_GENERATED_ASSETS_DIR = File.join(%w[public webpack], Rails.env).freeze
12
30
  DEFAULT_COMPONENT_REGISTRY_TIMEOUT = 5000
31
+ DEFAULT_SERVER_BUNDLE_OUTPUT_PATH = "ssr-generated"
13
32
 
14
33
  def self.configuration
15
34
  @configuration ||= Configuration.new(
@@ -46,7 +65,7 @@ module ReactOnRails
46
65
  # Set to 0 to disable the timeout and wait indefinitely for component registration.
47
66
  component_registry_timeout: DEFAULT_COMPONENT_REGISTRY_TIMEOUT,
48
67
  generated_component_packs_loading_strategy: nil,
49
- server_bundle_output_path: "ssr-generated",
68
+ server_bundle_output_path: DEFAULT_SERVER_BUNDLE_OUTPUT_PATH,
50
69
  enforce_private_server_bundles: false
51
70
  )
52
71
  end
@@ -184,6 +203,7 @@ module ReactOnRails
184
203
  check_component_registry_timeout
185
204
  validate_generated_component_packs_loading_strategy
186
205
  validate_enforce_private_server_bundles
206
+ auto_detect_server_bundle_path_from_shakapacker
187
207
  end
188
208
 
189
209
  private
@@ -257,6 +277,57 @@ module ReactOnRails
257
277
  "the public directory. Please set it to a directory outside of public."
258
278
  end
259
279
 
280
+ # Auto-detect server_bundle_output_path from Shakapacker 9.0+ private_output_path
281
+ # Checks if user explicitly set a value and warns them to use auto-detection instead
282
+ def auto_detect_server_bundle_path_from_shakapacker
283
+ # Skip if Shakapacker is not available
284
+ return unless defined?(::Shakapacker)
285
+
286
+ # Check if Shakapacker config has private_output_path method (9.0+)
287
+ return unless ::Shakapacker.config.respond_to?(:private_output_path)
288
+
289
+ # Get the private_output_path from Shakapacker
290
+ private_path = ::Shakapacker.config.private_output_path
291
+ return unless private_path
292
+
293
+ relative_path = ReactOnRails::Utils.normalize_to_relative_path(private_path)
294
+
295
+ # Check if user explicitly configured server_bundle_output_path
296
+ if server_bundle_output_path != ReactOnRails::DEFAULT_SERVER_BUNDLE_OUTPUT_PATH
297
+ warn_about_explicit_configuration(relative_path)
298
+ return
299
+ end
300
+
301
+ apply_shakapacker_private_output_path(relative_path)
302
+ rescue StandardError => e
303
+ # Fail gracefully - if auto-detection fails, keep the default
304
+ Rails.logger&.debug("ReactOnRails: Could not auto-detect server bundle path from " \
305
+ "Shakapacker: #{e.message}")
306
+ end
307
+
308
+ def warn_about_explicit_configuration(shakapacker_path)
309
+ # Normalize both paths for comparison
310
+ normalized_config = server_bundle_output_path.to_s.chomp("/")
311
+ normalized_shakapacker = shakapacker_path.to_s.chomp("/")
312
+
313
+ # Only warn if there's a mismatch
314
+ return if normalized_config == normalized_shakapacker
315
+
316
+ Rails.logger&.warn(
317
+ "ReactOnRails: server_bundle_output_path is explicitly set to '#{server_bundle_output_path}' " \
318
+ "but shakapacker.yml private_output_path is '#{shakapacker_path}'. " \
319
+ "Consider removing server_bundle_output_path from your React on Rails initializer " \
320
+ "to use the auto-detected value from shakapacker.yml."
321
+ )
322
+ end
323
+
324
+ def apply_shakapacker_private_output_path(relative_path)
325
+ self.server_bundle_output_path = relative_path
326
+
327
+ Rails.logger&.debug("ReactOnRails: Auto-detected server_bundle_output_path from " \
328
+ "shakapacker.yml private_output_path: '#{relative_path}'")
329
+ end
330
+
260
331
  def check_minimum_shakapacker_version
261
332
  ReactOnRails::PackerUtils.raise_shakapacker_version_incompatible_for_basic_pack_generation unless
262
333
  ReactOnRails::PackerUtils.supports_basic_pack_generation?
@@ -358,15 +429,23 @@ module ReactOnRails
358
429
  end
359
430
 
360
431
  def ensure_webpack_generated_files_exists
361
- return unless webpack_generated_files.empty?
362
-
363
- self.webpack_generated_files = [
364
- "manifest.json",
365
- server_bundle_js_file,
366
- rsc_bundle_js_file,
367
- react_client_manifest_file,
368
- react_server_client_manifest_file
369
- ].compact_blank
432
+ all_required_files = ["manifest.json", server_bundle_js_file]
433
+
434
+ if ReactOnRails::Utils.react_on_rails_pro?
435
+ pro_config = ReactOnRailsPro.configuration
436
+ all_required_files << pro_config.rsc_bundle_js_file
437
+ all_required_files << pro_config.react_client_manifest_file
438
+ all_required_files << pro_config.react_server_client_manifest_file
439
+ end
440
+
441
+ all_required_files = all_required_files.compact_blank
442
+
443
+ if webpack_generated_files.empty?
444
+ self.webpack_generated_files = all_required_files
445
+ else
446
+ missing_files = all_required_files.reject { |file| webpack_generated_files.include?(file) }
447
+ self.webpack_generated_files += missing_files if missing_files.any?
448
+ end
370
449
  end
371
450
 
372
451
  def configure_skip_display_none_deprecation
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "English"
4
4
  require "stringio"
5
+ require_relative "../packer_utils"
5
6
 
6
7
  module ReactOnRails
7
8
  module Dev
@@ -37,29 +38,40 @@ module ReactOnRails
37
38
 
38
39
  if verbose
39
40
  puts "šŸ“¦ Generating React on Rails packs..."
40
- success = run_pack_generation
41
+ success = run_pack_generation(silent: false, verbose: true)
41
42
  else
42
43
  print "šŸ“¦ Generating packs... "
43
- success = run_pack_generation(silent: true)
44
+ success = run_pack_generation(silent: true, verbose: false)
44
45
  puts success ? "āœ…" : "āŒ"
45
46
  end
46
47
 
47
48
  return if success
48
49
 
49
50
  puts "āŒ Pack generation failed"
51
+ unless verbose
52
+ puts ""
53
+ puts "šŸ’” Run with #{Rainbow('--verbose').cyan.bold} flag for detailed output:"
54
+ puts " #{Rainbow('bin/dev --verbose').green.bold}"
55
+ end
50
56
  exit 1
51
57
  end
52
58
 
53
59
  private
54
60
 
55
- def run_pack_generation(silent: false)
61
+ def run_pack_generation(silent: false, verbose: false)
62
+ # Set environment variable for child processes to respect verbose mode
63
+ ENV["REACT_ON_RAILS_VERBOSE"] = verbose ? "true" : "false"
64
+
56
65
  # If we're already inside a Bundler context AND Rails is available (e.g., called from bin/dev),
57
66
  # we can directly require and run the task. Otherwise, use bundle exec.
58
67
  if should_run_directly?
59
68
  run_rake_task_directly(silent: silent)
60
69
  else
61
- run_via_bundle_exec(silent: silent)
70
+ run_via_bundle_exec(silent: silent, verbose: verbose)
62
71
  end
72
+ ensure
73
+ # Clean up environment variable
74
+ ENV.delete("REACT_ON_RAILS_VERBOSE")
63
75
  end
64
76
 
65
77
  def should_run_directly?
@@ -139,14 +151,40 @@ module ReactOnRails
139
151
  # rubocop:enable Style/StderrPuts, Style/GlobalStdStream
140
152
  end
141
153
 
142
- def run_via_bundle_exec(silent: false)
143
- if silent
144
- system(
145
- "bundle", "exec", "rake", "react_on_rails:generate_packs",
146
- out: File::NULL, err: File::NULL
147
- )
154
+ def run_via_bundle_exec(silent: false, verbose: false)
155
+ # Environment variable is already set in run_pack_generation, but we make it explicit here
156
+ # for clarity and to ensure it's passed to the subprocess
157
+ env = { "REACT_ON_RAILS_VERBOSE" => verbose ? "true" : "false" }
158
+
159
+ # Need to unbundle to prevent Bundler from intercepting our bundle exec call
160
+ # when already running inside a Bundler context (e.g., from bin/dev)
161
+ with_unbundled_context do
162
+ if silent
163
+ system(
164
+ env,
165
+ "bundle", "exec", "rake", "react_on_rails:generate_packs",
166
+ out: File::NULL, err: File::NULL
167
+ )
168
+ else
169
+ system(env, "bundle", "exec", "rake", "react_on_rails:generate_packs")
170
+ end
171
+ end
172
+ end
173
+
174
+ # DRY helper method for Bundler context switching with API compatibility
175
+ # Supports both new (with_unbundled_env) and legacy (with_clean_env) Bundler APIs
176
+ def with_unbundled_context(&block)
177
+ if defined?(Bundler)
178
+ if Bundler.respond_to?(:with_unbundled_env)
179
+ Bundler.with_unbundled_env(&block)
180
+ elsif Bundler.respond_to?(:with_clean_env)
181
+ Bundler.with_clean_env(&block)
182
+ else
183
+ # Fallback if neither method is available (very old Bundler versions)
184
+ yield
185
+ end
148
186
  else
149
- system("bundle", "exec", "rake", "react_on_rails:generate_packs")
187
+ yield
150
188
  end
151
189
  end
152
190
  end
@@ -3,6 +3,7 @@
3
3
  require "English"
4
4
  require "open3"
5
5
  require "rainbow"
6
+ require_relative "../packer_utils"
6
7
 
7
8
  module ReactOnRails
8
9
  module Dev
@@ -667,6 +667,7 @@ module ReactOnRails
667
667
  end
668
668
  end
669
669
 
670
+ # rubocop:disable Metrics/CyclomaticComplexity
670
671
  def analyze_server_rendering_config(content)
671
672
  checker.add_info("\nšŸ–„ļø Server Rendering:")
672
673
 
@@ -678,6 +679,19 @@ module ReactOnRails
678
679
  checker.add_info(" server_bundle_js_file: server-bundle.js (default)")
679
680
  end
680
681
 
682
+ # Server bundle output path
683
+ server_bundle_path_match = content.match(/config\.server_bundle_output_path\s*=\s*["']([^"']+)["']/)
684
+ default_path = ReactOnRails::DEFAULT_SERVER_BUNDLE_OUTPUT_PATH
685
+ rails_bundle_path = server_bundle_path_match ? server_bundle_path_match[1] : default_path
686
+ checker.add_info(" server_bundle_output_path: #{rails_bundle_path}")
687
+
688
+ # Enforce private server bundles
689
+ enforce_private_match = content.match(/config\.enforce_private_server_bundles\s*=\s*([^\s\n,]+)/)
690
+ checker.add_info(" enforce_private_server_bundles: #{enforce_private_match[1]}") if enforce_private_match
691
+
692
+ # Check Shakapacker integration and provide recommendations
693
+ check_shakapacker_private_output_path(rails_bundle_path)
694
+
681
695
  # RSC bundle file (Pro feature)
682
696
  rsc_bundle_match = content.match(/config\.rsc_bundle_js_file\s*=\s*["']([^"']+)["']/)
683
697
  if rsc_bundle_match
@@ -702,9 +716,9 @@ module ReactOnRails
702
716
 
703
717
  checker.add_info(" raise_on_prerender_error: #{raise_on_error_match[1]}")
704
718
  end
705
- # rubocop:enable Metrics/AbcSize
719
+ # rubocop:enable Metrics/CyclomaticComplexity
706
720
 
707
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
721
+ # rubocop:disable Metrics/CyclomaticComplexity
708
722
  def analyze_performance_config(content)
709
723
  checker.add_info("\n⚔ Performance & Loading:")
710
724
 
@@ -1387,9 +1401,85 @@ module ReactOnRails
1387
1401
  end
1388
1402
 
1389
1403
  def log_debug(message)
1390
- return unless defined?(Rails.logger) && Rails.logger
1404
+ Rails.logger&.debug(message)
1405
+ end
1406
+
1407
+ # Check Shakapacker private_output_path integration and provide recommendations
1408
+ def check_shakapacker_private_output_path(rails_bundle_path)
1409
+ return report_no_shakapacker unless defined?(::Shakapacker)
1410
+ return report_upgrade_shakapacker unless ::Shakapacker.config.respond_to?(:private_output_path)
1411
+
1412
+ check_shakapacker_9_private_output_path(rails_bundle_path)
1413
+ rescue StandardError => e
1414
+ checker.add_info("\n ā„¹ļø Could not check Shakapacker config: #{e.message}")
1415
+ end
1416
+
1417
+ def report_no_shakapacker
1418
+ checker.add_info("\n ā„¹ļø Shakapacker not detected - using manual configuration")
1419
+ end
1420
+
1421
+ def report_upgrade_shakapacker
1422
+ checker.add_info(<<~MSG.strip)
1423
+ \n šŸ’” Recommendation: Upgrade to Shakapacker 9.0+
1424
+
1425
+ Shakapacker 9.0+ adds 'private_output_path' in shakapacker.yml for server bundles.
1426
+ This eliminates the need to configure server_bundle_output_path separately.
1427
+
1428
+ Benefits:
1429
+ - Single source of truth in shakapacker.yml
1430
+ - Automatic detection by React on Rails
1431
+ - No configuration duplication
1432
+ MSG
1433
+ end
1434
+
1435
+ def check_shakapacker_9_private_output_path(rails_bundle_path)
1436
+ private_path = ::Shakapacker.config.private_output_path
1437
+
1438
+ if private_path
1439
+ report_shakapacker_path_status(private_path, rails_bundle_path)
1440
+ else
1441
+ report_configure_private_output_path(rails_bundle_path)
1442
+ end
1443
+ end
1444
+
1445
+ def report_shakapacker_path_status(private_path, rails_bundle_path)
1446
+ relative_path = ReactOnRails::Utils.normalize_to_relative_path(private_path)
1447
+ # Normalize both paths for comparison (remove trailing slashes)
1448
+ normalized_relative = relative_path.to_s.chomp("/")
1449
+ normalized_rails = rails_bundle_path.to_s.chomp("/")
1450
+
1451
+ if normalized_relative == normalized_rails
1452
+ checker.add_success("\n āœ… Using Shakapacker 9.0+ private_output_path: '#{relative_path}'")
1453
+ checker.add_info(" Auto-detected from shakapacker.yml - no manual config needed")
1454
+ else
1455
+ report_configuration_mismatch(relative_path, rails_bundle_path)
1456
+ end
1457
+ end
1458
+
1459
+ def report_configuration_mismatch(relative_path, rails_bundle_path)
1460
+ checker.add_warning(<<~MSG.strip)
1461
+ \n āš ļø Configuration mismatch detected!
1462
+
1463
+ Shakapacker private_output_path: '#{relative_path}'
1464
+ React on Rails server_bundle_output_path: '#{rails_bundle_path}'
1465
+
1466
+ Recommendation: Remove server_bundle_output_path from your React on Rails
1467
+ initializer and let it auto-detect from shakapacker.yml private_output_path.
1468
+ MSG
1469
+ end
1470
+
1471
+ def report_configure_private_output_path(rails_bundle_path)
1472
+ checker.add_info(<<~MSG.strip)
1473
+ \n šŸ’” Recommendation: Configure private_output_path in shakapacker.yml
1474
+
1475
+ Add to config/shakapacker.yml:
1476
+ private_output_path: #{rails_bundle_path}
1391
1477
 
1392
- Rails.logger.debug(message)
1478
+ This will:
1479
+ - Keep webpack and Rails configs in sync automatically
1480
+ - Enable auto-detection by React on Rails
1481
+ - Serve as single source of truth for server bundle location
1482
+ MSG
1393
1483
  end
1394
1484
  end
1395
1485
  # rubocop:enable Metrics/ClassLength