react_on_rails 17.0.0.rc.5 → 17.0.0.rc.7

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 (72) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +0 -1
  3. data/Gemfile.development_dependencies +1 -1
  4. data/Gemfile.lock +21 -21
  5. data/Steepfile +2 -4
  6. data/lib/generators/USAGE +0 -6
  7. data/lib/generators/react_on_rails/base_generator.rb +232 -45
  8. data/lib/generators/react_on_rails/demo_page_config.rb +4 -4
  9. data/lib/generators/react_on_rails/generator_helper.rb +242 -0
  10. data/lib/generators/react_on_rails/generator_messages.rb +20 -4
  11. data/lib/generators/react_on_rails/install_generator.rb +122 -32
  12. data/lib/generators/react_on_rails/js_dependency_manager.rb +98 -124
  13. data/lib/generators/react_on_rails/pro_generator.rb +43 -41
  14. data/lib/generators/react_on_rails/react_no_redux_generator.rb +12 -7
  15. data/lib/generators/react_on_rails/react_with_redux_generator.rb +75 -53
  16. data/lib/generators/react_on_rails/rsc/USAGE +4 -4
  17. data/lib/generators/react_on_rails/rsc_generator.rb +21 -1
  18. data/lib/generators/react_on_rails/rsc_setup/client_references.rb +28 -36
  19. data/lib/generators/react_on_rails/rsc_setup/layouts.rb +36 -21
  20. data/lib/generators/react_on_rails/rsc_setup.rb +48 -47
  21. data/lib/generators/react_on_rails/shakapacker_precompile_hook_helper.rb +9 -44
  22. data/lib/generators/react_on_rails/templates/base/base/app/views/hello_world/index.html.erb.tt +10 -1
  23. data/lib/generators/react_on_rails/templates/base/base/app/views/home/index.html.erb.tt +5 -0
  24. data/lib/generators/react_on_rails/templates/base/base/app/views/layouts/react_on_rails_default.html.erb +2 -0
  25. data/lib/generators/react_on_rails/templates/base/base/bin/dev +3 -2
  26. data/lib/generators/react_on_rails/templates/base/base/bin/shakapacker-precompile-hook +84 -2
  27. data/lib/generators/react_on_rails/templates/base/base/config/webpack/development.js.tt +6 -1
  28. data/lib/generators/react_on_rails/templates/base/base/config/webpack/serverWebpackConfig.js.tt +16 -0
  29. data/lib/generators/react_on_rails/templates/base/tailwind/app/javascript/packs/react_on_rails_tailwind.js.tt +1 -0
  30. data/lib/generators/react_on_rails/templates/base/tailwind/app/javascript/src/HelloWorld/ror_components/HelloWorld.client.jsx +0 -1
  31. data/lib/generators/react_on_rails/templates/base/tailwind/app/javascript/src/HelloWorld/ror_components/HelloWorld.client.tsx +0 -1
  32. data/lib/generators/react_on_rails/templates/base/tailwind/app/javascript/stylesheets/application.css.tt +8 -0
  33. data/lib/generators/react_on_rails/templates/base/tailwind/app/views/layouts/react_on_rails_default.html.erb +18 -0
  34. data/lib/generators/react_on_rails/templates/rsc/base/app/views/hello_server/index.html.erb.tt +8 -1
  35. data/lib/generators/react_on_rails/templates/rsc/base/config/webpack/rscWebpackConfig.js.tt +4 -1
  36. data/lib/react_on_rails/configuration.rb +6 -92
  37. data/lib/react_on_rails/controller.rb +3 -9
  38. data/lib/react_on_rails/dev/server_manager.rb +234 -5
  39. data/lib/react_on_rails/doctor.rb +1279 -57
  40. data/lib/react_on_rails/helper.rb +52 -19
  41. data/lib/react_on_rails/length_prefixed_parser.rb +22 -0
  42. data/lib/react_on_rails/locales/base.rb +7 -4
  43. data/lib/react_on_rails/locales/to_js.rb +16 -14
  44. data/lib/react_on_rails/pro_helper.rb +9 -0
  45. data/lib/react_on_rails/react_component/render_options.rb +21 -1
  46. data/lib/react_on_rails/rsc_rspack_support.rb +235 -0
  47. data/lib/react_on_rails/shakapacker_config_helpers.rb +6 -1
  48. data/lib/react_on_rails/typescript_response_types/generated_file_writer.rb +136 -0
  49. data/lib/react_on_rails/typescript_response_types.rb +574 -0
  50. data/lib/react_on_rails/utils.rb +0 -14
  51. data/lib/react_on_rails/version.rb +1 -1
  52. data/lib/react_on_rails/version_checker.rb +180 -1
  53. data/lib/react_on_rails.rb +1 -3
  54. data/lib/tasks/doctor.rake +19 -3
  55. data/lib/tasks/generate_response_types.rake +15 -0
  56. data/rakelib/example_type.rb +7 -2
  57. data/react_on_rails.gemspec +2 -2
  58. data/sig/react_on_rails/configuration.rbs +0 -11
  59. data/sig/react_on_rails/typescript_response_types/generated_file_writer.rbs +10 -0
  60. data/sig/react_on_rails/typescript_response_types.rbs +34 -0
  61. data/sig/react_on_rails/utils.rbs +0 -3
  62. data/sig/react_on_rails.rbs +2 -5
  63. metadata +10 -10
  64. data/lib/generators/react_on_rails/templates/base/tailwind/app/javascript/stylesheets/application.css +0 -1
  65. data/lib/react_on_rails/js_code_builder.rb +0 -66
  66. data/lib/react_on_rails/render_request.rb +0 -74
  67. data/lib/react_on_rails/rendering_strategy/exec_js_strategy.rb +0 -29
  68. data/lib/react_on_rails/rendering_strategy.rb +0 -44
  69. data/sig/react_on_rails/js_code_builder.rbs +0 -11
  70. data/sig/react_on_rails/render_request.rbs +0 -28
  71. data/sig/react_on_rails/rendering_strategy/exec_js_strategy.rbs +0 -11
  72. data/sig/react_on_rails/rendering_strategy.rbs +0 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 483be34de15d43cc71b1f39a384275d9f3af3b90f201c5777785d24e9377cceb
4
- data.tar.gz: 2fa0039ba762f2a0e26882c1729c914c252197f99aedfc498b344a341863657f
3
+ metadata.gz: 9ce16b1a51d89bfe5fbfff93568259f0b49bdde7bed2e8df54a6fead70682112
4
+ data.tar.gz: b4f8f6b423781b9916807dc0153fa469eda85b9ca3764ebcefb4e168a28945a2
5
5
  SHA512:
6
- metadata.gz: 2c8e998f636ef7aa1465d12587c7536150b83ec2a9b72a546610cbdc38cc29237cb9bb27e247637b60cf3b58385cca6d2c0dc06d267c1aa0adfc112e2d987abf
7
- data.tar.gz: c98e20ac4c306db1faaa0eb49d4155f103057b5fc24baa8f57be7ac7c0325bdd1a2ab8e03f43dbefd01572ef9fbfd53a7c372549d44cb2f15b5072cdb81d155d
6
+ metadata.gz: 305cf7a49dffa50dd56c3c67f03ee375ed2900e35ec8538bfb02251faa66a13c41323e3f7915722431cd117f6cafff9860abc46609bcc354a68628a0ecbe8adb
7
+ data.tar.gz: 60f230a7ae4acee7fbe18623e276f48e3e8369179629262c7219a4e066aca0bb87458e92fee061aff74a032c399810ce818816036827f42031e0dfff0be71a1b
data/.rubocop.yml CHANGED
@@ -12,7 +12,6 @@ AllCops:
12
12
  Exclude:
13
13
  - 'spec/dummy/bin/*'
14
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
16
15
 
17
16
  Naming/FileName:
18
17
  Exclude:
@@ -2,7 +2,7 @@
2
2
 
3
3
  eval_gemfile File.expand_path("../Gemfile.shared_dev_dependencies", __dir__)
4
4
 
5
- gem "shakapacker", "10.1.0"
5
+ gem "shakapacker", "10.3.0"
6
6
  gem "bootsnap", require: false
7
7
  gem "rails", "~> 7.1.0"
8
8
 
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.5)
4
+ react_on_rails (17.0.0.rc.7)
5
5
  addressable
6
6
  connection_pool
7
7
  execjs (~> 2.5)
@@ -97,7 +97,7 @@ GEM
97
97
  ast (2.4.2)
98
98
  base64 (0.3.0)
99
99
  benchmark (0.5.0)
100
- bigdecimal (4.0.1)
100
+ bigdecimal (4.1.2)
101
101
  bootsnap (1.18.3)
102
102
  msgpack (~> 1.2)
103
103
  builder (3.3.0)
@@ -114,12 +114,12 @@ GEM
114
114
  capybara-screenshot (1.0.26)
115
115
  capybara (>= 1.0, < 4)
116
116
  launchy
117
- cgi (0.5.1)
117
+ cgi (0.5.2)
118
118
  childprocess (5.0.0)
119
119
  coderay (1.1.3)
120
- concurrent-ruby (1.3.6)
120
+ concurrent-ruby (1.3.7)
121
121
  connection_pool (3.0.2)
122
- crass (1.0.6)
122
+ crass (1.0.7)
123
123
  csv (3.3.5)
124
124
  cypress-on-rails (1.20.0)
125
125
  rack
@@ -132,7 +132,7 @@ GEM
132
132
  drb (2.2.3)
133
133
  equivalent-xml (0.6.0)
134
134
  nokogiri (>= 1.4.3)
135
- erb (6.0.2)
135
+ erb (6.0.4)
136
136
  erubi (1.13.1)
137
137
  execjs (2.9.1)
138
138
  ffi (1.16.3)
@@ -143,11 +143,11 @@ GEM
143
143
  railties (>= 3.0.0)
144
144
  globalid (1.3.0)
145
145
  activesupport (>= 6.1)
146
- i18n (1.14.8)
146
+ i18n (1.15.2)
147
147
  concurrent-ruby (~> 1.0)
148
148
  interception (0.5)
149
149
  io-console (0.8.2)
150
- irb (1.17.0)
150
+ irb (1.18.0)
151
151
  pp (>= 0.6.0)
152
152
  prism (>= 1.3.0)
153
153
  rdoc (>= 4.0.0)
@@ -183,7 +183,7 @@ GEM
183
183
  method_source (1.1.0)
184
184
  mini_mime (1.1.5)
185
185
  mini_portile2 (2.8.9)
186
- minitest (6.0.2)
186
+ minitest (6.0.6)
187
187
  drb (~> 2.0)
188
188
  prism (~> 1.5)
189
189
  msgpack (1.7.2)
@@ -198,19 +198,19 @@ GEM
198
198
  net-smtp (0.5.1)
199
199
  net-protocol
200
200
  nio4r (2.7.5)
201
- nokogiri (1.19.2)
201
+ nokogiri (1.19.4)
202
202
  mini_portile2 (~> 2.8.2)
203
203
  racc (~> 1.4)
204
- nokogiri (1.19.2-arm64-darwin)
204
+ nokogiri (1.19.4-arm64-darwin)
205
205
  racc (~> 1.4)
206
- nokogiri (1.19.2-x86_64-linux-gnu)
206
+ nokogiri (1.19.4-x86_64-linux-gnu)
207
207
  racc (~> 1.4)
208
208
  ostruct (0.6.1)
209
209
  package_json (0.2.0)
210
210
  parser (3.3.1.0)
211
211
  ast (~> 2.4.1)
212
212
  racc
213
- pp (0.6.3)
213
+ pp (0.6.4)
214
214
  prettyprint
215
215
  prettyprint (0.2.0)
216
216
  prism (1.9.0)
@@ -228,17 +228,17 @@ GEM
228
228
  pry-rescue (1.6.0)
229
229
  interception (>= 0.5)
230
230
  pry (>= 0.12.0)
231
- psych (5.3.1)
231
+ psych (5.4.0)
232
232
  date
233
233
  stringio
234
234
  public_suffix (6.0.2)
235
235
  puma (6.6.1)
236
236
  nio4r (~> 2.0)
237
237
  racc (1.8.1)
238
- rack (3.2.5)
239
- rack-proxy (0.8.2)
238
+ rack (3.2.6)
239
+ rack-proxy (0.8.3)
240
240
  rack
241
- rack-session (2.1.1)
241
+ rack-session (2.1.2)
242
242
  base64 (>= 0.1.0)
243
243
  rack (>= 3.0.0)
244
244
  rack-test (2.2.0)
@@ -277,7 +277,7 @@ GEM
277
277
  tsort (>= 0.2)
278
278
  zeitwerk (~> 2.6)
279
279
  rainbow (3.1.1)
280
- rake (13.3.1)
280
+ rake (13.4.2)
281
281
  rb-fsevent (0.11.2)
282
282
  rb-inotify (0.10.1)
283
283
  ffi (~> 1.0)
@@ -331,7 +331,7 @@ GEM
331
331
  rubyzip (>= 1.2.2, < 3.0)
332
332
  websocket (~> 1.0)
333
333
  semantic_range (3.1.1)
334
- shakapacker (10.1.0)
334
+ shakapacker (10.3.0)
335
335
  activesupport (>= 5.2)
336
336
  package_json
337
337
  rack-proxy (>= 0.6.1)
@@ -402,7 +402,7 @@ GEM
402
402
  xpath (3.2.0)
403
403
  nokogiri (~> 1.8)
404
404
  yard (0.9.36)
405
- zeitwerk (2.7.5)
405
+ zeitwerk (2.8.2)
406
406
 
407
407
  PLATFORMS
408
408
  arm64-darwin
@@ -443,7 +443,7 @@ DEPENDENCIES
443
443
  sass-rails (~> 6.0)
444
444
  sdoc
445
445
  selenium-webdriver (= 4.9.0)
446
- shakapacker (= 10.1.0)
446
+ shakapacker (= 10.3.0)
447
447
  simplecov (~> 0.16.1)
448
448
  spring (~> 4.0)
449
449
  sprockets (~> 4.0)
data/Steepfile CHANGED
@@ -36,13 +36,11 @@ target :lib do
36
36
  check "lib/react_on_rails/dev/service_checker.rb"
37
37
  check "lib/react_on_rails/git_utils.rb"
38
38
  check "lib/react_on_rails/helper.rb"
39
- check "lib/react_on_rails/js_code_builder.rb"
40
39
  check "lib/react_on_rails/packer_utils.rb"
41
- check "lib/react_on_rails/render_request.rb"
42
- check "lib/react_on_rails/rendering_strategy.rb"
43
- check "lib/react_on_rails/rendering_strategy/exec_js_strategy.rb"
44
40
  check "lib/react_on_rails/server_rendering_pool.rb"
45
41
  check "lib/react_on_rails/test_helper.rb"
42
+ check "lib/react_on_rails/typescript_response_types.rb"
43
+ check "lib/react_on_rails/typescript_response_types/generated_file_writer.rb"
46
44
  check "lib/react_on_rails/utils.rb"
47
45
  check "lib/react_on_rails/version_checker.rb"
48
46
 
data/lib/generators/USAGE CHANGED
@@ -2,12 +2,6 @@ Description:
2
2
 
3
3
  The react_on_rails:install generator integrates a React frontend, including SSR, with Rails.
4
4
 
5
- * Redux (Optional)
6
-
7
- Passing the --redux generator option causes the generated Hello World example
8
- to integrate the Redux state container framework. The necessary node modules
9
- will be automatically included for you.
10
-
11
5
  * Tailwind CSS v4 (Optional)
12
6
 
13
7
  Passing the --tailwind generator option installs Tailwind CSS v4, configures
@@ -17,12 +17,13 @@ module ReactOnRails
17
17
  Rails::Generators.hide_namespace(namespace)
18
18
  source_root(File.expand_path("templates", __dir__))
19
19
 
20
- # --redux
20
+ # Internal hidden legacy Redux plumbing for install_generator.
21
21
  class_option :redux,
22
22
  type: :boolean,
23
23
  default: false,
24
- desc: "Install Redux package and Redux version of Hello World Example",
25
- aliases: "-R"
24
+ desc: "Internal legacy Redux scaffolding flag",
25
+ aliases: "-R",
26
+ hide: true
26
27
 
27
28
  # --rspack / --no-rspack (Rspack is the default on fresh installs; --no-rspack selects Webpack)
28
29
  # IMPORTANT: do NOT add a `default:` here. The absence of a default is load-bearing — Thor
@@ -133,8 +134,24 @@ module ReactOnRails
133
134
  %w[webpack.config.js]).freeze
134
135
  DOCS_REFERENCE_MESSAGE = "// The source code including full typescript support is available at:"
135
136
  TEMPLATE_RENDER_FAILED = Object.new.freeze # unique sentinel compared by identity via .equal?
137
+ REACT_ON_RAILS_DEFAULT_LAYOUT_PATH = "app/views/layouts/react_on_rails_default.html.erb"
138
+ TAILWIND_LAYOUT_HELPER_LINES = [
139
+ "<!-- React on Rails injects generated packs here. Tailwind is layout-owned. -->",
140
+ '<% prepend_javascript_pack_tag "react_on_rails_tailwind" %>',
141
+ '<%= stylesheet_pack_tag "react_on_rails_tailwind", media: "all" %>',
142
+ "<%= javascript_pack_tag %>"
143
+ ].freeze
144
+ DEFAULT_LAYOUT_EMPTY_PACK_HELPERS_PATTERN = %r{
145
+ (?<indent>^[ \t]*)
146
+ <!--\s*Empty\ pack\ tags\ -\ React\ on\ Rails\ injects\ component\ CSS/JS\ here\s*-->\r?\n
147
+ [ \t]*<%=\s*stylesheet_pack_tag\s*%>\r?\n
148
+ [ \t]*<%=\s*javascript_pack_tag\s*%>
149
+ }x
150
+ CSRF_META_TAG_PATTERN = /^(?<indent>[ \t]*)<%=\s*csrf_meta_tags\s*%>\r?\n/
136
151
  private_constant :MANAGED_WEBPACK_FILE_TEMPLATES, :REMOVABLE_WEBPACK_FILES, :TemplateRenderContext,
137
- :DOCS_REFERENCE_MESSAGE, :TEMPLATE_RENDER_FAILED
152
+ :DOCS_REFERENCE_MESSAGE, :TEMPLATE_RENDER_FAILED, :REACT_ON_RAILS_DEFAULT_LAYOUT_PATH,
153
+ :TAILWIND_LAYOUT_HELPER_LINES, :DEFAULT_LAYOUT_EMPTY_PACK_HELPERS_PATTERN,
154
+ :CSRF_META_TAG_PATTERN
138
155
 
139
156
  def add_root_route
140
157
  return unless options.new_app?
@@ -188,11 +205,46 @@ module ReactOnRails
188
205
  route "get 'hello_world', to: 'hello_world#index'"
189
206
  end
190
207
 
208
+ def copy_packer_config
209
+ # Rails generator actions run in method definition order.
210
+ # Keep this before actions that call shakapacker_source_path or
211
+ # shakapacker_source_entry_path; those helpers memoize on first read.
212
+ if instance_variable_defined?(:@shakapacker_source_path) ||
213
+ instance_variable_defined?(:@shakapacker_source_entry_path)
214
+ raise Thor::Error, "copy_packer_config must run before path-dependent generator actions"
215
+ end
216
+
217
+ base_path = "base/base/"
218
+ config = "config/shakapacker.yml"
219
+ use_rspack = using_rspack?
220
+
221
+ if options.shakapacker_just_installed?
222
+ say "Replacing Shakapacker default config with React on Rails version"
223
+ # Shakapacker's installer just created this file from scratch (no pre-existing config).
224
+ # Safe to overwrite silently with RoR's version-aware template (e.g., private_output_path).
225
+ template("#{base_path}#{config}.tt", config, force: true)
226
+ else
227
+ say "Adding Shakapacker #{ReactOnRails::PackerUtils.shakapacker_version} config"
228
+ # Thor handles the conflict: prompts user interactively, or respects --force/--skip flags.
229
+ template("#{base_path}#{config}.tt", config)
230
+ end
231
+
232
+ # Configure bundler-specific settings
233
+ configure_rspack_in_shakapacker if use_rspack
234
+
235
+ # Always ensure precompile_hook is configured (Shakapacker 9.0+ only)
236
+ configure_precompile_hook_in_shakapacker
237
+
238
+ # For SSR bundles, configure Shakapacker private_output_path (9.0+ only)
239
+ # This keeps Shakapacker and React on Rails server bundle paths in sync.
240
+ configure_private_output_path_in_shakapacker
241
+ end
242
+
191
243
  def create_react_directories
192
244
  # Skip HelloWorld directory for Redux (uses HelloWorldApp) or RSC (uses HelloServer)
193
245
  return if options.redux? || use_rsc?
194
246
 
195
- empty_directory("app/javascript/src/HelloWorld/ror_components")
247
+ empty_directory(File.join(example_component_source_directory("HelloWorld"), "ror_components"))
196
248
  end
197
249
 
198
250
  def copy_base_files
@@ -206,16 +258,14 @@ module ReactOnRails
206
258
  .env.example
207
259
  bin/shakapacker-precompile-hook]
208
260
 
209
- # react_on_rails_default layout provides empty pack tags so React on Rails can
210
- # inject generated packs without requiring a hardcoded application.js pack entry.
211
- base_files << "app/views/layouts/react_on_rails_default.html.erb"
212
-
213
261
  # HelloWorld controller only when not using RSC (RSC uses HelloServer)
214
262
  # Exception: Redux still needs the HelloWorld controller even with RSC
215
263
  base_files << "app/controllers/hello_world_controller.rb" unless use_rsc? && !options.redux?
216
264
  base_files << "app/controllers/home_controller.rb" if generate_new_app_home_page?
217
265
  base_templates = %w[config/initializers/react_on_rails.rb]
218
266
  base_files.each { |file| copy_file("#{base_path}#{file}", file) }
267
+ copy_react_on_rails_default_layout(base_path)
268
+ warn_existing_hello_world_tailwind_layout
219
269
  base_templates.each do |file|
220
270
  template("#{base_path}/#{file}.tt", file)
221
271
  end
@@ -235,14 +285,15 @@ module ReactOnRails
235
285
 
236
286
  def copy_js_bundle_files
237
287
  base_path = "base/base/"
238
- base_files = %w[app/javascript/packs/server-bundle.js]
288
+ copy_file("#{base_path}app/javascript/packs/server-bundle.js",
289
+ shakapacker_entrypoint_path("server-bundle.js"))
239
290
 
240
291
  # Skip HelloWorld CSS for Redux (uses HelloWorldApp) or RSC (uses HelloServer)
241
- unless options.redux? || use_rsc? || use_tailwind?
242
- base_files << "app/javascript/src/HelloWorld/ror_components/HelloWorld.module.css"
243
- end
292
+ return if options.redux? || use_rsc? || use_tailwind?
244
293
 
245
- base_files.each { |file| copy_file("#{base_path}#{file}", file) }
294
+ copy_file("#{base_path}app/javascript/src/HelloWorld/ror_components/HelloWorld.module.css",
295
+ File.join(example_component_source_directory("HelloWorld"),
296
+ "ror_components/HelloWorld.module.css"))
246
297
  end
247
298
 
248
299
  def copy_webpack_config
@@ -273,34 +324,10 @@ module ReactOnRails
273
324
  return unless use_tailwind?
274
325
 
275
326
  base_path = "base/tailwind/"
276
- copy_file("#{base_path}app/javascript/stylesheets/application.css",
277
- "app/javascript/stylesheets/application.css")
278
- end
279
-
280
- def copy_packer_config
281
- base_path = "base/base/"
282
- config = "config/shakapacker.yml"
283
-
284
- if options.shakapacker_just_installed?
285
- say "Replacing Shakapacker default config with React on Rails version"
286
- # Shakapacker's installer just created this file from scratch (no pre-existing config).
287
- # Safe to overwrite silently with RoR's version-aware template (e.g., private_output_path).
288
- template("#{base_path}#{config}.tt", config, force: true)
289
- else
290
- say "Adding Shakapacker #{ReactOnRails::PackerUtils.shakapacker_version} config"
291
- # Thor handles the conflict: prompts user interactively, or respects --force/--skip flags.
292
- template("#{base_path}#{config}.tt", config)
293
- end
294
-
295
- # Configure bundler-specific settings
296
- configure_rspack_in_shakapacker if using_rspack?
297
-
298
- # Always ensure precompile_hook is configured (Shakapacker 9.0+ only)
299
- configure_precompile_hook_in_shakapacker
300
-
301
- # For SSR bundles, configure Shakapacker private_output_path (9.0+ only)
302
- # This keeps Shakapacker and React on Rails server bundle paths in sync.
303
- configure_private_output_path_in_shakapacker
327
+ template("#{base_path}app/javascript/stylesheets/application.css.tt",
328
+ tailwind_stylesheet_path)
329
+ template("#{base_path}app/javascript/packs/react_on_rails_tailwind.js.tt",
330
+ tailwind_pack_path)
304
331
  end
305
332
 
306
333
  def add_base_gems_to_gemfile
@@ -364,6 +391,166 @@ module ReactOnRails
364
391
  shakapacker_build_command(env: "RAILS_ENV=test NODE_ENV=test", environment: "test")
365
392
  end
366
393
 
394
+ def copy_react_on_rails_default_layout(base_path)
395
+ # react_on_rails_default layout provides pack tags so React on Rails can
396
+ # inject generated packs without requiring a hardcoded application.js pack entry.
397
+ if use_tailwind?
398
+ copy_or_update_tailwind_layout
399
+ else
400
+ copy_file("#{base_path}#{REACT_ON_RAILS_DEFAULT_LAYOUT_PATH}", REACT_ON_RAILS_DEFAULT_LAYOUT_PATH)
401
+ end
402
+ end
403
+
404
+ def copy_or_update_tailwind_layout
405
+ tailwind_layout_template = "base/tailwind/#{REACT_ON_RAILS_DEFAULT_LAYOUT_PATH}"
406
+ layout_full_path = File.join(destination_root, REACT_ON_RAILS_DEFAULT_LAYOUT_PATH)
407
+
408
+ unless File.exist?(layout_full_path)
409
+ copy_file(tailwind_layout_template, REACT_ON_RAILS_DEFAULT_LAYOUT_PATH)
410
+ return
411
+ end
412
+
413
+ if options[:force]
414
+ copy_file(tailwind_layout_template, REACT_ON_RAILS_DEFAULT_LAYOUT_PATH, force: true)
415
+ return
416
+ end
417
+
418
+ content = File.read(layout_full_path)
419
+ if layout_links_tailwind_pack?(content)
420
+ say_status :skip, "#{REACT_ON_RAILS_DEFAULT_LAYOUT_PATH} already links #{tailwind_pack_name}", :yellow
421
+ return
422
+ end
423
+
424
+ if options[:skip]
425
+ say_status :skip,
426
+ "#{REACT_ON_RAILS_DEFAULT_LAYOUT_PATH} exists and was not updated (--skip)",
427
+ :yellow
428
+ return
429
+ end
430
+
431
+ layout_match = content.match(DEFAULT_LAYOUT_EMPTY_PACK_HELPERS_PATTERN)
432
+ if options[:pretend]
433
+ pretend_message =
434
+ if layout_match
435
+ "Would update #{REACT_ON_RAILS_DEFAULT_LAYOUT_PATH} to link #{tailwind_pack_name}"
436
+ else
437
+ "#{REACT_ON_RAILS_DEFAULT_LAYOUT_PATH} is customized and would need the Tailwind pack-tag block " \
438
+ "added manually"
439
+ end
440
+
441
+ say_status :pretend, pretend_message, :yellow
442
+ return
443
+ end
444
+
445
+ unless layout_match
446
+ warn_tailwind_layout_manual_step(REACT_ON_RAILS_DEFAULT_LAYOUT_PATH, reason: "layout is customized")
447
+ return
448
+ end
449
+
450
+ gsub_file(REACT_ON_RAILS_DEFAULT_LAYOUT_PATH, DEFAULT_LAYOUT_EMPTY_PACK_HELPERS_PATTERN) do
451
+ tailwind_layout_helper_block(layout_match[:indent])
452
+ end
453
+ ensure_generated_layout_head_tags(REACT_ON_RAILS_DEFAULT_LAYOUT_PATH)
454
+ end
455
+
456
+ def warn_existing_hello_world_tailwind_layout
457
+ return unless use_tailwind?
458
+
459
+ controller_path = "app/controllers/hello_world_controller.rb"
460
+ controller_full_path = File.join(destination_root, controller_path)
461
+ return unless File.exist?(controller_full_path)
462
+
463
+ layout_name = extract_declared_layout_name(File.read(controller_full_path)) || inherited_application_layout_name
464
+ return if layout_name == File.basename(REACT_ON_RAILS_DEFAULT_LAYOUT_PATH, ".html.erb") && !options[:skip]
465
+ return if layout_file_links_tailwind_pack?(layout_name)
466
+
467
+ GeneratorMessages.add_warning(<<~MSG.strip)
468
+ #{controller_path} may not use the Tailwind-aware React on Rails layout.
469
+
470
+ Merge the Tailwind layout pack tags into app/views/layouts/#{layout_name}.html.erb while
471
+ preserving any existing app-specific pack names.
472
+ Keep an existing javascript_pack_tag call if it already renders your app packs; otherwise include the
473
+ empty javascript_pack_tag flush shown here:
474
+ #{tailwind_layout_helper_block(' ')}
475
+ MSG
476
+ end
477
+
478
+ def tailwind_layout_helper_block(indent = "")
479
+ TAILWIND_LAYOUT_HELPER_LINES.map { |line| "#{indent}#{line}" }.join("\n")
480
+ end
481
+
482
+ def ensure_generated_layout_head_tags(layout_path)
483
+ layout_full_path = File.join(destination_root, layout_path)
484
+ content = File.read(layout_full_path)
485
+
486
+ content = ensure_generated_layout_viewport_tag(layout_path, layout_full_path, content)
487
+ ensure_generated_layout_csp_tag(layout_path, content)
488
+ end
489
+
490
+ def ensure_generated_layout_viewport_tag(layout_path, layout_full_path, content)
491
+ unless content.match?(/<meta\b[^>]*\bname=["']viewport["']/)
492
+ title_match = content.match(%r{^(?<indent>[ \t]*)<title>React on Rails</title>\r?\n})
493
+ csrf_match = content.match(CSRF_META_TAG_PATTERN)
494
+ viewport_tag = '<meta name="viewport" content="width=device-width,initial-scale=1">'
495
+
496
+ if title_match
497
+ insert_into_file(
498
+ layout_path,
499
+ %(#{title_match[:indent]}#{viewport_tag}\n),
500
+ after: title_match[0]
501
+ )
502
+ say_status :insert, "Added viewport meta to #{layout_path}", :green
503
+ elsif csrf_match
504
+ insert_into_file(
505
+ layout_path,
506
+ %(#{csrf_match[:indent]}#{viewport_tag}\n),
507
+ before: csrf_match[0]
508
+ )
509
+ say_status :insert, "Added viewport meta to #{layout_path}", :green
510
+ else
511
+ say_status(
512
+ :warning,
513
+ "Could not insert viewport meta into #{layout_path}: no title or csrf_meta_tags anchor found. " \
514
+ "Add #{viewport_tag} manually.",
515
+ :yellow
516
+ )
517
+ end
518
+
519
+ content = File.read(layout_full_path) # Re-read after viewport insertion before checking CSP.
520
+ end
521
+
522
+ content
523
+ end
524
+
525
+ def ensure_generated_layout_csp_tag(layout_path, content)
526
+ csp_tag = "<%= csp_meta_tag %>"
527
+ return if content.match?(/<%=\s*csp_meta_tag\s*%>/)
528
+
529
+ unless (csrf_match = content.match(CSRF_META_TAG_PATTERN))
530
+ say_status(
531
+ :warning,
532
+ "Could not insert csp_meta_tag into #{layout_path}: no csrf_meta_tags anchor found. " \
533
+ "Add #{csp_tag} manually.",
534
+ :yellow
535
+ )
536
+ return
537
+ end
538
+
539
+ insert_into_file(layout_path, %(#{csrf_match[:indent]}#{csp_tag}\n), after: csrf_match[0])
540
+ say_status :insert, "Added csp_meta_tag to #{layout_path}", :green
541
+ end
542
+
543
+ def warn_tailwind_layout_manual_step(layout_path, reason:)
544
+ say_status :warning, "Could not update #{layout_path}: #{reason}.", :yellow
545
+ say <<~MSG, :yellow
546
+ Add the Tailwind layout pack tags in the layout head while preserving any existing app-specific pack tags.
547
+ Keep an existing javascript_pack_tag call if it already renders your app packs; otherwise include the
548
+ empty javascript_pack_tag flush shown here:
549
+
550
+ #{tailwind_layout_helper_block(' ')}
551
+ MSG
552
+ end
553
+
367
554
  def generate_new_app_home_page?
368
555
  options.new_app? && new_app_root_route_added?
369
556
  end
@@ -686,10 +873,10 @@ module ReactOnRails
686
873
  end
687
874
 
688
875
  def example_source_path
689
- return "app/javascript/src/HelloServer/" if use_rsc? && !options.redux?
690
- return "app/javascript/src/HelloWorldApp/" if options.redux?
876
+ return example_component_source_path("HelloServer") if use_rsc? && !options.redux?
877
+ return example_component_source_path("HelloWorldApp") if options.redux?
691
878
 
692
- "app/javascript/src/HelloWorld/"
879
+ example_component_source_path("HelloWorld")
693
880
  end
694
881
 
695
882
  def example_view_path
@@ -15,13 +15,13 @@ module ReactOnRails
15
15
  }
16
16
  end
17
17
 
18
- def build_hello_server_view_config(landing_page:, redux_demo:)
18
+ def build_hello_server_view_config(landing_page:, redux_demo:, source_path:)
19
19
  {
20
20
  title: "React Server Components Demo",
21
21
  intro: "This route shows the Pro React Server Components flow: Rails streams an async server " \
22
22
  "component response while only client islands ship JavaScript to the browser.",
23
23
  highlights: hello_server_highlights,
24
- file_hints: hello_server_file_hints,
24
+ file_hints: hello_server_file_hints(source_path:),
25
25
  quick_links: hello_server_quick_links(landing_page:, redux_demo:),
26
26
  learning_links: hello_server_learning_links
27
27
  }
@@ -167,10 +167,10 @@ module ReactOnRails
167
167
  ]
168
168
  end
169
169
 
170
- def hello_server_file_hints
170
+ def hello_server_file_hints(source_path:)
171
171
  [
172
172
  {
173
- path: "app/javascript/src/HelloServer/",
173
+ path: source_path,
174
174
  description: "Source for the generated server component example and client island."
175
175
  },
176
176
  {