react_on_rails 17.0.0.rc.1 → 17.0.0.rc.3

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 +1 -0
  3. data/Gemfile.lock +1 -33
  4. data/Rakefile +1 -1
  5. data/lib/generators/USAGE +6 -0
  6. data/lib/generators/react_on_rails/base_generator.rb +39 -11
  7. data/lib/generators/react_on_rails/demo_page_config.rb +6 -6
  8. data/lib/generators/react_on_rails/generator_helper.rb +46 -1
  9. data/lib/generators/react_on_rails/generator_messages/ci_section.rb +2 -2
  10. data/lib/generators/react_on_rails/generator_messages/package_manager_detection.rb +9 -9
  11. data/lib/generators/react_on_rails/generator_messages/shakapacker_status_section.rb +1 -1
  12. data/lib/generators/react_on_rails/generator_messages.rb +7 -7
  13. data/lib/generators/react_on_rails/install_generator.rb +66 -9
  14. data/lib/generators/react_on_rails/js_dependency_manager.rb +197 -64
  15. data/lib/generators/react_on_rails/pro_generator.rb +13 -13
  16. data/lib/generators/react_on_rails/react_no_redux_generator.rb +20 -8
  17. data/lib/generators/react_on_rails/react_with_redux_generator.rb +29 -5
  18. data/lib/generators/react_on_rails/rsc_generator.rb +5 -0
  19. data/lib/generators/react_on_rails/rsc_setup/client_references.rb +514 -89
  20. data/lib/generators/react_on_rails/rsc_setup.rb +82 -29
  21. data/lib/generators/react_on_rails/shakapacker_precompile_hook_helper.rb +338 -22
  22. data/lib/generators/react_on_rails/templates/agent_files/.cursor/rules/react-on-rails.mdc +11 -0
  23. data/lib/generators/react_on_rails/templates/agent_files/.github/copilot-instructions.md +7 -0
  24. data/lib/generators/react_on_rails/templates/agent_files/AGENTS.md +164 -0
  25. data/lib/generators/react_on_rails/templates/agent_files/CLAUDE.md +8 -0
  26. data/lib/generators/react_on_rails/templates/base/base/bin/shakapacker-precompile-hook +88 -0
  27. data/lib/generators/react_on_rails/templates/base/base/config/webpack/clientWebpackConfig.js.tt +2 -2
  28. data/lib/generators/react_on_rails/templates/base/base/config/webpack/commonWebpackConfig.js.tt +81 -1
  29. data/lib/generators/react_on_rails/templates/base/base/config/webpack/serverWebpackConfig.js.tt +5 -5
  30. data/lib/generators/react_on_rails/templates/base/tailwind/app/javascript/src/HelloWorld/ror_components/HelloWorld.client.jsx +30 -0
  31. data/lib/generators/react_on_rails/templates/base/tailwind/app/javascript/src/HelloWorld/ror_components/HelloWorld.client.tsx +34 -0
  32. data/lib/generators/react_on_rails/templates/base/tailwind/app/javascript/stylesheets/application.css +1 -0
  33. data/lib/generators/react_on_rails/templates/dev_tests/spec/rails_helper.rb +1 -1
  34. data/lib/generators/react_on_rails/templates/redux/tailwind/app/javascript/bundles/HelloWorld/components/HelloWorld.jsx +25 -0
  35. data/lib/generators/react_on_rails/templates/redux/tailwind/app/javascript/bundles/HelloWorld/components/HelloWorld.tsx +29 -0
  36. data/lib/generators/react_on_rails/templates/rsc/base/config/webpack/rscWebpackConfig.js.tt +133 -13
  37. data/lib/react_on_rails/controller.rb +2 -2
  38. data/lib/react_on_rails/dev/pack_generator.rb +5 -5
  39. data/lib/react_on_rails/dev/port_selector.rb +4 -4
  40. data/lib/react_on_rails/dev/process_manager.rb +3 -3
  41. data/lib/react_on_rails/dev/server_manager.rb +48 -148
  42. data/lib/react_on_rails/dev/service_checker.rb +1 -1
  43. data/lib/react_on_rails/doctor.rb +200 -128
  44. data/lib/react_on_rails/error.rb +3 -0
  45. data/lib/react_on_rails/font_helper.rb +162 -0
  46. data/lib/react_on_rails/git_utils.rb +3 -3
  47. data/lib/react_on_rails/helper.rb +183 -32
  48. data/lib/react_on_rails/locales/base.rb +2 -2
  49. data/lib/react_on_rails/packer_utils.rb +3 -3
  50. data/lib/react_on_rails/packs_generator.rb +309 -56
  51. data/lib/react_on_rails/prerender_error.rb +5 -5
  52. data/lib/react_on_rails/pro_helper.rb +2 -2
  53. data/lib/react_on_rails/pro_migration.rb +2 -2
  54. data/lib/react_on_rails/react_component/render_options.rb +1 -1
  55. data/lib/react_on_rails/server_rendering_pool/ruby_embedded_java_script.rb +200 -14
  56. data/lib/react_on_rails/shakapacker_config_helpers.rb +139 -0
  57. data/lib/react_on_rails/smart_error.rb +135 -5
  58. data/lib/react_on_rails/system_checker.rb +4 -53
  59. data/lib/react_on_rails/test_helper/dev_assets_detector.rb +4 -4
  60. data/lib/react_on_rails/test_helper.rb +5 -5
  61. data/lib/react_on_rails/version.rb +1 -1
  62. data/lib/react_on_rails/version_synchronizer.rb +22 -22
  63. data/lib/tasks/doctor.rake +5 -2
  64. data/lib/tasks/locale.rake +1 -1
  65. data/lib/tasks/sync_versions.rake +1 -1
  66. data/rakelib/lint.rake +15 -2
  67. data/rakelib/run_rspec.rake +1 -1
  68. data/rakelib/task_helpers.rb +1 -0
  69. data/rakelib/update_changelog.rake +2 -2
  70. data/sig/react_on_rails/helper.rbs +3 -0
  71. data/sig/react_on_rails/smart_error.rbs +25 -2
  72. metadata +14 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0feb399078c3e60a35b5e36bc257cc2711bf793fd04dab39c3ccfcfdd8448cd8
4
- data.tar.gz: 3848ca13d934d2fda5ca63f8b7660aaac360ac3daec07d7a15433a647d058c44
3
+ metadata.gz: 0630c2730fc7389d31e1f99d88e8df8dc6eb18b670e2909afd25e1e71b0c50ab
4
+ data.tar.gz: 6db462a802556e23085ae038585dd7c310c7710fd41a0a336d350383e85b9333
5
5
  SHA512:
6
- metadata.gz: 3d2a9e86976672ce1b5ae6b869a9d777bb652b35423cb7b361614db8ccdb710423ad8434115f460ffd7522336a9eb8eec74382bd793f4cd537dfe2d12b69df46
7
- data.tar.gz: b75324ae9ad146861baebd20716ef293c28509734afbf206da63275db4d29c6a24115a9e2b834781d275a1a586fd9215cdd208c1e93a1101a47d275436cbfda1
6
+ metadata.gz: 395e61f25e352b69df2788554dd0fcba85834df3e2f42bd768b6c2a81c8579e2d2504da1a47417abcc8ee5369181260c54f828e206f36dba409cbeef5a5818a9
7
+ data.tar.gz: 5ee8406c0ce296a4dca6e38299c1274fd2e5b6dde7f62f83f9790122dfc44cf88c0ca2cc093a9da7089265e53ed08ded28a018efc1929828c3a3ced000ba41c4
data/.rubocop.yml CHANGED
@@ -11,6 +11,7 @@ AllCops:
11
11
 
12
12
  Exclude:
13
13
  - 'spec/dummy/bin/*'
14
+ - 'spec/react_on_rails/dummy-for-generators/**/*' # Generated fixture contains intentionally invalid Ruby
14
15
  - 'spike/**/*' # Exploratory spike code outside lib/ — not part of the production surface
15
16
 
16
17
  Naming/FileName:
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.1)
4
+ react_on_rails (17.0.0.rc.3)
5
5
  addressable
6
6
  connection_pool
7
7
  execjs (~> 2.5)
@@ -207,7 +207,6 @@ GEM
207
207
  racc (~> 1.4)
208
208
  ostruct (0.6.1)
209
209
  package_json (0.2.0)
210
- parallel (1.24.0)
211
210
  parser (3.3.1.0)
212
211
  ast (~> 2.4.1)
213
212
  racc
@@ -313,34 +312,6 @@ GEM
313
312
  rspec-support (3.13.1)
314
313
  rspec_junit_formatter (0.6.0)
315
314
  rspec-core (>= 2, < 4, != 2.12.0)
316
- rubocop (1.61.0)
317
- json (~> 2.3)
318
- language_server-protocol (>= 3.17.0)
319
- parallel (~> 1.10)
320
- parser (>= 3.3.0.2)
321
- rainbow (>= 2.2.2, < 4.0)
322
- regexp_parser (>= 1.8, < 3.0)
323
- rexml (>= 3.2.5, < 4.0)
324
- rubocop-ast (>= 1.30.0, < 2.0)
325
- ruby-progressbar (~> 1.7)
326
- unicode-display_width (>= 2.4.0, < 3.0)
327
- rubocop-ast (1.31.3)
328
- parser (>= 3.3.1.0)
329
- rubocop-capybara (2.20.0)
330
- rubocop (~> 1.41)
331
- rubocop-factory_bot (2.25.1)
332
- rubocop (~> 1.41)
333
- rubocop-performance (1.20.2)
334
- rubocop (>= 1.48.1, < 2.0)
335
- rubocop-ast (>= 1.30.0, < 2.0)
336
- rubocop-rspec (2.29.2)
337
- rubocop (~> 1.40)
338
- rubocop-capybara (~> 2.17)
339
- rubocop-factory_bot (~> 2.22)
340
- rubocop-rspec_rails (~> 2.28)
341
- rubocop-rspec_rails (2.28.3)
342
- rubocop (~> 1.40)
343
- ruby-progressbar (1.13.0)
344
315
  rubyzip (2.3.2)
345
316
  sass-rails (6.0.0)
346
317
  sassc-rails (~> 2.1, >= 2.1.1)
@@ -469,9 +440,6 @@ DEPENDENCIES
469
440
  rspec-rails
470
441
  rspec-retry
471
442
  rspec_junit_formatter
472
- rubocop (= 1.61.0)
473
- rubocop-performance (~> 1.20.0)
474
- rubocop-rspec (~> 2.26)
475
443
  sass-rails (~> 6.0)
476
444
  sdoc
477
445
  selenium-webdriver (= 4.9.0)
data/Rakefile CHANGED
@@ -13,7 +13,7 @@ desc "All actions but no examples, good for local developer run."
13
13
  task all_but_examples: ["run_rspec:all_but_examples", "lint"]
14
14
 
15
15
  desc "Prepare for ci, including node_package, dummy app, and generator examples"
16
- task prepare_for_ci: prepare_for_ci
16
+ task(prepare_for_ci:)
17
17
 
18
18
  desc "Runs prepare_for_ci and tasks"
19
19
  task ci: [:prepare_for_ci, *tasks]
data/lib/generators/USAGE CHANGED
@@ -8,6 +8,12 @@ The react_on_rails:install generator integrates a React frontend, including SSR,
8
8
  to integrate the Redux state container framework. The necessary node modules
9
9
  will be automatically included for you.
10
10
 
11
+ * Tailwind CSS v4 (Optional)
12
+
13
+ Passing the --tailwind generator option installs Tailwind CSS v4, configures
14
+ the PostCSS plugin for Webpack or Rspack, and styles the generated
15
+ server-rendered Hello World example.
16
+
11
17
  *******************************************************************************
12
18
 
13
19
  After running the generator, you will want to:
@@ -41,6 +41,12 @@ module ReactOnRails
41
41
  type: :boolean,
42
42
  desc: "Use Webpack as the bundler (alias for --no-rspack; --no-webpack is equivalent to --rspack)"
43
43
 
44
+ # --tailwind
45
+ class_option :tailwind,
46
+ type: :boolean,
47
+ default: false,
48
+ desc: "Install Tailwind CSS v4 and style the generated SSR example"
49
+
44
50
  # --pro
45
51
  class_option :pro,
46
52
  type: :boolean,
@@ -106,9 +112,21 @@ module ReactOnRails
106
112
  generator.__send__(:use_rsc?)
107
113
  end
108
114
 
115
+ def use_tailwind?
116
+ generator.__send__(:use_tailwind?)
117
+ end
118
+
109
119
  def shakapacker_version_9_or_higher?
110
120
  generator.__send__(:shakapacker_version_9_or_higher?)
111
121
  end
122
+
123
+ def rsc_plugin_class_name
124
+ generator.__send__(:rsc_plugin_class_name)
125
+ end
126
+
127
+ def rsc_plugin_import_path
128
+ generator.__send__(:rsc_plugin_import_path)
129
+ end
112
130
  end
113
131
 
114
132
  REMOVABLE_WEBPACK_FILES = (MANAGED_WEBPACK_FILE_TEMPLATES.keys +
@@ -220,7 +238,7 @@ module ReactOnRails
220
238
  base_files = %w[app/javascript/packs/server-bundle.js]
221
239
 
222
240
  # Skip HelloWorld CSS for Redux (uses HelloWorldApp) or RSC (uses HelloServer)
223
- unless options.redux? || use_rsc?
241
+ unless options.redux? || use_rsc? || use_tailwind?
224
242
  base_files << "app/javascript/src/HelloWorld/ror_components/HelloWorld.module.css"
225
243
  end
226
244
 
@@ -251,6 +269,14 @@ module ReactOnRails
251
269
  copy_webpack_main_config(base_path, config)
252
270
  end
253
271
 
272
+ def copy_tailwind_files
273
+ return unless use_tailwind?
274
+
275
+ base_path = "base/tailwind/"
276
+ copy_file("#{base_path}app/javascript/stylesheets/application.css",
277
+ "app/javascript/stylesheets/application.css")
278
+ end
279
+
254
280
  def copy_packer_config
255
281
  base_path = "base/base/"
256
282
  config = "config/shakapacker.yml"
@@ -372,8 +398,8 @@ module ReactOnRails
372
398
  end
373
399
 
374
400
  {
375
- app_name: app_name,
376
- docs_url: docs_url,
401
+ app_name:,
402
+ docs_url:,
377
403
  examples: home_page_examples,
378
404
  file_hints: home_page_file_hints,
379
405
  stack_badges: home_page_stack_badges,
@@ -962,9 +988,10 @@ module ReactOnRails
962
988
  # Note: files originally generated with --pro or --rsc will not match when the
963
989
  # current run omits those options; in that case, we preserve the directory.
964
990
  # Templates rely on config[:message] plus a small helper subset exposed by
965
- # TemplateRenderContext (add_documentation_reference, use_pro?, use_rsc?,
966
- # shakapacker_version_9_or_higher?). Missing method delegates raise
967
- # NoMethodError and are caught below, treating the file as non-removable.
991
+ # TemplateRenderContext (add_documentation_reference, use_pro?, use_rsc?, use_tailwind?,
992
+ # shakapacker_version_9_or_higher?, rsc_plugin_class_name, rsc_plugin_import_path).
993
+ # Missing method delegates raise NoMethodError and are caught below, treating the
994
+ # file as non-removable.
968
995
  # Missing config hash keys return nil silently, so any new config key
969
996
  # required by templates must be added to template_doc_config above.
970
997
  # Use TemplateRenderContext#erb_binding to avoid leaking method-local
@@ -1009,13 +1036,13 @@ module ReactOnRails
1009
1036
  expected_configs = shakapacker_default_configs
1010
1037
 
1011
1038
  # Check if the content matches any of the known default configurations
1012
- expected_configs.any? { |config| content_matches_template?(content, config, strip_comments: strip_comments) }
1039
+ expected_configs.any? { |config| content_matches_template?(content, config, strip_comments:) }
1013
1040
  end
1014
1041
 
1015
1042
  def content_matches_template?(content, template, strip_comments: false)
1016
1043
  # Normalize whitespace and compare
1017
- normalize_config_content(content, strip_comments: strip_comments) ==
1018
- normalize_config_content(template, strip_comments: strip_comments)
1044
+ normalize_config_content(content, strip_comments:) ==
1045
+ normalize_config_content(template, strip_comments:)
1019
1046
  end
1020
1047
 
1021
1048
  def normalize_config_content(content, strip_comments: false)
@@ -1221,8 +1248,9 @@ module ReactOnRails
1221
1248
 
1222
1249
  content = File.read(shakapacker_config_path)
1223
1250
 
1224
- # Already has an active (non-commented) precompile_hook configured? Don't overwrite.
1225
- return if content.match?(/^\s+precompile_hook:\s*['"][^'"]+['"]/)
1251
+ # Don't materialize placeholders when any placeholder section already has
1252
+ # a direct or inherited active precompile_hook.
1253
+ return if active_precompile_hook_configured?(content)
1226
1254
 
1227
1255
  # Replace the commented placeholder with the actual value
1228
1256
  # Shakapacker 9.x default config has: # precompile_hook: ~
@@ -5,12 +5,12 @@ module ReactOnRails
5
5
  module DemoPageConfig # rubocop:disable Metrics/ModuleLength
6
6
  def build_hello_world_view_config(component_name:, source_path:, landing_page:, redux:, rsc_demo:)
7
7
  {
8
- component_name: component_name,
8
+ component_name:,
9
9
  title: redux ? "Redux SSR Demo" : "React SSR Demo",
10
- intro: hello_world_intro(redux: redux),
11
- highlights: hello_world_highlights(redux: redux),
12
- file_hints: hello_world_file_hints(source_path: source_path, redux: redux),
13
- quick_links: hello_world_quick_links(landing_page: landing_page, rsc_demo: rsc_demo),
10
+ intro: hello_world_intro(redux:),
11
+ highlights: hello_world_highlights(redux:),
12
+ file_hints: hello_world_file_hints(source_path:, redux:),
13
+ quick_links: hello_world_quick_links(landing_page:, rsc_demo:),
14
14
  learning_links: hello_world_learning_links
15
15
  }
16
16
  end
@@ -22,7 +22,7 @@ module ReactOnRails
22
22
  "component response while only client islands ship JavaScript to the browser.",
23
23
  highlights: hello_server_highlights,
24
24
  file_hints: hello_server_file_hints,
25
- quick_links: hello_server_quick_links(landing_page: landing_page, redux_demo: redux_demo),
25
+ quick_links: hello_server_quick_links(landing_page:, redux_demo:),
26
26
  learning_links: hello_server_learning_links
27
27
  }
28
28
  end
@@ -41,7 +41,7 @@ module GeneratorHelper
41
41
  result != false
42
42
  rescue StandardError => e
43
43
  say_status :warning, "Could not add packages via package_json gem: #{e.message}", :yellow
44
- say_status :warning, "Will fall back to direct npm commands.", :yellow
44
+ say_status :warning, "Will fall back to direct package manager commands.", :yellow
45
45
  false
46
46
  end
47
47
  end
@@ -121,6 +121,13 @@ module GeneratorHelper
121
121
  options[:rsc]
122
122
  end
123
123
 
124
+ # Check if Tailwind CSS should be installed and wired into the generated example.
125
+ #
126
+ # @return [Boolean] true if --tailwind is set
127
+ def use_tailwind?
128
+ options[:tailwind]
129
+ end
130
+
124
131
  # Determine if the project is using rspack as the bundler.
125
132
  #
126
133
  # Detection priority:
@@ -197,6 +204,26 @@ module GeneratorHelper
197
204
  path.sub(%r{\Aconfig/webpack/}, "config/rspack/")
198
205
  end
199
206
 
207
+ # RSC client-manifest plugin class name for the active bundler.
208
+ # Rspack uses the native `RSCRspackPlugin`; webpack uses `RSCWebpackPlugin`.
209
+ # Both expose the same `{ isServer, clientReferences }` API and emit the same
210
+ # manifest schema, so only the import path and class name differ.
211
+ # Shared by the base webpack-config templates and the standalone RSC migration
212
+ # so both paths scaffold the bundler-correct plugin from one source of truth.
213
+ #
214
+ # @return [String] "RSCRspackPlugin" when rspack, "RSCWebpackPlugin" otherwise
215
+ def rsc_plugin_class_name
216
+ using_rspack? ? "RSCRspackPlugin" : "RSCWebpackPlugin"
217
+ end
218
+
219
+ # `react-on-rails-rsc` subpath that exports {#rsc_plugin_class_name}.
220
+ #
221
+ # @return [String] "react-on-rails-rsc/RspackPlugin" when rspack,
222
+ # "react-on-rails-rsc/WebpackPlugin" otherwise
223
+ def rsc_plugin_import_path
224
+ using_rspack? ? "react-on-rails-rsc/RspackPlugin" : "react-on-rails-rsc/WebpackPlugin"
225
+ end
226
+
200
227
  # Detect the installed React version from package.json
201
228
  # Uses VERSION_PARTS_REGEX pattern from VersionChecker for consistency
202
229
  #
@@ -319,6 +346,24 @@ module GeneratorHelper
319
346
  @pro_gem_install_deferred = true
320
347
  end
321
348
 
349
+ # The other bundler's plugin class name — the one this project should NOT be using.
350
+ # Used to detect a config left in a mixed state (e.g. a legacy `RSCWebpackPlugin` surviving
351
+ # in an rspack project) so diagnostics can say "wrong bundler plugin" rather than "missing".
352
+ #
353
+ # @return [String] "RSCWebpackPlugin" when rspack, "RSCRspackPlugin" otherwise
354
+ def inactive_rsc_plugin_class_name
355
+ using_rspack? ? "RSCWebpackPlugin" : "RSCRspackPlugin"
356
+ end
357
+
358
+ # Import path for the inactive bundler's plugin — the counterpart to {#rsc_plugin_import_path},
359
+ # used when migrating a legacy config to the active bundler's plugin.
360
+ #
361
+ # @return [String] "react-on-rails-rsc/WebpackPlugin" when rspack,
362
+ # "react-on-rails-rsc/RspackPlugin" otherwise
363
+ def inactive_rsc_plugin_import_path
364
+ using_rspack? ? "react-on-rails-rsc/WebpackPlugin" : "react-on-rails-rsc/RspackPlugin"
365
+ end
366
+
322
367
  # NOTE: only the `default:` section is inspected — same assumption as
323
368
  # rspack_configured_in_project?. Projects that set `javascript_transpiler`
324
369
  # only in per-environment sections (without a `default:` block) will not be
@@ -15,7 +15,7 @@ module GeneratorMessages
15
15
  # Read package.json once and reuse for both package-manager detection and the
16
16
  # build:test script presence check to avoid a second I/O pass.
17
17
  package_json = read_package_json(app_root)
18
- package_manager = detect_package_manager(app_root: app_root, package_json: package_json)
18
+ package_manager = detect_package_manager(app_root:, package_json:)
19
19
  ci_status = if ci_workflow_generated
20
20
  "A GitHub Actions workflow has been generated at .github/workflows/ci.yml."
21
21
  else
@@ -30,7 +30,7 @@ module GeneratorMessages
30
30
  end
31
31
  manual_build_command = shakapacker_build_command(
32
32
  env: "RAILS_ENV=test NODE_ENV=test",
33
- app_root: app_root,
33
+ app_root:,
34
34
  environment: "test"
35
35
  )
36
36
 
@@ -37,8 +37,8 @@ module GeneratorMessages
37
37
  # wants detection to fall through directly to lockfile heuristics.
38
38
  def detect_package_manager(app_root: Dir.pwd, package_json: PACKAGE_JSON_UNSET)
39
39
  detect_package_manager_with_source(
40
- app_root: app_root,
41
- package_json: package_json
40
+ app_root:,
41
+ package_json:
42
42
  ).first
43
43
  end
44
44
 
@@ -52,13 +52,13 @@ module GeneratorMessages
52
52
  return [env_package_manager, :env] if supported_package_manager?(env_package_manager)
53
53
 
54
54
  content = package_json_content(
55
- app_root: app_root,
56
- package_json: package_json
55
+ app_root:,
56
+ package_json:
57
57
  )
58
58
  pm_from_json = content ? package_manager_name_from_content(content) : nil
59
59
  return [pm_from_json, :package_json] if pm_from_json
60
60
 
61
- pm_from_lockfile = detect_package_manager_from_lockfiles(app_root: app_root)
61
+ pm_from_lockfile = detect_package_manager_from_lockfiles(app_root:)
62
62
  return [pm_from_lockfile, :lockfile] if pm_from_lockfile
63
63
 
64
64
  ["npm", :default]
@@ -84,8 +84,8 @@ module GeneratorMessages
84
84
  # package_json: nil to preserve a cached missing/unreadable read.
85
85
  def package_manager_declared?(manager:, app_root: Dir.pwd, package_json: PACKAGE_JSON_UNSET)
86
86
  content = package_json_content(
87
- app_root: app_root,
88
- package_json: package_json
87
+ app_root:,
88
+ package_json:
89
89
  )
90
90
  return false unless content
91
91
 
@@ -99,12 +99,12 @@ module GeneratorMessages
99
99
  # that's not on disk (e.g. `packageManager: pnpm` without `pnpm-lock.yaml`, which
100
100
  # breaks `actions/setup-node`'s cache step).
101
101
  def lockfile_for_manager?(package_manager, app_root: Dir.pwd)
102
- !lockfile_filename_for(package_manager, app_root: app_root).nil?
102
+ !lockfile_filename_for(package_manager, app_root:).nil?
103
103
  end
104
104
 
105
105
  def detect_package_manager_from_lockfiles(app_root: Dir.pwd)
106
106
  LOCKFILE_CANDIDATES_BY_MANAGER.keys.find do |pm|
107
- lockfile_for_manager?(pm, app_root: app_root)
107
+ lockfile_for_manager?(pm, app_root:)
108
108
  end
109
109
  end
110
110
 
@@ -7,7 +7,7 @@ module GeneratorMessages
7
7
  private
8
8
 
9
9
  def build_shakapacker_status_section(shakapacker_just_installed: false, app_root: Dir.pwd)
10
- version_warning = check_shakapacker_version_warning(app_root: app_root)
10
+ version_warning = check_shakapacker_version_warning(app_root:)
11
11
  if shakapacker_just_installed
12
12
  base = <<~SHAKAPACKER
13
13
 
@@ -58,13 +58,13 @@ module GeneratorMessages
58
58
  rsc: false, shakapacker_just_installed: false, landing_page: false,
59
59
  ci_workflow_generated: false, app_root: Dir.pwd)
60
60
  process_manager_section = build_process_manager_section
61
- testing_section = build_testing_section(app_root: app_root)
62
- ci_section = build_ci_section(app_root: app_root, ci_workflow_generated: ci_workflow_generated)
63
- package_manager = detect_package_manager(app_root: app_root)
64
- shakapacker_status = build_shakapacker_status_section(shakapacker_just_installed: shakapacker_just_installed,
65
- app_root: app_root)
66
- render_example = build_render_example(component_name: component_name, route: route, rsc: rsc)
67
- render_label = build_render_label(route: route, rsc: rsc)
61
+ testing_section = build_testing_section(app_root:)
62
+ ci_section = build_ci_section(app_root:, ci_workflow_generated:)
63
+ package_manager = detect_package_manager(app_root:)
64
+ shakapacker_status = build_shakapacker_status_section(shakapacker_just_installed:,
65
+ app_root:)
66
+ render_example = build_render_example(component_name:, route:, rsc:)
67
+ render_label = build_render_label(route:, rsc:)
68
68
  normalized_route = route.to_s.sub(%r{\A/+}, "")
69
69
  visit_url = if landing_page || normalized_route.empty?
70
70
  "http://localhost:3000"
@@ -43,6 +43,12 @@ module ReactOnRails
43
43
  desc: "Generate TypeScript files and install TypeScript dependencies. Default: false",
44
44
  aliases: "-T"
45
45
 
46
+ # --tailwind
47
+ class_option :tailwind,
48
+ type: :boolean,
49
+ default: false,
50
+ desc: "Install Tailwind CSS v4 and style the generated SSR example. Default: false"
51
+
46
52
  # --rspack / --no-rspack (Rspack is the default on fresh installs; --no-rspack selects Webpack)
47
53
  # IMPORTANT: do NOT add a `default:` here. The absence of a default is load-bearing — Thor
48
54
  # only includes :rspack in the options hash when the flag is explicitly passed, which is how
@@ -66,6 +72,15 @@ module ReactOnRails
66
72
  default: false,
67
73
  desc: "Skip warnings. Default: false"
68
74
 
75
+ # --agent-files / --no-agent-files
76
+ # Emits consumer-scoped AI-agent guidance (AGENTS.md) plus thin editor pointer
77
+ # files (CLAUDE.md, .cursor/rules/react-on-rails.mdc, .github/copilot-instructions.md).
78
+ # Default ON; pass --no-agent-files to skip. Existing files are never overwritten.
79
+ class_option :agent_files,
80
+ type: :boolean,
81
+ default: true,
82
+ desc: "Write AI-agent guidance files (AGENTS.md + editor pointers). Default: true"
83
+
69
84
  # --pro
70
85
  class_option :pro,
71
86
  type: :boolean,
@@ -183,6 +198,7 @@ module ReactOnRails
183
198
  add_package_json_scripts
184
199
  add_ci_workflow
185
200
  add_bin_scripts
201
+ add_agent_files
186
202
  add_post_install_message
187
203
  else
188
204
  error = <<~MSG.strip
@@ -227,7 +243,7 @@ module ReactOnRails
227
243
  # --pretend/--force/--skip must be forwarded explicitly at each boundary.
228
244
  invoke "react_on_rails:base", [],
229
245
  { typescript: options.typescript?, redux: options.redux?, rspack: using_rspack?,
230
- pro: use_pro?, rsc: use_rsc?, new_app: options.new_app?,
246
+ pro: use_pro?, rsc: use_rsc?, tailwind: use_tailwind?, new_app: options.new_app?,
231
247
  shakapacker_just_installed: shakapacker_just_installed?,
232
248
  force: options[:force], skip: options[:skip], pretend: options[:pretend] }
233
249
 
@@ -237,6 +253,7 @@ module ReactOnRails
237
253
  # - Without --rsc: Normal behavior (HelloWorld or HelloWorldApp based on --redux)
238
254
  if options.redux?
239
255
  invoke "react_on_rails:react_with_redux", [], { typescript: options.typescript?,
256
+ tailwind: use_tailwind?,
240
257
  invoked_by_install: true,
241
258
  new_app: options.new_app?,
242
259
  rsc: use_rsc?,
@@ -246,6 +263,7 @@ module ReactOnRails
246
263
  # Only generate HelloWorld if RSC is not enabled
247
264
  # For RSC, HelloServer replaces HelloWorld as the example component
248
265
  invoke "react_on_rails:react_no_redux", [], { typescript: options.typescript?,
266
+ tailwind: use_tailwind?,
249
267
  new_app: options.new_app?,
250
268
  force: options[:force], skip: options[:skip],
251
269
  pretend: options[:pretend] }
@@ -265,6 +283,7 @@ module ReactOnRails
265
283
 
266
284
  invoke "react_on_rails:rsc", [], { typescript: options.typescript?, invoked_by_install: true,
267
285
  new_app: options.new_app?, redux: options.redux?,
286
+ tailwind: use_tailwind?,
268
287
  force: options[:force], skip: options[:skip],
269
288
  pretend: options[:pretend] }
270
289
  end
@@ -294,7 +313,7 @@ module ReactOnRails
294
313
  package_json = GeneratorMessages.read_package_json(destination_root)
295
314
  package_manager = GeneratorMessages.detect_package_manager(
296
315
  app_root: destination_root,
297
- package_json: package_json
316
+ package_json:
298
317
  )
299
318
  # Scope the lockfile check to the detected manager: a generic "any lockfile exists" check
300
319
  # would emit `cache: "pnpm"` in CI when only `yarn.lock` is on disk, breaking setup-node.
@@ -307,16 +326,16 @@ module ReactOnRails
307
326
  GeneratorMessages.package_manager_declared?(
308
327
  app_root: destination_root,
309
328
  manager: "pnpm",
310
- package_json: package_json
329
+ package_json:
311
330
  )
312
331
  has_active_record = File.exist?(File.join(destination_root, "config/database.yml"))
313
332
  has_rspec = File.exist?(File.join(destination_root, "spec/rails_helper.rb")) ||
314
333
  File.exist?(File.join(destination_root, "spec/spec_helper.rb"))
315
334
  template("templates/base/base/.github/workflows/ci.yml.tt", ci_path,
316
- { package_manager: package_manager, has_lockfile: has_lockfile,
317
- pnpm_version_declared: pnpm_version_declared,
335
+ { package_manager:, has_lockfile:,
336
+ pnpm_version_declared:,
318
337
  pnpm_fallback_version: CI_PNPM_FALLBACK_VERSION,
319
- has_active_record: has_active_record, has_rspec: has_rspec,
338
+ has_active_record:, has_rspec:,
320
339
  precompile_hook_command: shakapacker_precompile_hook_command(environment: "test") })
321
340
  @ci_workflow_generated = true
322
341
  end
@@ -442,7 +461,7 @@ module ReactOnRails
442
461
  content = JSON.parse(original_text)
443
462
  content["scripts"] = existing_scripts.merge(scripts_to_add)
444
463
  indent = original_text[/\A\{\n(\s+)/, 1] || " "
445
- "#{JSON.pretty_generate(content, indent: indent)}\n"
464
+ "#{JSON.pretty_generate(content, indent:)}\n"
446
465
  end
447
466
 
448
467
  def ensure_jsx_in_js_compatibility
@@ -622,6 +641,40 @@ module ReactOnRails
622
641
  File.chmod(0o755, *files_to_become_executable)
623
642
  end
624
643
 
644
+ # Consumer-scoped AI-agent guidance written into the generated app. The canonical
645
+ # AGENTS.md content lives in templates/agent_files/ and is the single source of truth;
646
+ # create-react-on-rails-app gets it for free because it delegates to this generator.
647
+ # Each file is copied only when absent so we never clobber an app's existing agent files.
648
+ AGENT_FILES = %w[
649
+ AGENTS.md
650
+ CLAUDE.md
651
+ .cursor/rules/react-on-rails.mdc
652
+ .github/copilot-instructions.md
653
+ ].freeze
654
+ private_constant :AGENT_FILES
655
+
656
+ def add_agent_files
657
+ return unless options.agent_files?
658
+
659
+ # AGENTS.md is the canonical file the editor pointers (CLAUDE.md, Cursor, Copilot) all
660
+ # reference. If the app already has its own AGENTS.md, it may document unrelated
661
+ # conventions, so leave it untouched AND skip the pointer files rather than emit editor
662
+ # guidance pointing at an AGENTS.md we did not write.
663
+ if File.exist?(File.join(destination_root, "AGENTS.md"))
664
+ say_status :skip, "AGENTS.md already exists; leaving it and the editor pointer files untouched", :yellow
665
+ return
666
+ end
667
+
668
+ AGENT_FILES.each do |relative_path|
669
+ if File.exist?(File.join(destination_root, relative_path))
670
+ say_status :skip, "#{relative_path} already exists; leaving it untouched", :yellow
671
+ next
672
+ end
673
+
674
+ copy_file("templates/agent_files/#{relative_path}", relative_path)
675
+ end
676
+ end
677
+
625
678
  def replace_stock_rails_bin_dev!
626
679
  @preserve_existing_bin_dev = false
627
680
 
@@ -690,8 +743,8 @@ module ReactOnRails
690
743
  end
691
744
 
692
745
  GeneratorMessages.add_info(GeneratorMessages.helpful_message_after_installation(
693
- component_name: component_name,
694
- route: route,
746
+ component_name:,
747
+ route:,
695
748
  pro: use_pro?,
696
749
  rsc: use_rsc?,
697
750
  shakapacker_just_installed: shakapacker_just_installed?,
@@ -722,6 +775,10 @@ module ReactOnRails
722
775
  flags << "--pro"
723
776
  end
724
777
 
778
+ # Preserve an explicit agent-files opt-out so the suggested re-run doesn't emit
779
+ # AGENTS.md/editor files a user deliberately skipped (--agent-files defaults to on).
780
+ flags << "--no-agent-files" unless options.agent_files?
781
+
725
782
  ["rails generate react_on_rails:install", *flags].join(" ")
726
783
  end
727
784