react_on_rails 16.7.0.rc.3 → 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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -0
  3. data/Gemfile.development_dependencies +7 -3
  4. data/Gemfile.lock +9 -9
  5. data/lib/generators/react_on_rails/base_generator.rb +28 -3
  6. data/lib/generators/react_on_rails/generator_helper.rb +82 -27
  7. data/lib/generators/react_on_rails/generator_messages/ci_section.rb +9 -1
  8. data/lib/generators/react_on_rails/install_generator.rb +48 -21
  9. data/lib/generators/react_on_rails/js_dependency_manager.rb +7 -0
  10. data/lib/generators/react_on_rails/pro_setup.rb +56 -4
  11. data/lib/generators/react_on_rails/rsc_setup/client_references.rb +158 -40
  12. data/lib/generators/react_on_rails/rsc_setup.rb +19 -0
  13. data/lib/generators/react_on_rails/shakapacker_precompile_hook_helper.rb +160 -0
  14. data/lib/generators/react_on_rails/templates/base/base/.github/workflows/ci.yml.tt +7 -1
  15. data/lib/generators/react_on_rails/templates/base/base/config/initializers/react_on_rails.rb.tt +1 -1
  16. data/lib/generators/react_on_rails/templates/pro/base/config/initializers/react_on_rails_pro.rb.tt +6 -1
  17. data/lib/generators/react_on_rails/templates/pro/base/renderer/{node-renderer.js → node-renderer.js.tt} +6 -1
  18. data/lib/react_on_rails/config_path_resolver.rb +0 -2
  19. data/lib/react_on_rails/dev/server_manager.rb +260 -44
  20. data/lib/react_on_rails/dev/server_mode.rb +211 -0
  21. data/lib/react_on_rails/dev.rb +1 -0
  22. data/lib/react_on_rails/doctor.rb +188 -41
  23. data/lib/react_on_rails/engine.rb +9 -1
  24. data/lib/react_on_rails/helper.rb +35 -1
  25. data/lib/react_on_rails/length_prefixed_parser.rb +5 -4
  26. data/lib/react_on_rails/packs_generator.rb +24 -8
  27. data/lib/react_on_rails/prerender_error.rb +14 -6
  28. data/lib/react_on_rails/pro_helper.rb +2 -0
  29. data/lib/react_on_rails/system_checker.rb +48 -16
  30. data/lib/react_on_rails/test_helper/dev_assets_detector.rb +19 -17
  31. data/lib/react_on_rails/test_helper/ensure_assets_compiled.rb +2 -2
  32. data/lib/react_on_rails/test_helper/webpack_assets_compiler.rb +8 -8
  33. data/lib/react_on_rails/test_helper/webpack_assets_status_checker.rb +2 -2
  34. data/lib/react_on_rails/test_helper.rb +6 -6
  35. data/lib/react_on_rails/version.rb +1 -1
  36. data/rakelib/lint.rake +1 -1
  37. data/rakelib/run_rspec.rake +0 -2
  38. data/rakelib/shakapacker_version.rake +4 -1
  39. data/react_on_rails.gemspec +2 -2
  40. metadata +6 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a330ac2b76a6a565ea1f64291f54d2ff5a90b39e33bb7c1929f3caa11ed8358e
4
- data.tar.gz: 4d06374dfcb3c2004138c3386ed326634b03c91a27e96c4d442b0bcd5a9c39c9
3
+ metadata.gz: 0feb399078c3e60a35b5e36bc257cc2711bf793fd04dab39c3ccfcfdd8448cd8
4
+ data.tar.gz: 3848ca13d934d2fda5ca63f8b7660aaac360ac3daec07d7a15433a647d058c44
5
5
  SHA512:
6
- metadata.gz: 55392f01cd6ccad3792eac088c3f59d354131e159d22bc15dc086b17abc75ca248da294269d3e73ee25cea846af246e0d59ab2b93558ab0cf9bd9558e3ef52bf
7
- data.tar.gz: c8fed4f8f74704860bff9a4784906ebcf5bab48cf7a50d9e7240a750d71c4d87dd228c55b3f0bad80edcc0c06565a20f606f8a2f1938ab8b3fa0108c52241bbf
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:
@@ -2,18 +2,22 @@
2
2
 
3
3
  eval_gemfile File.expand_path("../Gemfile.shared_dev_dependencies", __dir__)
4
4
 
5
- gem "shakapacker", "9.6.1"
5
+ gem "shakapacker", "10.1.0"
6
6
  gem "bootsnap", require: false
7
7
  gem "rails", "~> 7.1.0"
8
8
 
9
- gem "sqlite3", "~> 1.6"
9
+ gem "sqlite3", "~> 2.0"
10
10
  gem "sass-rails", "~> 6.0"
11
11
  gem "uglifier"
12
12
  gem "jquery-rails"
13
13
  gem "puma", "~> 6.0"
14
14
 
15
15
  # Turbolinks makes following links in your web application faster. Read more: https://github.com/rails/turbolinks
16
- gem "turbolinks" if ENV["DISABLE_TURBOLINKS"].nil? || ENV["DISABLE_TURBOLINKS"].strip.empty?
16
+ # Declared unconditionally so the gemset always matches Gemfile.lock. DISABLE_TURBOLINKS
17
+ # toggles Turbolinks at the application layer (the require_asset in
18
+ # spec/dummy/app/assets/javascripts/application_non_webpack.js.erb), not gem inclusion.
19
+ # A conditional declaration broke `bundle install --frozen` in CI (see AGENTS.md).
20
+ gem "turbolinks"
17
21
 
18
22
  # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
19
23
  gem "jbuilder"
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- react_on_rails (16.7.0.rc.3)
4
+ react_on_rails (17.0.0.rc.1)
5
5
  addressable
6
6
  connection_pool
7
7
  execjs (~> 2.5)
@@ -237,7 +237,7 @@ GEM
237
237
  nio4r (~> 2.0)
238
238
  racc (1.8.1)
239
239
  rack (3.2.5)
240
- rack-proxy (0.7.7)
240
+ rack-proxy (0.8.2)
241
241
  rack
242
242
  rack-session (2.1.1)
243
243
  base64 (>= 0.1.0)
@@ -360,7 +360,7 @@ GEM
360
360
  rubyzip (>= 1.2.2, < 3.0)
361
361
  websocket (~> 1.0)
362
362
  semantic_range (3.1.1)
363
- shakapacker (9.6.1)
363
+ shakapacker (10.1.0)
364
364
  activesupport (>= 5.2)
365
365
  package_json
366
366
  rack-proxy (>= 0.6.1)
@@ -379,10 +379,10 @@ GEM
379
379
  actionpack (>= 5.2)
380
380
  activesupport (>= 5.2)
381
381
  sprockets (>= 3.0.0)
382
- sqlite3 (1.7.3)
382
+ sqlite3 (2.9.4)
383
383
  mini_portile2 (~> 2.8.0)
384
- sqlite3 (1.7.3-arm64-darwin)
385
- sqlite3 (1.7.3-x86_64-linux)
384
+ sqlite3 (2.9.4-arm64-darwin)
385
+ sqlite3 (2.9.4-x86_64-linux-gnu)
386
386
  steep (1.9.4)
387
387
  activesupport (>= 5.1)
388
388
  concurrent-ruby (>= 1.1.10)
@@ -475,11 +475,11 @@ DEPENDENCIES
475
475
  sass-rails (~> 6.0)
476
476
  sdoc
477
477
  selenium-webdriver (= 4.9.0)
478
- shakapacker (= 9.6.1)
478
+ shakapacker (= 10.1.0)
479
479
  simplecov (~> 0.16.1)
480
480
  spring (~> 4.0)
481
481
  sprockets (~> 4.0)
482
- sqlite3 (~> 1.6)
482
+ sqlite3 (~> 2.0)
483
483
  steep
484
484
  turbo-rails
485
485
  turbolinks
@@ -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
@@ -578,12 +578,19 @@ module ReactOnRails
578
578
  end
579
579
 
580
580
  File.write("package.json", "#{JSON.pretty_generate(content)}\n")
581
+ GeneratorMessages.add_warning(package_json_pin_fallback_warning(versioned_packages))
581
582
  true
582
583
  rescue StandardError => e
583
584
  GeneratorMessages.add_warning("⚠️ Could not write dependency pins to package.json: #{e.message}")
584
585
  false
585
586
  end
586
587
 
588
+ def package_json_pin_fallback_warning(versioned_packages)
589
+ pinned_list = versioned_packages.map { |name, version| "#{name}@#{version}" }.join(", ")
590
+ "⚠️ Package manager install failed. Wrote the following version pins to package.json " \
591
+ "so you can rerun your package manager manually: #{pinned_list}"
592
+ end
593
+
587
594
  def fallback_package_manager
588
595
  package_manager = GeneratorMessages.detect_package_manager(app_root: destination_root)
589
596
  return package_manager if GeneratorMessages.supported_package_manager?(package_manager)
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "securerandom"
3
4
  require_relative "generator_messages"
4
5
  require "react_on_rails/node_renderer_procfile"
5
6
  require "react_on_rails/pro_migration"
@@ -46,16 +47,58 @@ module ReactOnRails
46
47
  say set_color("🚀 REACT ON RAILS PRO SETUP", :cyan, :bold)
47
48
  say set_color("=" * 80, :cyan)
48
49
 
49
- create_pro_initializer
50
+ # The Rails initializer and Node renderer bootstrap must share the same
51
+ # password literal. Only mint a fresh random password when BOTH files will
52
+ # be created — otherwise nil so each template falls back to the env-only
53
+ # branch, avoiding a literal mismatch with any existing file.
54
+ # Always reassign so a stale value from a prior invocation on the same
55
+ # instance can't leak into a later partial-install run.
56
+ @generated_renderer_password = nil
57
+ if pro_initializer_will_be_created? && node_renderer_will_be_created?
58
+ @generated_renderer_password = SecureRandom.hex(32)
59
+ end
60
+
61
+ initializer_created = create_pro_initializer
50
62
  legacy_renderer_detected = create_node_renderer
51
63
  add_pro_to_procfiles unless legacy_renderer_detected
52
64
  update_webpack_config_for_pro
53
65
 
66
+ say_renderer_password_setup_summary(initializer_created)
67
+
54
68
  say set_color("=" * 80, :cyan)
55
69
  say "✅ React on Rails Pro setup complete!", :green
56
70
  say set_color("=" * 80, :cyan)
57
71
  end
58
72
 
73
+ def say_renderer_password_setup_summary(initializer_created)
74
+ if @generated_renderer_password
75
+ say ""
76
+ say set_color("🔐 A random renderer password was written into your config files.", :yellow, :bold)
77
+ say " For production, set RENDERER_PASSWORD as an env var instead and"
78
+ say " remove the literal value from version control."
79
+ say " See: https://www.shakacode.com/react-on-rails/docs/pro/node-renderer/"
80
+ say ""
81
+ elsif initializer_created
82
+ # Initializer was newly created but the Node renderer file already exists;
83
+ # the new initializer falls back to ENV["RENDERER_PASSWORD"] only so it doesn't
84
+ # disagree with whatever literal the existing renderer file contains.
85
+ say ""
86
+ say set_color("⚠️ Existing Node renderer detected — Rails initializer uses " \
87
+ "ENV[\"RENDERER_PASSWORD\"] only.", :yellow, :bold)
88
+ say " Set RENDERER_PASSWORD in your environment to match the password in your existing renderer."
89
+ say ""
90
+ end
91
+ end
92
+
93
+ def pro_initializer_will_be_created?
94
+ !File.exist?(File.join(destination_root, "config/initializers/react_on_rails_pro.rb"))
95
+ end
96
+
97
+ def node_renderer_will_be_created?
98
+ !File.exist?(File.join(destination_root, "renderer/node-renderer.js")) &&
99
+ !File.exist?(File.join(destination_root, "client/node-renderer.js"))
100
+ end
101
+
59
102
  # Check if the Pro gem is missing. When the base react_on_rails gem is in
60
103
  # the Gemfile, installation is deferred to the later Gemfile swap (which
61
104
  # preserves the user's version pin); otherwise auto-install via `bundle
@@ -205,14 +248,18 @@ module ReactOnRails
205
248
 
206
249
  if File.exist?(File.join(destination_root, initializer_path))
207
250
  say "ℹ️ #{initializer_path} already exists, skipping", :yellow
208
- return
251
+ return false
209
252
  end
210
253
 
211
254
  say "📝 Creating React on Rails Pro initializer...", :yellow
212
255
 
256
+ # @generated_renderer_password is set by setup_pro only when both this
257
+ # file and the Node renderer bootstrap will be created together; nil here
258
+ # means the template emits the env-only fallback (no literal password).
213
259
  template("templates/pro/base/config/initializers/react_on_rails_pro.rb.tt", initializer_path)
214
260
 
215
261
  say "✅ Created #{initializer_path}", :green
262
+ true
216
263
  end
217
264
 
218
265
  # Matches active (uncommented) Procfile.dev node-renderer lines that
@@ -267,6 +314,11 @@ module ReactOnRails
267
314
  "to migrate, move it to #{node_renderer_path} and update any references " \
268
315
  "(e.g. Procfile.dev, Procfile.prod, Docker CMD / command):", :yellow
269
316
  say " #{node_renderer_procfile_command('Procfile.dev')}", :yellow
317
+ say set_color(
318
+ "⚠️ Ensure the password in #{legacy_node_renderer_path} matches " \
319
+ "config/initializers/react_on_rails_pro.rb. Both must use the same RENDERER_PASSWORD.",
320
+ :yellow
321
+ )
270
322
  warn_on_stale_legacy_procfile_entry
271
323
  return true
272
324
  end
@@ -275,8 +327,8 @@ module ReactOnRails
275
327
 
276
328
  empty_directory("renderer")
277
329
 
278
- template_path = "templates/pro/base/renderer/node-renderer.js"
279
- copy_file(template_path, node_renderer_path)
330
+ template_path = "templates/pro/base/renderer/node-renderer.js.tt"
331
+ template(template_path, node_renderer_path)
280
332
 
281
333
  say "✅ Created #{node_renderer_path}", :green
282
334
  false