react_on_rails 17.0.0.rc.0 → 17.0.0.rc.1

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 (31) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -0
  3. data/Gemfile.lock +2 -2
  4. data/lib/generators/react_on_rails/base_generator.rb +28 -3
  5. data/lib/generators/react_on_rails/generator_helper.rb +82 -27
  6. data/lib/generators/react_on_rails/generator_messages/ci_section.rb +9 -1
  7. data/lib/generators/react_on_rails/install_generator.rb +48 -21
  8. data/lib/generators/react_on_rails/rsc_setup/client_references.rb +76 -12
  9. data/lib/generators/react_on_rails/rsc_setup.rb +19 -0
  10. data/lib/generators/react_on_rails/shakapacker_precompile_hook_helper.rb +160 -0
  11. data/lib/generators/react_on_rails/templates/base/base/.github/workflows/ci.yml.tt +7 -1
  12. data/lib/generators/react_on_rails/templates/base/base/config/initializers/react_on_rails.rb.tt +1 -1
  13. data/lib/react_on_rails/config_path_resolver.rb +0 -2
  14. data/lib/react_on_rails/dev/server_manager.rb +260 -44
  15. data/lib/react_on_rails/dev/server_mode.rb +211 -0
  16. data/lib/react_on_rails/dev.rb +1 -0
  17. data/lib/react_on_rails/doctor.rb +169 -32
  18. data/lib/react_on_rails/engine.rb +9 -2
  19. data/lib/react_on_rails/length_prefixed_parser.rb +5 -4
  20. data/lib/react_on_rails/packs_generator.rb +24 -8
  21. data/lib/react_on_rails/pro_helper.rb +2 -0
  22. data/lib/react_on_rails/system_checker.rb +48 -16
  23. data/lib/react_on_rails/test_helper/dev_assets_detector.rb +19 -17
  24. data/lib/react_on_rails/test_helper/ensure_assets_compiled.rb +2 -2
  25. data/lib/react_on_rails/test_helper/webpack_assets_compiler.rb +4 -4
  26. data/lib/react_on_rails/test_helper/webpack_assets_status_checker.rb +2 -2
  27. data/lib/react_on_rails/test_helper.rb +6 -6
  28. data/lib/react_on_rails/version.rb +1 -1
  29. data/rakelib/shakapacker_version.rake +4 -1
  30. data/react_on_rails.gemspec +2 -2
  31. metadata +5 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 02f0fa1befbed99b17f624249398f47b5a1395ed5407079a472ad39218d2cfc1
4
- data.tar.gz: f8b4e5a3abf309b98f7b244d1a807acb29096ff7ae1ea115e780eefd35f2d609
3
+ metadata.gz: 0feb399078c3e60a35b5e36bc257cc2711bf793fd04dab39c3ccfcfdd8448cd8
4
+ data.tar.gz: 3848ca13d934d2fda5ca63f8b7660aaac360ac3daec07d7a15433a647d058c44
5
5
  SHA512:
6
- metadata.gz: c594211d78c05817226dd5d4ff06e7bb4f377b57320cdb824de7a8902ec40d66473b28da64e0f6d2df5b445786a050d1c6b61779af3fea0c1c4793f3955efa63
7
- data.tar.gz: e86d35866011de451f725c441539bc7f8400e83d9702a4a3b9058d385e483a926229b4e33e18685636d73700bc7a9a1106246d78174a726327b47b8b0f1645d9
6
+ metadata.gz: 3d2a9e86976672ce1b5ae6b869a9d777bb652b35423cb7b361614db8ccdb710423ad8434115f460ffd7522336a9eb8eec74382bd793f4cd537dfe2d12b69df46
7
+ data.tar.gz: b75324ae9ad146861baebd20716ef293c28509734afbf206da63275db4d29c6a24115a9e2b834781d275a1a586fd9215cdd208c1e93a1101a47d275436cbfda1
data/.rubocop.yml CHANGED
@@ -11,6 +11,7 @@ AllCops:
11
11
 
12
12
  Exclude:
13
13
  - 'spec/dummy/bin/*'
14
+ - 'spike/**/*' # Exploratory spike code outside lib/ — not part of the production surface
14
15
 
15
16
  Naming/FileName:
16
17
  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.1)
5
5
  addressable
6
6
  connection_pool
7
7
  execjs (~> 2.5)
@@ -487,4 +487,4 @@ DEPENDENCIES
487
487
  webdrivers (= 5.3.0)
488
488
 
489
489
  BUNDLED WITH
490
- 2.5.9
490
+ 4.0.10
@@ -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,
@@ -313,6 +326,18 @@ module ReactOnRails
313
326
 
314
327
  private
315
328
 
329
+ # Fresh-install context: default to Rspack (when Shakapacker supports it) unless the
330
+ # app already declares a bundler. See GeneratorHelper#fresh_install_rspack_default.
331
+ # NOTE: InstallGenerator#rspack_bundler_default is an intentional twin of this override
332
+ # (both generators are independently CLI-invocable); keep the two in sync.
333
+ def rspack_bundler_default
334
+ fresh_install_rspack_default
335
+ end
336
+
337
+ def generated_build_test_command
338
+ shakapacker_build_command(env: "RAILS_ENV=test NODE_ENV=test", environment: "test")
339
+ end
340
+
316
341
  def generate_new_app_home_page?
317
342
  options.new_app? && new_app_root_route_added?
318
343
  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
 
@@ -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.
@@ -291,25 +341,6 @@ module GeneratorHelper
291
341
  shakapacker_version_9_3_or_higher?
292
342
  end
293
343
 
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
344
  # Check if Shakapacker 9.3.0 or higher is available
314
345
  # This version made SWC the default JavaScript transpiler
315
346
  def shakapacker_version_9_3_or_higher?
@@ -330,10 +361,34 @@ module GeneratorHelper
330
361
  # `assets_bundler` inside the `default: &default` block, and our generator writes
331
362
  # it there too via configure_rspack_in_shakapacker.
332
363
  def rspack_configured_in_project?
364
+ shakapacker_assets_bundler_value == "rspack"
365
+ end
366
+
367
+ # Fresh-install bundler default used by InstallGenerator/BaseGenerator: prefer Rspack
368
+ # when Shakapacker supports it (Rspack landed in Shakapacker 9.0), but never override an
369
+ # existing app's explicit assets_bundler choice. On a brand-new install where Shakapacker
370
+ # isn't loaded yet, shakapacker_version_9_or_higher? optimistically returns true.
371
+ def fresh_install_rspack_default
372
+ return rspack_configured_in_project? if project_declares_assets_bundler?
373
+
374
+ shakapacker_version_9_or_higher?
375
+ end
376
+
377
+ # True when config/shakapacker.yml exists and its default: section declares an
378
+ # assets_bundler (i.e., the project has already made an explicit bundler choice).
379
+ def project_declares_assets_bundler?
380
+ !shakapacker_assets_bundler_value.nil?
381
+ end
382
+
383
+ # Single source for the config/shakapacker.yml default-section read shared by
384
+ # rspack_configured_in_project? and project_declares_assets_bundler?. Returns the
385
+ # assets_bundler value (e.g. "rspack"), or nil when the file is absent or the key is unset.
386
+ # Only the default: section is inspected (see rspack_configured_in_project? for the rationale).
387
+ def shakapacker_assets_bundler_value
333
388
  shakapacker_yml_path = File.join(destination_root, "config/shakapacker.yml")
334
- return false unless File.exist?(shakapacker_yml_path)
389
+ return nil unless File.exist?(shakapacker_yml_path)
335
390
 
336
- config = parse_shakapacker_yml(shakapacker_yml_path)
337
- config.dig("default", "assets_bundler") == "rspack"
391
+ parse_shakapacker_yml(shakapacker_yml_path).dig("default", "assets_bundler")
338
392
  end
339
393
  end
394
+ # 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)
@@ -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: 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
@@ -9,6 +9,7 @@ require_relative "generator_messages"
9
9
  require_relative "js_dependency_manager"
10
10
  require_relative "pro_setup"
11
11
  require_relative "rsc_setup"
12
+ require_relative "shakapacker_precompile_hook_helper"
12
13
  # Load-path require: git_utils lives under react_on_rails/lib, not relative to this generator directory.
13
14
  require "react_on_rails/git_utils"
14
15
 
@@ -23,6 +24,7 @@ module ReactOnRails
23
24
  include JsDependencyManager
24
25
  include ProSetup
25
26
  include RscSetup
27
+ include ShakapackerPrecompileHookHelper
26
28
 
27
29
  # fetch USAGE file for details generator description
28
30
  source_root(File.expand_path(__dir__))
@@ -41,11 +43,22 @@ module ReactOnRails
41
43
  desc: "Generate TypeScript files and install TypeScript dependencies. Default: false",
42
44
  aliases: "-T"
43
45
 
44
- # --rspack
46
+ # --rspack / --no-rspack (Rspack is the default on fresh installs; --no-rspack selects Webpack)
47
+ # IMPORTANT: do NOT add a `default:` here. The absence of a default is load-bearing — Thor
48
+ # only includes :rspack in the options hash when the flag is explicitly passed, which is how
49
+ # GeneratorHelper#using_rspack? tells an explicit choice from "no flag given" (the latter
50
+ # falls back to rspack_bundler_default). Adding `default: false` would make
51
+ # options.key?(:rspack) always true and silently break the fresh-install Rspack default.
52
+ # (Thor's omit-when-no-default behavior verified against Thor 1.5.0; see Gemfile.lock.)
45
53
  class_option :rspack,
46
54
  type: :boolean,
47
- default: false,
48
- desc: "Use Rspack instead of Webpack as the bundler. Default: false"
55
+ desc: "Use Rspack (default) as the bundler; pass --no-rspack to use Webpack"
56
+
57
+ # --webpack: friendly alias for --no-rspack (reconciled in GeneratorHelper#explicit_bundler_choice).
58
+ # No `default:` here either — same load-bearing reason as --rspack above.
59
+ class_option :webpack,
60
+ type: :boolean,
61
+ desc: "Use Webpack as the bundler (alias for --no-rspack; --no-webpack is equivalent to --rspack)"
49
62
 
50
63
  # --ignore-warnings
51
64
  class_option :ignore_warnings,
@@ -87,7 +100,6 @@ module ReactOnRails
87
100
 
88
101
  # Removed: --skip-shakapacker-install (Shakapacker is now a required dependency)
89
102
 
90
- SHAKAPACKER_YML_PATH = "config/shakapacker.yml"
91
103
  HELLO_WORLD_ROUTE = "hello_world"
92
104
  HELLO_SERVER_ROUTE = "hello_server"
93
105
  # Matches the stock `bin/dev` written by Rails 8.x. Rails 7.1 commonly
@@ -196,6 +208,14 @@ module ReactOnRails
196
208
 
197
209
  private
198
210
 
211
+ # Fresh-install context: default to Rspack (when Shakapacker supports it) unless the
212
+ # app already declares a bundler. See GeneratorHelper#fresh_install_rspack_default.
213
+ # NOTE: BaseGenerator#rspack_bundler_default is an intentional twin of this override
214
+ # (both generators are independently CLI-invocable); keep the two in sync.
215
+ def rspack_bundler_default
216
+ fresh_install_rspack_default
217
+ end
218
+
199
219
  def invoke_generators
200
220
  ensure_shakapacker_installed
201
221
  if options.typescript?
@@ -206,7 +226,7 @@ module ReactOnRails
206
226
  # `invoke` instantiates child generators with a fresh options hash, so
207
227
  # --pretend/--force/--skip must be forwarded explicitly at each boundary.
208
228
  invoke "react_on_rails:base", [],
209
- { typescript: options.typescript?, redux: options.redux?, rspack: options.rspack?,
229
+ { typescript: options.typescript?, redux: options.redux?, rspack: using_rspack?,
210
230
  pro: use_pro?, rsc: use_rsc?, new_app: options.new_app?,
211
231
  shakapacker_just_installed: shakapacker_just_installed?,
212
232
  force: options[:force], skip: options[:skip], pretend: options[:pretend] }
@@ -296,18 +316,19 @@ module ReactOnRails
296
316
  { package_manager: package_manager, has_lockfile: has_lockfile,
297
317
  pnpm_version_declared: pnpm_version_declared,
298
318
  pnpm_fallback_version: CI_PNPM_FALLBACK_VERSION,
299
- has_active_record: has_active_record, has_rspec: has_rspec })
319
+ has_active_record: has_active_record, has_rspec: has_rspec,
320
+ precompile_hook_command: shakapacker_precompile_hook_command(environment: "test") })
300
321
  @ci_workflow_generated = true
301
322
  end
302
323
 
303
- # NODE_ENV=production ensures Shakapacker emits a minified production bundle;
304
- # without it the default is "development" which produces an unminified dev bundle
305
- # and is almost never what `npm run build` is expected to do.
306
- DEFAULT_PACKAGE_JSON_SCRIPTS = {
307
- "build" => "NODE_ENV=production bin/shakapacker",
308
- "build:test" => "RAILS_ENV=test NODE_ENV=test bin/shakapacker"
309
- }.freeze
310
- private_constant :DEFAULT_PACKAGE_JSON_SCRIPTS
324
+ # RAILS_ENV=production runs the hook with production Rails config, while
325
+ # NODE_ENV=production makes Shakapacker emit a minified production bundle.
326
+ def default_package_json_scripts
327
+ {
328
+ "build" => shakapacker_build_command(env: "RAILS_ENV=production NODE_ENV=production"),
329
+ "build:test" => shakapacker_build_command(env: "RAILS_ENV=test NODE_ENV=test", environment: "test")
330
+ }
331
+ end
311
332
 
312
333
  def add_package_json_scripts
313
334
  return if options[:pretend]
@@ -317,7 +338,7 @@ module ReactOnRails
317
338
 
318
339
  original_text = File.read(package_json_path)
319
340
  existing_scripts = JSON.parse(original_text)["scripts"] || {}
320
- scripts_to_add = DEFAULT_PACKAGE_JSON_SCRIPTS.reject { |key, _| existing_scripts.key?(key) }
341
+ scripts_to_add = default_package_json_scripts.reject { |key, _| existing_scripts.key?(key) }
321
342
 
322
343
  if scripts_to_add.empty?
323
344
  say_status :skip, "build scripts already present in package.json", :yellow
@@ -690,7 +711,10 @@ module ReactOnRails
690
711
  flags = []
691
712
  flags << "--redux" if options.redux?
692
713
  flags << "--typescript" if options.typescript?
693
- flags << "--rspack" if options.rspack?
714
+ # Echo the resolved bundler choice (normalized to --rspack/--no-rspack, so a --webpack
715
+ # alias re-runs as --no-rspack) only when the user passed one explicitly. An unset choice
716
+ # re-resolves to the fresh-install default on re-run, so we don't pin it here.
717
+ flags << (using_rspack? ? "--rspack" : "--no-rspack") if bundler_flag_given?
694
718
 
695
719
  if options.rsc?
696
720
  flags << "--rsc"
@@ -838,11 +862,14 @@ module ReactOnRails
838
862
 
839
863
  seed_package_manager_in_package_json_from_lockfile!
840
864
 
841
- # Then run the shakapacker installer
842
- # Use options.rspack? directly (not using_rspack?): shakapacker.yml doesn't exist yet at this
843
- # point, so using_rspack? would fall back to rspack_configured_in_project? which returns false,
844
- # causing Shakapacker to install webpack configs into config/webpack/ instead of rspack.
845
- shakapacker_install_env = options.rspack? ? { "SHAKAPACKER_ASSETS_BUNDLER" => "rspack" } : {}
865
+ # Then run the shakapacker installer.
866
+ # Resolve the bundler via using_rspack?. shakapacker.yml doesn't exist yet at this point,
867
+ # so the fresh-install default applies: an unset --rspack flag resolves to Rspack when
868
+ # Shakapacker supports it (shakapacker_version_9_or_higher? is optimistically true on a
869
+ # brand-new install where Shakapacker isn't loaded yet). An explicit --no-rspack still
870
+ # selects Webpack. using_rspack? memoizes, so the rest of the run (e.g.
871
+ # configure_rspack_in_shakapacker) stays consistent with this decision.
872
+ shakapacker_install_env = using_rspack? ? { "SHAKAPACKER_ASSETS_BUNDLER" => "rspack" } : {}
846
873
  success = Bundler.with_unbundled_env do
847
874
  system(shakapacker_install_env, "bundle exec rails shakapacker:install")
848
875
  end
@@ -6,6 +6,8 @@ module ReactOnRails
6
6
  module ClientReferences # rubocop:disable Metrics/ModuleLength
7
7
  JS_STRING_DELIMITERS = ["'", '"', "`"].freeze
8
8
  JS_COMMENT_STATES = %i[line_comment block_comment].freeze
9
+ # Characters that complete `//` or `/*` after a leading `/`.
10
+ JS_COMMENT_SECOND_CHARS = ["/", "*"].freeze
9
11
  # Known limitation: this list only covers single-character regex preceders. Multi-character
10
12
  # JavaScript keywords that legally precede a regex literal (`return`, `typeof`, `void`,
11
13
  # `delete`, `throw`, `case`, `in`, `instanceof`) are not represented. A regex like `/\{/`
@@ -243,9 +245,9 @@ module ReactOnRails
243
245
  # No parseable `isServer: <bool>` section means this file's plugin call sits outside
244
246
  # what the generator's scanner can match (e.g. options are computed at runtime, or the
245
247
  # plugin is invoked without an options object). Verification callers intentionally
246
- # under-report here: warning about "missing scoped clientReferences" when there's no
247
- # section to inspect would only surface noise for dynamic invocations like
248
- # `RSCWebpackPlugin(buildOptions())`, where the user has nothing actionable to do.
248
+ # keep these out of the missing-pattern list because there is no literal section to
249
+ # classify as broken. They warn separately for dynamic invocations so users can verify
250
+ # those configs manually.
249
251
  return true if sections.empty?
250
252
 
251
253
  sections.all? do |section|
@@ -258,6 +260,30 @@ module ReactOnRails
258
260
  end
259
261
  end
260
262
 
263
+ # Counts active `new RSCWebpackPlugin(...)` calls whose first argument is present but
264
+ # not an object literal. This includes computed options plus static string/template
265
+ # literals; all are outside the verifier's parseable `{...}` contract.
266
+ def non_object_literal_rsc_plugin_invocation_count(content)
267
+ count = 0
268
+ search_from = 0
269
+
270
+ while (match = content.match(RSC_PLUGIN_INVOCATION_REGEX, search_from))
271
+ call_start = match.begin(0)
272
+ after_open_paren = match.end(0)
273
+ unless js_code_position?(content, call_start)
274
+ search_from = after_open_paren
275
+ next
276
+ end
277
+
278
+ options_start = first_js_token_index(content, after_open_paren)
279
+ options_start_char = options_start ? content[options_start] : nil
280
+ count += 1 if options_start_char && options_start_char != "{" && options_start_char != ")"
281
+ search_from = after_open_paren
282
+ end
283
+
284
+ count
285
+ end
286
+
261
287
  def rsc_plugin_defines_client_references?(content, is_server:)
262
288
  rsc_plugin_option_sections(content, is_server: is_server).any? do |section|
263
289
  rsc_plugin_body_has_top_level_key?(section.fetch(:body), "clientReferences")
@@ -411,28 +437,66 @@ module ReactOnRails
411
437
  false
412
438
  end
413
439
 
440
+ # Skips whitespace and JS line/block comments, preserving strings as the next token.
441
+ # Non-object-literal option verification uses this so `new RSCWebpackPlugin("opts")`
442
+ # warns instead of having the string skipped as incidental syntax.
443
+ def first_js_token_index(content, start_index)
444
+ index = start_index
445
+ state = nil
446
+ escaped = false
447
+
448
+ while index < content.length
449
+ char = content[index]
450
+ next_char = content[index + 1]
451
+
452
+ if state
453
+ state, escaped, index = advance_js_scan_state(state, escaped, char, next_char, index)
454
+ index += 1
455
+ next
456
+ end
457
+
458
+ return index unless char.match?(/\s/) || (char == "/" && JS_COMMENT_SECOND_CHARS.include?(next_char))
459
+
460
+ state, escaped, index = advance_js_scan_state(state, escaped, char, next_char, index)
461
+
462
+ index += 1
463
+ end
464
+
465
+ nil
466
+ end
467
+
414
468
  # Skips whitespace, JS line/block comments, and leading string literals so callers see the
415
469
  # next structural character. Without comment skipping, configurations like
416
470
  # `new RSCWebpackPlugin( /* opts */ {` would land on `/` and be rejected as "no plugin
417
471
  # options" even though the options object is present.
418
472
  def first_significant_js_index(content, start_index)
419
- index = start_index
473
+ index = first_js_token_index(content, start_index)
474
+
475
+ while index && JS_STRING_DELIMITERS.include?(content[index])
476
+ string_end = js_string_end_index(content, index)
477
+ return nil unless string_end
478
+
479
+ index = first_js_token_index(content, string_end + 1)
480
+ end
481
+
482
+ index
483
+ end
484
+
485
+ # Scans from a string-opening delimiter at `string_start_index` and returns the index
486
+ # of the matching closing delimiter, not one past it. Returns nil for unterminated
487
+ # strings. Callers must ensure `content[string_start_index]` is in JS_STRING_DELIMITERS.
488
+ def js_string_end_index(content, string_start_index)
420
489
  state = nil
421
490
  escaped = false
491
+ index = string_start_index
422
492
 
423
493
  while index < content.length
424
494
  char = content[index]
425
495
  next_char = content[index + 1]
426
- prev_state = state
496
+ previous_state = state
427
497
  state, escaped, index = advance_js_scan_state(state, escaped, char, next_char, index)
428
- # Exiting a block comment leaves `char` as `*` and `index` pointing at the closing
429
- # `/`; advance past it so the next iteration evaluates the first character after `*/`.
430
- if state || prev_state == :block_comment
431
- index += 1
432
- next
433
- end
434
498
 
435
- return index unless char.match?(/\s/) || JS_STRING_DELIMITERS.include?(prev_state)
499
+ return index if previous_state && state.nil?
436
500
 
437
501
  index += 1
438
502
  end
@@ -538,6 +538,7 @@ module ReactOnRails
538
538
  content = File.read(path)
539
539
  missing = []
540
540
  if content.include?("RSCWebpackPlugin")
541
+ warn_non_object_literal_rsc_plugin_options_for_config(content)
541
542
  unless rsc_plugin_client_references_configured?(content, is_server: true)
542
543
  missing << "generated scoped clientReferences in serverWebpackConfig.js"
543
544
  end
@@ -555,6 +556,7 @@ module ReactOnRails
555
556
  content = File.read(path)
556
557
  missing = []
557
558
  if content.include?("RSCWebpackPlugin")
559
+ warn_non_object_literal_rsc_plugin_options_for_config(content)
558
560
  unless rsc_plugin_client_references_configured?(content, is_server: false)
559
561
  missing << "generated scoped clientReferences in clientWebpackConfig.js"
560
562
  end
@@ -571,6 +573,23 @@ module ReactOnRails
571
573
  content = File.read(File.join(destination_root, scob_path))
572
574
  content.include?("rscWebpackConfig") ? [] : ["rscWebpackConfig in ServerClientOrBoth.js"]
573
575
  end
576
+
577
+ def warn_non_object_literal_rsc_plugin_options_for_config(content)
578
+ return unless non_object_literal_rsc_plugin_invocation_count(content).positive?
579
+
580
+ warn_non_object_literal_rsc_plugin_options_once
581
+ end
582
+
583
+ def warn_non_object_literal_rsc_plugin_options_once
584
+ return if @non_object_literal_rsc_plugin_options_warned
585
+
586
+ @non_object_literal_rsc_plugin_options_warned = true
587
+ GeneratorMessages.add_warning(
588
+ "RSCWebpackPlugin calls use non-object-literal options in one or more webpack configs, " \
589
+ "so the generator cannot verify whether scoped clientReferences are configured. " \
590
+ "Please verify your webpack configs manually."
591
+ )
592
+ end
574
593
  end
575
594
  end
576
595
  end