react_on_rails 17.0.0.rc.0 → 17.0.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 (67) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +2 -0
  3. data/Gemfile.lock +2 -34
  4. data/Rakefile +1 -1
  5. data/lib/generators/react_on_rails/base_generator.rb +47 -12
  6. data/lib/generators/react_on_rails/demo_page_config.rb +6 -6
  7. data/lib/generators/react_on_rails/generator_helper.rb +121 -28
  8. data/lib/generators/react_on_rails/generator_messages/ci_section.rb +10 -2
  9. data/lib/generators/react_on_rails/generator_messages/package_manager_detection.rb +9 -9
  10. data/lib/generators/react_on_rails/generator_messages/shakapacker_status_section.rb +1 -1
  11. data/lib/generators/react_on_rails/generator_messages.rb +7 -7
  12. data/lib/generators/react_on_rails/install_generator.rb +55 -28
  13. data/lib/generators/react_on_rails/js_dependency_manager.rb +161 -64
  14. data/lib/generators/react_on_rails/pro_generator.rb +13 -13
  15. data/lib/generators/react_on_rails/react_with_redux_generator.rb +1 -1
  16. data/lib/generators/react_on_rails/rsc_setup/client_references.rb +590 -101
  17. data/lib/generators/react_on_rails/rsc_setup.rb +64 -26
  18. data/lib/generators/react_on_rails/shakapacker_precompile_hook_helper.rb +476 -0
  19. data/lib/generators/react_on_rails/templates/base/base/.github/workflows/ci.yml.tt +7 -1
  20. data/lib/generators/react_on_rails/templates/base/base/bin/shakapacker-precompile-hook +88 -0
  21. data/lib/generators/react_on_rails/templates/base/base/config/initializers/react_on_rails.rb.tt +1 -1
  22. data/lib/generators/react_on_rails/templates/base/base/config/webpack/clientWebpackConfig.js.tt +2 -2
  23. data/lib/generators/react_on_rails/templates/base/base/config/webpack/serverWebpackConfig.js.tt +5 -5
  24. data/lib/generators/react_on_rails/templates/dev_tests/spec/rails_helper.rb +1 -1
  25. data/lib/generators/react_on_rails/templates/rsc/base/config/webpack/rscWebpackConfig.js.tt +133 -13
  26. data/lib/react_on_rails/config_path_resolver.rb +0 -2
  27. data/lib/react_on_rails/controller.rb +2 -2
  28. data/lib/react_on_rails/dev/pack_generator.rb +5 -5
  29. data/lib/react_on_rails/dev/port_selector.rb +4 -4
  30. data/lib/react_on_rails/dev/process_manager.rb +3 -3
  31. data/lib/react_on_rails/dev/server_manager.rb +208 -92
  32. data/lib/react_on_rails/dev/server_mode.rb +211 -0
  33. data/lib/react_on_rails/dev/service_checker.rb +1 -1
  34. data/lib/react_on_rails/dev.rb +1 -0
  35. data/lib/react_on_rails/doctor.rb +81 -35
  36. data/lib/react_on_rails/engine.rb +9 -2
  37. data/lib/react_on_rails/error.rb +3 -0
  38. data/lib/react_on_rails/git_utils.rb +3 -3
  39. data/lib/react_on_rails/helper.rb +33 -32
  40. data/lib/react_on_rails/length_prefixed_parser.rb +5 -4
  41. data/lib/react_on_rails/locales/base.rb +2 -2
  42. data/lib/react_on_rails/packer_utils.rb +3 -3
  43. data/lib/react_on_rails/packs_generator.rb +331 -62
  44. data/lib/react_on_rails/prerender_error.rb +5 -5
  45. data/lib/react_on_rails/pro_helper.rb +4 -2
  46. data/lib/react_on_rails/pro_migration.rb +2 -2
  47. data/lib/react_on_rails/react_component/render_options.rb +1 -1
  48. data/lib/react_on_rails/server_rendering_pool/ruby_embedded_java_script.rb +200 -14
  49. data/lib/react_on_rails/shakapacker_config_helpers.rb +139 -0
  50. data/lib/react_on_rails/system_checker.rb +33 -50
  51. data/lib/react_on_rails/test_helper/dev_assets_detector.rb +23 -21
  52. data/lib/react_on_rails/test_helper/ensure_assets_compiled.rb +2 -2
  53. data/lib/react_on_rails/test_helper/webpack_assets_compiler.rb +4 -4
  54. data/lib/react_on_rails/test_helper/webpack_assets_status_checker.rb +2 -2
  55. data/lib/react_on_rails/test_helper.rb +11 -11
  56. data/lib/react_on_rails/version.rb +1 -1
  57. data/lib/react_on_rails/version_synchronizer.rb +22 -22
  58. data/lib/tasks/doctor.rake +1 -1
  59. data/lib/tasks/locale.rake +1 -1
  60. data/lib/tasks/sync_versions.rake +1 -1
  61. data/rakelib/lint.rake +15 -2
  62. data/rakelib/run_rspec.rake +1 -1
  63. data/rakelib/shakapacker_version.rake +4 -1
  64. data/rakelib/task_helpers.rb +1 -0
  65. data/rakelib/update_changelog.rake +2 -2
  66. data/react_on_rails.gemspec +2 -2
  67. metadata +7 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 02f0fa1befbed99b17f624249398f47b5a1395ed5407079a472ad39218d2cfc1
4
- data.tar.gz: f8b4e5a3abf309b98f7b244d1a807acb29096ff7ae1ea115e780eefd35f2d609
3
+ metadata.gz: 5610a42464b29a2f8066e2a91d342838cdd1f3734c3965864e0a5bd0d18e4c8a
4
+ data.tar.gz: 551fc429af6eea0798b929d0aece8b417b4646dfed702c1dcd39ae26d52084ad
5
5
  SHA512:
6
- metadata.gz: c594211d78c05817226dd5d4ff06e7bb4f377b57320cdb824de7a8902ec40d66473b28da64e0f6d2df5b445786a050d1c6b61779af3fea0c1c4793f3955efa63
7
- data.tar.gz: e86d35866011de451f725c441539bc7f8400e83d9702a4a3b9058d385e483a926229b4e33e18685636d73700bc7a9a1106246d78174a726327b47b8b0f1645d9
6
+ metadata.gz: 4b1df35fc94bc088e6a9afe96ca54ec651951149ae82415e55d1db023ef582089fb58f9536db698d0971633678b3d2d70188c0ca355217fd9f6a854ead1f177c
7
+ data.tar.gz: c1371c43729036f9f9dbb78d68b85be81919a98a16fb8d3d4185b94c0f48c92506404bedb6e1a7a804b52596ae784b4f60487cc4bad8333f0933e38aa005551a
data/.rubocop.yml CHANGED
@@ -11,6 +11,8 @@ AllCops:
11
11
 
12
12
  Exclude:
13
13
  - 'spec/dummy/bin/*'
14
+ - 'spec/react_on_rails/dummy-for-generators/**/*' # Generated fixture contains intentionally invalid Ruby
15
+ - 'spike/**/*' # Exploratory spike code outside lib/ — not part of the production surface
14
16
 
15
17
  Naming/FileName:
16
18
  Exclude:
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- react_on_rails (17.0.0.rc.0)
4
+ react_on_rails (17.0.0.rc.2)
5
5
  addressable
6
6
  connection_pool
7
7
  execjs (~> 2.5)
@@ -207,7 +207,6 @@ GEM
207
207
  racc (~> 1.4)
208
208
  ostruct (0.6.1)
209
209
  package_json (0.2.0)
210
- parallel (1.24.0)
211
210
  parser (3.3.1.0)
212
211
  ast (~> 2.4.1)
213
212
  racc
@@ -313,34 +312,6 @@ GEM
313
312
  rspec-support (3.13.1)
314
313
  rspec_junit_formatter (0.6.0)
315
314
  rspec-core (>= 2, < 4, != 2.12.0)
316
- rubocop (1.61.0)
317
- json (~> 2.3)
318
- language_server-protocol (>= 3.17.0)
319
- parallel (~> 1.10)
320
- parser (>= 3.3.0.2)
321
- rainbow (>= 2.2.2, < 4.0)
322
- regexp_parser (>= 1.8, < 3.0)
323
- rexml (>= 3.2.5, < 4.0)
324
- rubocop-ast (>= 1.30.0, < 2.0)
325
- ruby-progressbar (~> 1.7)
326
- unicode-display_width (>= 2.4.0, < 3.0)
327
- rubocop-ast (1.31.3)
328
- parser (>= 3.3.1.0)
329
- rubocop-capybara (2.20.0)
330
- rubocop (~> 1.41)
331
- rubocop-factory_bot (2.25.1)
332
- rubocop (~> 1.41)
333
- rubocop-performance (1.20.2)
334
- rubocop (>= 1.48.1, < 2.0)
335
- rubocop-ast (>= 1.30.0, < 2.0)
336
- rubocop-rspec (2.29.2)
337
- rubocop (~> 1.40)
338
- rubocop-capybara (~> 2.17)
339
- rubocop-factory_bot (~> 2.22)
340
- rubocop-rspec_rails (~> 2.28)
341
- rubocop-rspec_rails (2.28.3)
342
- rubocop (~> 1.40)
343
- ruby-progressbar (1.13.0)
344
315
  rubyzip (2.3.2)
345
316
  sass-rails (6.0.0)
346
317
  sassc-rails (~> 2.1, >= 2.1.1)
@@ -469,9 +440,6 @@ DEPENDENCIES
469
440
  rspec-rails
470
441
  rspec-retry
471
442
  rspec_junit_formatter
472
- rubocop (= 1.61.0)
473
- rubocop-performance (~> 1.20.0)
474
- rubocop-rspec (~> 2.26)
475
443
  sass-rails (~> 6.0)
476
444
  sdoc
477
445
  selenium-webdriver (= 4.9.0)
@@ -487,4 +455,4 @@ DEPENDENCIES
487
455
  webdrivers (= 5.3.0)
488
456
 
489
457
  BUNDLED WITH
490
- 2.5.9
458
+ 4.0.10
data/Rakefile CHANGED
@@ -13,7 +13,7 @@ desc "All actions but no examples, good for local developer run."
13
13
  task all_but_examples: ["run_rspec:all_but_examples", "lint"]
14
14
 
15
15
  desc "Prepare for ci, including node_package, dummy app, and generator examples"
16
- task prepare_for_ci: prepare_for_ci
16
+ task(prepare_for_ci:)
17
17
 
18
18
  desc "Runs prepare_for_ci and tasks"
19
19
  task ci: [:prepare_for_ci, *tasks]
@@ -6,11 +6,13 @@ require "erb"
6
6
  require_relative "generator_messages"
7
7
  require_relative "generator_helper"
8
8
  require_relative "js_dependency_manager"
9
+ require_relative "shakapacker_precompile_hook_helper"
9
10
  module ReactOnRails
10
11
  module Generators
11
12
  class BaseGenerator < Rails::Generators::Base
12
13
  include GeneratorHelper
13
14
  include JsDependencyManager
15
+ include ShakapackerPrecompileHookHelper
14
16
 
15
17
  Rails::Generators.hide_namespace(namespace)
16
18
  source_root(File.expand_path("templates", __dir__))
@@ -22,11 +24,22 @@ module ReactOnRails
22
24
  desc: "Install Redux package and Redux version of Hello World Example",
23
25
  aliases: "-R"
24
26
 
25
- # --rspack
27
+ # --rspack / --no-rspack (Rspack is the default on fresh installs; --no-rspack selects Webpack)
28
+ # IMPORTANT: do NOT add a `default:` here. The absence of a default is load-bearing — Thor
29
+ # only includes :rspack in the options hash when the flag is explicitly passed, which is how
30
+ # GeneratorHelper#using_rspack? tells an explicit choice from "no flag given" (the latter
31
+ # falls back to rspack_bundler_default). Adding `default: false` would make
32
+ # options.key?(:rspack) always true and silently break the fresh-install Rspack default.
33
+ # (Thor's omit-when-no-default behavior verified against Thor 1.5.0; see Gemfile.lock.)
26
34
  class_option :rspack,
27
35
  type: :boolean,
28
- default: false,
29
- desc: "Use Rspack instead of Webpack as the bundler"
36
+ desc: "Use Rspack (default) as the bundler; pass --no-rspack to use Webpack"
37
+
38
+ # --webpack: friendly alias for --no-rspack (reconciled in GeneratorHelper#explicit_bundler_choice).
39
+ # No `default:` here either — same load-bearing reason as --rspack above.
40
+ class_option :webpack,
41
+ type: :boolean,
42
+ desc: "Use Webpack as the bundler (alias for --no-rspack; --no-webpack is equivalent to --rspack)"
30
43
 
31
44
  # --pro
32
45
  class_option :pro,
@@ -96,6 +109,14 @@ module ReactOnRails
96
109
  def shakapacker_version_9_or_higher?
97
110
  generator.__send__(:shakapacker_version_9_or_higher?)
98
111
  end
112
+
113
+ def rsc_plugin_class_name
114
+ generator.__send__(:rsc_plugin_class_name)
115
+ end
116
+
117
+ def rsc_plugin_import_path
118
+ generator.__send__(:rsc_plugin_import_path)
119
+ end
99
120
  end
100
121
 
101
122
  REMOVABLE_WEBPACK_FILES = (MANAGED_WEBPACK_FILE_TEMPLATES.keys +
@@ -313,6 +334,18 @@ module ReactOnRails
313
334
 
314
335
  private
315
336
 
337
+ # Fresh-install context: default to Rspack (when Shakapacker supports it) unless the
338
+ # app already declares a bundler. See GeneratorHelper#fresh_install_rspack_default.
339
+ # NOTE: InstallGenerator#rspack_bundler_default is an intentional twin of this override
340
+ # (both generators are independently CLI-invocable); keep the two in sync.
341
+ def rspack_bundler_default
342
+ fresh_install_rspack_default
343
+ end
344
+
345
+ def generated_build_test_command
346
+ shakapacker_build_command(env: "RAILS_ENV=test NODE_ENV=test", environment: "test")
347
+ end
348
+
316
349
  def generate_new_app_home_page?
317
350
  options.new_app? && new_app_root_route_added?
318
351
  end
@@ -347,8 +380,8 @@ module ReactOnRails
347
380
  end
348
381
 
349
382
  {
350
- app_name: app_name,
351
- docs_url: docs_url,
383
+ app_name:,
384
+ docs_url:,
352
385
  examples: home_page_examples,
353
386
  file_hints: home_page_file_hints,
354
387
  stack_badges: home_page_stack_badges,
@@ -938,8 +971,9 @@ module ReactOnRails
938
971
  # current run omits those options; in that case, we preserve the directory.
939
972
  # Templates rely on config[:message] plus a small helper subset exposed by
940
973
  # TemplateRenderContext (add_documentation_reference, use_pro?, use_rsc?,
941
- # shakapacker_version_9_or_higher?). Missing method delegates raise
942
- # NoMethodError and are caught below, treating the file as non-removable.
974
+ # shakapacker_version_9_or_higher?, rsc_plugin_class_name, rsc_plugin_import_path).
975
+ # Missing method delegates raise NoMethodError and are caught below, treating the
976
+ # file as non-removable.
943
977
  # Missing config hash keys return nil silently, so any new config key
944
978
  # required by templates must be added to template_doc_config above.
945
979
  # Use TemplateRenderContext#erb_binding to avoid leaking method-local
@@ -984,13 +1018,13 @@ module ReactOnRails
984
1018
  expected_configs = shakapacker_default_configs
985
1019
 
986
1020
  # Check if the content matches any of the known default configurations
987
- expected_configs.any? { |config| content_matches_template?(content, config, strip_comments: strip_comments) }
1021
+ expected_configs.any? { |config| content_matches_template?(content, config, strip_comments:) }
988
1022
  end
989
1023
 
990
1024
  def content_matches_template?(content, template, strip_comments: false)
991
1025
  # Normalize whitespace and compare
992
- normalize_config_content(content, strip_comments: strip_comments) ==
993
- normalize_config_content(template, strip_comments: strip_comments)
1026
+ normalize_config_content(content, strip_comments:) ==
1027
+ normalize_config_content(template, strip_comments:)
994
1028
  end
995
1029
 
996
1030
  def normalize_config_content(content, strip_comments: false)
@@ -1196,8 +1230,9 @@ module ReactOnRails
1196
1230
 
1197
1231
  content = File.read(shakapacker_config_path)
1198
1232
 
1199
- # Already has an active (non-commented) precompile_hook configured? Don't overwrite.
1200
- return if content.match?(/^\s+precompile_hook:\s*['"][^'"]+['"]/)
1233
+ # Don't materialize placeholders when any placeholder section already has
1234
+ # a direct or inherited active precompile_hook.
1235
+ return if active_precompile_hook_configured?(content)
1201
1236
 
1202
1237
  # Replace the commented placeholder with the actual value
1203
1238
  # Shakapacker 9.x default config has: # precompile_hook: ~
@@ -5,12 +5,12 @@ module ReactOnRails
5
5
  module DemoPageConfig # rubocop:disable Metrics/ModuleLength
6
6
  def build_hello_world_view_config(component_name:, source_path:, landing_page:, redux:, rsc_demo:)
7
7
  {
8
- component_name: component_name,
8
+ component_name:,
9
9
  title: redux ? "Redux SSR Demo" : "React SSR Demo",
10
- intro: hello_world_intro(redux: redux),
11
- highlights: hello_world_highlights(redux: redux),
12
- file_hints: hello_world_file_hints(source_path: source_path, redux: redux),
13
- quick_links: hello_world_quick_links(landing_page: landing_page, rsc_demo: rsc_demo),
10
+ intro: hello_world_intro(redux:),
11
+ highlights: hello_world_highlights(redux:),
12
+ file_hints: hello_world_file_hints(source_path:, redux:),
13
+ quick_links: hello_world_quick_links(landing_page:, rsc_demo:),
14
14
  learning_links: hello_world_learning_links
15
15
  }
16
16
  end
@@ -22,7 +22,7 @@ module ReactOnRails
22
22
  "component response while only client islands ship JavaScript to the browser.",
23
23
  highlights: hello_server_highlights,
24
24
  file_hints: hello_server_file_hints,
25
- quick_links: hello_server_quick_links(landing_page: landing_page, redux_demo: redux_demo),
25
+ quick_links: hello_server_quick_links(landing_page:, redux_demo:),
26
26
  learning_links: hello_server_learning_links
27
27
  }
28
28
  end
@@ -1,8 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "json"
4
+ require_relative "shakapacker_precompile_hook_helper"
4
5
 
6
+ # rubocop:disable Metrics/ModuleLength
5
7
  module GeneratorHelper
8
+ include ReactOnRails::Generators::ShakapackerPrecompileHookHelper
9
+
6
10
  def package_json
7
11
  # Lazy load package_json gem only when actually needed for dependency management
8
12
 
@@ -37,7 +41,7 @@ module GeneratorHelper
37
41
  result != false
38
42
  rescue StandardError => e
39
43
  say_status :warning, "Could not add packages via package_json gem: #{e.message}", :yellow
40
- say_status :warning, "Will fall back to direct npm commands.", :yellow
44
+ say_status :warning, "Will fall back to direct package manager commands.", :yellow
41
45
  false
42
46
  end
43
47
  end
@@ -128,11 +132,57 @@ module GeneratorHelper
128
132
  def using_rspack?
129
133
  return @using_rspack if defined?(@using_rspack)
130
134
 
131
- # options.key?(:rspack) is true when the generator declares --rspack (e.g. InstallGenerator),
132
- # false when it does not (e.g. RscGenerator, ProGenerator). Using .key? rather than .nil?
133
- # check on the value makes the intent explicit and avoids relying on Thor returning nil for
134
- # undeclared options.
135
- @using_rspack = options.key?(:rspack) ? options[:rspack] : rspack_configured_in_project?
135
+ # An explicit bundler flag always wins. When none was passed (or the generator doesn't
136
+ # declare the flags, e.g. RscGenerator/ProGenerator), fall back to the bundler default,
137
+ # which each generator defines for its own context.
138
+ explicit = explicit_bundler_choice
139
+ @using_rspack = explicit.nil? ? rspack_bundler_default : explicit
140
+ end
141
+
142
+ # Resolve the explicit bundler flags into a single choice.
143
+ #
144
+ # --rspack selects Rspack; --no-rspack and --webpack select Webpack (--webpack is a friendly
145
+ # alias for --no-rspack, and the auto-generated --no-webpack mirrors --rspack). Returns true
146
+ # for Rspack, false for Webpack, or nil when no bundler flag was passed (so the caller falls
147
+ # back to rspack_bundler_default).
148
+ #
149
+ # IMPORTANT: this relies on Thor NOT including a nil-defaulted option in the hash when the flag
150
+ # is absent — options.key?(:rspack)/(:webpack) is true only when the user passed that flag.
151
+ # Re-adding `default:` to either class_option would make the key always present and break both
152
+ # the "no flag given" fallback and the conflict detection here.
153
+ # (Thor's omit-when-no-default behavior verified against Thor 1.5.0; see Gemfile.lock.)
154
+ #
155
+ # Passing contradictory flags (e.g. --rspack --webpack) raises a Thor::Error.
156
+ def explicit_bundler_choice
157
+ choices = []
158
+ choices << options[:rspack] if options.key?(:rspack)
159
+ # --webpack means "use Webpack" (rspack = false); --no-webpack means "use Rspack".
160
+ # Name the inverted webpack flag so the rspack-boolean intent reads directly.
161
+ rspack_via_webpack_flag = !options[:webpack]
162
+ choices << rspack_via_webpack_flag if options.key?(:webpack)
163
+ return nil if choices.empty?
164
+
165
+ if choices.uniq.length > 1
166
+ raise Thor::Error,
167
+ "Conflicting bundler flags: pass either Rspack (--rspack) or Webpack " \
168
+ "(--webpack / --no-rspack), not both."
169
+ end
170
+
171
+ choices.first
172
+ end
173
+
174
+ # True when the user passed any explicit bundler flag
175
+ # (--rspack/--no-rspack/--webpack/--no-webpack).
176
+ def bundler_flag_given?
177
+ options.key?(:rspack) || options.key?(:webpack)
178
+ end
179
+
180
+ # Bundler to use when no explicit bundler flag was passed.
181
+ # Default (standalone generators like RscGenerator/ProGenerator): respect the existing
182
+ # project's shakapacker.yml and never impose a bundler. InstallGenerator/BaseGenerator
183
+ # override this to default fresh installs to Rspack.
184
+ def rspack_bundler_default
185
+ rspack_configured_in_project?
136
186
  end
137
187
 
138
188
  # Remap a config path from config/webpack/ to config/rspack/ when using rspack.
@@ -147,6 +197,26 @@ module GeneratorHelper
147
197
  path.sub(%r{\Aconfig/webpack/}, "config/rspack/")
148
198
  end
149
199
 
200
+ # RSC client-manifest plugin class name for the active bundler.
201
+ # Rspack uses the native `RSCRspackPlugin`; webpack uses `RSCWebpackPlugin`.
202
+ # Both expose the same `{ isServer, clientReferences }` API and emit the same
203
+ # manifest schema, so only the import path and class name differ.
204
+ # Shared by the base webpack-config templates and the standalone RSC migration
205
+ # so both paths scaffold the bundler-correct plugin from one source of truth.
206
+ #
207
+ # @return [String] "RSCRspackPlugin" when rspack, "RSCWebpackPlugin" otherwise
208
+ def rsc_plugin_class_name
209
+ using_rspack? ? "RSCRspackPlugin" : "RSCWebpackPlugin"
210
+ end
211
+
212
+ # `react-on-rails-rsc` subpath that exports {#rsc_plugin_class_name}.
213
+ #
214
+ # @return [String] "react-on-rails-rsc/RspackPlugin" when rspack,
215
+ # "react-on-rails-rsc/WebpackPlugin" otherwise
216
+ def rsc_plugin_import_path
217
+ using_rspack? ? "react-on-rails-rsc/RspackPlugin" : "react-on-rails-rsc/WebpackPlugin"
218
+ end
219
+
150
220
  # Detect the installed React version from package.json
151
221
  # Uses VERSION_PARTS_REGEX pattern from VersionChecker for consistency
152
222
  #
@@ -269,6 +339,24 @@ module GeneratorHelper
269
339
  @pro_gem_install_deferred = true
270
340
  end
271
341
 
342
+ # The other bundler's plugin class name — the one this project should NOT be using.
343
+ # Used to detect a config left in a mixed state (e.g. a legacy `RSCWebpackPlugin` surviving
344
+ # in an rspack project) so diagnostics can say "wrong bundler plugin" rather than "missing".
345
+ #
346
+ # @return [String] "RSCWebpackPlugin" when rspack, "RSCRspackPlugin" otherwise
347
+ def inactive_rsc_plugin_class_name
348
+ using_rspack? ? "RSCWebpackPlugin" : "RSCRspackPlugin"
349
+ end
350
+
351
+ # Import path for the inactive bundler's plugin — the counterpart to {#rsc_plugin_import_path},
352
+ # used when migrating a legacy config to the active bundler's plugin.
353
+ #
354
+ # @return [String] "react-on-rails-rsc/WebpackPlugin" when rspack,
355
+ # "react-on-rails-rsc/RspackPlugin" otherwise
356
+ def inactive_rsc_plugin_import_path
357
+ using_rspack? ? "react-on-rails-rsc/WebpackPlugin" : "react-on-rails-rsc/RspackPlugin"
358
+ end
359
+
272
360
  # NOTE: only the `default:` section is inspected — same assumption as
273
361
  # rspack_configured_in_project?. Projects that set `javascript_transpiler`
274
362
  # only in per-environment sections (without a `default:` block) will not be
@@ -291,25 +379,6 @@ module GeneratorHelper
291
379
  shakapacker_version_9_3_or_higher?
292
380
  end
293
381
 
294
- def parse_shakapacker_yml(path)
295
- require "yaml"
296
- # Use safe_load_file for security (defense-in-depth, even though this is user's own config)
297
- # permitted_classes: [Symbol] allows symbol keys which shakapacker.yml may use
298
- # aliases: true allows YAML anchors (&default, *default) commonly used in Rails configs
299
- YAML.safe_load_file(path, permitted_classes: [Symbol], aliases: true)
300
- rescue ArgumentError
301
- # Older Psych versions don't support all parameters - try without aliases
302
- begin
303
- YAML.safe_load_file(path, permitted_classes: [Symbol])
304
- rescue ArgumentError
305
- # Very old Psych - fall back to safe_load with File.read
306
- YAML.safe_load(File.read(path), permitted_classes: [Symbol]) # rubocop:disable Style/YAMLFileRead
307
- end
308
- rescue StandardError
309
- # If we can't parse the file, return empty config
310
- {}
311
- end
312
-
313
382
  # Check if Shakapacker 9.3.0 or higher is available
314
383
  # This version made SWC the default JavaScript transpiler
315
384
  def shakapacker_version_9_3_or_higher?
@@ -330,10 +399,34 @@ module GeneratorHelper
330
399
  # `assets_bundler` inside the `default: &default` block, and our generator writes
331
400
  # it there too via configure_rspack_in_shakapacker.
332
401
  def rspack_configured_in_project?
402
+ shakapacker_assets_bundler_value == "rspack"
403
+ end
404
+
405
+ # Fresh-install bundler default used by InstallGenerator/BaseGenerator: prefer Rspack
406
+ # when Shakapacker supports it (Rspack landed in Shakapacker 9.0), but never override an
407
+ # existing app's explicit assets_bundler choice. On a brand-new install where Shakapacker
408
+ # isn't loaded yet, shakapacker_version_9_or_higher? optimistically returns true.
409
+ def fresh_install_rspack_default
410
+ return rspack_configured_in_project? if project_declares_assets_bundler?
411
+
412
+ shakapacker_version_9_or_higher?
413
+ end
414
+
415
+ # True when config/shakapacker.yml exists and its default: section declares an
416
+ # assets_bundler (i.e., the project has already made an explicit bundler choice).
417
+ def project_declares_assets_bundler?
418
+ !shakapacker_assets_bundler_value.nil?
419
+ end
420
+
421
+ # Single source for the config/shakapacker.yml default-section read shared by
422
+ # rspack_configured_in_project? and project_declares_assets_bundler?. Returns the
423
+ # assets_bundler value (e.g. "rspack"), or nil when the file is absent or the key is unset.
424
+ # Only the default: section is inspected (see rspack_configured_in_project? for the rationale).
425
+ def shakapacker_assets_bundler_value
333
426
  shakapacker_yml_path = File.join(destination_root, "config/shakapacker.yml")
334
- return false unless File.exist?(shakapacker_yml_path)
427
+ return nil unless File.exist?(shakapacker_yml_path)
335
428
 
336
- config = parse_shakapacker_yml(shakapacker_yml_path)
337
- config.dig("default", "assets_bundler") == "rspack"
429
+ parse_shakapacker_yml(shakapacker_yml_path).dig("default", "assets_bundler")
338
430
  end
339
431
  end
432
+ # rubocop:enable Metrics/ModuleLength
@@ -1,9 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "rainbow"
4
+ require_relative "../shakapacker_precompile_hook_helper"
4
5
 
5
6
  module GeneratorMessages
6
7
  module CiSection
8
+ include ReactOnRails::Generators::ShakapackerPrecompileHookHelper
9
+
7
10
  private
8
11
 
9
12
  def build_ci_section(app_root: Dir.pwd, ci_workflow_generated: false)
@@ -12,7 +15,7 @@ module GeneratorMessages
12
15
  # Read package.json once and reuse for both package-manager detection and the
13
16
  # build:test script presence check to avoid a second I/O pass.
14
17
  package_json = read_package_json(app_root)
15
- package_manager = detect_package_manager(app_root: app_root, package_json: package_json)
18
+ package_manager = detect_package_manager(app_root:, package_json:)
16
19
  ci_status = if ci_workflow_generated
17
20
  "A GitHub Actions workflow has been generated at .github/workflows/ci.yml."
18
21
  else
@@ -25,6 +28,11 @@ module GeneratorMessages
25
28
  else
26
29
  ""
27
30
  end
31
+ manual_build_command = shakapacker_build_command(
32
+ env: "RAILS_ENV=test NODE_ENV=test",
33
+ app_root:,
34
+ environment: "test"
35
+ )
28
36
 
29
37
  <<~CI
30
38
 
@@ -35,7 +43,7 @@ module GeneratorMessages
35
43
  #{ci_status}
36
44
 
37
45
  To build bundles manually before tests:
38
- #{Rainbow('RAILS_ENV=test NODE_ENV=test bin/shakapacker').cyan}#{build_test_hint}
46
+ #{Rainbow(manual_build_command).cyan}#{build_test_hint}
39
47
  CI
40
48
  end
41
49
  end
@@ -37,8 +37,8 @@ module GeneratorMessages
37
37
  # wants detection to fall through directly to lockfile heuristics.
38
38
  def detect_package_manager(app_root: Dir.pwd, package_json: PACKAGE_JSON_UNSET)
39
39
  detect_package_manager_with_source(
40
- app_root: app_root,
41
- package_json: package_json
40
+ app_root:,
41
+ package_json:
42
42
  ).first
43
43
  end
44
44
 
@@ -52,13 +52,13 @@ module GeneratorMessages
52
52
  return [env_package_manager, :env] if supported_package_manager?(env_package_manager)
53
53
 
54
54
  content = package_json_content(
55
- app_root: app_root,
56
- package_json: package_json
55
+ app_root:,
56
+ package_json:
57
57
  )
58
58
  pm_from_json = content ? package_manager_name_from_content(content) : nil
59
59
  return [pm_from_json, :package_json] if pm_from_json
60
60
 
61
- pm_from_lockfile = detect_package_manager_from_lockfiles(app_root: app_root)
61
+ pm_from_lockfile = detect_package_manager_from_lockfiles(app_root:)
62
62
  return [pm_from_lockfile, :lockfile] if pm_from_lockfile
63
63
 
64
64
  ["npm", :default]
@@ -84,8 +84,8 @@ module GeneratorMessages
84
84
  # package_json: nil to preserve a cached missing/unreadable read.
85
85
  def package_manager_declared?(manager:, app_root: Dir.pwd, package_json: PACKAGE_JSON_UNSET)
86
86
  content = package_json_content(
87
- app_root: app_root,
88
- package_json: package_json
87
+ app_root:,
88
+ package_json:
89
89
  )
90
90
  return false unless content
91
91
 
@@ -99,12 +99,12 @@ module GeneratorMessages
99
99
  # that's not on disk (e.g. `packageManager: pnpm` without `pnpm-lock.yaml`, which
100
100
  # breaks `actions/setup-node`'s cache step).
101
101
  def lockfile_for_manager?(package_manager, app_root: Dir.pwd)
102
- !lockfile_filename_for(package_manager, app_root: app_root).nil?
102
+ !lockfile_filename_for(package_manager, app_root:).nil?
103
103
  end
104
104
 
105
105
  def detect_package_manager_from_lockfiles(app_root: Dir.pwd)
106
106
  LOCKFILE_CANDIDATES_BY_MANAGER.keys.find do |pm|
107
- lockfile_for_manager?(pm, app_root: app_root)
107
+ lockfile_for_manager?(pm, app_root:)
108
108
  end
109
109
  end
110
110
 
@@ -7,7 +7,7 @@ module GeneratorMessages
7
7
  private
8
8
 
9
9
  def build_shakapacker_status_section(shakapacker_just_installed: false, app_root: Dir.pwd)
10
- version_warning = check_shakapacker_version_warning(app_root: app_root)
10
+ version_warning = check_shakapacker_version_warning(app_root:)
11
11
  if shakapacker_just_installed
12
12
  base = <<~SHAKAPACKER
13
13
 
@@ -58,13 +58,13 @@ module GeneratorMessages
58
58
  rsc: false, shakapacker_just_installed: false, landing_page: false,
59
59
  ci_workflow_generated: false, app_root: Dir.pwd)
60
60
  process_manager_section = build_process_manager_section
61
- testing_section = build_testing_section(app_root: app_root)
62
- ci_section = build_ci_section(app_root: app_root, ci_workflow_generated: ci_workflow_generated)
63
- package_manager = detect_package_manager(app_root: app_root)
64
- shakapacker_status = build_shakapacker_status_section(shakapacker_just_installed: shakapacker_just_installed,
65
- app_root: app_root)
66
- render_example = build_render_example(component_name: component_name, route: route, rsc: rsc)
67
- render_label = build_render_label(route: route, rsc: rsc)
61
+ testing_section = build_testing_section(app_root:)
62
+ ci_section = build_ci_section(app_root:, ci_workflow_generated:)
63
+ package_manager = detect_package_manager(app_root:)
64
+ shakapacker_status = build_shakapacker_status_section(shakapacker_just_installed:,
65
+ app_root:)
66
+ render_example = build_render_example(component_name:, route:, rsc:)
67
+ render_label = build_render_label(route:, rsc:)
68
68
  normalized_route = route.to_s.sub(%r{\A/+}, "")
69
69
  visit_url = if landing_page || normalized_route.empty?
70
70
  "http://localhost:3000"