react_on_rails 16.4.0.rc.8 → 16.4.0.rc.10

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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/lib/generators/react_on_rails/base_generator.rb +369 -45
  4. data/lib/generators/react_on_rails/generator_helper.rb +17 -9
  5. data/lib/generators/react_on_rails/generator_messages.rb +20 -17
  6. data/lib/generators/react_on_rails/install_generator.rb +104 -26
  7. data/lib/generators/react_on_rails/js_dependency_manager.rb +24 -20
  8. data/lib/generators/react_on_rails/pro_generator.rb +2 -2
  9. data/lib/generators/react_on_rails/pro_setup.rb +224 -68
  10. data/lib/generators/react_on_rails/react_with_redux_generator.rb +7 -0
  11. data/lib/generators/react_on_rails/rsc_generator.rb +2 -2
  12. data/lib/generators/react_on_rails/rsc_setup.rb +257 -31
  13. data/lib/generators/react_on_rails/templates/base/base/app/controllers/hello_world_controller.rb +1 -1
  14. data/lib/generators/react_on_rails/templates/base/base/app/views/layouts/{hello_world.html.erb → react_on_rails_default.html.erb} +1 -1
  15. data/lib/generators/react_on_rails/templates/base/base/config/initializers/react_on_rails.rb.tt +14 -10
  16. data/lib/generators/react_on_rails/templates/base/base/config/shakapacker.yml.tt +9 -2
  17. data/lib/generators/react_on_rails/templates/dev_tests/spec/rails_helper.rb +9 -7
  18. data/lib/generators/react_on_rails/templates/dev_tests/spec/spec_helper.rb +2 -0
  19. data/lib/generators/react_on_rails/templates/rsc/base/app/controllers/{hello_server_controller.rb → hello_server_controller.rb.tt} +1 -1
  20. data/lib/generators/react_on_rails/templates/rsc/base/app/views/hello_server/index.html.erb +1 -2
  21. data/lib/react_on_rails/configuration.rb +4 -4
  22. data/lib/react_on_rails/dev/server_manager.rb +128 -3
  23. data/lib/react_on_rails/doctor.rb +744 -63
  24. data/lib/react_on_rails/helper.rb +52 -38
  25. data/lib/react_on_rails/locales/base.rb +2 -2
  26. data/lib/react_on_rails/locales/to_js.rb +2 -2
  27. data/lib/react_on_rails/packer_utils.rb +4 -4
  28. data/lib/react_on_rails/pro_helper.rb +1 -1
  29. data/lib/react_on_rails/server_rendering_pool/ruby_embedded_java_script.rb +1 -1
  30. data/lib/react_on_rails/system_checker.rb +1 -1
  31. data/lib/react_on_rails/test_helper/dev_assets_detector.rb +242 -0
  32. data/lib/react_on_rails/test_helper/ensure_assets_compiled.rb +22 -9
  33. data/lib/react_on_rails/test_helper/webpack_assets_compiler.rb +44 -14
  34. data/lib/react_on_rails/test_helper.rb +3 -2
  35. data/lib/react_on_rails/utils.rb +11 -12
  36. data/lib/react_on_rails/version.rb +1 -1
  37. data/lib/react_on_rails/version_checker.rb +39 -9
  38. data/lib/react_on_rails.rb +1 -0
  39. data/rakelib/shakapacker_examples.rake +6 -0
  40. data/rakelib/update_changelog.rake +169 -26
  41. metadata +5 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8836b6e37dab21e6ee52f045e1f1dbb0ddce044107ce4ff6b48724ac57003bb9
4
- data.tar.gz: 3deef582db0eccf9d692813848f8dc0e1f7c610f819497bb563fb6de0b5b2091
3
+ metadata.gz: cdd0253666ff913084d12f9f5b3f24b41f0f33ed418165f54a5d038cb567012d
4
+ data.tar.gz: 510bcdc628f66dd6c9ddfae7be24582fef28ea35c263440e86a640b419bff379
5
5
  SHA512:
6
- metadata.gz: 38982dfff467e79045d3c6017a991f8dedac9c17abee867cb55197e2514cbe238317ec99ef89e5ad0c258a7d306bf8325ea1266926ae85a98111af1f759023fb
7
- data.tar.gz: 124b251b8a0ca66b4f38a8d2990012690be96408c768a4f1081ad43f87c6d73b525a5bfd2de56d023a138288a28f5dbf6a6f988bdcbab8dd8bf7f1e2ea585be7
6
+ metadata.gz: fa0ec02748d25524f70fa7fe73803fc25c8c73249a17b953517ab3b226fa2101efc0443b3bab201737711f91d4a4f4d9d280e29005c28b505ae3d2d6d4bda153
7
+ data.tar.gz: 365c80cbf379d64e9c74958e96334fe494d2f9770f8fa0f1b01eae03f89e11fd08590459bbe3a021f6a4e3ff697a7ad78bcfa9fd93ac0646ada6007f6d471b8a
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.8)
4
+ react_on_rails (16.4.0.rc.10)
5
5
  addressable
6
6
  connection_pool
7
7
  execjs (~> 2.5)
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "rails/generators"
4
4
  require "fileutils"
5
+ require "erb"
5
6
  require_relative "generator_messages"
6
7
  require_relative "generator_helper"
7
8
  require_relative "js_dependency_manager"
@@ -47,6 +48,56 @@ module ReactOnRails
47
48
  default: false,
48
49
  hide: true
49
50
 
51
+ # Keep this map in sync with templates under:
52
+ # lib/generators/react_on_rails/templates/**/config/webpack/*.tt
53
+ MANAGED_WEBPACK_FILE_TEMPLATES = {
54
+ "clientWebpackConfig.js" => "base/base/config/webpack/clientWebpackConfig.js.tt",
55
+ "commonWebpackConfig.js" => "base/base/config/webpack/commonWebpackConfig.js.tt",
56
+ "test.js" => "base/base/config/webpack/test.js.tt",
57
+ "development.js" => "base/base/config/webpack/development.js.tt",
58
+ "production.js" => "base/base/config/webpack/production.js.tt",
59
+ "serverWebpackConfig.js" => "base/base/config/webpack/serverWebpackConfig.js.tt",
60
+ "rscWebpackConfig.js" => "rsc/base/config/webpack/rscWebpackConfig.js.tt",
61
+ "ServerClientOrBoth.js" => "base/base/config/webpack/ServerClientOrBoth.js.tt",
62
+ # Legacy filename generated by older React on Rails versions.
63
+ # We compare against the current options (including --pro/--rsc); if those
64
+ # don't match the original generation, cleanup intentionally preserves files.
65
+ "generateWebpackConfigs.js" => "base/base/config/webpack/ServerClientOrBoth.js.tt"
66
+ }.freeze
67
+
68
+ # Keep these helper delegates in sync with ERB calls in managed webpack
69
+ # templates. This is the intended template-facing API; missing delegates
70
+ # intentionally fail rendering and keep cleanup conservative by treating
71
+ # files as non-removable.
72
+ TemplateRenderContext = Struct.new(:generator, :config) do
73
+ def erb_binding
74
+ binding
75
+ end
76
+
77
+ def add_documentation_reference(*args)
78
+ generator.__send__(:add_documentation_reference, *args)
79
+ end
80
+
81
+ def use_pro?
82
+ generator.__send__(:use_pro?)
83
+ end
84
+
85
+ def use_rsc?
86
+ generator.__send__(:use_rsc?)
87
+ end
88
+
89
+ def shakapacker_version_9_or_higher?
90
+ generator.__send__(:shakapacker_version_9_or_higher?)
91
+ end
92
+ end
93
+
94
+ REMOVABLE_WEBPACK_FILES = (MANAGED_WEBPACK_FILE_TEMPLATES.keys +
95
+ %w[webpack.config.js]).freeze
96
+ DOCS_REFERENCE_MESSAGE = "// The source code including full typescript support is available at:"
97
+ TEMPLATE_RENDER_FAILED = Object.new.freeze # unique sentinel compared by identity via .equal?
98
+ private_constant :MANAGED_WEBPACK_FILE_TEMPLATES, :REMOVABLE_WEBPACK_FILES, :TemplateRenderContext,
99
+ :DOCS_REFERENCE_MESSAGE, :TEMPLATE_RENDER_FAILED
100
+
50
101
  def add_hello_world_route
51
102
  # RSC uses HelloServer instead of HelloWorld, but Redux still needs hello_world route
52
103
  return if use_rsc? && !options.redux?
@@ -70,9 +121,9 @@ module ReactOnRails
70
121
  .env.example
71
122
  bin/shakapacker-precompile-hook]
72
123
 
73
- # HelloServer uses the hello_world layout so React on Rails can inject generated
74
- # packs without requiring a hardcoded application.js pack entry.
75
- base_files << "app/views/layouts/hello_world.html.erb"
124
+ # react_on_rails_default layout provides empty pack tags so React on Rails can
125
+ # inject generated packs without requiring a hardcoded application.js pack entry.
126
+ base_files << "app/views/layouts/react_on_rails_default.html.erb"
76
127
 
77
128
  # HelloWorld controller only when not using RSC (RSC uses HelloServer)
78
129
  # Exception: Redux still needs the HelloWorld controller even with RSC
@@ -104,7 +155,11 @@ module ReactOnRails
104
155
  end
105
156
 
106
157
  def copy_webpack_config
107
- puts "Adding #{using_rspack? ? 'Rspack' : 'Webpack'} config"
158
+ # Cleanup must run before writing new webpack/rspack configs so we only
159
+ # evaluate pre-existing stale entries, never files generated in this run.
160
+ cleanup_stale_webpack_config_dir_for_rspack
161
+
162
+ say "Adding #{using_rspack? ? 'Rspack' : 'Webpack'} config"
108
163
  base_path = "base/base"
109
164
  base_files = %w[babel.config.js
110
165
  config/webpack/clientWebpackConfig.js
@@ -114,9 +169,7 @@ module ReactOnRails
114
169
  config/webpack/production.js
115
170
  config/webpack/serverWebpackConfig.js
116
171
  config/webpack/ServerClientOrBoth.js]
117
- config = {
118
- message: "// The source code including full typescript support is available at:"
119
- }
172
+ config = { message: DOCS_REFERENCE_MESSAGE }
120
173
  base_files.each do |file|
121
174
  template("#{base_path}/#{file}.tt", destination_config_path(file), config)
122
175
  end
@@ -130,12 +183,12 @@ module ReactOnRails
130
183
  config = "config/shakapacker.yml"
131
184
 
132
185
  if options.shakapacker_just_installed?
133
- puts "Replacing Shakapacker default config with React on Rails version"
186
+ say "Replacing Shakapacker default config with React on Rails version"
134
187
  # Shakapacker's installer just created this file from scratch (no pre-existing config).
135
188
  # Safe to overwrite silently with RoR's version-aware template (e.g., private_output_path).
136
189
  template("#{base_path}#{config}.tt", config, force: true)
137
190
  else
138
- puts "Adding Shakapacker #{ReactOnRails::PackerUtils.shakapacker_version} config"
191
+ say "Adding Shakapacker #{ReactOnRails::PackerUtils.shakapacker_version} config"
139
192
  # Thor handles the conflict: prompts user interactively, or respects --force/--skip flags.
140
193
  template("#{base_path}#{config}.tt", config)
141
194
  end
@@ -176,25 +229,42 @@ module ReactOnRails
176
229
  end
177
230
 
178
231
  def append_to_spec_rails_helper
179
- rails_helper = File.join(destination_root, "spec/rails_helper.rb")
180
- if File.exist?(rails_helper)
181
- add_configure_rspec_to_compile_assets(rails_helper)
182
- else
183
- spec_helper = File.join(destination_root, "spec/spec_helper.rb")
184
- add_configure_rspec_to_compile_assets(spec_helper) if File.exist?(spec_helper)
185
- end
232
+ rspec_helper = preferred_rspec_helper_file
233
+ add_configure_rspec_to_compile_assets(rspec_helper) if rspec_helper
234
+
235
+ test_helper = File.join(destination_root, "test/test_helper.rb")
236
+ add_configure_minitest_to_compile_assets(test_helper) if File.exist?(test_helper)
186
237
  end
187
238
 
188
- CONFIGURE_RSPEC_TO_COMPILE_ASSETS = <<-STR.strip_heredoc
239
+ CONFIGURE_RSPEC_TO_COMPILE_ASSETS = <<~STR
189
240
  RSpec.configure do |config|
190
241
  # Ensure that if we are running js tests, we are using latest webpack assets
191
242
  # This will use the defaults of :js and :server_rendering meta tags
243
+ # Requires config.build_test_command in config/initializers/react_on_rails.rb.
244
+ # This is the default setup for React on Rails generated apps.
192
245
  ReactOnRails::TestHelper.configure_rspec_to_compile_assets(config)
193
246
  end
194
247
  STR
195
248
 
249
+ CONFIGURE_MINITEST_TO_COMPILE_ASSETS = <<~STR
250
+ # Ensure that tests run against fresh webpack assets.
251
+ ActiveSupport::TestCase.setup do
252
+ ReactOnRails::TestHelper.ensure_assets_compiled
253
+ end
254
+ STR
255
+
196
256
  private
197
257
 
258
+ def preferred_rspec_helper_file
259
+ rails_helper = File.join(destination_root, "spec/rails_helper.rb")
260
+ return rails_helper if File.exist?(rails_helper)
261
+
262
+ spec_helper = File.join(destination_root, "spec/spec_helper.rb")
263
+ return spec_helper if File.exist?(spec_helper)
264
+
265
+ nil
266
+ end
267
+
198
268
  def copy_webpack_main_config(base_path, config)
199
269
  webpack_config_path = bundler_main_config_path
200
270
 
@@ -202,16 +272,16 @@ module ReactOnRails
202
272
  existing_content = File.read(webpack_config_path)
203
273
 
204
274
  # Check if it's the standard Shakapacker config that we can safely replace
205
- if standard_shakapacker_config?(existing_content)
275
+ if standard_shakapacker_config?(existing_content, strip_comments: true)
206
276
  # Remove the file first to avoid conflict prompt, then recreate it
207
277
  remove_file(webpack_config_path, verbose: false)
208
278
  # Show what we're doing
209
- puts " #{set_color('replace', :green)} #{webpack_config_path} " \
210
- "(auto-upgrading from standard Shakapacker to React on Rails config)"
279
+ say_status :replace,
280
+ "#{webpack_config_path} (auto-upgrading from standard Shakapacker to React on Rails config)",
281
+ :green
211
282
  template("#{base_path}/config/webpack/webpack.config.js.tt", webpack_config_path, config)
212
283
  elsif react_on_rails_config?(existing_content)
213
- puts " #{set_color('identical', :blue)} #{webpack_config_path} " \
214
- "(already React on Rails compatible)"
284
+ say_status :identical, "#{webpack_config_path} (already React on Rails compatible)", :blue
215
285
  # Skip - don't need to do anything
216
286
  else
217
287
  handle_custom_webpack_config(base_path, config, webpack_config_path)
@@ -226,23 +296,25 @@ module ReactOnRails
226
296
  # Custom config - ask user
227
297
  config_file_name = File.basename(webpack_config_path)
228
298
  bundler_name = using_rspack? ? "rspack" : "webpack"
229
- puts "\n#{set_color('NOTICE:', :yellow)} Your #{config_file_name} appears to be customized."
230
- puts "React on Rails needs to replace it with an environment-specific loader."
231
- puts "Your current config will be backed up to #{config_file_name}.backup"
299
+ say ""
300
+ say_status :notice, "Your #{config_file_name} appears to be customized.", :yellow
301
+ say "React on Rails needs to replace it with an environment-specific loader."
302
+ say "Your current config will be backed up to #{config_file_name}.backup"
232
303
 
233
304
  if yes?("Replace #{config_file_name} with React on Rails version? (Y/n)")
234
305
  # Create backup
235
306
  backup_path = "#{webpack_config_path}.backup"
236
307
  if File.exist?(webpack_config_path)
237
308
  FileUtils.cp(webpack_config_path, backup_path)
238
- puts " #{set_color('create', :green)} #{backup_path} (backup of your custom config)"
309
+ say_status :create, "#{backup_path} (backup of your custom config)", :green
239
310
  end
240
311
 
241
312
  template("#{base_path}/config/webpack/webpack.config.js.tt", webpack_config_path, config)
242
313
  else
243
- puts " #{set_color('skip', :yellow)} #{webpack_config_path}"
244
- puts " #{set_color('WARNING:', :red)} React on Rails may not work correctly " \
245
- "without the environment-specific #{bundler_name} config"
314
+ say_status :skip, webpack_config_path, :yellow
315
+ say_status :warning,
316
+ "React on Rails may not work correctly without the environment-specific #{bundler_name} config",
317
+ :red
246
318
  end
247
319
  end
248
320
 
@@ -254,25 +326,265 @@ module ReactOnRails
254
326
  end
255
327
  end
256
328
 
257
- def standard_shakapacker_config?(content)
329
+ def cleanup_stale_webpack_config_dir_for_rspack
330
+ return unless cleanup_stale_webpack_config_dir?
331
+
332
+ webpack_config_relative_dir = "config/webpack"
333
+ all_entries = stale_webpack_config_entries
334
+ unless all_entries
335
+ say_status :warning,
336
+ "Keeping #{webpack_config_relative_dir}; could not read directory entries " \
337
+ "(permission denied or path changed)",
338
+ :yellow
339
+ return
340
+ end
341
+ if all_entries.empty?
342
+ say_status :skip, "#{webpack_config_relative_dir} (empty directory, leaving as-is)", :yellow
343
+ return
344
+ end
345
+
346
+ return warn_dotfiles_only_webpack_dir(webpack_config_relative_dir, all_entries) if all_dotfiles?(all_entries)
347
+
348
+ # We only clean up known top-level files. Any directory or unknown entry is
349
+ # treated as non-removable so we don't recurse into user-managed content.
350
+ non_removable_entries = non_removable_webpack_entries(all_entries)
351
+ removable_entries = removable_webpack_entries(all_entries)
352
+ if non_removable_entries.empty?
353
+ remove_stale_webpack_dir(webpack_config_relative_dir)
354
+ return
355
+ end
356
+
357
+ if all_dotfiles?(non_removable_entries)
358
+ handle_dotfile_only_non_removable_entries(
359
+ webpack_config_relative_dir,
360
+ removable_entries,
361
+ non_removable_entries
362
+ )
363
+ return
364
+ end
365
+
366
+ removed_entries = remove_stale_webpack_files(webpack_config_relative_dir, removable_entries)
367
+ warn_non_removable_webpack_entries(webpack_config_relative_dir, non_removable_entries, removed_entries)
368
+ end
369
+
370
+ def remove_stale_webpack_dir(webpack_config_relative_dir)
371
+ # Thor's remove_dir uses paths relative to destination_root.
372
+ # TOCTOU: a file created between the removability check and remove_dir
373
+ # would be deleted. Acceptable for this generator cleanup path.
374
+ remove_dir(webpack_config_relative_dir, verbose: false)
375
+ say_status :remove,
376
+ "#{webpack_config_relative_dir} (stale webpack configs after switching to --rspack)",
377
+ :green
378
+ end
379
+
380
+ def handle_dotfile_only_non_removable_entries(
381
+ webpack_config_relative_dir,
382
+ removable_entries,
383
+ non_removable_entries
384
+ )
385
+ removed_entries = remove_stale_webpack_files(webpack_config_relative_dir, removable_entries)
386
+ warn_dotfiles_in_webpack_dir(webpack_config_relative_dir, non_removable_entries, removed_entries)
387
+ end
388
+
389
+ def cleanup_stale_webpack_config_dir?
390
+ using_rspack? && Dir.exist?(stale_webpack_config_dir)
391
+ end
392
+
393
+ def all_dotfiles?(entries)
394
+ entries.any? && entries.all? { |entry| entry.start_with?(".") }
395
+ end
396
+
397
+ def warn_dotfiles_only_webpack_dir(webpack_config_relative_dir, all_entries)
398
+ say_status :warning,
399
+ "Keeping #{webpack_config_relative_dir}; only dotfiles found: " \
400
+ "#{all_entries.join(', ')}",
401
+ :yellow
402
+ end
403
+
404
+ def warn_non_removable_webpack_entries(webpack_config_relative_dir, non_removable_entries, removed_entries = [])
405
+ non_dotfile_entries = non_removable_entries.reject { |entry| entry.start_with?(".") }
406
+ dotfiles = non_removable_entries.select { |entry| entry.start_with?(".") }
407
+ non_removable_directories, non_removable_files = non_dotfile_entries.partition do |entry|
408
+ directory_entry?(entry)
409
+ end
410
+
411
+ non_removable_sections = []
412
+ non_removable_sections << "files: #{non_removable_files.join(', ')}" if non_removable_files.any?
413
+ if non_removable_directories.any?
414
+ non_removable_sections << "directories: #{non_removable_directories.join(', ')}"
415
+ end
416
+ non_removable_sections << "dotfiles: #{dotfiles.join(', ')}" if dotfiles.any?
417
+
418
+ non_removable_sections_message = non_removable_sections.join("; ")
419
+ removed_entries_message = if removed_entries.any?
420
+ " Removed stale managed files: #{removed_entries.join(', ')}."
421
+ else
422
+ ""
423
+ end
424
+ say_status :warning,
425
+ "Keeping #{webpack_config_relative_dir}; " \
426
+ "custom/non-removable entries detected: #{non_removable_sections_message}." \
427
+ "#{removed_entries_message}",
428
+ :yellow
429
+ end
430
+
431
+ def directory_entry?(entry)
432
+ File.directory?(File.join(stale_webpack_config_dir, entry))
433
+ rescue Errno::EACCES, Errno::ELOOP, Errno::ENOENT
434
+ false
435
+ end
436
+
437
+ def warn_dotfiles_in_webpack_dir(webpack_config_relative_dir, dotfiles, removed_entries)
438
+ removed_entries_message = if removed_entries.any?
439
+ " Removed stale managed files: #{removed_entries.join(', ')}."
440
+ else
441
+ ""
442
+ end
443
+ say_status :warning,
444
+ "Keeping #{webpack_config_relative_dir}; dotfiles present: #{dotfiles.join(', ')}. " \
445
+ "Remove them manually if you want the directory cleaned up." \
446
+ "#{removed_entries_message}",
447
+ :yellow
448
+ end
449
+
450
+ def non_removable_webpack_entries(all_entries)
451
+ all_entries.select do |entry|
452
+ entry.start_with?(".") || !removable_webpack_entry?(entry)
453
+ end
454
+ end
455
+
456
+ def removable_webpack_entries(all_entries)
457
+ all_entries.select { |entry| removable_webpack_entry?(entry) }
458
+ end
459
+
460
+ def remove_stale_webpack_files(webpack_config_relative_dir, entries)
461
+ entries.each_with_object([]) do |entry, removed_entries|
462
+ relative_path = File.join(webpack_config_relative_dir, entry)
463
+ full_path = File.join(destination_root, relative_path)
464
+ remove_file(relative_path, verbose: false)
465
+ removed_entries << entry unless File.exist?(full_path)
466
+ rescue Errno::EACCES, Errno::ELOOP, Errno::ENOENT, Errno::ENOTDIR
467
+ # If we cannot stat after removal attempt, conservatively report as not removed.
468
+ nil
469
+ end
470
+ end
471
+
472
+ def removable_webpack_entry?(entry)
473
+ return false unless REMOVABLE_WEBPACK_FILES.include?(entry)
474
+
475
+ full_path = File.join(stale_webpack_config_dir, entry)
476
+ return false if File.symlink?(full_path)
477
+ return false unless File.file?(full_path)
478
+
479
+ content = safe_read_cleanup_file(full_path)
480
+ return false unless content
481
+
482
+ return removable_webpack_main_config?(content) if entry == "webpack.config.js"
483
+
484
+ removable_managed_webpack_entry?(entry, content)
485
+ rescue Errno::EACCES, Errno::ELOOP, Errno::ENOENT, Errno::ENOTDIR
486
+ false
487
+ end
488
+
489
+ def removable_webpack_main_config?(content)
490
+ webpack_template = rendered_template_for_cleanup("base/base/config/webpack/webpack.config.js.tt")
491
+ return standard_shakapacker_config?(content) if webpack_template.equal?(TEMPLATE_RENDER_FAILED)
492
+
493
+ # Cleanup is deliberately comment-sensitive (unlike copy_webpack_main_config)
494
+ # so comment-only edits keep the file as a potential customization.
495
+ standard_shakapacker_config?(content) || content_matches_template?(content, webpack_template)
496
+ end
497
+
498
+ def removable_managed_webpack_entry?(entry, content)
499
+ template_path = MANAGED_WEBPACK_FILE_TEMPLATES[entry]
500
+ return false unless template_path
501
+
502
+ rendered_template = rendered_template_for_cleanup(template_path)
503
+ return false if rendered_template.equal?(TEMPLATE_RENDER_FAILED)
504
+
505
+ content_matches_template?(content, rendered_template)
506
+ end
507
+
508
+ def rendered_template_for_cleanup(template_path)
509
+ @rendered_template_cache ||= {}
510
+ @rendered_template_cache[template_path] ||= begin
511
+ # Cleanup comparisons only need the injected documentation comment.
512
+ template_doc_config = { message: DOCS_REFERENCE_MESSAGE }
513
+ template_content = File.read(File.join(self.class.source_root, template_path))
514
+ # Render against current generator options. Any mismatch is treated as non-removable,
515
+ # which is intentional because cleanup should be conservative.
516
+ # Note: files originally generated with --pro or --rsc will not match when the
517
+ # current run omits those options; in that case, we preserve the directory.
518
+ # Templates rely on config[:message] plus a small helper subset exposed by
519
+ # TemplateRenderContext (add_documentation_reference, use_pro?, use_rsc?,
520
+ # shakapacker_version_9_or_higher?). Missing method delegates raise
521
+ # NoMethodError and are caught below, treating the file as non-removable.
522
+ # Missing config hash keys return nil silently, so any new config key
523
+ # required by templates must be added to template_doc_config above.
524
+ # Use TemplateRenderContext#erb_binding to avoid leaking method-local
525
+ # variables from rendered_template_for_cleanup into the ERB scope.
526
+ template_render_context = TemplateRenderContext.new(self, template_doc_config)
527
+ ERB.new(template_content, trim_mode: "-").result(template_render_context.erb_binding)
528
+ rescue StandardError => e
529
+ say_status :warning,
530
+ "Could not render template #{template_path} for cleanup check (#{e.class}: #{e.message}); " \
531
+ "treating as non-removable",
532
+ :yellow
533
+ # Rendering failures should never abort installation. Returning a sentinel
534
+ # guarantees a non-match so the file is treated as non-removable.
535
+ # We cache the sentinel intentionally (same options within one generator run)
536
+ # so repeated checks stay consistent and avoid noisy duplicate warnings.
537
+ TEMPLATE_RENDER_FAILED
538
+ end
539
+ end
540
+
541
+ def stale_webpack_config_dir
542
+ File.join(destination_root, "config/webpack")
543
+ end
544
+
545
+ def stale_webpack_config_entries
546
+ Dir.children(stale_webpack_config_dir).sort
547
+ rescue Errno::EACCES, Errno::ENOENT, Errno::ENOTDIR
548
+ # TOCTOU: directory may disappear between exist? check and read
549
+ nil
550
+ end
551
+
552
+ def safe_read_cleanup_file(path)
553
+ File.read(path)
554
+ rescue Errno::EACCES, Errno::ELOOP, Errno::ENOENT, Errno::EISDIR, Errno::ENOTDIR
555
+ nil
556
+ end
557
+
558
+ def standard_shakapacker_config?(content, strip_comments: false)
559
+ # Keep strip_comments false by default so comment-only edits are treated as
560
+ # potential customizations during cleanup. Callers can pass true when they
561
+ # need historical comment-insensitive replacement matching.
258
562
  # Get the expected default config based on Shakapacker version
259
563
  expected_configs = shakapacker_default_configs
260
564
 
261
565
  # Check if the content matches any of the known default configurations
262
- expected_configs.any? { |config| content_matches_template?(content, config) }
566
+ expected_configs.any? { |config| content_matches_template?(content, config, strip_comments: strip_comments) }
263
567
  end
264
568
 
265
- def content_matches_template?(content, template)
569
+ def content_matches_template?(content, template, strip_comments: false)
266
570
  # Normalize whitespace and compare
267
- normalize_config_content(content) == normalize_config_content(template)
571
+ normalize_config_content(content, strip_comments: strip_comments) ==
572
+ normalize_config_content(template, strip_comments: strip_comments)
268
573
  end
269
574
 
270
- def normalize_config_content(content)
271
- # Remove comments, normalize whitespace, and clean up for comparison
272
- content.gsub(%r{//.*$}, "") # Remove single-line comments
273
- .gsub(%r{/\*.*?\*/}m, "") # Remove multi-line comments
274
- .gsub(/\s+/, " ") # Normalize whitespace
275
- .strip
575
+ def normalize_config_content(content, strip_comments: false)
576
+ normalized_content = content
577
+ if strip_comments
578
+ # Replacement detection historically removed inline // comments too.
579
+ # Note: this also strips `//` in string literals (for example URLs),
580
+ # which can yield false-negatives and prompt for replacement.
581
+ normalized_content = normalized_content.gsub(%r{//.*$}, "")
582
+ .gsub(%r{/\*.*?\*/}m, "")
583
+ end
584
+
585
+ # Normalize whitespace while preserving comments by default so added comments
586
+ # count as potential customizations and keep cleanup conservative.
587
+ normalized_content.gsub(/\s+/, " ").strip
276
588
  end
277
589
 
278
590
  def shakapacker_default_configs
@@ -359,15 +671,27 @@ module ReactOnRails
359
671
  end
360
672
 
361
673
  def add_configure_rspec_to_compile_assets(helper_file)
362
- search_str = "RSpec.configure do |config|"
363
- gsub_file(helper_file, search_str, CONFIGURE_RSPEC_TO_COMPILE_ASSETS)
674
+ content = File.read(helper_file)
675
+ return if content.match?(/^\s*[^#\s][^#]*ReactOnRails::TestHelper\.configure_rspec_to_compile_assets/)
676
+
677
+ updated_content = content.sub("RSpec.configure do |config|", CONFIGURE_RSPEC_TO_COMPILE_ASSETS)
678
+ return if updated_content == content
679
+
680
+ File.write(helper_file, updated_content)
681
+ end
682
+
683
+ def add_configure_minitest_to_compile_assets(helper_file)
684
+ content = File.read(helper_file)
685
+ return if content.match?(/^\s*[^#\s][^#]*ReactOnRails::TestHelper\.ensure_assets_compiled/)
686
+
687
+ append_to_file(helper_file, "\n\n#{CONFIGURE_MINITEST_TO_COMPILE_ASSETS}\n")
364
688
  end
365
689
 
366
690
  def configure_rspack_in_shakapacker
367
691
  shakapacker_config_path = "config/shakapacker.yml"
368
692
  return unless File.exist?(shakapacker_config_path)
369
693
 
370
- puts Rainbow("🔧 Configuring Shakapacker for Rspack...").yellow
694
+ say "🔧 Configuring Shakapacker for Rspack...", :yellow
371
695
 
372
696
  # Use regex replacement to preserve file structure (comments, anchors, aliases)
373
697
  # This replaces ALL occurrences of assets_bundler, not just in default section
@@ -385,7 +709,7 @@ module ReactOnRails
385
709
  '\1swc\2'
386
710
  )
387
711
 
388
- puts Rainbow("✅ Updated shakapacker.yml for Rspack").green
712
+ say "✅ Updated shakapacker.yml for Rspack", :green
389
713
  end
390
714
 
391
715
  def configure_precompile_hook_in_shakapacker
@@ -406,7 +730,7 @@ module ReactOnRails
406
730
  /^(\s*)#\s*precompile_hook:\s*~\s*$/,
407
731
  "\\1precompile_hook: 'bin/shakapacker-precompile-hook'"
408
732
 
409
- puts Rainbow("✅ Configured precompile_hook in shakapacker.yml").green
733
+ say "✅ Configured precompile_hook in shakapacker.yml", :green
410
734
  end
411
735
 
412
736
  def configure_private_output_path_in_shakapacker
@@ -446,7 +770,7 @@ module ReactOnRails
446
770
 
447
771
  return unless File.read(shakapacker_config_path).match?(/^\s+private_output_path:\s*ssr-generated/)
448
772
 
449
- puts Rainbow("✅ Configured private_output_path in shakapacker.yml").green
773
+ say "✅ Configured private_output_path in shakapacker.yml", :green
450
774
  end
451
775
  end
452
776
  end
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "rainbow"
4
3
  require "json"
5
4
 
5
+ # rubocop:disable Metrics/ModuleLength
6
6
  module GeneratorHelper
7
7
  def package_json
8
8
  # Lazy load package_json gem only when actually needed for dependency management
@@ -10,12 +10,13 @@ module GeneratorHelper
10
10
  require "package_json" unless defined?(PackageJson)
11
11
  @package_json ||= PackageJson.read
12
12
  rescue LoadError
13
- puts "Warning: package_json gem not available. This is expected before Shakapacker installation."
14
- puts "Dependencies will be installed using the default package manager after Shakapacker setup."
13
+ say_status :warning, "package_json gem not available. This is expected before Shakapacker installation.", :yellow
14
+ say_status :warning, "Dependencies will be installed using the default package manager after Shakapacker setup.",
15
+ :yellow
15
16
  nil
16
17
  rescue StandardError => e
17
- puts "Warning: Could not read package.json: #{e.message}"
18
- puts "This is normal before Shakapacker creates the package.json file."
18
+ say_status :warning, "Could not read package.json: #{e.message}", :yellow
19
+ say_status :warning, "This is normal before Shakapacker creates the package.json file.", :yellow
19
20
  nil
20
21
  end
21
22
 
@@ -32,8 +33,8 @@ module GeneratorHelper
32
33
  end
33
34
  true
34
35
  rescue StandardError => e
35
- puts "Warning: Could not add packages via package_json gem: #{e.message}"
36
- puts "Will fall back to direct npm commands."
36
+ say_status :warning, "Could not add packages via package_json gem: #{e.message}", :yellow
37
+ say_status :warning, "Will fall back to direct npm commands.", :yellow
37
38
  false
38
39
  end
39
40
  end
@@ -93,9 +94,11 @@ module GeneratorHelper
93
94
  end
94
95
 
95
96
  def print_generator_messages
97
+ # GeneratorMessages stores pre-colored strings, so we strip ANSI manually for --no-color output.
98
+ no_color = !shell.is_a?(Thor::Shell::Color)
96
99
  GeneratorMessages.messages.each do |message|
97
- puts message
98
- puts "" # Blank line after each message for readability
100
+ say(no_color ? message.to_s.gsub(/\e\[[0-9;]*m/, "") : message)
101
+ say "" # Blank line after each message for readability
99
102
  end
100
103
  end
101
104
 
@@ -129,6 +132,10 @@ module GeneratorHelper
129
132
  @pro_gem_installed = Gem.loaded_specs.key?("react_on_rails_pro") || gem_in_lockfile?("react_on_rails_pro")
130
133
  end
131
134
 
135
+ def mark_pro_gem_installed!
136
+ @pro_gem_installed = true
137
+ end
138
+
132
139
  # Check if Pro features should be enabled
133
140
  # Returns true if --pro flag is set OR --rsc flag is set (RSC implies Pro)
134
141
  #
@@ -347,3 +354,4 @@ module GeneratorHelper
347
354
  config.dig("default", "assets_bundler") == "rspack"
348
355
  end
349
356
  end
357
+ # rubocop:enable Metrics/ModuleLength