react_on_rails 17.0.0.rc.6 → 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 (64) 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 +187 -10
  8. data/lib/generators/react_on_rails/generator_helper.rb +143 -8
  9. data/lib/generators/react_on_rails/generator_messages.rb +20 -4
  10. data/lib/generators/react_on_rails/install_generator.rb +109 -18
  11. data/lib/generators/react_on_rails/js_dependency_manager.rb +66 -110
  12. data/lib/generators/react_on_rails/pro_generator.rb +43 -41
  13. data/lib/generators/react_on_rails/react_with_redux_generator.rb +64 -43
  14. data/lib/generators/react_on_rails/rsc/USAGE +4 -4
  15. data/lib/generators/react_on_rails/rsc_generator.rb +21 -1
  16. data/lib/generators/react_on_rails/rsc_setup/client_references.rb +28 -36
  17. data/lib/generators/react_on_rails/rsc_setup/layouts.rb +36 -21
  18. data/lib/generators/react_on_rails/rsc_setup.rb +45 -44
  19. data/lib/generators/react_on_rails/shakapacker_precompile_hook_helper.rb +9 -44
  20. data/lib/generators/react_on_rails/templates/base/base/app/views/layouts/react_on_rails_default.html.erb +2 -0
  21. data/lib/generators/react_on_rails/templates/base/base/bin/dev +3 -2
  22. data/lib/generators/react_on_rails/templates/base/base/bin/shakapacker-precompile-hook +84 -2
  23. data/lib/generators/react_on_rails/templates/base/base/config/webpack/development.js.tt +6 -1
  24. data/lib/generators/react_on_rails/templates/base/tailwind/app/javascript/packs/react_on_rails_tailwind.js.tt +1 -0
  25. data/lib/generators/react_on_rails/templates/base/tailwind/app/javascript/src/HelloWorld/ror_components/HelloWorld.client.jsx +0 -1
  26. data/lib/generators/react_on_rails/templates/base/tailwind/app/javascript/src/HelloWorld/ror_components/HelloWorld.client.tsx +0 -1
  27. data/lib/generators/react_on_rails/templates/base/tailwind/app/javascript/stylesheets/application.css.tt +8 -0
  28. data/lib/generators/react_on_rails/templates/base/tailwind/app/views/layouts/react_on_rails_default.html.erb +18 -0
  29. data/lib/react_on_rails/configuration.rb +6 -92
  30. data/lib/react_on_rails/controller.rb +3 -9
  31. data/lib/react_on_rails/dev/server_manager.rb +234 -5
  32. data/lib/react_on_rails/doctor.rb +1279 -57
  33. data/lib/react_on_rails/helper.rb +52 -19
  34. data/lib/react_on_rails/length_prefixed_parser.rb +3 -0
  35. data/lib/react_on_rails/locales/to_js.rb +13 -2
  36. data/lib/react_on_rails/pro_helper.rb +1 -0
  37. data/lib/react_on_rails/react_component/render_options.rb +2 -0
  38. data/lib/react_on_rails/rsc_rspack_support.rb +235 -0
  39. data/lib/react_on_rails/shakapacker_config_helpers.rb +6 -1
  40. data/lib/react_on_rails/typescript_response_types/generated_file_writer.rb +136 -0
  41. data/lib/react_on_rails/typescript_response_types.rb +574 -0
  42. data/lib/react_on_rails/utils.rb +0 -14
  43. data/lib/react_on_rails/version.rb +1 -1
  44. data/lib/react_on_rails/version_checker.rb +180 -1
  45. data/lib/react_on_rails.rb +1 -3
  46. data/lib/tasks/doctor.rake +19 -3
  47. data/lib/tasks/generate_response_types.rake +15 -0
  48. data/rakelib/example_type.rb +7 -2
  49. data/react_on_rails.gemspec +2 -2
  50. data/sig/react_on_rails/configuration.rbs +0 -11
  51. data/sig/react_on_rails/typescript_response_types/generated_file_writer.rbs +10 -0
  52. data/sig/react_on_rails/typescript_response_types.rbs +34 -0
  53. data/sig/react_on_rails/utils.rbs +0 -3
  54. data/sig/react_on_rails.rbs +2 -5
  55. metadata +10 -10
  56. data/lib/generators/react_on_rails/templates/base/tailwind/app/javascript/stylesheets/application.css +0 -1
  57. data/lib/react_on_rails/js_code_builder.rb +0 -66
  58. data/lib/react_on_rails/render_request.rb +0 -74
  59. data/lib/react_on_rails/rendering_strategy/exec_js_strategy.rb +0 -29
  60. data/lib/react_on_rails/rendering_strategy.rb +0 -44
  61. data/sig/react_on_rails/js_code_builder.rbs +0 -11
  62. data/sig/react_on_rails/render_request.rbs +0 -28
  63. data/sig/react_on_rails/rendering_strategy/exec_js_strategy.rbs +0 -11
  64. 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: 60603cec3dbd4b84deedfe50bc3abcee226363d53fb0250e175f4b9af3f88492
4
- data.tar.gz: 2a72fffbf70d1b840aeedfed0f7eafa2e378684e8688bef5c1b0b750749a4654
3
+ metadata.gz: 9ce16b1a51d89bfe5fbfff93568259f0b49bdde7bed2e8df54a6fead70682112
4
+ data.tar.gz: b4f8f6b423781b9916807dc0153fa469eda85b9ca3764ebcefb4e168a28945a2
5
5
  SHA512:
6
- metadata.gz: f0dfddc3d2db936834d1d03f2c69c491359b482d1849d53cb08c6cbc07a052a96a73619ac6deccee5263cb8a72d03770ff9da8217309447330fde064f586a5c6
7
- data.tar.gz: 525a931c6f9999180a3b388cd09f0cafabad9864b64adf5bb3fd824757c8f1c0511be22b6b0214d3d4c79ba9ef0c4ea5302e58378e659b9b478aae53979040c4
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.6)
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?
@@ -241,16 +258,14 @@ module ReactOnRails
241
258
  .env.example
242
259
  bin/shakapacker-precompile-hook]
243
260
 
244
- # react_on_rails_default layout provides empty pack tags so React on Rails can
245
- # inject generated packs without requiring a hardcoded application.js pack entry.
246
- base_files << "app/views/layouts/react_on_rails_default.html.erb"
247
-
248
261
  # HelloWorld controller only when not using RSC (RSC uses HelloServer)
249
262
  # Exception: Redux still needs the HelloWorld controller even with RSC
250
263
  base_files << "app/controllers/hello_world_controller.rb" unless use_rsc? && !options.redux?
251
264
  base_files << "app/controllers/home_controller.rb" if generate_new_app_home_page?
252
265
  base_templates = %w[config/initializers/react_on_rails.rb]
253
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
254
269
  base_templates.each do |file|
255
270
  template("#{base_path}/#{file}.tt", file)
256
271
  end
@@ -309,8 +324,10 @@ module ReactOnRails
309
324
  return unless use_tailwind?
310
325
 
311
326
  base_path = "base/tailwind/"
312
- copy_file("#{base_path}app/javascript/stylesheets/application.css",
313
- shakapacker_stylesheet_path("application.css"))
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)
314
331
  end
315
332
 
316
333
  def add_base_gems_to_gemfile
@@ -374,6 +391,166 @@ module ReactOnRails
374
391
  shakapacker_build_command(env: "RAILS_ENV=test NODE_ENV=test", environment: "test")
375
392
  end
376
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
+
377
554
  def generate_new_app_home_page?
378
555
  options.new_app? && new_app_root_route_added?
379
556
  end
@@ -10,7 +10,28 @@ module GeneratorHelper
10
10
 
11
11
  DEFAULT_SHAKAPACKER_SOURCE_PATH = "app/javascript"
12
12
  DEFAULT_SHAKAPACKER_SOURCE_ENTRY_PATH = "packs"
13
- private_constant :DEFAULT_SHAKAPACKER_SOURCE_PATH, :DEFAULT_SHAKAPACKER_SOURCE_ENTRY_PATH
13
+ TAILWIND_PACK_NAME = "react_on_rails_tailwind"
14
+ TAILWIND_STYLESHEET_NAME = "application.css"
15
+ RAILS_APP_SOURCE_PATH = "app"
16
+ TAILWIND_LAYOUT_PACK_HELPER_BLOCK_PATTERN = /
17
+ ^[ \t]*<%\s*prepend_javascript_pack_tag(?:\s|\()
18
+ \s*["']react_on_rails_tailwind["'][^\n]*%>(?:\r?\n[ \t]*)*
19
+ ^[ \t]*<%=\s*stylesheet_pack_tag(?:\s|\()
20
+ \s*["']react_on_rails_tailwind["'][^\n]*%>(?:\r?\n[ \t]*)*
21
+ ^[ \t]*<%=\s*javascript_pack_tag(?:
22
+ \s*%> |
23
+ \s*\(\s*\)\s*%> |
24
+ \s+(?:\*\*[A-Za-z_]\w*|[a-z_]\w*\s*:)[^\n]*%> |
25
+ \(\s*(?:\*\*[A-Za-z_]\w*|[a-z_]\w*\s*:)[^\n]*\)\s*%>
26
+ )
27
+ /x
28
+ HTML_COMMENT_PATTERN = /<!--[\s\S]*?-->/
29
+ CONTROLLER_LAYOUT_DECLARATION_PATTERN =
30
+ /^\s*layout(?:\s+|\s*\(\s*)(?:"([^"]+)"|'([^']+)')(?=\s*(?:\)|,|#|$))/
31
+ private_constant :DEFAULT_SHAKAPACKER_SOURCE_PATH, :DEFAULT_SHAKAPACKER_SOURCE_ENTRY_PATH,
32
+ :TAILWIND_PACK_NAME, :TAILWIND_STYLESHEET_NAME, :RAILS_APP_SOURCE_PATH,
33
+ :TAILWIND_LAYOUT_PACK_HELPER_BLOCK_PATTERN, :HTML_COMMENT_PATTERN,
34
+ :CONTROLLER_LAYOUT_DECLARATION_PATTERN
14
35
 
15
36
  def package_json
16
37
  # Lazy load package_json gem only when actually needed for dependency management
@@ -107,15 +128,87 @@ module GeneratorHelper
107
128
  File.join(shakapacker_source_path, "stylesheets", filename)
108
129
  end
109
130
 
110
- def relative_stylesheet_import_path(entry_path, filename: "application.css")
111
- # InstallGenerator copies the final Shakapacker config before path-dependent demo files are generated.
112
- safe_entry_path = safe_generator_destination_path(entry_path, default: nil)
113
- raise ArgumentError, "entry_path must stay inside the generator destination" if safe_entry_path.nil?
131
+ def tailwind_pack_name
132
+ TAILWIND_PACK_NAME
133
+ end
134
+
135
+ def tailwind_pack_filename
136
+ "#{tailwind_pack_name}.js"
137
+ end
138
+
139
+ def tailwind_pack_path
140
+ shakapacker_entrypoint_path(tailwind_pack_filename)
141
+ end
142
+
143
+ def tailwind_stylesheet_path
144
+ shakapacker_stylesheet_path(TAILWIND_STYLESHEET_NAME)
145
+ end
146
+
147
+ def relative_tailwind_stylesheet_import_path
148
+ javascript_relative_import_path(tailwind_pack_path, tailwind_stylesheet_path)
149
+ end
150
+
151
+ def tailwind_css_source_directives
152
+ stylesheet_dir = absolute_generator_path(tailwind_stylesheet_path).dirname
153
+ rails_app_source = absolute_generator_path(RAILS_APP_SOURCE_PATH)
154
+ shakapacker_source = absolute_generator_path(shakapacker_source_path)
155
+
156
+ if path_inside_or_equal?(shakapacker_source, rails_app_source)
157
+ rails_app_relative_source = stylesheet_dir_relative_path(stylesheet_dir, rails_app_source)
158
+ tailwind_import_statement(source: rails_app_relative_source)
159
+ else
160
+ sources = [shakapacker_source, rails_app_source].uniq.map do |source_path|
161
+ tailwind_source_statement(stylesheet_dir_relative_path(stylesheet_dir, source_path))
162
+ end
163
+
164
+ [tailwind_import_statement(source: "none"), *sources].join("\n")
165
+ end
166
+ end
167
+
168
+ def layout_links_tailwind_pack?(content)
169
+ comment_ranges = html_comment_ranges(content)
170
+ return content.match?(TAILWIND_LAYOUT_PACK_HELPER_BLOCK_PATTERN) if comment_ranges.empty?
171
+
172
+ content.to_enum(:scan, TAILWIND_LAYOUT_PACK_HELPER_BLOCK_PATTERN).any? do
173
+ helper_match = Regexp.last_match
174
+
175
+ !range_overlaps_any?(helper_match.begin(0)...helper_match.end(0), comment_ranges)
176
+ end
177
+ end
178
+
179
+ def html_comment_ranges(content)
180
+ content.to_enum(:scan, HTML_COMMENT_PATTERN).map do
181
+ comment_match = Regexp.last_match
182
+
183
+ comment_match.begin(0)...comment_match.end(0)
184
+ end
185
+ end
186
+
187
+ def range_overlaps_any?(range, ranges)
188
+ ranges.any? { |candidate| range.begin < candidate.end && candidate.begin < range.end }
189
+ end
190
+
191
+ def extract_declared_layout_name(controller_content)
192
+ match = controller_content.match(CONTROLLER_LAYOUT_DECLARATION_PATTERN)
193
+ match&.captures&.compact&.first
194
+ end
195
+
196
+ def inherited_application_layout_name
197
+ application_controller_path = File.join(destination_root, "app/controllers/application_controller.rb")
198
+ return "application" unless File.exist?(application_controller_path)
199
+
200
+ extract_declared_layout_name(File.read(application_controller_path)) || "application"
201
+ end
202
+
203
+ def layout_destination_path(layout_name)
204
+ "app/views/layouts/#{layout_name}.html.erb"
205
+ end
114
206
 
115
- entry_dir = Pathname.new(File.join(destination_root, safe_entry_path)).dirname
116
- stylesheet = Pathname.new(File.join(destination_root, shakapacker_stylesheet_path(filename)))
207
+ def layout_file_links_tailwind_pack?(layout_name)
208
+ layout_full_path = File.join(destination_root, layout_destination_path(layout_name))
209
+ return false unless File.exist?(layout_full_path)
117
210
 
118
- stylesheet.relative_path_from(entry_dir).to_s
211
+ layout_links_tailwind_pack?(File.read(layout_full_path))
119
212
  end
120
213
 
121
214
  def example_component_source_directory(component_name)
@@ -178,6 +271,48 @@ module GeneratorHelper
178
271
  nil # Signals the caller to fall back to the default path.
179
272
  end
180
273
 
274
+ def absolute_generator_path(relative_path)
275
+ Pathname.new(File.join(destination_root, relative_path)).cleanpath
276
+ end
277
+
278
+ def stylesheet_dir_relative_path(stylesheet_dir, source_path)
279
+ source_path.relative_path_from(stylesheet_dir).to_s
280
+ end
281
+
282
+ def javascript_relative_import_path(from_file, to_file)
283
+ from_dir = absolute_generator_path(from_file).dirname
284
+ relative_path = absolute_generator_path(to_file).relative_path_from(from_dir).to_s
285
+
286
+ relative_path.start_with?(".") ? relative_path : "./#{relative_path}"
287
+ end
288
+
289
+ def path_inside_or_equal?(child_path, parent_path)
290
+ relative_path = child_path.relative_path_from(parent_path).to_s
291
+ relative_path == "." || (relative_path != ".." && !relative_path.start_with?("../"))
292
+ rescue ArgumentError
293
+ false
294
+ end
295
+
296
+ def tailwind_import_statement(source:)
297
+ css_source = source == "none" ? "none" : tailwind_css_string(source)
298
+
299
+ %( @import "tailwindcss" source(#{css_source});).strip
300
+ end
301
+
302
+ def tailwind_source_statement(source)
303
+ %( @source #{tailwind_css_string(source)};).strip
304
+ end
305
+
306
+ def tailwind_css_string(value)
307
+ css_string = value.to_s
308
+ if css_string.match?(/[[:cntrl:]\u2028\u2029]/)
309
+ raise ArgumentError, "Tailwind source paths cannot contain control characters"
310
+ end
311
+
312
+ # JSON quoting keeps CSS punctuation inside the string literal.
313
+ JSON.generate(css_string)
314
+ end
315
+
181
316
  def unsafe_generator_destination_path?(path)
182
317
  path.nil? || path == "." || path == ".." || path.start_with?("../")
183
318
  end
@@ -56,7 +56,7 @@ module GeneratorMessages
56
56
 
57
57
  def helpful_message_after_installation(component_name: "HelloWorld", route: "hello_world", pro: false,
58
58
  rsc: false, shakapacker_just_installed: false, landing_page: false,
59
- ci_workflow_generated: false, app_root: Dir.pwd)
59
+ ci_workflow_generated: false, tailwind: false, app_root: Dir.pwd)
60
60
  process_manager_section = build_process_manager_section
61
61
  testing_section = build_testing_section(app_root:)
62
62
  ci_section = build_ci_section(app_root:, ci_workflow_generated:)
@@ -65,6 +65,7 @@ module GeneratorMessages
65
65
  app_root:)
66
66
  render_example = build_render_example(component_name:, route:, rsc:)
67
67
  render_label = build_render_label(route:, rsc:)
68
+ layout_pack_tags = build_layout_pack_tags(tailwind:)
68
69
  normalized_route = route.to_s.sub(%r{\A/+}, "")
69
70
  visit_url = if landing_page || normalized_route.empty?
70
71
  "http://localhost:3000"
@@ -99,9 +100,7 @@ module GeneratorMessages
99
100
  4. Visit: #{Rainbow(visit_url).cyan.underline}#{landing_page_hint}
100
101
  ✨ KEY FEATURES:
101
102
  ─────────────────────────────────────────────────────────────────────────
102
- • Auto-registration enabled - Your layout only needs:
103
- <%= javascript_pack_tag %>
104
- <%= stylesheet_pack_tag %>
103
+ #{layout_pack_tags}
105
104
 
106
105
  #{render_label}
107
106
  #{render_example}
@@ -117,6 +116,23 @@ module GeneratorMessages
117
116
 
118
117
  private
119
118
 
119
+ def build_layout_pack_tags(tailwind:)
120
+ if tailwind
121
+ <<~MSG.chomp
122
+ • Auto-registration enabled - Tailwind is declared from your layout:
123
+ <% prepend_javascript_pack_tag "react_on_rails_tailwind" %>
124
+ <%= stylesheet_pack_tag "react_on_rails_tailwind", media: "all" %>
125
+ <%= javascript_pack_tag %>
126
+ MSG
127
+ else
128
+ <<~MSG.chomp
129
+ • Auto-registration enabled - Your layout only needs:
130
+ <%= javascript_pack_tag %>
131
+ <%= stylesheet_pack_tag %>
132
+ MSG
133
+ end
134
+ end
135
+
120
136
  def build_render_example(component_name:, route:, rsc:)
121
137
  if rsc
122
138
  "<%= stream_react_component(\"#{component_name}\", props: @#{route}_props) %>"